Compare commits

...

15 Commits

Author SHA1 Message Date
Srikanth Chekuri
43933f3a33 chore: move converter/formatter to pkg/units/... (#10408)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-02-26 23:52:58 +05:30
Naman Verma
d2e1a24b20 chore: choose latest seen unit for metrics instead of any unit (#10431) 2026-02-26 16:48:22 +00:00
Nikhil Mantri
887b3c7b3a chore: improve error messaging and UI edge cases in infra hosts monitoring (#10304) 2026-02-26 13:38:57 +00:00
Vinicius Lourenço
476fe7a29a perf(service-map): use react-force-graph-2d dep to reduce bundle size (#10191)
Co-authored-by: Yunus M <myounis.ar@live.com>
2026-02-26 10:10:19 -03:00
Ashwin Bhatkal
c1d38d86f1 chore: add nuqs and zustand to the repo (#10434) 2026-02-26 12:21:53 +00:00
Vinicius Lourenço
4f2594c31d perf(tooltip-value): cache intl number object (#9965) 2026-02-26 11:45:19 +00:00
Karan Balani
c9985b56bc feat: add org id support in root user config (#10418)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: add org id support in root user config

* chore: address review comments

* fix: use zero value uuid for org id in example.conf
2026-02-26 13:44:14 +05:30
Abhi kumar
f9868e2221 fix: thresholds working correctly with number panel (#10394)
* fix: fixed unit converstion support across thresholds and yaxisunit

* fix: thresholds working correctly with number panel

* fix: fixed tsc

* chore: fixed unit tests

* chore: reverted the extractnumberfromstring change

* chore: replaced select component with yaxisunitselector in threshold

* fix: fixed test

* chore: pr review fix

* chore: added test for yaxisunitselector
2026-02-26 07:42:29 +00:00
Amlan Kumar Nandy
72b0398eaf chore: metrics explorer v2 api migration in explorer section (#10111) 2026-02-26 06:57:41 +00:00
Abhi kumar
5b75a39777 chore: removed sentry instrumentation for querysearch (#10426) 2026-02-26 12:04:58 +05:30
Abhi kumar
6948b69012 fix: throttled legend color picker in dashboard + memory leak fix due to tooltip persistance (#10421)
* fix: creating tooltip plugin container only once

* chore: throttled legendcolor change

* fix: fixed tooltip plugin test
2026-02-26 11:28:20 +05:30
Amlan Kumar Nandy
bc9701397e chore: migrate metric details side drawer in metrics explorer to v2 APIs (#9995) 2026-02-26 04:18:33 +00:00
Naman Verma
396cf3194e feat: add support for count based aggregation in histogram metrics (#10355)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-25 17:03:06 +00:00
Abhi kumar
8be96a0ded fix: fetch the current version changelog instead of latest version (#10422) 2026-02-25 21:03:25 +05:30
primus-bot[bot]
82c54b1d36 chore(release): bump to v0.113.0 (#10420)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-02-25 18:37:05 +05:30
125 changed files with 5668 additions and 2064 deletions

View File

@@ -320,3 +320,4 @@ user:
# The name of the organization to create or look up for the root user.
org:
name: default
id: 00000000-0000-0000-0000-000000000000

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.1
image: signoz/signoz:v0.113.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.142.1
image: signoz/signoz-otel-collector:v0.144.1
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
image: signoz/signoz-otel-collector:v0.144.1
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.112.1
image: signoz/signoz:v0.113.0
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.142.1
image: signoz/signoz-otel-collector:v0.144.1
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
image: signoz/signoz-otel-collector:v0.144.1
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.1}
image: signoz/signoz:${VERSION:-v0.113.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.112.1}
image: signoz/signoz:${VERSION:-v0.113.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -842,6 +842,17 @@ components:
- temporality
- isMonotonic
type: object
MetrictypesComparisonSpaceAggregationParam:
properties:
operator:
type: string
threshold:
format: double
type: number
required:
- operator
- threshold
type: object
MetrictypesSpaceAggregation:
enum:
- sum
@@ -1138,6 +1149,8 @@ components:
type: object
Querybuildertypesv5MetricAggregation:
properties:
comparisonSpaceAggregationParam:
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
metricName:
type: string
reduceTo:

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
@@ -335,7 +335,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
var res ruletypes.Vector
var err error

View File

@@ -117,6 +117,7 @@
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
@@ -130,7 +131,7 @@
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph": "^1.43.0",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
@@ -162,7 +163,8 @@
"webpack": "5.94.0",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
@@ -287,4 +289,4 @@
"on-headers": "^1.1.0",
"tmp": "0.2.4"
}
}
}

View File

@@ -1006,6 +1006,18 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
unit: string;
}
export interface MetrictypesComparisonSpaceAggregationParamDTO {
/**
* @type string
*/
operator: string;
/**
* @type number
* @format double
*/
threshold: number;
}
export enum MetrictypesSpaceAggregationDTO {
sum = 'sum',
avg = 'avg',
@@ -1367,6 +1379,7 @@ export interface Querybuildertypesv5LogAggregationDTO {
}
export interface Querybuildertypesv5MetricAggregationDTO {
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
/**
* @type string
*/

View File

@@ -50,6 +50,7 @@ export interface HostListResponse {
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -1,26 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export const getMetricMetadata = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
try {
const encodedMetricName = encodeURIComponent(metricName);
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
signal,
headers,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -248,6 +248,11 @@ declare module 'chart.js' {
}
}
const intlNumberFormatter = new Intl.NumberFormat('en-US', {
useGrouping: false,
maximumFractionDigits: 20,
});
/**
* Formats a number for display, preserving leading zeros after the decimal point
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
@@ -270,10 +275,7 @@ export const formatDecimalWithLeadingZeros = (
}
// Use toLocaleString to get a full decimal representation without scientific notation.
const numStr = value.toLocaleString('en-US', {
useGrouping: false,
maximumFractionDigits: 20,
});
const numStr = intlNumberFormatter.format(value);
const [integerPart, decimalPart = ''] = numStr.split('.');

View File

@@ -11,7 +11,6 @@ import {
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
@@ -564,15 +563,7 @@ function QuerySearch({
const lastPos = lastPosRef.current;
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
setCursorPos((lastPos) => {
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
Sentry.captureEvent({
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
level: 'warning',
});
}
return newPos;
});
setCursorPos(newPos);
lastPosRef.current = newPos;
if (doc) {

View File

@@ -40,6 +40,7 @@ function ValueGraph({
value,
rawValue,
thresholds,
yAxisUnit,
}: ValueGraphProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
const containerRef = useRef<HTMLDivElement>(null);
@@ -87,7 +88,7 @@ function ValueGraph({
const {
threshold,
isConflictingThresholds,
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
return (
<div
@@ -155,6 +156,7 @@ interface ValueGraphProps {
value: string;
rawValue: number;
thresholds: ThresholdProps[];
yAxisUnit?: string;
}
export default ValueGraph;

View File

@@ -1,9 +1,10 @@
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
function compareThreshold(
function doesValueSatisfyThreshold(
rawValue: number,
threshold: ThresholdProps,
yAxisUnit?: string,
): boolean {
if (
threshold.thresholdOperator === undefined ||
@@ -11,31 +12,14 @@ function compareThreshold(
) {
return false;
}
switch (threshold.thresholdOperator) {
case '>':
return rawValue > threshold.thresholdValue;
case '>=':
return rawValue >= threshold.thresholdValue;
case '<':
return rawValue < threshold.thresholdValue;
case '<=':
return rawValue <= threshold.thresholdValue;
case '=':
return rawValue === threshold.thresholdValue;
default:
return false;
}
}
function extractNumbersFromString(inputString: string): number[] {
const regex = /[+-]?\d+(\.\d+)?/g;
const matches = inputString.match(regex);
if (matches) {
return matches.map(Number);
}
return [];
return evaluateThresholdWithConvertedValue(
rawValue,
threshold.thresholdValue,
threshold.thresholdOperator,
threshold.thresholdUnit,
yAxisUnit,
);
}
function getHighestPrecedenceThreshold(
@@ -60,21 +44,32 @@ function getHighestPrecedenceThreshold(
return highestPrecedenceThreshold;
}
function extractNumbersFromString(inputString: string): number[] {
const regex = /[+-]?\d+(\.\d+)?/g;
const matches = inputString.match(regex);
if (matches) {
return matches.map(Number);
}
return [];
}
export function getBackgroundColorAndThresholdCheck(
thresholds: ThresholdProps[],
rawValue: number,
yAxisUnit?: string,
): {
threshold: ThresholdProps;
isConflictingThresholds: boolean;
} {
const matchingThresholds = thresholds.filter((threshold) =>
compareThreshold(
extractNumbersFromString(
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
)[0],
threshold,
),
);
const matchingThresholds = thresholds.filter((threshold) => {
const numbers = extractNumbersFromString(rawValue.toString());
if (numbers.length === 0) {
return false;
}
return doesValueSatisfyThreshold(numbers[0], threshold, yAxisUnit);
});
if (matchingThresholds.length === 0) {
return {

View File

@@ -22,6 +22,8 @@ function YAxisUnitSelector({
'data-testid': dataTestId,
source,
initialValue,
categoriesOverride,
containerClassName,
}: YAxisUnitSelectorProps): JSX.Element {
const universalUnit = mapMetricUnitToUniversalUnit(value);
@@ -66,10 +68,14 @@ function YAxisUnitSelector({
return aliases.some((alias) => alias.toLowerCase().includes(search));
};
const categories = getYAxisCategories(source);
const categoriesToRender = useMemo(() => {
return categoriesOverride || getYAxisCategories(source);
}, [categoriesOverride, source]);
return (
<div className="y-axis-unit-selector-component">
<div
className={classNames('y-axis-unit-selector-component', containerClassName)}
>
<Select
showSearch
value={universalUnit}
@@ -90,7 +96,7 @@ function YAxisUnitSelector({
data-testid={dataTestId}
allowClear
>
{categories.map((category) => (
{categoriesToRender.map((category) => (
<Select.OptGroup key={category.name} label={category.name}>
{category.units.map((unit) => (
<Select.Option key={unit.id} value={unit.id}>

View File

@@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { YAxisSource } from '../types';
import { YAxisCategoryNames } from '../constants';
import { UniversalYAxisUnit, YAxisSource } from '../types';
import YAxisUnitSelector from '../YAxisUnitSelector';
describe('YAxisUnitSelector', () => {
@@ -123,4 +124,34 @@ describe('YAxisUnitSelector', () => {
const warningIcon = screen.queryByLabelText('warning');
expect(warningIcon).not.toBeInTheDocument();
});
it('uses categories override to render custom units', () => {
const customCategories = [
{
name: YAxisCategoryNames.Data,
units: [
{
id: UniversalYAxisUnit.BYTES,
name: 'Custom Bytes (B)',
},
],
},
];
render(
<YAxisUnitSelector
value=""
onChange={mockOnChange}
source={YAxisSource.ALERTS}
categoriesOverride={customCategories}
/>,
);
const select = screen.getByRole('combobox');
fireEvent.mouseDown(select);
expect(screen.getByText('Custom Bytes (B)')).toBeInTheDocument();
expect(screen.queryByText('Bytes (B)')).not.toBeInTheDocument();
});
});

View File

@@ -9,6 +9,8 @@ export interface YAxisUnitSelectorProps {
'data-testid'?: string;
source: YAxisSource;
initialValue?: string;
categoriesOverride?: YAxisCategory[];
containerClassName?: string;
}
export enum UniversalYAxisUnit {

View File

@@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
const { latestVersion } = useSelector<AppState, AppReducer>(
const { currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
@@ -213,9 +213,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
},
{
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
getChangelogByVersion(latestVersion, changelogForTenant),
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
enabled: isLoggedIn && Boolean(latestVersion),
getChangelogByVersion(currentVersion, changelogForTenant),
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
enabled: isLoggedIn && Boolean(currentVersion),
},
]);
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
!changelog &&
!getChangelogByVersionResponse.isLoading &&
isLoggedIn &&
Boolean(latestVersion)
Boolean(currentVersion)
) {
getChangelogByVersionResponse.refetch();
}
@@ -237,9 +237,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
let timer: ReturnType<typeof setTimeout>;
if (
isCloudUserVal &&
Boolean(latestVersion) &&
Boolean(currentVersion) &&
seenChangelogVersion != null &&
latestVersion !== seenChangelogVersion &&
currentVersion !== seenChangelogVersion &&
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
!isWorkspaceAccessRestricted
) {
@@ -255,7 +255,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isCloudUserVal,
latestVersion,
currentVersion,
seenChangelogVersion,
toggleChangelogModal,
isWorkspaceAccessRestricted,

View File

@@ -49,7 +49,7 @@ function evaluateCondition(
* @param columnUnit - The current unit of the value.
* @returns A boolean indicating whether the value meets the threshold condition.
*/
function evaluateThresholdWithConvertedValue(
export function evaluateThresholdWithConvertedValue(
value: number,
thresholdValue: number,
thresholdOperator?: string,

View File

@@ -99,6 +99,7 @@ function GridValueComponent({
<ValueGraph
thresholds={thresholds || []}
rawValue={value}
yAxisUnit={yAxisUnit}
value={
yAxisUnit
? getYAxisFormattedValue(

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
@@ -14,12 +14,93 @@ import { InfraMonitoringEvents } from 'constants/events';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import {
EmptyOrLoadingViewProps,
formatDataForTable,
getHostsListColumns,
HostRowData,
HostsListTableProps,
} from './utils';
function EmptyOrLoadingView(
viewState: EmptyOrLoadingViewProps,
): React.ReactNode {
const { isError, errorMessage } = viewState;
if (isError) {
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
}
if (viewState.showHostsEmptyState) {
return (
<HostsEmptyOrIncorrectMetrics
noData={!viewState.sentAnyHostMetricsData}
incorrectData={viewState.isSendingIncorrectK8SAgentMetrics}
/>
);
}
if (viewState.showEndTimeBeforeRetentionMessage) {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
Queried time range is before earliest host metrics
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Your requested end time is earlier than the earliest detected time of
host metrics data, please adjust your end time.
</Typography.Text>
</div>
</div>
</div>
);
}
if (viewState.showNoRecordsInSelectedTimeRangeMessage) {
return (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Title level={5} className="no-filtered-hosts-title">
No host metrics found
</Typography.Title>
<Typography.Text className="no-filtered-hosts-message">
No host metrics in the selected time range and filters. Please adjust your
time range or filters.
</Typography.Text>
</div>
</div>
);
}
if (viewState.showTableLoadingState) {
return (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
);
}
return null;
}
export default function HostsListTable({
isLoading,
isFetching,
@@ -46,6 +127,11 @@ export default function HostsListTable({
[data],
);
const endTimeBeforeRetention = useMemo(
() => data?.payload?.data?.endTimeBeforeRetention || false,
[data],
);
const formattedHostMetricsData = useMemo(
() => formatDataForTable(hostMetricsData),
[hostMetricsData],
@@ -84,12 +170,6 @@ export default function HostsListTable({
});
};
const showNoFilteredHostsMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
filters.items.length > 0;
const showHostsEmptyState =
!isFetching &&
!isLoading &&
@@ -97,63 +177,36 @@ export default function HostsListTable({
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
!filters.items.length;
const showEndTimeBeforeRetentionMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
endTimeBeforeRetention &&
!filters.items.length;
const showNoRecordsInSelectedTimeRangeMessage =
!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
!showEndTimeBeforeRetentionMessage &&
!showHostsEmptyState;
const showTableLoadingState =
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
if (isError) {
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
}
const emptyOrLoadingView = EmptyOrLoadingView({
isError,
errorMessage: data?.error ?? '',
showHostsEmptyState,
sentAnyHostMetricsData,
isSendingIncorrectK8SAgentMetrics,
showEndTimeBeforeRetentionMessage,
showNoRecordsInSelectedTimeRangeMessage,
showTableLoadingState,
});
if (showHostsEmptyState) {
return (
<HostsEmptyOrIncorrectMetrics
noData={!sentAnyHostMetricsData}
incorrectData={isSendingIncorrectK8SAgentMetrics}
/>
);
}
if (showNoFilteredHostsMessage) {
return (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
);
}
if (showTableLoadingState) {
return (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
);
if (emptyOrLoadingView) {
return <>{emptyOrLoadingView}</>;
}
return (

View File

@@ -1,12 +1,16 @@
/* eslint-disable react/jsx-props-no-spreading */
import { render, screen } from '@testing-library/react';
import { HostData, HostListResponse } from 'api/infraMonitoring/getHostLists';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import HostsListTable from '../HostsListTable';
import { HostsListTableProps } from '../utils';
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
describe('HostsListTable', () => {
const mockHost = {
const createMockHost = (): HostData =>
({
hostName: 'test-host-1',
active: true,
cpu: 0.75,
@@ -14,20 +18,46 @@ describe('HostsListTable', () => {
wait: 0.03,
load15: 1.5,
os: 'linux',
};
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
} as HostData);
const mockTableData = {
const createMockTableData = (
overrides: Partial<HostListResponse['data']> = {},
): SuccessResponse<HostListResponse> => {
const mockHost = createMockHost();
return {
statusCode: 200,
message: 'Success',
error: null,
payload: {
status: 'success',
data: {
hosts: [mockHost],
type: 'list',
records: [mockHost],
groups: null,
total: 1,
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: false,
...overrides,
},
},
};
};
describe('HostsListTable', () => {
const mockHost = createMockHost();
const mockTableData = createMockTableData();
const mockOnHostClick = jest.fn();
const mockSetCurrentPage = jest.fn();
const mockSetOrderBy = jest.fn();
const mockSetPageSize = jest.fn();
const mockProps = {
const mockProps: HostsListTableProps = {
isLoading: false,
isError: false,
isFetching: false,
@@ -43,7 +73,7 @@ describe('HostsListTable', () => {
pageSize: 10,
setOrderBy: mockSetOrderBy,
setPageSize: mockSetPageSize,
} as any;
};
it('renders loading state if isLoading is true and tableData is empty', () => {
const { container } = render(
@@ -51,7 +81,7 @@ describe('HostsListTable', () => {
{...mockProps}
isLoading
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
tableData={createMockTableData({ records: [] })}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
@@ -63,7 +93,7 @@ describe('HostsListTable', () => {
{...mockProps}
isFetching
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
tableData={createMockTableData({ records: [] })}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
@@ -74,19 +104,56 @@ describe('HostsListTable', () => {
expect(screen.getByText('Something went wrong')).toBeTruthy();
});
it('renders "Something went wrong" fallback when isError is true and error message is empty', () => {
const tableDataWithEmptyError: ErrorResponse = {
statusCode: 500,
payload: null,
error: '',
message: null,
};
render(
<HostsListTable
{...mockProps}
isError
hostMetricsData={[]}
tableData={tableDataWithEmptyError}
/>,
);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('renders custom error message when isError is true and error message is provided', () => {
const customErrorMessage = 'Failed to fetch host metrics';
const tableDataWithError: ErrorResponse = {
statusCode: 500,
payload: null,
error: customErrorMessage,
message: null,
};
render(
<HostsListTable
{...mockProps}
isError
hostMetricsData={[]}
tableData={tableDataWithError}
/>,
);
expect(screen.getByText(customErrorMessage)).toBeInTheDocument();
});
it('renders empty state if no hosts are found', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
payload: {
data: { hosts: [] },
},
}}
tableData={createMockTableData({
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
expect(
container.querySelector('.no-filtered-hosts-message-container'),
).toBeTruthy();
});
it('renders empty state if sentAnyHostMetricsData is false', () => {
@@ -94,58 +161,114 @@ describe('HostsListTable', () => {
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
sentAnyHostMetricsData: false,
hosts: [],
},
},
}}
tableData={createMockTableData({
sentAnyHostMetricsData: false,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
});
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
isSendingIncorrectK8SAgentMetrics: true,
hosts: [],
},
},
}}
tableData={createMockTableData({
isSendingK8SAgentMetrics: true,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
});
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={createMockTableData({
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: true,
records: [],
})}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
expect(
screen.getByText(
/Your requested end time is earlier than the earliest detected time of host metrics data, please adjust your end time\./,
),
).toBeInTheDocument();
});
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={createMockTableData({
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
records: [],
})}
/>,
);
expect(
container.querySelector('.no-filtered-hosts-message-container'),
).toBeTruthy();
expect(
screen.getByText(/No host metrics in the selected time range and filters/),
).toBeInTheDocument();
});
it('renders no filtered hosts message when filters are present and no hosts are found', () => {
const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
filters={{
items: [
{
id: 'host_name',
key: {
key: 'host_name',
dataType: DataTypes.String,
type: 'tag',
isIndexed: true,
},
op: '=',
value: 'unknown',
},
],
op: 'AND',
}}
tableData={createMockTableData({
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
records: [],
})}
/>,
);
expect(container.querySelector('.no-filtered-hosts-message')).toBeTruthy();
expect(
screen.getByText(
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
),
).toBeInTheDocument();
});
it('renders table data', () => {
const { container } = render(
<HostsListTable
{...mockProps}
tableData={{
...mockTableData,
payload: {
...mockTableData.payload,
data: {
...mockTableData.payload.data,
isSendingIncorrectK8SAgentMetrics: false,
sentAnyHostMetricsData: true,
},
},
}}
tableData={createMockTableData({
isSendingK8SAgentMetrics: false,
sentAnyHostMetricsData: true,
})}
/>,
);
expect(container.querySelector('.hosts-list-table')).toBeTruthy();

View File

@@ -107,6 +107,17 @@ export interface HostsListTableProps {
setPageSize: (pageSize: number) => void;
}
export interface EmptyOrLoadingViewProps {
isError: boolean;
errorMessage: string;
showHostsEmptyState: boolean;
sentAnyHostMetricsData: boolean;
isSendingIncorrectK8SAgentMetrics: boolean;
showEndTimeBeforeRetentionMessage: boolean;
showNoRecordsInSelectedTimeRangeMessage: boolean;
showTableLoadingState: boolean;
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],

View File

@@ -87,7 +87,7 @@ function Explorer(): JSX.Element {
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
const unitsLength = useMemo(() => units.length, [units]);
const firstUnit = useMemo(() => units?.[0], [units]);
const firstUnit = useMemo(() => units[0], [units]);
useEffect(() => {
// Set the y axis unit to the first metric unit if
@@ -349,7 +349,7 @@ function Explorer(): JSX.Element {
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
/>
{isMetricDetailsOpen && (
{isMetricDetailsOpen && selectedMetricName && (
<MetricDetails
metricName={selectedMetricName}
isOpen={isMetricDetailsOpen}

View File

@@ -39,10 +39,7 @@ function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
dataSource={DataSource.METRICS}
/>
)}
<DashboardsAndAlertsPopover
dashboards={metric.dashboards}
alerts={metric.alerts}
/>
<DashboardsAndAlertsPopover metricName={metric.name} />
</div>
);
}

View File

@@ -1,8 +1,13 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useQueries, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import { toast } from '@signozhq/sonner';
import { Button, Tooltip, Typography } from 'antd';
import {
invalidateGetMetricMetadata,
useUpdateMetricMetadata,
} from 'api/generated/services/metrics';
import { isAxiosError } from 'axios';
import classNames from 'classnames';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
@@ -23,7 +28,10 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TimeSeriesProps } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
import {
buildUpdateMetricYAxisUnitPayload,
splitQueryIntoOneChartPerQuery,
} from './utils';
function TimeSeries({
showOneChartPerQuery,
@@ -35,6 +43,7 @@ function TimeSeries({
yAxisUnit,
setYAxisUnit,
showYAxisUnitSelector,
metrics,
}: TimeSeriesProps): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
@@ -42,6 +51,7 @@ function TimeSeries({
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryClient = useQueryClient();
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
@@ -138,54 +148,51 @@ function TimeSeries({
setYAxisUnit(value);
};
// TODO: Enable once we have resolved all related metrics v2 api issues
// Show the save unit button if
// 1. There is only one metric
// 2. The metric has no saved unit
// 3. The user has selected a unit
// const showSaveUnitButton = useMemo(
// () =>
// metricUnits.length === 1 &&
// Boolean(metrics?.[0]) &&
// !metricUnits[0] &&
// yAxisUnit,
// [metricUnits, metrics, yAxisUnit],
// );
const showSaveUnitButton = useMemo(
() =>
metricUnits.length === 1 &&
Boolean(metrics[0]) &&
!metricUnits[0] &&
yAxisUnit,
[metricUnits, metrics, yAxisUnit],
);
// const {
// mutate: updateMetricMetadata,
// isLoading: isUpdatingMetricMetadata,
// } = useUpdateMetricMetadata();
const {
mutate: updateMetricMetadata,
isLoading: isUpdatingMetricMetadata,
} = useUpdateMetricMetadata();
// const handleSaveUnit = (): void => {
// updateMetricMetadata(
// {
// metricName: metricNames[0],
// payload: {
// unit: yAxisUnit,
// description: metrics[0]?.description ?? '',
// metricType: metrics[0]?.type as MetricType,
// temporality: metrics[0]?.temporality,
// },
// },
// {
// onSuccess: () => {
// notifications.success({
// message: 'Unit saved successfully',
// });
// queryClient.invalidateQueries([
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
// metricNames[0],
// ]);
// },
// onError: () => {
// notifications.error({
// message: 'Failed to save unit',
// });
// },
// },
// );
// };
const handleSaveUnit = (): void => {
if (metrics[0] && yAxisUnit) {
updateMetricMetadata(
{
pathParams: {
metricName: metricNames[0],
},
data: buildUpdateMetricYAxisUnitPayload(
metricNames[0],
metrics[0],
yAxisUnit,
),
},
{
onSuccess: () => {
toast.success('Unit saved successfully');
invalidateGetMetricMetadata(queryClient, {
metricName: metricNames[0],
});
},
onError: () => {
toast.error('Failed to save unit');
},
},
);
}
};
return (
<>
@@ -198,8 +205,7 @@ function TimeSeries({
source={YAxisSource.EXPLORER}
data-testid="y-axis-unit-selector"
/>
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
{/* {showSaveUnitButton && (
{showSaveUnitButton && (
<div className="save-unit-container">
<Typography.Text>
Save the selected unit for this metric?
@@ -213,7 +219,7 @@ function TimeSeries({
<Typography.Paragraph>Yes</Typography.Paragraph>
</Button>
</div>
)} */}
)}
</>
)}
</div>

View File

@@ -3,8 +3,10 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { render, screen } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
@@ -14,12 +16,12 @@ import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import * as timezoneHooks from 'providers/Timezone';
import store from 'store';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import Explorer from '../Explorer';
import * as useGetMetricsHooks from '../utils';
import { MOCK_METRIC_METADATA } from './testUtils';
const mockSetSearchParams = jest.fn();
const queryClient = new QueryClient();
@@ -135,14 +137,6 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
isMonotonic: true,
};
function renderExplorer(): void {
render(
<QueryClientProvider client={queryClient}>
@@ -190,7 +184,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -207,7 +201,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -220,7 +214,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric],
metrics: [MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -237,7 +231,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -250,7 +244,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -269,10 +263,10 @@ describe('Explorer', () => {
isError: false,
metrics: [
{
type: MetricType.SUM,
type: MetrictypesTypeDTO.sum,
description: 'metric1 description',
unit: '',
temporality: Temporality.CUMULATIVE,
temporality: MetrictypesTemporalityDTO.cumulative,
isMonotonic: true,
},
],
@@ -289,7 +283,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric],
metrics: [MOCK_METRIC_METADATA],
});
renderExplorer();
@@ -324,7 +318,7 @@ describe('Explorer', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
});
renderExplorer();

View File

@@ -1,29 +1,19 @@
import { UseMutationResult } from 'react-query';
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataResponse } from 'api/metricsExplorer/updateMetricMetadata';
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { UseUpdateMetricMetadataProps } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import TimeSeries from '../TimeSeries';
import { TimeSeriesProps } from '../types';
import { MOCK_METRIC_METADATA } from './testUtils';
type MockUpdateMetricMetadata = UseMutationResult<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
>;
const mockUpdateMetricMetadata = jest.fn();
jest
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
.mockReturnValue(({
mutate: mockUpdateMetricMetadata,
isLoading: false,
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
const updateMetricMetadataSpy = jest.spyOn(
metricsExplorerHooks,
'useUpdateMetricMetadata',
);
type UseUpdateMetricMetadataReturnType = ReturnType<
typeof metricsExplorerHooks.useUpdateMetricMetadata
>;
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
__esModule: true,
@@ -60,14 +50,6 @@ jest.mock('react-redux', () => ({
}),
}));
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
isMonotonic: true,
};
const mockSetWarning = jest.fn();
const mockSetIsMetricDetailsOpen = jest.fn();
const mockSetYAxisUnit = jest.fn();
@@ -96,6 +78,13 @@ function renderTimeSeries(
}
describe('TimeSeries', () => {
beforeEach(() => {
updateMetricMetadataSpy.mockReturnValue(({
mutate: mockUpdateMetricMetadata,
isLoading: false,
} as Partial<UseUpdateMetricMetadataReturnType>) as UseUpdateMetricMetadataReturnType);
});
it('should render a warning icon when a metric has no unit among multiple metrics', () => {
const user = userEvent.setup();
const { container } = renderTimeSeries({
@@ -118,7 +107,7 @@ describe('TimeSeries', () => {
const { container } = renderTimeSeries({
metricUnits: ['', 'count'],
metricNames: ['metric1', 'metric2'],
metrics: [mockMetric, mockMetric],
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
yAxisUnit: 'seconds',
});
@@ -133,18 +122,17 @@ describe('TimeSeries', () => {
);
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('shows Save unit button when metric had no unit but one is selected', () => {
it('shows Save unit button when metric had no unit but one is selected', async () => {
const { findByText, getByRole } = renderTimeSeries({
metricUnits: [undefined],
metricNames: ['metric1'],
metrics: [mockMetric],
metrics: [MOCK_METRIC_METADATA],
yAxisUnit: 'seconds',
showYAxisUnitSelector: true,
});
expect(
findByText('Save the selected unit for this metric?'),
await findByText('Save the selected unit for this metric?'),
).toBeInTheDocument();
const yesButton = getByRole('button', { name: 'Yes' });
@@ -152,24 +140,25 @@ describe('TimeSeries', () => {
expect(yesButton).toBeEnabled();
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('clicking on save unit button shoould upated metric metadata', () => {
it('clicking on save unit button shoould upated metric metadata', async () => {
const user = userEvent.setup();
const { getByRole } = renderTimeSeries({
metricUnits: [''],
metricNames: ['metric1'],
metrics: [mockMetric],
metrics: [MOCK_METRIC_METADATA],
yAxisUnit: 'seconds',
showYAxisUnitSelector: true,
});
const yesButton = getByRole('button', { name: /Yes/i });
user.click(yesButton);
await user.click(yesButton);
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
{
metricName: 'metric1',
payload: expect.objectContaining({ unit: 'seconds' }),
pathParams: {
metricName: 'metric1',
},
data: expect.objectContaining({ unit: 'seconds' }),
},
expect.objectContaining({
onSuccess: expect.any(Function),

View File

@@ -0,0 +1,13 @@
import {
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
export const MOCK_METRIC_METADATA: MetricsexplorertypesMetricMetadataDTO = {
type: MetrictypesTypeDTO.sum,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: MetrictypesTemporalityDTO.cumulative,
isMonotonic: true,
};

View File

@@ -1,14 +1,8 @@
import { UseQueryResult } from 'react-query';
import { renderHook } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as useGetMultipleMetricsHook from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { SuccessResponseV2 } from 'types/api';
import {
MetricMetadata,
MetricMetadataResponse,
} from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
@@ -22,6 +16,7 @@ import {
splitQueryIntoOneChartPerQuery,
useGetMetrics,
} from '../utils';
import { MOCK_METRIC_METADATA } from './testUtils';
const MOCK_QUERY_DATA_1: IBuilderQuery = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
@@ -91,32 +86,19 @@ describe('splitQueryIntoOneChartPerQuery', () => {
});
});
const MOCK_METRIC_METADATA: MetricMetadata = {
description: 'Metric 1 description',
unit: 'unit1',
type: MetricType.GAUGE,
temporality: Temporality.DELTA,
isMonotonic: true,
};
describe('useGetMetrics', () => {
beforeEach(() => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
{
isLoading: false,
isError: false,
data: {
httpStatusCode: 200,
data: {
status: 'success',
data: MOCK_METRIC_METADATA,
},
data: MOCK_METRIC_METADATA,
status: 'success',
},
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
} as UseQueryResult<GetMetricMetadata200, Error>,
]);
});
@@ -133,12 +115,11 @@ describe('useGetMetrics', () => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
{
isLoading: true,
isError: false,
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
data: undefined,
} as UseQueryResult<GetMetricMetadata200, Error>,
]);
const { result } = renderHook(() => useGetMetrics(['metric1']));
expect(result.current.metrics).toHaveLength(1);

View File

@@ -1,9 +1,9 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
@@ -18,7 +18,7 @@ export interface TimeSeriesProps {
isMetricUnitsError: boolean;
metricUnits: (string | undefined)[];
metricNames: string[];
metrics: (MetricMetadata | undefined)[];
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
handleOpenMetricDetails: (metricName: string) => void;
yAxisUnit: string | undefined;
setYAxisUnit: (unit: string) => void;

View File

@@ -1,9 +1,12 @@
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { determineIsMonotonic } from '../MetricDetails/utils';
/**
* Split a query with multiple queryData to multiple distinct queries, each with a single queryData.
* @param query - The query to split
@@ -68,16 +71,14 @@ export function useGetMetrics(
): {
isLoading: boolean;
isError: boolean;
metrics: (MetricMetadata | undefined)[];
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
} {
const metricsData = useGetMultipleMetrics(metricNames, {
enabled: metricNames.length > 0 && isEnabled,
});
return {
isLoading: metricsData.some((metric) => metric.isLoading),
metrics: metricsData
.map((metric) => metric.data?.data)
.map((data) => data?.data),
metrics: metricsData.map((metric) => metric.data?.data),
isError: metricsData.some((metric) => metric.isError),
};
}
@@ -89,9 +90,24 @@ export function useGetMetrics(
* @returns The units of the metrics, can be undefined if the metric has no unit
*/
export function getMetricUnits(
metrics: (MetricMetadata | undefined)[],
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[],
): (string | undefined)[] {
return metrics
.map((metric) => metric?.unit)
.map((unit) => mapMetricUnitToUniversalUnit(unit) || undefined);
}
export function buildUpdateMetricYAxisUnitPayload(
metricName: string,
metric: MetricsexplorertypesMetricMetadataDTO,
yAxisUnit: string,
): UpdateMetricMetadataMutationBody {
return {
metricName,
type: metric.type,
description: metric.description,
unit: yAxisUnit || '',
temporality: metric.temporality,
isMonotonic: determineIsMonotonic(metric?.type, metric?.temporality),
};
}

View File

@@ -2,8 +2,8 @@
import { useMemo, useState } from 'react';
import { Card, Input, Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import classNames from 'classnames';
import { initialQueriesMap } from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
@@ -40,8 +40,10 @@ import {
* returns true if the feature flag is enabled, false otherwise
* Show the inspect button in metrics explorer if the feature flag is enabled
*/
export function isInspectEnabled(metricType: MetricType | undefined): boolean {
return metricType === MetricType.GAUGE;
export function isInspectEnabled(
metricType: MetrictypesTypeDTO | undefined,
): boolean {
return metricType === MetrictypesTypeDTO.gauge;
}
export function getAllTimestampsOfMetrics(

View File

@@ -1,8 +1,17 @@
import { useCallback, useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
import {
Button,
Collapse,
Input,
Menu,
Popover,
Skeleton,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { useGetMetricAttributes } from 'api/generated/services/metrics';
import { ResizeTable } from 'components/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView';
import { useNotifications } from 'hooks/useNotifications';
@@ -12,9 +21,33 @@ import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { AllAttributesProps, AllAttributesValueProps } from './types';
import MetricDetailsErrorState from './MetricDetailsErrorState';
import {
AllAttributesEmptyTextProps,
AllAttributesProps,
AllAttributesValueProps,
} from './types';
import { getMetricDetailsQuery } from './utils';
const ALL_ATTRIBUTES_KEY = 'all-attributes';
function AllAttributesEmptyText({
isErrorAttributes,
refetchAttributes,
}: AllAttributesEmptyTextProps): JSX.Element {
if (isErrorAttributes) {
return (
<div className="all-attributes-error-state">
<MetricDetailsErrorState
refetch={refetchAttributes}
errorMessage="Something went wrong while fetching attributes"
/>
</div>
);
}
return <Typography.Text>No attributes found</Typography.Text>;
}
export function AllAttributesValue({
filterKey,
filterValue,
@@ -110,13 +143,23 @@ export function AllAttributesValue({
function AllAttributes({
metricName,
attributes,
metricType,
}: AllAttributesProps): JSX.Element {
const [searchString, setSearchString] = useState('');
const [activeKey, setActiveKey] = useState<string | string[]>(
'all-attributes',
);
const [activeKey, setActiveKey] = useState<string[]>([ALL_ATTRIBUTES_KEY]);
const {
data: attributesData,
isLoading: isLoadingAttributes,
isError: isErrorAttributes,
refetch: refetchAttributes,
} = useGetMetricAttributes({
metricName,
});
const attributes = useMemo(() => attributesData?.data.attributes ?? [], [
attributesData,
]);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
@@ -178,7 +221,7 @@ function AllAttributes({
attributes.filter(
(attribute) =>
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
attribute.value.some((value) =>
attribute.values?.some((value) =>
value.toLowerCase().includes(searchString.toLowerCase()),
),
),
@@ -195,7 +238,7 @@ function AllAttributes({
},
value: {
key: attribute.key,
value: attribute.value,
value: attribute.values,
},
}))
: [],
@@ -270,6 +313,7 @@ function AllAttributes({
onClick={(e): void => {
e.stopPropagation();
}}
disabled={isLoadingAttributes || isErrorAttributes}
/>
</div>
),
@@ -277,25 +321,49 @@ function AllAttributes({
children: (
<ResizeTable
columns={columns}
loading={isLoadingAttributes}
tableLayout="fixed"
dataSource={tableData}
pagination={false}
showHeader={false}
className="metrics-accordion-content all-attributes-content"
scroll={{ y: 600 }}
locale={{
emptyText: (
<AllAttributesEmptyText
isErrorAttributes={isErrorAttributes}
refetchAttributes={refetchAttributes}
/>
),
}}
/>
),
},
],
[columns, tableData, searchString],
[
searchString,
isLoadingAttributes,
isErrorAttributes,
columns,
tableData,
refetchAttributes,
],
);
if (isLoadingAttributes) {
return (
<div className="all-attributes-skeleton-container">
<Skeleton active paragraph={{ rows: 8 }} />
</div>
);
}
return (
<Collapse
bordered
className="metrics-accordion metrics-metadata-accordion"
className="metrics-accordion"
activeKey={activeKey}
onChange={(keys): void => setActiveKey(keys)}
onChange={(keys): void => setActiveKey(keys as string[])}
items={items}
/>
);

View File

@@ -2,36 +2,84 @@ import { useMemo } from 'react';
import { generatePath } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Dropdown, Typography } from 'antd';
import { Skeleton } from 'antd/lib';
import {
useGetMetricAlerts,
useGetMetricDashboards,
} from 'api/generated/services/metrics';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Bell, Grid } from 'lucide-react';
import { pluralize } from 'utils/pluralize';
import { DashboardsAndAlertsPopoverProps } from './types';
function DashboardsAndAlertsPopover({
alerts,
dashboards,
metricName,
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
const { safeNavigate } = useSafeNavigate();
const params = useUrlQuery();
const {
data: alertsData,
isLoading: isLoadingAlerts,
isError: isErrorAlerts,
} = useGetMetricAlerts(
{
metricName,
},
{
query: {
enabled: !!metricName,
},
},
);
const {
data: dashboardsData,
isLoading: isLoadingDashboards,
isError: isErrorDashboards,
} = useGetMetricDashboards(
{
metricName,
},
{
query: {
enabled: !!metricName,
},
},
);
const alerts = useMemo(() => {
return alertsData?.data.alerts ?? [];
}, [alertsData]);
const dashboards = useMemo(() => {
const currentDashboards = dashboardsData?.data.dashboards ?? [];
// Remove duplicate dashboards
return currentDashboards.filter(
(dashboard, index, self) =>
index === self.findIndex((t) => t.dashboardId === dashboard.dashboardId),
);
}, [dashboardsData]);
const alertsPopoverContent = useMemo(() => {
if (alerts && alerts.length > 0) {
return alerts.map((alert) => ({
key: alert.alert_id,
key: alert.alertId,
label: (
<Typography.Link
key={alert.alert_id}
key={alert.alertId}
onClick={(): void => {
params.set(QueryParams.ruleId, alert.alert_id);
params.set(QueryParams.ruleId, alert.alertId);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}}
className="dashboards-popover-content-item"
>
{alert.alert_name || alert.alert_id}
{alert.alertName || alert.alertId}
</Typography.Link>
),
}));
@@ -39,41 +87,44 @@ function DashboardsAndAlertsPopover({
return null;
}, [alerts, params]);
const uniqueDashboards = useMemo(
() =>
dashboards?.filter(
(item, index, self) =>
index === self.findIndex((t) => t.dashboard_id === item.dashboard_id),
),
[dashboards],
);
const dashboardsPopoverContent = useMemo(() => {
if (uniqueDashboards && uniqueDashboards.length > 0) {
return uniqueDashboards.map((dashboard) => ({
key: dashboard.dashboard_id,
if (dashboards && dashboards.length > 0) {
return dashboards.map((dashboard) => ({
key: dashboard.dashboardId,
label: (
<Typography.Link
key={dashboard.dashboard_id}
key={dashboard.dashboardId}
onClick={(): void => {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboard_id,
dashboardId: dashboard.dashboardId,
}),
);
}}
className="dashboards-popover-content-item"
>
{dashboard.dashboard_name || dashboard.dashboard_id}
{dashboard.dashboardName || dashboard.dashboardId}
</Typography.Link>
),
}));
}
return null;
}, [uniqueDashboards, safeNavigate]);
}, [dashboards, safeNavigate]);
if (!dashboardsPopoverContent && !alertsPopoverContent) {
return null;
if (isLoadingAlerts || isLoadingDashboards) {
return (
<div className="dashboards-and-alerts-popover-container">
<Skeleton title={false} paragraph={{ rows: 1 }} active />
</div>
);
}
// If there are no dashboards or alerts or both have errors, don't show the popover
const hidePopover =
(!dashboardsPopoverContent && !alertsPopoverContent) ||
(isErrorAlerts && isErrorDashboards);
if (hidePopover) {
return <div className="dashboards-and-alerts-popover-container" />;
}
return (
@@ -92,8 +143,7 @@ function DashboardsAndAlertsPopover({
>
<Grid size={12} color={Color.BG_SIENNA_500} />
<Typography.Text>
{uniqueDashboards?.length} dashboard
{uniqueDashboards?.length === 1 ? '' : 's'}
{pluralize(dashboards.length, 'dashboard')}
</Typography.Text>
</div>
</Dropdown>
@@ -112,7 +162,7 @@ function DashboardsAndAlertsPopover({
>
<Bell size={12} color={Color.BG_SAKURA_500} />
<Typography.Text>
{alerts?.length} alert {alerts?.length === 1 ? 'rule' : 'rules'}
{pluralize(alerts.length, 'alert rule')}
</Typography.Text>
</div>
</Dropdown>

View File

@@ -0,0 +1,123 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton, Tooltip, Typography } from 'antd';
import { useGetMetricHighlights } from 'api/generated/services/metrics';
import { InfoIcon } from 'lucide-react';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { HighlightsProps } from './types';
import {
formatNumberToCompactFormat,
formatTimestampToReadableDate,
} from './utils';
function Highlights({ metricName }: HighlightsProps): JSX.Element {
const {
data: metricHighlightsData,
isLoading: isLoadingMetricHighlights,
isError: isErrorMetricHighlights,
refetch: refetchMetricHighlights,
} = useGetMetricHighlights(
{
metricName,
},
{
query: {
enabled: !!metricName,
},
},
);
const metricHighlights = metricHighlightsData?.data;
const timeSeriesActive = formatNumberToCompactFormat(
metricHighlights?.activeTimeSeries,
);
const timeSeriesTotal = formatNumberToCompactFormat(
metricHighlights?.totalTimeSeries,
);
const lastReceivedText = formatTimestampToReadableDate(
metricHighlights?.lastReceived,
);
if (isLoadingMetricHighlights) {
return (
<div
className="metric-details-content-grid"
data-testid="metric-highlights-loading-state"
>
<Skeleton title={false} paragraph={{ rows: 2 }} active />
</div>
);
}
if (isErrorMetricHighlights) {
return (
<div className="metric-details-content-grid">
<div
className="metric-highlights-error-state"
data-testid="metric-highlights-error-state"
>
<InfoIcon size={16} color={Color.BG_CHERRY_500} />
<Typography.Text>
Something went wrong while fetching metric highlights
</Typography.Text>
<Button
type="link"
size="large"
onClick={(): void => {
refetchMetricHighlights();
}}
>
Retry ?
</Button>
</div>
</div>
);
}
return (
<div className="metric-details-content-grid">
<div className="labels-row">
<Typography.Text type="secondary" className="metric-details-grid-label">
SAMPLES
</Typography.Text>
<Typography.Text type="secondary" className="metric-details-grid-label">
TIME SERIES
</Typography.Text>
<Typography.Text type="secondary" className="metric-details-grid-label">
LAST RECEIVED
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-data-points"
>
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
</Tooltip>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-time-series-total"
>
<Tooltip
title="Active time series are those that have received data points in the last 1
hour."
placement="top"
>
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
</Tooltip>
</Typography.Text>
<Typography.Text
className="metric-details-grid-value"
data-testid="metric-highlights-last-received"
>
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
</Typography.Text>
</div>
</div>
);
}
export default Highlights;

View File

@@ -1,45 +1,58 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button, Collapse, Input, Select, Typography } from 'antd';
import { Button, Collapse, Input, Select, Skeleton, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
import {
invalidateGetMetricMetadata,
invalidateListMetrics,
useUpdateMetricMetadata,
} from 'api/generated/services/metrics';
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { ResizeTable } from 'components/ResizeTable';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
import { DataType } from 'container/LogDetailedView/TableView';
import { useUpdateMetricMetadata } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { useNotifications } from 'hooks/useNotifications';
import { Edit2, Save, X } from 'lucide-react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { MetricTypeViewRenderer } from '../Summary/utils';
import {
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
} from '../Summary/constants';
import { MetricTypeRenderer } from '../Summary/utils';
import { METRIC_METADATA_KEYS } from './constants';
import { MetadataProps } from './types';
import { determineIsMonotonic } from './utils';
METRIC_METADATA_KEYS,
METRIC_METADATA_TEMPORALITY_OPTIONS,
METRIC_METADATA_TYPE_OPTIONS,
METRIC_METADATA_UPDATE_ERROR_MESSAGE,
} from './constants';
import MetricDetailsErrorState from './MetricDetailsErrorState';
import { MetadataProps, MetricMetadataFormState, TableFields } from './types';
import { transformUpdateMetricMetadataRequest } from './utils';
function Metadata({
metricName,
metadata,
refetchMetricDetails,
isErrorMetricMetadata,
isLoadingMetricMetadata,
refetchMetricMetadata,
}: MetadataProps): JSX.Element {
const [isEditing, setIsEditing] = useState(false);
const [
metricMetadata,
setMetricMetadata,
] = useState<UpdateMetricMetadataProps>({
metricType: metadata?.metric_type || MetricType.SUM,
description: metadata?.description || '',
temporality: metadata?.temporality,
unit: metadata?.unit,
metricMetadataState,
setMetricMetadataState,
] = useState<MetricMetadataFormState>({
type: MetrictypesTypeDTO.sum,
description: '',
temporality: MetrictypesTemporalityDTO.unspecified,
unit: '',
isMonotonic: false,
});
const { notifications } = useNotifications();
const {
@@ -51,110 +64,135 @@ function Metadata({
);
const queryClient = useQueryClient();
// Initialize state from metadata api data
useEffect(() => {
if (metadata) {
setMetricMetadataState({
type: metadata.type,
description: metadata.description,
temporality: metadata.temporality,
unit: metadata.unit,
isMonotonic: metadata.isMonotonic,
});
}
}, [metadata]);
const tableData = useMemo(
() =>
metadata
? Object.keys({
...metadata,
temporality: metadata?.temporality,
})
// Filter out monotonic as user input is not required
.filter((key) => key !== 'monotonic')
.map((key) => ({
? Object.keys(metadata).map((key) => ({
key,
value: {
value: metadata[key as keyof typeof metadata],
key,
value: {
value: metadata[key as keyof typeof metadata],
key,
},
}))
},
}))
: [],
[metadata],
);
// Render un-editable field value
const renderUneditableField = useCallback((key: string, value: string) => {
if (key === 'metric_type') {
return <MetricTypeRenderer type={value as MetricType} />;
}
let fieldValue = value;
if (key === 'unit') {
fieldValue = getUniversalNameFromMetricUnit(value);
}
return <FieldRenderer field={fieldValue || '-'} />;
}, []);
const renderUneditableField = useCallback(
(key: keyof MetricMetadataFormState, value: string) => {
if (isErrorMetricMetadata) {
return <FieldRenderer field="-" />;
}
if (key === TableFields.TYPE) {
return <MetricTypeViewRenderer type={value as MetrictypesTypeDTO} />;
}
if (key === TableFields.IS_MONOTONIC) {
return <FieldRenderer field={value ? 'Yes' : 'No'} />;
}
if (key === TableFields.Temporality) {
const temporality = METRIC_METADATA_TEMPORALITY_OPTIONS.find(
(option) => option.value === value,
);
return <FieldRenderer field={temporality?.label || '-'} />;
}
let fieldValue = value;
if (key === TableFields.UNIT) {
fieldValue = getUniversalNameFromMetricUnit(value);
}
return <FieldRenderer field={fieldValue || '-'} />;
},
[isErrorMetricMetadata],
);
const renderColumnValue = useCallback(
(field: { value: string; key: string }): JSX.Element => {
(field: {
value: string;
key: keyof MetricMetadataFormState;
}): JSX.Element => {
if (!isEditing) {
return renderUneditableField(field.key, field.value);
}
// Don't allow editing of unit if it's already set
const metricUnitAlreadySet = field.key === 'unit' && Boolean(metadata?.unit);
const metricUnitAlreadySet =
field.key === TableFields.UNIT && Boolean(metadata?.unit);
if (metricUnitAlreadySet) {
return renderUneditableField(field.key, field.value);
}
if (field.key === 'metric_type') {
// Monotonic is not editable
if (field.key === TableFields.IS_MONOTONIC) {
return renderUneditableField(field.key, field.value);
}
if (field.key === TableFields.TYPE) {
return (
<Select
data-testid="metric-type-select"
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
value: key,
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
}))}
value={metricMetadata.metricType}
options={METRIC_METADATA_TYPE_OPTIONS}
value={metricMetadataState.type}
onChange={(value): void => {
setMetricMetadata((prev) => ({
setMetricMetadataState((prev) => ({
...prev,
metricType: value as MetricType,
type: value,
}));
}}
/>
);
}
if (field.key === 'unit') {
if (field.key === TableFields.UNIT) {
return (
<YAxisUnitSelector
value={metricMetadata.unit}
value={metricMetadataState.unit}
onChange={(value): void => {
setMetricMetadata((prev) => ({ ...prev, unit: value }));
setMetricMetadataState((prev) => ({ ...prev, unit: value }));
}}
data-testid="unit-select"
source={YAxisSource.EXPLORER}
/>
);
}
if (field.key === 'temporality') {
if (field.key === TableFields.Temporality) {
const temporalityValue =
metricMetadataState.temporality === MetrictypesTemporalityDTO.unspecified
? undefined
: metricMetadataState.temporality;
return (
<Select
data-testid="temporality-select"
options={Object.values(Temporality).map((key) => ({
value: key,
label: key,
}))}
value={metricMetadata.temporality}
options={METRIC_METADATA_TEMPORALITY_OPTIONS}
value={temporalityValue}
onChange={(value): void => {
setMetricMetadata((prev) => ({
setMetricMetadataState((prev) => ({
...prev,
temporality: value as Temporality,
temporality: value,
}));
}}
/>
);
}
if (field.key === 'description') {
if (field.key === TableFields.DESCRIPTION) {
return (
<Input
data-testid="description-input"
name={field.key}
defaultValue={
metricMetadata[
field.key as Exclude<keyof UpdateMetricMetadataProps, 'isMonotonic'>
]
}
defaultValue={metricMetadataState.description}
onChange={(e): void => {
setMetricMetadata((prev) => ({
setMetricMetadataState((prev) => ({
...prev,
[field.key]: e.target.value,
}));
@@ -164,7 +202,7 @@ function Metadata({
}
return <FieldRenderer field="-" />;
},
[isEditing, metadata?.unit, metricMetadata, renderUneditableField],
[isEditing, metadata?.unit, metricMetadataState, renderUneditableField],
);
const columns: ColumnsType<DataType> = useMemo(
@@ -201,52 +239,61 @@ function Metadata({
const handleSave = useCallback(() => {
updateMetricMetadata(
{
metricName,
payload: {
...metricMetadata,
isMonotonic: determineIsMonotonic(
metricMetadata.metricType,
metricMetadata.temporality,
),
pathParams: {
metricName,
},
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
},
{
onSuccess: (response): void => {
if (response?.statusCode === 200) {
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
notifications.success({
message: 'Metadata updated successfully',
});
refetchMetricDetails();
setIsEditing(false);
queryClient.invalidateQueries(['metricsList']);
} else {
notifications.error({
message:
'Failed to update metadata, please try again. If the issue persists, please contact support.',
});
}
onSuccess: (): void => {
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
notifications.success({
message: 'Metadata updated successfully',
});
setIsEditing(false);
invalidateListMetrics(queryClient);
invalidateGetMetricMetadata(queryClient, {
metricName,
});
},
onError: (): void =>
onError: (error): void => {
const errorMessage = (error as AxiosError<RenderErrorResponseDTO>).response
?.data.error?.message;
notifications.error({
message:
'Failed to update metadata, please try again. If the issue persists, please contact support.',
}),
message: errorMessage || METRIC_METADATA_UPDATE_ERROR_MESSAGE,
});
},
},
);
}, [
updateMetricMetadata,
metricName,
metricMetadata,
metricMetadataState,
notifications,
refetchMetricDetails,
queryClient,
]);
const cancelEdit = useCallback(
(e: React.MouseEvent<HTMLElement, MouseEvent>): void => {
e.stopPropagation();
if (metadata) {
setMetricMetadataState({
type: metadata.type,
description: metadata.description,
unit: metadata.unit,
temporality: metadata.temporality,
isMonotonic: metadata.isMonotonic,
});
}
setIsEditing(false);
},
[metadata],
);
const actionButton = useMemo(() => {
if (isEditing) {
return (
@@ -254,10 +301,7 @@ function Metadata({
<Button
className="action-button"
type="text"
onClick={(e): void => {
e.stopPropagation();
setIsEditing(false);
}}
onClick={cancelEdit}
disabled={isUpdatingMetricsMetadata}
>
<X size={14} />
@@ -278,6 +322,9 @@ function Metadata({
</div>
);
}
if (isErrorMetricMetadata) {
return null;
}
return (
<div className="action-menu">
<Button
@@ -294,7 +341,13 @@ function Metadata({
</Button>
</div>
);
}, [handleSave, isEditing, isUpdatingMetricsMetadata]);
}, [
isEditing,
isErrorMetricMetadata,
isUpdatingMetricsMetadata,
cancelEdit,
handleSave,
]);
const items = useMemo(
() => [
@@ -306,7 +359,14 @@ function Metadata({
</div>
),
key: 'metric-metadata',
children: (
children: isErrorMetricMetadata ? (
<div className="metric-metadata-error-state">
<MetricDetailsErrorState
refetch={refetchMetricMetadata}
errorMessage="Something went wrong while fetching metric metadata"
/>
</div>
) : (
<ResizeTable
columns={columns}
tableLayout="fixed"
@@ -318,9 +378,23 @@ function Metadata({
),
},
],
[actionButton, columns, tableData],
[
actionButton,
columns,
isErrorMetricMetadata,
refetchMetricMetadata,
tableData,
],
);
if (isLoadingMetricMetadata) {
return (
<div className="metrics-metadata-skeleton-container">
<Skeleton active paragraph={{ rows: 8 }} />
</div>
);
}
return (
<Collapse
bordered

View File

@@ -38,7 +38,12 @@
flex-direction: column;
gap: 12px;
.metrics-metadata-error {
padding: 16px !important;
}
.metric-details-content-grid {
height: 50px;
.labels-row,
.values-row {
display: grid;
@@ -47,6 +52,18 @@
align-items: center;
}
.metric-highlights-error-state {
display: flex;
gap: 8px;
height: 100%;
width: 100%;
align-items: center;
.ant-btn {
padding: 2px 4px;
}
}
.labels-row {
margin-bottom: 8px;
@@ -72,6 +89,7 @@
.dashboards-and-alerts-popover-container {
display: flex;
gap: 16px;
height: 32px;
.dashboards-and-alerts-popover {
border-radius: 20px;
@@ -102,7 +120,19 @@
}
}
.metrics-metadata-skeleton-container {
height: 330px;
}
.all-attributes-skeleton-container {
height: 600px;
}
.metrics-accordion {
.all-attributes-error-state {
height: 300px;
}
.ant-table-body {
&::-webkit-scrollbar {
width: 2px;
@@ -148,7 +178,6 @@
.all-attributes-search-input {
width: 300px;
border: 1px solid var(--bg-slate-300);
}
}
@@ -161,6 +190,7 @@
.ant-typography:first-child {
font-family: 'Geist Mono';
color: var(--bg-robin-400);
background-color: transparent;
}
}
.all-attributes-contribution {
@@ -217,6 +247,10 @@
.ant-collapse-content-box {
padding: 0;
.metric-metadata-error-state {
height: 267px;
}
}
.ant-collapse-header {
@@ -237,6 +271,7 @@
}
.metric-metadata-value {
height: 67px;
background: rgba(22, 25, 34, 0.4);
overflow-x: scroll;
.field-renderer-container {
@@ -330,18 +365,26 @@
.metric-details-content {
.metrics-accordion {
.metrics-accordion-header {
.action-button {
.ant-typography {
color: var(--bg-slate-400);
.action-menu {
.action-button {
.ant-typography {
color: var(--bg-slate-400);
}
}
}
}
.metrics-accordion-content {
.metric-metadata-key {
.field-renderer-container {
.label {
color: var(--bg-slate-300);
}
}
.all-attributes-key {
.ant-typography:last-child {
color: var(--bg-slate-400);
color: var(--bg-vanilla-200);
background-color: var(--bg-robin-300);
}
}
@@ -395,3 +438,13 @@
}
}
}
.metric-details-error-state {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}

View File

@@ -1,16 +1,8 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import {
Button,
Divider,
Drawer,
Empty,
Skeleton,
Tooltip,
Typography,
} from 'antd';
import { Button, Divider, Drawer, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useGetMetricMetadata } from 'api/generated/services/metrics';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass, Crosshair, X } from 'lucide-react';
@@ -19,16 +11,12 @@ import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { isInspectEnabled } from '../Inspect/utils';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import AllAttributes from './AllAttributes';
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
import Highlights from './Highlights';
import Metadata from './Metadata';
import { MetricDetailsProps } from './types';
import {
formatNumberToCompactFormat,
formatTimestampToReadableDate,
getMetricDetailsQuery,
} from './utils';
import { getMetricDetailsQuery } from './utils';
import './MetricDetails.styles.scss';
import '../Summary/Summary.styles.scss';
@@ -43,55 +31,49 @@ function MetricDetails({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const {
data,
isLoading,
isFetching,
error: metricDetailsError,
refetch: refetchMetricDetails,
} = useGetMetricDetails(metricName ?? '', {
enabled: !!metricName,
});
const metric = data?.payload?.data;
const lastReceived = useMemo(() => {
if (!metric) {
return null;
}
return formatTimestampToReadableDate(metric.lastReceived);
}, [metric]);
const showInspectFeature = useMemo(
() => isInspectEnabled(metric?.metadata?.metric_type),
[metric],
data: metricMetadataResponse,
isLoading: isLoadingMetricMetadata,
isError: isErrorMetricMetadata,
refetch: refetchMetricMetadata,
} = useGetMetricMetadata(
{
metricName,
},
{
query: {
enabled: !!metricName,
},
},
);
const isMetricDetailsLoading = isLoading || isFetching;
const timeSeries = useMemo(() => {
if (!metric) {
const metadata = useMemo(() => {
if (!metricMetadataResponse) {
return null;
}
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
const {
type,
description,
unit,
temporality,
isMonotonic,
} = metricMetadataResponse.data;
return (
<Tooltip
title="Active time series are those that have received data points in the last 1
hour."
placement="top"
>
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
</Tooltip>
);
}, [metric]);
return {
type,
description,
unit,
temporality,
isMonotonic,
};
}, [metricMetadataResponse]);
const showInspectFeature = useMemo(() => isInspectEnabled(metadata?.type), [
metadata?.type,
]);
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
if (metricName) {
const compositeQuery = getMetricDetailsQuery(
metricName,
metric?.metadata?.metric_type,
);
const compositeQuery = getMetricDetailsQuery(metricName, metadata?.type);
handleExplorerTabChange(
PANEL_TYPES.TIME_SERIES,
{
@@ -107,9 +89,7 @@ function MetricDetails({
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
}
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
const isMetricDetailsError = metricDetailsError || !metric;
}, [metricName, handleExplorerTabChange, metadata?.type]);
useEffect(() => {
logEvent(MetricsExplorerEvents.ModalOpened, {
@@ -117,6 +97,9 @@ function MetricDetails({
});
}, []);
const isActionButtonDisabled =
!metricName || isLoadingMetricMetadata || isErrorMetricMetadata;
return (
<Drawer
width="60%"
@@ -124,13 +107,13 @@ function MetricDetails({
<div className="metric-details-header">
<div className="metric-details-title">
<Divider type="vertical" />
<Typography.Text>{metric?.name}</Typography.Text>
<Typography.Text>{metricName}</Typography.Text>
</div>
<div className="metric-details-header-buttons">
<Button
onClick={goToMetricsExplorerwithSelectedMetric}
icon={<Compass size={16} />}
disabled={!metricName}
disabled={isActionButtonDisabled}
data-testid="open-in-explorer-button"
>
Open in Explorer
@@ -140,10 +123,11 @@ function MetricDetails({
<Button
className="inspect-metrics-button"
aria-label="Inspect Metric"
disabled={isActionButtonDisabled}
icon={<Crosshair size={18} />}
onClick={(): void => {
if (metric?.name) {
openInspectModal(metric.name);
if (metricName) {
openInspectModal(metricName);
}
}}
data-testid="inspect-metric-button"
@@ -163,60 +147,18 @@ function MetricDetails({
destroyOnClose
closeIcon={<X size={16} />}
>
{isMetricDetailsLoading && (
<div data-testid="metric-details-skeleton">
<Skeleton active />
</div>
)}
{isMetricDetailsError && !isMetricDetailsLoading && (
<Empty description="Error fetching metric details" />
)}
{!isMetricDetailsLoading && !isMetricDetailsError && (
<div className="metric-details-content">
<div className="metric-details-content-grid">
<div className="labels-row">
<Typography.Text type="secondary" className="metric-details-grid-label">
SAMPLES
</Typography.Text>
<Typography.Text type="secondary" className="metric-details-grid-label">
TIME SERIES
</Typography.Text>
<Typography.Text type="secondary" className="metric-details-grid-label">
LAST RECEIVED
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="metric-details-grid-value">
<Tooltip title={metric?.samples.toLocaleString()}>
{formatNumberIntoHumanReadableFormat(metric?.samples)}
</Tooltip>
</Typography.Text>
<Typography.Text className="metric-details-grid-value">
<Tooltip title={timeSeries}>{timeSeries}</Tooltip>
</Typography.Text>
<Typography.Text className="metric-details-grid-value">
<Tooltip title={lastReceived}>{lastReceived}</Tooltip>
</Typography.Text>
</div>
</div>
<DashboardsAndAlertsPopover
dashboards={metric.dashboards}
alerts={metric.alerts}
/>
<Metadata
metricName={metric?.name}
metadata={metric.metadata}
refetchMetricDetails={refetchMetricDetails}
/>
{metric.attributes && (
<AllAttributes
metricName={metric?.name}
attributes={metric.attributes}
metricType={metric?.metadata?.metric_type}
/>
)}
</div>
)}
<div className="metric-details-content">
<Highlights metricName={metricName} />
<DashboardsAndAlertsPopover metricName={metricName} />
<Metadata
metricName={metricName}
metadata={metadata}
isErrorMetricMetadata={isErrorMetricMetadata}
isLoadingMetricMetadata={isLoadingMetricMetadata}
refetchMetricMetadata={refetchMetricMetadata}
/>
<AllAttributes metricName={metricName} metricType={metadata?.type} />
</div>
</Drawer>
);
}

View File

@@ -0,0 +1,20 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Typography } from 'antd';
import { InfoIcon } from 'lucide-react';
import { MetricDetailsErrorStateProps } from './types';
function MetricDetailsErrorState({
refetch,
errorMessage,
}: MetricDetailsErrorStateProps): JSX.Element {
return (
<div className="metric-details-error-state">
<InfoIcon size={20} color={Color.BG_CHERRY_500} />
<Typography.Text>{errorMessage}</Typography.Text>
{refetch && <Button onClick={refetch}>Retry</Button>}
</div>
);
}
export default MetricDetailsErrorState;

View File

@@ -1,11 +1,13 @@
import * as reactUseHooks from 'react-use';
import { fireEvent, render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { render, screen } from '@testing-library/react';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
import { userEvent } from 'tests/test-utils';
import { MetricDetailsAttribute } from '../../../../api/metricsExplorer/getMetricDetails';
import ROUTES from '../../../../constants/routes';
import AllAttributes, { AllAttributesValue } from '../AllAttributes';
import { getMockMetricAttributesData, MOCK_METRIC_NAME } from './testUtlls';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -20,33 +22,28 @@ jest
handleExplorerTabChange: mockHandleExplorerTabChange,
});
const mockMetricName = 'test-metric';
const mockMetricType = MetricType.GAUGE;
const mockAttributes: MetricDetailsAttribute[] = [
{
key: 'attribute1',
value: ['value1', 'value2'],
valueCount: 2,
},
{
key: 'attribute2',
value: ['value3'],
valueCount: 1,
},
];
const mockUseCopyToClipboard = jest.fn();
jest
.spyOn(reactUseHooks, 'useCopyToClipboard')
.mockReturnValue([{ value: 'value1' }, mockUseCopyToClipboard] as any);
const useGetMetricAttributesMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricAttributes',
);
describe('AllAttributes', () => {
beforeEach(() => {
useGetMetricAttributesMock.mockReturnValue({
...getMockMetricAttributesData(),
});
});
it('renders attributes section with title', () => {
render(
<AllAttributes
metricName={mockMetricName}
attributes={mockAttributes}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
@@ -56,9 +53,8 @@ describe('AllAttributes', () => {
it('renders all attribute keys and values', () => {
render(
<AllAttributes
metricName={mockMetricName}
attributes={mockAttributes}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
@@ -75,9 +71,8 @@ describe('AllAttributes', () => {
it('renders value counts correctly', () => {
render(
<AllAttributes
metricName={mockMetricName}
attributes={mockAttributes}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
@@ -86,41 +81,44 @@ describe('AllAttributes', () => {
});
it('handles empty attributes array', () => {
useGetMetricAttributesMock.mockReturnValue({
...getMockMetricAttributesData({
data: {
attributes: [],
totalKeys: 0,
},
}),
});
render(
<AllAttributes
metricName={mockMetricName}
attributes={[]}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
expect(screen.getByText('All Attributes')).toBeInTheDocument();
expect(screen.queryByText('No data')).toBeInTheDocument();
expect(screen.getByText('No attributes found')).toBeInTheDocument();
});
it('clicking on an attribute key opens the explorer with the attribute filter applied', () => {
it('clicking on an attribute key opens the explorer with the attribute filter applied', async () => {
render(
<AllAttributes
metricName={mockMetricName}
attributes={mockAttributes}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
fireEvent.click(screen.getByText('attribute1'));
await userEvent.click(screen.getByText('attribute1'));
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
});
it('filters attributes based on search input', () => {
it('filters attributes based on search input', async () => {
render(
<AllAttributes
metricName={mockMetricName}
attributes={mockAttributes}
metricType={mockMetricType}
metricName={MOCK_METRIC_NAME}
metricType={MetrictypesTypeDTO.gauge}
/>,
);
fireEvent.change(screen.getByPlaceholderText('Search'), {
target: { value: 'value1' },
});
await userEvent.type(screen.getByPlaceholderText('Search'), 'value1');
expect(screen.getByText('attribute1')).toBeInTheDocument();
expect(screen.getByText('value1')).toBeInTheDocument();
@@ -144,7 +142,7 @@ describe('AllAttributesValue', () => {
expect(screen.getByText('value2')).toBeInTheDocument();
});
it('loads more attributes when show more button is clicked', () => {
it('loads more attributes when show more button is clicked', async () => {
render(
<AllAttributesValue
filterKey="attribute1"
@@ -155,7 +153,7 @@ describe('AllAttributesValue', () => {
/>,
);
expect(screen.queryByText('value6')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Show More'));
await userEvent.click(screen.getByText('Show More'));
expect(screen.getByText('value6')).toBeInTheDocument();
});
@@ -172,7 +170,7 @@ describe('AllAttributesValue', () => {
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
});
it('copy button should copy the attribute value to the clipboard', () => {
it('copy button should copy the attribute value to the clipboard', async () => {
render(
<AllAttributesValue
filterKey="attribute1"
@@ -183,13 +181,13 @@ describe('AllAttributesValue', () => {
/>,
);
expect(screen.getByText('value1')).toBeInTheDocument();
fireEvent.click(screen.getByText('value1'));
await userEvent.click(screen.getByText('value1'));
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
fireEvent.click(screen.getByText('Copy Attribute'));
await userEvent.click(screen.getByText('Copy Attribute'));
expect(mockUseCopyToClipboard).toHaveBeenCalledWith('value1');
});
it('explorer button should go to metrics explore with the attribute filter applied', () => {
it('explorer button should go to metrics explore with the attribute filter applied', async () => {
render(
<AllAttributesValue
filterKey="attribute1"
@@ -200,10 +198,10 @@ describe('AllAttributesValue', () => {
/>,
);
expect(screen.getByText('value1')).toBeInTheDocument();
fireEvent.click(screen.getByText('value1'));
await userEvent.click(screen.getByText('value1'));
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
fireEvent.click(screen.getByText('Open in Explorer'));
await userEvent.click(screen.getByText('Open in Explorer'));
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
'attribute1',
'value1',

View File

@@ -1,26 +1,18 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import { QueryParams } from 'constants/query';
import { userEvent } from 'tests/test-utils';
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
const mockAlert1 = {
alert_id: '1',
alert_name: 'Alert 1',
};
const mockAlert2 = {
alert_id: '2',
alert_name: 'Alert 2',
};
const mockDashboard1 = {
dashboard_id: '1',
dashboard_name: 'Dashboard 1',
};
const mockDashboard2 = {
dashboard_id: '2',
dashboard_name: 'Dashboard 2',
};
const mockAlerts = [mockAlert1, mockAlert2];
const mockDashboards = [mockDashboard1, mockDashboard2];
import {
getMockAlertsData,
getMockDashboardsData,
MOCK_ALERT_1,
MOCK_ALERT_2,
MOCK_DASHBOARD_1,
MOCK_DASHBOARD_2,
MOCK_METRIC_NAME,
} from './testUtlls';
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
@@ -28,7 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
safeNavigate: mockSafeNavigate,
}),
}));
const mockSetQuery = jest.fn();
const mockUrlQuery = {
set: mockSetQuery,
@@ -39,125 +30,156 @@ jest.mock('hooks/useUrlQuery', () => ({
default: jest.fn(() => mockUrlQuery),
}));
describe('DashboardsAndAlertsPopover', () => {
it('renders the popover correctly with multiple dashboards and alerts', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
const useGetMetricAlertsMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricAlerts',
);
const useGetMetricDashboardsMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricDashboards',
);
expect(
screen.getByText(`${mockDashboards.length} dashboards`),
).toBeInTheDocument();
expect(
screen.getByText(`${mockAlerts.length} alert rules`),
).toBeInTheDocument();
describe('DashboardsAndAlertsPopover', () => {
beforeEach(() => {
useGetMetricAlertsMock.mockReturnValue(getMockAlertsData());
useGetMetricDashboardsMock.mockReturnValue(getMockDashboardsData());
});
it('renders the popover correctly with multiple dashboards and alerts', () => {
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
expect(screen.getByText(`2 dashboards`)).toBeInTheDocument();
expect(screen.getByText(`2 alert rules`)).toBeInTheDocument();
});
it('renders null with no dashboards and alerts', () => {
const { container } = render(
<DashboardsAndAlertsPopover alerts={[]} dashboards={[]} />,
useGetMetricAlertsMock.mockReturnValue(
getMockAlertsData({
data: {
alerts: [],
},
}),
);
expect(container).toBeEmptyDOMElement();
useGetMetricDashboardsMock.mockReturnValue(
getMockDashboardsData({
data: {
dashboards: [],
},
}),
);
const { container } = render(
<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />,
);
expect(
container.querySelector('dashboards-and-alerts-popover-container'),
).toBeNull();
});
it('renders popover with single dashboard and alert', () => {
render(
<DashboardsAndAlertsPopover
alerts={[mockAlert1]}
dashboards={[mockDashboard1]}
/>,
useGetMetricAlertsMock.mockReturnValue(
getMockAlertsData({
data: {
alerts: [MOCK_ALERT_1],
},
}),
);
useGetMetricDashboardsMock.mockReturnValue(
getMockDashboardsData({
data: {
dashboards: [MOCK_DASHBOARD_1],
},
}),
);
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
expect(screen.getByText(`1 alert rule`)).toBeInTheDocument();
});
it('renders popover with dashboard id if name is not available', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={[{ ...mockDashboard1, dashboard_name: undefined } as any]}
/>,
it('renders popover with dashboard id if name is not available', async () => {
useGetMetricDashboardsMock.mockReturnValue(
getMockDashboardsData({
data: {
dashboards: [{ ...MOCK_DASHBOARD_1, dashboardName: '' }],
},
}),
);
fireEvent.click(screen.getByText(`1 dashboard`));
expect(screen.getByText(mockDashboard1.dashboard_id)).toBeInTheDocument();
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
await userEvent.click(screen.getByText(`1 dashboard`));
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardId)).toBeInTheDocument();
});
it('renders popover with alert id if name is not available', () => {
render(
<DashboardsAndAlertsPopover
alerts={[{ ...mockAlert1, alert_name: undefined } as any]}
dashboards={mockDashboards}
/>,
it('renders popover with alert id if name is not available', async () => {
useGetMetricAlertsMock.mockReturnValue(
getMockAlertsData({
data: {
alerts: [{ ...MOCK_ALERT_1, alertName: '' }],
},
}),
);
fireEvent.click(screen.getByText(`1 alert rule`));
expect(screen.getByText(mockAlert1.alert_id)).toBeInTheDocument();
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
await userEvent.click(screen.getByText(`1 alert rule`));
expect(screen.getByText(MOCK_ALERT_1.alertId)).toBeInTheDocument();
});
it('navigates to the dashboard when the dashboard is clicked', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
it('navigates to the dashboard when the dashboard is clicked', async () => {
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
// Click on 2 dashboards button
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
await userEvent.click(screen.getByText(`2 dashboards`));
// Popover showing list of 2 dashboards should be visible
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
// Click on the first dashboard
fireEvent.click(screen.getByText(mockDashboard1.dashboard_name));
await userEvent.click(screen.getByText(MOCK_DASHBOARD_1.dashboardName));
// Should navigate to the dashboard
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${mockDashboard1.dashboard_id}`,
`/dashboard/${MOCK_DASHBOARD_1.dashboardId}`,
);
});
it('navigates to the alert when the alert is clicked', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={mockDashboards}
/>,
);
it('navigates to the alert when the alert is clicked', async () => {
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
// Click on 2 alert rules button
fireEvent.click(screen.getByText(`${mockAlerts.length} alert rules`));
await userEvent.click(screen.getByText(`2 alert rules`));
// Popover showing list of 2 alert rules should be visible
expect(screen.getByText(mockAlert1.alert_name)).toBeInTheDocument();
expect(screen.getByText(mockAlert2.alert_name)).toBeInTheDocument();
expect(screen.getByText(MOCK_ALERT_1.alertName)).toBeInTheDocument();
expect(screen.getByText(MOCK_ALERT_2.alertName)).toBeInTheDocument();
// Click on the first alert rule
fireEvent.click(screen.getByText(mockAlert1.alert_name));
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
// Should navigate to the alert rule
expect(mockSetQuery).toHaveBeenCalledWith(
QueryParams.ruleId,
mockAlert1.alert_id,
MOCK_ALERT_1.alertId,
);
});
it('renders unique dashboards even when there are duplicates', () => {
render(
<DashboardsAndAlertsPopover
alerts={mockAlerts}
dashboards={[...mockDashboards, mockDashboard1]}
/>,
it('renders unique dashboards even when there are duplicates', async () => {
useGetMetricDashboardsMock.mockReturnValue(
getMockDashboardsData({
data: {
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2, MOCK_DASHBOARD_1],
},
}),
);
expect(
screen.getByText(`${mockDashboards.length} dashboards`),
).toBeInTheDocument();
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
expect(screen.getByText('2 dashboards')).toBeInTheDocument();
await userEvent.click(screen.getByText('2 dashboards'));
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,67 @@
import { render, screen } from '@testing-library/react';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import Highlights from '../Highlights';
import { formatTimestampToReadableDate } from '../utils';
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
const useGetMetricHighlightsMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricHighlights',
);
describe('Highlights', () => {
beforeEach(() => {
useGetMetricHighlightsMock.mockReturnValue(getMockMetricHighlightsData());
});
it('should render all highlights data correctly', () => {
render(<Highlights metricName={MOCK_METRIC_NAME} />);
const dataPoints = screen.getByTestId('metric-highlights-data-points');
const timeSeriesTotal = screen.getByTestId(
'metric-highlights-time-series-total',
);
const lastReceived = screen.getByTestId('metric-highlights-last-received');
expect(dataPoints.textContent).toBe('1M+');
expect(timeSeriesTotal.textContent).toBe('1M total ⎯ 1M active');
expect(lastReceived.textContent).toBe(
formatTimestampToReadableDate('2026-01-24T00:00:00Z'),
);
});
it('should render error state correctly', () => {
useGetMetricHighlightsMock.mockReturnValue(
getMockMetricHighlightsData(
{},
{
isError: true,
},
),
);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
expect(
screen.getByTestId('metric-highlights-error-state'),
).toBeInTheDocument();
});
it('should render loading state when data is loading', () => {
useGetMetricHighlightsMock.mockReturnValue(
getMockMetricHighlightsData(
{},
{
isLoading: true,
},
),
);
render(<Highlights metricName={MOCK_METRIC_NAME} />);
expect(
screen.getByTestId('metric-highlights-loading-state'),
).toBeInTheDocument();
});
});

View File

@@ -1,16 +1,24 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import {
GetMetricMetadata200,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
UniversalYAxisUnit,
YAxisUnitSelectorProps,
} from 'components/YAxisUnitSelector/types';
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import * as useNotificationsHooks from 'hooks/useNotifications';
import { userEvent } from 'tests/test-utils';
import { SelectOption } from 'types/common/select';
import Metadata from '../Metadata';
import { MetricMetadata } from '../types';
import { transformMetricMetadata } from '../utils';
import { getMockMetricMetadataData, MOCK_METRIC_NAME } from './testUtlls';
// Mock antd select for testing
jest.mock('antd', () => ({
@@ -72,13 +80,18 @@ jest.mock('react-query', () => ({
}),
}));
const mockUseUpdateMetricMetadataHook = jest.spyOn(
metricsExplorerHooks,
'useUpdateMetricMetadata',
);
type UseUpdateMetricMetadataResult = ReturnType<
typeof metricsExplorerHooks.useUpdateMetricMetadata
>;
const mockUseUpdateMetricMetadata = jest.fn();
jest
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
.mockReturnValue({
mutate: mockUseUpdateMetricMetadata,
isLoading: false,
} as any);
const mockMetricMetadata = transformMetricMetadata(
getMockMetricMetadataData().data as GetMetricMetadata200,
) as MetricMetadata;
const mockErrorNotification = jest.fn();
const mockSuccessNotification = jest.fn();
@@ -89,47 +102,50 @@ jest.spyOn(useNotificationsHooks, 'useNotifications').mockReturnValue({
},
} as any);
const mockMetricName = 'test_metric';
const mockMetricMetadata = {
metric_type: MetricType.GAUGE,
description: 'test_description',
unit: 'test_unit',
temporality: Temporality.DELTA,
};
const mockRefetchMetricDetails = jest.fn();
const mockRefetchMetricMetadata = jest.fn();
describe('Metadata', () => {
beforeEach(() => {
mockUseUpdateMetricMetadataHook.mockReturnValue(({
mutate: mockUseUpdateMetricMetadata,
} as Partial<UseUpdateMetricMetadataResult>) as UseUpdateMetricMetadataResult);
});
it('should render the metadata properly', () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
expect(screen.getByText('Metric Type')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.metric_type)).toBeInTheDocument();
expect(screen.getByText('Gauge')).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.description)).toBeInTheDocument();
expect(screen.getByText('Unit')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.unit)).toBeInTheDocument();
expect(screen.getByText('Temporality')).toBeInTheDocument();
expect(screen.getByText(mockMetricMetadata.temporality)).toBeInTheDocument();
expect(screen.getByText('Delta')).toBeInTheDocument();
});
it('editing the metadata should show the form inputs', () => {
it('editing the metadata should show the form inputs', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
await userEvent.click(editButton);
expect(screen.getByTestId('metric-type-select')).toBeInTheDocument();
expect(screen.getByTestId('temporality-select')).toBeInTheDocument();
@@ -139,57 +155,53 @@ describe('Metadata', () => {
it('should update the metadata when the form is submitted', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={{
...mockMetricMetadata,
unit: '',
}}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
await userEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
expect(metricDescriptionInput).toBeInTheDocument();
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
await userEvent.clear(metricDescriptionInput);
await userEvent.type(metricDescriptionInput, 'Updated description');
const metricTypeSelect = screen.getByTestId('metric-type-select');
expect(metricTypeSelect).toBeInTheDocument();
fireEvent.change(metricTypeSelect, {
target: { value: MetricType.SUM },
});
await userEvent.selectOptions(metricTypeSelect, MetrictypesTypeDTO.sum);
const temporalitySelect = screen.getByTestId('temporality-select');
expect(temporalitySelect).toBeInTheDocument();
fireEvent.change(temporalitySelect, {
target: { value: Temporality.CUMULATIVE },
});
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
const unitSelect = screen.getByTestId('unit-select');
expect(unitSelect).toBeInTheDocument();
fireEvent.change(unitSelect, {
target: { value: 'By' },
});
await userEvent.selectOptions(unitSelect, 'By');
const saveButton = screen.getByText('Save');
expect(saveButton).toBeInTheDocument();
fireEvent.click(saveButton);
await userEvent.click(saveButton);
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
expect.objectContaining({
metricName: mockMetricName,
payload: expect.objectContaining({
description: 'Updated description',
metricType: MetricType.SUM,
temporality: Temporality.CUMULATIVE,
data: expect.objectContaining({
type: MetrictypesTypeDTO.sum,
temporality: MetrictypesTemporalityDTO.cumulative,
unit: 'By',
isMonotonic: true,
}),
pathParams: {
metricName: MOCK_METRIC_NAME,
},
}),
expect.objectContaining({
onSuccess: expect.any(Function),
@@ -201,56 +213,56 @@ describe('Metadata', () => {
it('should show success notification when metadata is updated successfully', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
await userEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
await userEvent.clear(metricDescriptionInput);
await userEvent.type(metricDescriptionInput, 'Updated description');
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
await userEvent.click(saveButton);
const onSuccessCallback =
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
onSuccessCallback({ statusCode: 200 });
onSuccessCallback({ status: 200 });
expect(mockSuccessNotification).toHaveBeenCalledWith({
message: 'Metadata updated successfully',
});
expect(mockRefetchMetricDetails).toHaveBeenCalled();
});
it('should show error notification when metadata update fails with non-200 response', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
await userEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
await userEvent.clear(metricDescriptionInput);
await userEvent.type(metricDescriptionInput, 'Updated description');
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
await userEvent.click(saveButton);
const onSuccessCallback =
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
onSuccessCallback({ statusCode: 500 });
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
onErrorCallback({ status: 500 });
expect(mockErrorNotification).toHaveBeenCalledWith({
message:
@@ -261,22 +273,23 @@ describe('Metadata', () => {
it('should show error notification when metadata update fails', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
await userEvent.click(editButton);
const metricDescriptionInput = screen.getByTestId('description-input');
fireEvent.change(metricDescriptionInput, {
target: { value: 'Updated description' },
});
await userEvent.clear(metricDescriptionInput);
await userEvent.type(metricDescriptionInput, 'Updated description');
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
await userEvent.click(saveButton);
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
@@ -289,39 +302,43 @@ describe('Metadata', () => {
});
});
it('cancel button should cancel the edit mode', () => {
it('cancel button should cancel the edit mode', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
await userEvent.click(editButton);
const cancelButton = screen.getByText('Cancel');
expect(cancelButton).toBeInTheDocument();
fireEvent.click(cancelButton);
await userEvent.click(cancelButton);
const editButton2 = screen.getByText('Edit');
expect(editButton2).toBeInTheDocument();
});
it('should not allow editing of unit if it is already set', () => {
it('should not allow editing of unit if it is already set', async () => {
render(
<Metadata
metricName={mockMetricName}
metricName={MOCK_METRIC_NAME}
metadata={mockMetricMetadata}
refetchMetricDetails={mockRefetchMetricDetails}
isErrorMetricMetadata={false}
isLoadingMetricMetadata={false}
refetchMetricMetadata={mockRefetchMetricMetadata}
/>,
);
const editButton = screen.getByText('Edit');
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
await userEvent.click(editButton);
const unitSelect = screen.queryByTestId('unit-select');
expect(unitSelect).not.toBeInTheDocument();

View File

@@ -1,68 +1,16 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { MetricDetails as MetricDetailsType } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
import { render, screen } from '@testing-library/react';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import ROUTES from 'constants/routes';
import * as useGetMetricDetails from 'hooks/metricsExplorer/useGetMetricDetails';
import * as useUpdateMetricMetadata from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
import { userEvent } from 'tests/test-utils';
import MetricDetails from '../MetricDetails';
import { getMockMetricMetadataData } from './testUtlls';
const mockMetricName = 'test-metric';
const mockMetricDescription = 'description for a test metric';
const mockMetricData: MetricDetailsType = {
name: mockMetricName,
description: mockMetricDescription,
unit: 'count',
attributes: [
{
key: 'test-attribute',
value: ['test-value'],
valueCount: 1,
},
],
alerts: [],
dashboards: [],
metadata: {
metric_type: MetricType.SUM,
description: mockMetricDescription,
unit: 'count',
},
type: '',
timeseries: 0,
samples: 0,
timeSeriesTotal: 0,
timeSeriesActive: 0,
lastReceived: '',
};
const mockOpenInspectModal = jest.fn();
const mockOnClose = jest.fn();
const mockUseGetMetricDetailsData = {
data: {
payload: {
data: mockMetricData,
},
},
isLoading: false,
isFetching: false,
isError: false,
error: null,
refetch: jest.fn(),
};
jest
.spyOn(useGetMetricDetails, 'useGetMetricDetails')
.mockReturnValue(mockUseGetMetricDetailsData as any);
jest.spyOn(useUpdateMetricMetadata, 'useUpdateMetricMetadata').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
isError: false,
error: null,
} as any);
const mockHandleExplorerTabChange = jest.fn();
jest
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
@@ -88,7 +36,50 @@ jest.mock('react-query', () => ({
}),
}));
jest.mock(
'container/MetricsExplorer/MetricDetails/AllAttributes',
() =>
function MockAllAttributes(): JSX.Element {
return <div data-testid="all-attributes">All Attributes</div>;
},
);
jest.mock(
'container/MetricsExplorer/MetricDetails/DashboardsAndAlertsPopover',
() =>
function MockDashboardsAndAlertsPopover(): JSX.Element {
return (
<div data-testid="dashboards-and-alerts-popover">
Dashboards and Alerts Popover
</div>
);
},
);
jest.mock(
'container/MetricsExplorer/MetricDetails/Highlights',
() =>
function MockHighlights(): JSX.Element {
return <div data-testid="highlights">Highlights</div>;
},
);
jest.mock(
'container/MetricsExplorer/MetricDetails/Metadata',
() =>
function MockMetadata(): JSX.Element {
return <div data-testid="metadata">Metadata</div>;
},
);
const useGetMetricMetadataMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricMetadata',
);
describe('MetricDetails', () => {
beforeEach(() => {
useGetMetricMetadataMock.mockReturnValue(getMockMetricMetadataData());
});
it('renders metric details correctly', () => {
render(
<MetricDetails
@@ -101,27 +92,15 @@ describe('MetricDetails', () => {
);
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
expect(screen.getByText(mockMetricDescription)).toBeInTheDocument();
expect(screen.getByTestId('all-attributes')).toBeInTheDocument();
expect(
screen.getByText(getUniversalNameFromMetricUnit(mockMetricData.unit)),
screen.getByTestId('dashboards-and-alerts-popover'),
).toBeInTheDocument();
expect(screen.getByTestId('highlights')).toBeInTheDocument();
expect(screen.getByTestId('metadata')).toBeInTheDocument();
});
it('renders the "open in explorer" and "inspect" buttons', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValueOnce({
...mockUseGetMetricDetailsData,
data: {
payload: {
data: {
...mockMetricData,
metadata: {
...mockMetricData.metadata,
metric_type: MetricType.GAUGE,
},
},
},
},
} as any);
it('renders the "open in explorer" and "inspect" buttons', async () => {
render(
<MetricDetails
onClose={mockOnClose}
@@ -135,93 +114,10 @@ describe('MetricDetails', () => {
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
expect(screen.getByTestId('inspect-metric-button')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('open-in-explorer-button'));
await userEvent.click(screen.getByTestId('open-in-explorer-button'));
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('inspect-metric-button'));
await userEvent.click(screen.getByTestId('inspect-metric-button'));
expect(mockOpenInspectModal).toHaveBeenCalled();
});
it('should render error state when metric details are not found', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
...mockUseGetMetricDetailsData,
isError: true,
error: {
message: 'Error fetching metric details',
},
} as any);
render(
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
isModalTimeSelection
openInspectModal={mockOpenInspectModal}
/>,
);
expect(screen.getByText('Error fetching metric details')).toBeInTheDocument();
});
it('should render loading state when metric details are loading', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
...mockUseGetMetricDetailsData,
isLoading: true,
} as any);
render(
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
isModalTimeSelection
openInspectModal={mockOpenInspectModal}
/>,
);
expect(screen.getByTestId('metric-details-skeleton')).toBeInTheDocument();
});
it('should render all attributes section', () => {
jest
.spyOn(useGetMetricDetails, 'useGetMetricDetails')
.mockReturnValue(mockUseGetMetricDetailsData as any);
render(
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
isModalTimeSelection
openInspectModal={mockOpenInspectModal}
/>,
);
expect(screen.getByText('All Attributes')).toBeInTheDocument();
});
it('should not render all attributes section when relevant data is not present', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
...mockUseGetMetricDetailsData,
data: {
payload: {
data: {
...mockMetricData,
attributes: null,
},
},
},
} as any);
render(
<MetricDetails
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
isModalTimeSelection
openInspectModal={mockOpenInspectModal}
/>,
);
expect(screen.queryByText('All Attributes')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,168 @@
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import {
GetMetricAlerts200,
GetMetricAttributes200,
GetMetricDashboards200,
GetMetricHighlights200,
GetMetricMetadata200,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
export const MOCK_METRIC_NAME = 'test-metric';
export function getMockMetricHighlightsData(
overrides?: Partial<GetMetricHighlights200>,
{
isLoading = false,
isError = false,
}: {
isLoading?: boolean;
isError?: boolean;
} = {},
): ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights> {
return {
data: {
data: {
dataPoints: 1000000,
lastReceived: '2026-01-24T00:00:00Z',
totalTimeSeries: 1000000,
activeTimeSeries: 1000000,
},
status: 'success',
...overrides,
},
isLoading,
isError,
} as ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights>;
}
export const MOCK_DASHBOARD_1 = {
dashboardName: 'Dashboard 1',
dashboardId: '1',
widgetId: '1',
widgetName: 'Widget 1',
};
export const MOCK_DASHBOARD_2 = {
dashboardName: 'Dashboard 2',
dashboardId: '2',
widgetId: '2',
widgetName: 'Widget 2',
};
export const MOCK_ALERT_1 = {
alertName: 'Alert 1',
alertId: '1',
};
export const MOCK_ALERT_2 = {
alertName: 'Alert 2',
alertId: '2',
};
export function getMockDashboardsData(
overrides?: Partial<GetMetricDashboards200>,
{
isLoading = false,
isError = false,
}: {
isLoading?: boolean;
isError?: boolean;
} = {},
): ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards> {
return {
data: {
data: {
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2],
},
status: 'success',
...overrides,
},
isLoading,
isError,
} as ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards>;
}
export function getMockAlertsData(
overrides?: Partial<GetMetricAlerts200>,
{
isLoading = false,
isError = false,
}: {
isLoading?: boolean;
isError?: boolean;
} = {},
): ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts> {
return {
data: {
data: {
alerts: [MOCK_ALERT_1, MOCK_ALERT_2],
},
status: 'success',
...overrides,
},
isLoading,
isError,
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts>;
}
export function getMockMetricAttributesData(
overrides?: Partial<GetMetricAttributes200>,
{
isLoading = false,
isError = false,
}: {
isLoading?: boolean;
isError?: boolean;
} = {},
): ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes> {
return {
data: {
data: {
attributes: [
{
key: 'attribute1',
values: ['value1', 'value2'],
valueCount: 2,
},
{
key: 'attribute2',
values: ['value3'],
valueCount: 1,
},
],
totalKeys: 2,
},
status: 'success',
...overrides,
},
isLoading,
isError,
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes>;
}
export function getMockMetricMetadataData(
overrides?: Partial<GetMetricMetadata200>,
{
isLoading = false,
isError = false,
}: {
isLoading?: boolean;
isError?: boolean;
} = {},
): ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata> {
return {
data: {
data: {
description: 'test_description',
type: MetrictypesTypeDTO.gauge,
unit: 'test_unit',
temporality: MetrictypesTemporalityDTO.delta,
isMonotonic: false,
},
status: 'success',
...overrides,
},
isLoading,
isError,
} as ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata>;
}

View File

@@ -1,5 +1,7 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
determineIsMonotonic,
@@ -10,35 +12,48 @@ import {
describe('MetricDetails utils', () => {
describe('determineIsMonotonic', () => {
it('should return true for histogram metrics', () => {
expect(determineIsMonotonic(MetricType.HISTOGRAM)).toBe(true);
expect(determineIsMonotonic(MetrictypesTypeDTO.histogram)).toBe(true);
});
it('should return true for exponential histogram metrics', () => {
expect(determineIsMonotonic(MetricType.EXPONENTIAL_HISTOGRAM)).toBe(true);
});
it('should return false for gauge metrics', () => {
expect(determineIsMonotonic(MetricType.GAUGE)).toBe(false);
});
it('should return false for summary metrics', () => {
expect(determineIsMonotonic(MetricType.SUMMARY)).toBe(false);
});
it('should return true for sum metrics with cumulative temporality', () => {
expect(determineIsMonotonic(MetricType.SUM, Temporality.CUMULATIVE)).toBe(
expect(determineIsMonotonic(MetrictypesTypeDTO.exponentialhistogram)).toBe(
true,
);
});
it('should return false for gauge metrics', () => {
expect(determineIsMonotonic(MetrictypesTypeDTO.gauge)).toBe(false);
});
it('should return false for summary metrics', () => {
expect(determineIsMonotonic(MetrictypesTypeDTO.summary)).toBe(false);
});
it('should return true for sum metrics with cumulative temporality', () => {
expect(
determineIsMonotonic(
MetrictypesTypeDTO.sum,
MetrictypesTemporalityDTO.cumulative,
),
).toBe(true);
});
it('should return false for sum metrics with delta temporality', () => {
expect(determineIsMonotonic(MetricType.SUM, Temporality.DELTA)).toBe(false);
expect(
determineIsMonotonic(
MetrictypesTypeDTO.sum,
MetrictypesTemporalityDTO.delta,
),
).toBe(false);
});
it('should return false by default', () => {
expect(determineIsMonotonic('' as MetricType, '' as Temporality)).toBe(
false,
);
expect(
determineIsMonotonic(
'' as MetrictypesTypeDTO,
'' as MetrictypesTemporalityDTO,
),
).toBe(false);
});
});
@@ -115,13 +130,16 @@ describe('MetricDetails utils', () => {
const API_GATEWAY = 'api-gateway';
it('should create correct query for SUM metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetrictypesTypeDTO.sum,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
MetricType.SUM,
MetrictypesTypeDTO.sum,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
@@ -129,13 +147,16 @@ describe('MetricDetails utils', () => {
});
it('should create correct query for GAUGE metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.GAUGE);
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetrictypesTypeDTO.gauge,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
MetricType.GAUGE,
MetrictypesTypeDTO.gauge,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
@@ -143,13 +164,16 @@ describe('MetricDetails utils', () => {
});
it('should create correct query for SUMMARY metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUMMARY);
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetrictypesTypeDTO.summary,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
MetricType.SUMMARY,
MetrictypesTypeDTO.summary,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
@@ -157,13 +181,16 @@ describe('MetricDetails utils', () => {
});
it('should create correct query for HISTOGRAM metric type', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.HISTOGRAM);
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetrictypesTypeDTO.histogram,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
MetricType.HISTOGRAM,
MetrictypesTypeDTO.histogram,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
@@ -173,14 +200,14 @@ describe('MetricDetails utils', () => {
it('should create correct query for EXPONENTIAL_HISTOGRAM metric type', () => {
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.EXPONENTIAL_HISTOGRAM,
MetrictypesTypeDTO.exponentialhistogram,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
TEST_METRIC_NAME,
);
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
MetricType.EXPONENTIAL_HISTOGRAM,
MetrictypesTypeDTO.exponentialhistogram,
);
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
@@ -203,7 +230,7 @@ describe('MetricDetails utils', () => {
const filter = { key: 'service', value: API_GATEWAY };
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
MetrictypesTypeDTO.sum,
filter,
);
@@ -221,7 +248,7 @@ describe('MetricDetails utils', () => {
const groupBy = 'service';
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
MetrictypesTypeDTO.sum,
undefined,
groupBy,
);
@@ -236,7 +263,7 @@ describe('MetricDetails utils', () => {
const groupBy = 'endpoint';
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetricType.SUM,
MetrictypesTypeDTO.sum,
filter,
groupBy,
);
@@ -250,7 +277,10 @@ describe('MetricDetails utils', () => {
});
it('should not include filters or groupBy when not provided', () => {
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
const query = getMetricDetailsQuery(
TEST_METRIC_NAME,
MetrictypesTypeDTO.sum,
);
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);

View File

@@ -1,6 +1,55 @@
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
export const METRIC_METADATA_KEYS = {
description: 'Description',
unit: 'Unit',
metric_type: 'Metric Type',
type: 'Metric Type',
temporality: 'Temporality',
isMonotonic: 'Monotonic',
};
export const METRIC_METADATA_TEMPORALITY_OPTIONS: Array<{
value: MetrictypesTemporalityDTO;
label: string;
}> = [
{
value: MetrictypesTemporalityDTO.delta,
label: 'Delta',
},
{
value: MetrictypesTemporalityDTO.cumulative,
label: 'Cumulative',
},
];
export const METRIC_METADATA_TYPE_OPTIONS: Array<{
value: MetrictypesTypeDTO;
label: string;
}> = [
{
value: MetrictypesTypeDTO.sum,
label: 'Sum',
},
{
value: MetrictypesTypeDTO.gauge,
label: 'Gauge',
},
{
value: MetrictypesTypeDTO.histogram,
label: 'Histogram',
},
{
value: MetrictypesTypeDTO.summary,
label: 'Summary',
},
{
value: MetrictypesTypeDTO.exponentialhistogram,
label: 'Exponential Histogram',
},
];
export const METRIC_METADATA_UPDATE_ERROR_MESSAGE =
'Failed to update metadata, please try again. If the issue persists, please contact support.';

View File

@@ -1,34 +1,39 @@
import {
MetricDetails,
MetricDetailsAlert,
MetricDetailsAttribute,
MetricDetailsDashboard,
} from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
MetricsexplorertypesMetricAlertDTO,
MetricsexplorertypesMetricAttributeDTO,
MetricsexplorertypesMetricDashboardDTO,
MetricsexplorertypesMetricHighlightsResponseDTO,
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
export interface MetricDetailsProps {
onClose: () => void;
isOpen: boolean;
metricName: string | null;
metricName: string;
isModalTimeSelection: boolean;
openInspectModal?: (metricName: string) => void;
}
export interface HighlightsProps {
metricName: string;
}
export interface DashboardsAndAlertsPopoverProps {
dashboards: MetricDetailsDashboard[] | null;
alerts: MetricDetailsAlert[] | null;
metricName: string;
}
export interface MetadataProps {
metricName: string;
metadata: MetricDetails['metadata'] | undefined;
refetchMetricDetails: () => void;
metadata: MetricMetadata | null;
isErrorMetricMetadata: boolean;
isLoadingMetricMetadata: boolean;
refetchMetricMetadata: () => void;
}
export interface AllAttributesProps {
attributes: MetricDetailsAttribute[];
metricName: string;
metricType: MetricType | undefined;
metricType: MetrictypesTypeDTO | undefined;
}
export interface AllAttributesValueProps {
@@ -36,3 +41,38 @@ export interface AllAttributesValueProps {
filterValue: string[];
goToMetricsExploreWithAppliedAttribute: (key: string, value: string) => void;
}
export interface AllAttributesEmptyTextProps {
isErrorAttributes: boolean;
refetchAttributes: () => void;
}
export type MetricHighlight = MetricsexplorertypesMetricHighlightsResponseDTO;
export type MetricAlert = MetricsexplorertypesMetricAlertDTO;
export type MetricDashboard = MetricsexplorertypesMetricDashboardDTO;
export type MetricMetadata = MetricsexplorertypesMetricMetadataDTO;
export interface MetricMetadataFormState {
type: MetrictypesTypeDTO;
description: string;
temporality?: MetrictypesTemporalityDTO;
unit: string;
isMonotonic: boolean;
}
export type MetricAttribute = MetricsexplorertypesMetricAttributeDTO;
export enum TableFields {
DESCRIPTION = 'description',
UNIT = 'unit',
TYPE = 'type',
Temporality = 'temporality',
IS_MONOTONIC = 'isMonotonic',
}
export interface MetricDetailsErrorStateProps {
refetch?: () => void;
errorMessage: string;
}

View File

@@ -1,12 +1,23 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
import {
GetMetricMetadata200,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
import { initialQueriesMap } from 'constants/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
export function formatTimestampToReadableDate(timestamp: string): string {
import { MetricMetadata, MetricMetadataFormState } from './types';
export function formatTimestampToReadableDate(
timestamp: number | string | undefined,
): string {
if (!timestamp) {
return '-';
}
const date = new Date(timestamp);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
@@ -39,7 +50,10 @@ export function formatTimestampToReadableDate(timestamp: string): string {
return date.toLocaleDateString();
}
export function formatNumberToCompactFormat(num: number): string {
export function formatNumberToCompactFormat(num: number | undefined): string {
if (!num) {
return '-';
}
return new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
@@ -47,27 +61,30 @@ export function formatNumberToCompactFormat(num: number): string {
}
export function determineIsMonotonic(
metricType: MetricType,
temporality?: Temporality,
metricType: MetrictypesTypeDTO,
temporality?: MetrictypesTemporalityDTO,
): boolean {
if (
metricType === MetricType.HISTOGRAM ||
metricType === MetricType.EXPONENTIAL_HISTOGRAM
metricType === MetrictypesTypeDTO.histogram ||
metricType === MetrictypesTypeDTO.exponentialhistogram
) {
return true;
}
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
if (
metricType === MetrictypesTypeDTO.gauge ||
metricType === MetrictypesTypeDTO.summary
) {
return false;
}
if (metricType === MetricType.SUM) {
return temporality === Temporality.CUMULATIVE;
if (metricType === MetrictypesTypeDTO.sum) {
return temporality === MetrictypesTemporalityDTO.cumulative;
}
return false;
}
export function getMetricDetailsQuery(
metricName: string,
metricType: MetricType | undefined,
metricType: MetrictypesTypeDTO | undefined,
filter?: { key: string; value: string },
groupBy?: string,
): Query {
@@ -75,23 +92,23 @@ export function getMetricDetailsQuery(
let spaceAggregation;
let aggregateOperator;
switch (metricType) {
case MetricType.SUM:
case MetrictypesTypeDTO.sum:
timeAggregation = 'rate';
spaceAggregation = 'sum';
aggregateOperator = 'rate';
break;
case MetricType.GAUGE:
case MetrictypesTypeDTO.gauge:
timeAggregation = 'avg';
spaceAggregation = 'avg';
aggregateOperator = 'avg';
break;
case MetricType.SUMMARY:
case MetrictypesTypeDTO.summary:
timeAggregation = 'noop';
spaceAggregation = 'sum';
aggregateOperator = 'noop';
break;
case MetricType.HISTOGRAM:
case MetricType.EXPONENTIAL_HISTOGRAM:
case MetrictypesTypeDTO.histogram:
case MetrictypesTypeDTO.exponentialhistogram:
timeAggregation = 'noop';
spaceAggregation = 'p90';
aggregateOperator = 'noop';
@@ -160,3 +177,38 @@ export function getMetricDetailsQuery(
},
};
}
export function transformMetricMetadata(
apiData: GetMetricMetadata200 | undefined,
): MetricMetadata | null {
if (!apiData || !apiData.data) {
return null;
}
const { type, description, unit, temporality, isMonotonic } = apiData.data;
return {
type,
description,
unit,
temporality,
isMonotonic,
};
}
export function transformUpdateMetricMetadataRequest(
metricName: string,
metricMetadata: MetricMetadataFormState,
): UpdateMetricMetadataMutationBody {
return {
metricName: metricName,
type: metricMetadata.type,
description: metricMetadata.description,
unit: metricMetadata.unit,
temporality:
metricMetadata.temporality ?? MetrictypesTemporalityDTO.unspecified,
isMonotonic: determineIsMonotonic(
metricMetadata.type,
metricMetadata.temporality,
),
};
}

View File

@@ -5,6 +5,7 @@ import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
@@ -128,7 +129,7 @@ function Summary(): JSX.Element {
} = useGetMetricsList(metricsListQuery, {
enabled: !!metricsListQuery && !isInspectModalOpen,
queryKey: [
'metricsList',
REACT_QUERY_KEY.GET_METRICS_LIST,
queryFiltersWithoutId,
orderBy,
pageSize,
@@ -323,7 +324,7 @@ function Summary(): JSX.Element {
</>
)}
</div>
{isMetricDetailsOpen && (
{isMetricDetailsOpen && selectedMetricName && (
<MetricDetails
isOpen={isMetricDetailsOpen}
onClose={closeMetricDetails}

View File

@@ -1,3 +1,4 @@
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { TreemapViewType } from './types';
@@ -25,7 +26,16 @@ export const METRIC_TYPE_LABEL_MAP = {
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
};
export const METRIC_TYPE_VALUES_MAP = {
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',
[MetrictypesTypeDTO.histogram]: 'Histogram',
[MetrictypesTypeDTO.summary]: 'Summary',
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
};
// TODO(@amlannandy): To remove this once API migration is complete
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
[MetricType.HISTOGRAM]: 'Histogram',
@@ -33,6 +43,14 @@ export const METRIC_TYPE_VALUES_MAP = {
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
};
export const METRIC_TYPE_VIEW_VALUES_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',
[MetrictypesTypeDTO.histogram]: 'Histogram',
[MetrictypesTypeDTO.summary]: 'Summary',
[MetrictypesTypeDTO.exponentialhistogram]: 'ExponentialHistogram',
};
export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';

View File

@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import { ColumnType } from 'antd/es/table';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import {
MetricsListItemData,
MetricsListPayload,
@@ -21,7 +22,7 @@ import {
} from 'lucide-react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { METRIC_TYPE_LABEL_MAP } from './constants';
import { METRIC_TYPE_LABEL_MAP, METRIC_TYPE_VIEW_LABEL_MAP } from './constants';
import MetricNameSearch from './MetricNameSearch';
import MetricTypeSearch from './MetricTypeSearch';
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
@@ -143,6 +144,66 @@ export function MetricTypeRenderer({
);
}
export function MetricTypeViewRenderer({
type,
}: {
type: MetrictypesTypeDTO;
}): JSX.Element {
const [icon, color] = useMemo(() => {
switch (type) {
case MetrictypesTypeDTO.sum:
return [
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
Color.BG_ROBIN_500,
];
case MetrictypesTypeDTO.gauge:
return [
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
Color.BG_SAKURA_500,
];
case MetrictypesTypeDTO.histogram:
return [
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
Color.BG_SIENNA_500,
];
case MetrictypesTypeDTO.summary:
return [
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
Color.BG_FOREST_500,
];
case MetrictypesTypeDTO.exponentialhistogram:
return [
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
Color.BG_AQUA_500,
];
default:
return [null, ''];
}
}, [type]);
const metricTypeRendererStyle = useMemo(
() => ({
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
}),
[color],
);
const metricTypeRendererTextStyle = useMemo(() => ({ color, fontSize: 12 }), [
color,
]);
return (
<div className="metric-type-renderer" style={metricTypeRendererStyle}>
{icon}
<Typography.Text style={metricTypeRendererTextStyle}>
{METRIC_TYPE_VIEW_LABEL_MAP[type]}
</Typography.Text>
</div>
);
}
function ValidateRowValueWrapper({
value,
children,
@@ -160,6 +221,9 @@ export const formatNumberIntoHumanReadableFormat = (
num: number,
addPlusSign = true,
): string => {
if (!num) {
return '-';
}
function format(num: number, divisor: number, suffix: string): string {
const value = num / divisor;
return value % 1 === 0

View File

@@ -0,0 +1,131 @@
import { ColorPickerProps } from 'antd';
import { Color } from 'antd/es/color-picker';
import { render, screen, userEvent } from 'tests/test-utils';
import LegendColors from './LegendColors';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
__esModule: true,
useQueryBuilder: (): { currentQuery: unknown } => ({
currentQuery: {
builder: {
queryData: [
{
queryName: 'A',
legend: '{service.name}',
},
],
},
},
}),
}));
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('antd', () => {
const actual = jest.requireActual('antd');
return {
...actual,
ColorPicker: ({ onChange, children }: ColorPickerProps): JSX.Element => (
<button
type="button"
data-testid="legend-color-picker"
onClick={(): void =>
onChange!({ toHexString: (): string => '#ffffff' } as Color, '#ffffff')
}
>
{children}
</button>
),
};
});
describe('LegendColors', () => {
it('renders legend colors panel and items', async () => {
const user = userEvent.setup();
render(
<LegendColors
customLegendColors={{}}
setCustomLegendColors={jest.fn()}
queryResponse={undefined}
/>,
);
expect(screen.getByText('Legend Colors')).toBeInTheDocument();
// Expand the collapse to reveal legend items
await user.click(
screen.getByRole('tab', {
name: /Legend Colors/i,
}),
);
expect(screen.getByText('{service.name}')).toBeInTheDocument();
});
it('calls setCustomLegendColors when color is changed', async () => {
const user = userEvent.setup();
const setCustomLegendColors = jest.fn();
render(
<LegendColors
customLegendColors={{}}
setCustomLegendColors={setCustomLegendColors}
queryResponse={undefined}
/>,
);
// Expand to render the mocked ColorPicker button
await user.click(
screen.getByRole('tab', {
name: /Legend Colors/i,
}),
);
const colorTrigger = screen.getByTestId('legend-color-picker');
await user.click(colorTrigger);
expect(setCustomLegendColors).toHaveBeenCalled();
});
it('throttles rapid color changes', async () => {
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const setCustomLegendColors = jest.fn();
render(
<LegendColors
customLegendColors={{}}
setCustomLegendColors={setCustomLegendColors}
queryResponse={undefined}
/>,
);
// Expand panel to render the mocked ColorPicker button
await user.click(
screen.getByRole('tab', {
name: /Legend Colors/i,
}),
);
const colorTrigger = screen.getByTestId('legend-color-picker');
// Fire multiple rapid changes
await user.click(colorTrigger);
await user.click(colorTrigger);
await user.click(colorTrigger);
await user.click(colorTrigger);
// Flush pending throttled calls
jest.advanceTimersByTime(500);
// Throttling should ensure we don't invoke the setter once per click
expect(setCustomLegendColors).toHaveBeenCalledTimes(2);
jest.useRealTimers();
});
});

View File

@@ -14,6 +14,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import throttle from 'lodash-es/throttle';
import { Palette } from 'lucide-react';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@@ -95,13 +96,24 @@ function LegendColors({
);
};
// Handle color change
const handleColorChange = (label: string, color: string): void => {
setCustomLegendColors((prev) => ({
...prev,
[label]: color,
}));
};
// Handle color change (throttled to avoid excessive updates)
const handleColorChange = useMemo(
() =>
throttle((label: string, color: string): void => {
setCustomLegendColors((prev) => ({
...prev,
[label]: color,
}));
}, 200), // 200ms is a good compromise between responsiveness and performance
[setCustomLegendColors],
);
// Clean up throttled handler on unmount
useEffect(() => {
return (): void => {
handleColorChange.cancel();
};
}, [handleColorChange]);
// Reset to default color
const resetToDefault = (label: string): void => {

View File

@@ -169,6 +169,10 @@
font-weight: 400;
line-height: 16px; /* 133.333% */
.ant-select {
width: 100%;
}
.ant-select-selector {
border: none;
height: unset;

View File

@@ -2,6 +2,9 @@
import { useMemo, useRef, useState } from 'react';
import { useDrag, useDrop, XYCoord } from 'react-dnd';
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { unitOptions } from 'container/NewWidget/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -204,6 +207,18 @@ function Threshold({
return unit !== 'none' && convertUnit(value, unit, toUnitId) === null;
}, [selectedGraph, yAxisUnit, tableSelectedOption, columnUnits, unit, value]);
const unitSelectCategories = useMemo(() => {
return unitOptions(
selectedGraph === PANEL_TYPES.TABLE
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
: yAxisUnit || '',
);
}, [selectedGraph, yAxisUnit, tableSelectedOption, columnUnits]);
const unitLabel = useMemo(() => {
return Y_AXIS_UNIT_NAMES[unit as keyof typeof Y_AXIS_UNIT_NAMES];
}, [unit]);
return (
<div
ref={allowDragAndDrop ? ref : null}
@@ -313,19 +328,17 @@ function Threshold({
<ShowCaseValue value={value} className="unit-input" />
)}
{isEditMode ? (
<Select
defaultValue={unit}
options={unitOptions(
selectedGraph === PANEL_TYPES.TABLE
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
: yAxisUnit || '',
)}
<YAxisUnitSelector
value={unit}
onChange={handleUnitChange}
showSearch
className="unit-selection"
placeholder="Select unit"
source={YAxisSource.DASHBOARDS}
initialValue={unit}
categoriesOverride={unitSelectCategories}
containerClassName="unit-selection"
/>
) : (
<ShowCaseValue value={unit} className="unit-selection-prev" />
<ShowCaseValue value={unitLabel} className="unit-selection-prev" />
)}
</div>
<div className="thresholds-color-selector">
@@ -356,7 +369,10 @@ function Threshold({
)}
</div>
{isInvalidUnitComparison && (
<Typography.Text className="invalid-unit">
<Typography.Text
className="invalid-unit"
data-testid="invalid-unit-comparison"
>
Threshold unit ({unit}) is not valid in comparison with the{' '}
{selectedGraph === PANEL_TYPES.TABLE ? 'column' : 'y-axis'} unit (
{selectedGraph === PANEL_TYPES.TABLE

View File

@@ -1,6 +1,8 @@
/* eslint-disable react/jsx-props-no-spreading */
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { render, screen } from 'tests/test-utils';
@@ -14,12 +16,26 @@ jest.mock('lib/query/createTableColumnsFromQuery', () => ({
),
}));
// Mock the unitOptions function
// Mock the unitOptions function to return YAxisCategory-shaped data
jest.mock('container/NewWidget/utils', () => ({
unitOptions: jest.fn(() => [
{ value: 'none', label: 'None' },
{ value: '%', label: 'Percent (0 - 100)' },
{ value: 'ms', label: 'Milliseconds (ms)' },
{
name: 'Mock Category',
units: [
{
id: UniversalYAxisUnit.NONE,
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
},
{
id: UniversalYAxisUnit.PERCENT,
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
},
{
id: UniversalYAxisUnit.MILLISECONDS,
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS],
},
],
},
]),
}));
@@ -28,7 +44,7 @@ const defaultProps = {
keyIndex: 0,
thresholdOperator: '>' as const,
thresholdValue: 50,
thresholdUnit: 'none',
thresholdUnit: UniversalYAxisUnit.NONE,
thresholdColor: 'Red',
thresholdFormat: 'Text' as const,
isEditEnabled: true,
@@ -38,8 +54,11 @@ const defaultProps = {
{ value: 'memory_usage', label: 'Memory Usage' },
],
thresholdTableOptions: 'cpu_usage',
columnUnits: { cpu_usage: 'percent', memory_usage: 'bytes' },
yAxisUnit: '%',
columnUnits: {
cpu_usage: UniversalYAxisUnit.PERCENT,
memory_usage: UniversalYAxisUnit.BYTES,
},
yAxisUnit: UniversalYAxisUnit.PERCENT,
moveThreshold: jest.fn(),
};
@@ -68,28 +87,27 @@ describe('Threshold Component Unit Validation', () => {
it('should show validation error when threshold unit is not "none" and units are incompatible', () => {
// Act - Render component with incompatible units (ms vs percent)
renderThreshold({
thresholdUnit: 'ms',
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
thresholdValue: 50,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(ms\) is not valid in comparison with the column unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
);
});
it('should not show validation error when threshold unit matches column unit', () => {
// Act - Render component with matching units
renderThreshold({
thresholdUnit: 'percent',
thresholdUnit: UniversalYAxisUnit.PERCENT,
thresholdValue: 50,
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
@@ -97,17 +115,16 @@ describe('Threshold Component Unit Validation', () => {
// Act - Render component for time series with incompatible units
renderThreshold({
selectedGraph: PANEL_TYPES.TIME_SERIES,
thresholdUnit: 'ms',
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(ms\) is not valid in comparison with the y-axis unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the y-axis unit (${UniversalYAxisUnit.PERCENT})`,
);
});
it('should not show validation error for time series graph when threshold unit is "none"', () => {
@@ -116,43 +133,39 @@ describe('Threshold Component Unit Validation', () => {
selectedGraph: PANEL_TYPES.TIME_SERIES,
thresholdUnit: 'none',
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
it('should not show validation error when threshold unit is compatible with column unit', () => {
// Act - Render component with compatible units (both in same category - Time)
renderThreshold({
thresholdUnit: 's',
thresholdUnit: UniversalYAxisUnit.SECONDS,
thresholdValue: 100,
columnUnits: { cpu_usage: 'ms' },
columnUnits: { cpu_usage: UniversalYAxisUnit.MILLISECONDS },
thresholdTableOptions: 'cpu_usage',
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
it('should show validation error when threshold unit is in different category than column unit', () => {
// Act - Render component with units from different categories
renderThreshold({
thresholdUnit: 'bytes',
thresholdUnit: UniversalYAxisUnit.BYTES,
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(bytes\) is not valid in comparison with the column unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.BYTES}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
);
});
});

View File

@@ -1,9 +1,12 @@
import { Layout } from 'react-grid-layout';
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/types';
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import {
UniversalYAxisUnit,
YAxisCategory,
YAxisSource,
} from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import {
initialQueryBuilderFormValuesMap,
@@ -606,7 +609,7 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
*/
export const getCategorySelectOptionByName = (
name?: YAxisCategoryNames,
): DefaultOptionType[] => {
): { name: string; id: UniversalYAxisUnit }[] => {
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
if (!categories.length) {
return [];
@@ -615,8 +618,8 @@ export const getCategorySelectOptionByName = (
categories
.find((category) => category.name === name)
?.units.map((unit) => ({
label: unit.name,
value: unit.id,
name: unit.name,
id: unit.id,
})) || []
);
};
@@ -628,19 +631,19 @@ export const getCategorySelectOptionByName = (
* select options. If a valid category is found, it filters the supported categories
* to return only the options for the matched category.
*/
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
export const unitOptions = (columnUnit: string): YAxisCategory[] => {
const category = getCategoryName(columnUnit);
if (isEmpty(category)) {
return categoryToSupport.map((category) => ({
label: category,
options: getCategorySelectOptionByName(category),
name: category,
units: getCategorySelectOptionByName(category),
}));
}
return categoryToSupport
.filter((supportedCategory) => supportedCategory === category)
.map((filteredCategory) => ({
label: filteredCategory,
options: getCategorySelectOptionByName(filteredCategory),
name: filteredCategory,
units: getCategorySelectOptionByName(filteredCategory),
}));
};

View File

@@ -1,32 +1,37 @@
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
import { getMetricMetadata } from 'api/metricsExplorer/v2/getMetricMetadata';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import {
getGetMetricMetadataQueryKey,
getMetricMetadata,
} from 'api/generated/services/metrics';
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
type QueryResult = UseQueryResult<
SuccessResponseV2<MetricMetadataResponse>,
Error
>;
type QueryResult = UseQueryResult<GetMetricMetadata200, Error>;
type UseGetMultipleMetrics = (
metricNames: string[],
options?: UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>,
options?: UseQueryOptions<GetMetricMetadata200, Error>,
headers?: Record<string, string>,
) => QueryResult[];
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
metricNames,
options,
headers,
) =>
useQueries(
metricNames.map(
(metricName) =>
({
queryKey: [REACT_QUERY_KEY.GET_METRIC_METADATA, metricName],
queryFn: ({ signal }) => getMetricMetadata(metricName, signal, headers),
queryKey: getGetMetricMetadataQueryKey({
metricName,
}),
queryFn: ({ signal }) =>
getMetricMetadata(
{
metricName,
},
signal,
),
...options,
} as UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>),
} as UseQueryOptions<GetMetricMetadata200, Error>),
),
);

View File

@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { useGetMetrics } from 'container/MetricsExplorer/Explorer/utils';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
@@ -24,13 +24,13 @@ const mockUseGetMetrics = useGetMetrics as jest.MockedFunction<
const MOCK_METRIC_1 = {
unit: UniversalYAxisUnit.BYTES,
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
const MOCK_METRIC_2 = {
unit: UniversalYAxisUnit.SECONDS,
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
const MOCK_METRIC_3 = {
unit: '',
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
function createMockCurrentQuery(
queryType: EQueryType,

View File

@@ -1,14 +1,22 @@
.tooltip-plugin-container {
top: 0;
left: 0;
width: 100%;
z-index: 1070;
white-space: pre;
border-radius: 4px;
position: fixed;
overflow: auto;
transform: translate(-1000px, -1000px); // hide the tooltip initially
opacity: 0;
pointer-events: none;
&.pinned {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
&.visible {
opacity: 1;
pointer-events: all;
}
}

View File

@@ -339,8 +339,9 @@ export default function TooltipPlugin({
return;
}
const layout = layoutRef.current;
layout.observer.disconnect();
if (containerRef.current) {
layout.observer.disconnect();
layout.observer.observe(containerRef.current);
const { width, height } = containerRef.current.getBoundingClientRect();
layout.width = width;
@@ -351,24 +352,28 @@ export default function TooltipPlugin({
}
}, [isHovering, plot]);
if (!plot || !isHovering) {
if (!plot) {
return null;
}
return createPortal(
<div
className={cx('tooltip-plugin-container', { pinned: isPinned })}
className={cx('tooltip-plugin-container', {
pinned: isPinned,
visible: isHovering,
})}
style={{
...style,
maxWidth: `${maxWidth}px`,
maxHeight: `${maxHeight}px`,
width: '100%',
}}
aria-live="polite"
aria-atomic="true"
aria-hidden={!isHovering}
ref={containerRef}
data-testid="tooltip-plugin-container"
>
{contents}
{isHovering ? contents : null}
</div>,
portalRoot.current,
);

View File

@@ -187,9 +187,7 @@ describe('TooltipPlugin', () => {
canPinTooltip: true,
});
const container = document.querySelector(
'.tooltip-plugin-container',
) as HTMLElement;
const container = screen.getByTestId('tooltip-plugin-container');
expect(container.classList.contains('pinned')).toBe(false);
act(() => {
@@ -197,11 +195,9 @@ describe('TooltipPlugin', () => {
});
return waitFor(() => {
const updated = document.querySelector(
'.tooltip-plugin-container',
) as HTMLElement | null;
expect(updated).not.toBeNull();
expect(updated?.classList.contains('pinned')).toBe(true);
const updated = screen.getByTestId('tooltip-plugin-container');
expect(updated).toBeInTheDocument();
expect(updated.classList.contains('pinned')).toBe(true);
});
});
@@ -249,7 +245,13 @@ describe('TooltipPlugin', () => {
await user.click(button);
await waitFor(() => {
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
const container = screen.getByTestId('tooltip-plugin-container');
expect(container).toBeInTheDocument();
expect(container.getAttribute('aria-hidden')).toBe('true');
expect(container.classList.contains('visible')).toBe(false);
expect(container.classList.contains('pinned')).toBe(false);
expect(container.textContent).toBe('');
});
});
@@ -292,12 +294,16 @@ describe('TooltipPlugin', () => {
jest.runAllTimers();
});
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
const container = screen.getByTestId('tooltip-plugin-container');
expect(container).toBeInTheDocument();
expect(container.getAttribute('aria-hidden')).toBe('true');
expect(container.classList.contains('visible')).toBe(false);
expect(container.classList.contains('pinned')).toBe(false);
jest.useRealTimers();
});
it('unpins the tooltip on outside mousedown', () => {
it('unpins the tooltip on outside mousedown', async () => {
jest.useFakeTimers();
const config = createConfigMock();
@@ -335,12 +341,19 @@ describe('TooltipPlugin', () => {
jest.runAllTimers();
});
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
await waitFor(() => {
const container = screen.getByTestId('tooltip-plugin-container');
expect(container).toBeInTheDocument();
expect(container.getAttribute('aria-hidden')).toBe('true');
expect(container.classList.contains('visible')).toBe(false);
expect(container.classList.contains('pinned')).toBe(false);
});
jest.useRealTimers();
});
it('unpins the tooltip on outside keydown', () => {
it('unpins the tooltip on outside keydown', async () => {
jest.useFakeTimers();
const config = createConfigMock();
@@ -380,7 +393,13 @@ describe('TooltipPlugin', () => {
jest.runAllTimers();
});
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
await waitFor(() => {
const container = screen.getByTestId('tooltip-plugin-container');
expect(container).toBeInTheDocument();
expect(container.getAttribute('aria-hidden')).toBe('true');
expect(container.classList.contains('visible')).toBe(false);
expect(container.classList.contains('pinned')).toBe(false);
});
jest.useRealTimers();
});

View File

@@ -2,7 +2,7 @@
//@ts-nocheck
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo } from 'react';
import { ForceGraph2D } from 'react-force-graph';
import ForceGraph2D from 'react-force-graph-2d';
import { getGraphData, getTooltip, transformLabel } from './utils';

View File

@@ -0,0 +1,13 @@
export function pluralize(
count: number,
singular: string,
plural?: string,
): string {
if (count === 1) {
return `${count} ${singular}`;
}
if (plural) {
return `${count} ${plural}`;
}
return `${count} ${singular}s`;
}

View File

@@ -2,37 +2,6 @@
# yarn lockfile v1
"3d-force-graph-ar@1":
version "1.8.3"
resolved "https://registry.npmjs.org/3d-force-graph-ar/-/3d-force-graph-ar-1.8.3.tgz"
integrity sha512-irj1Kk2qsRGSMsddSXdiVzzwtsNMZRbsJYBg+0ypkk5t5arZpo3h/PoN9Gfusvlu5fCAOk1wKVPUWN9Vkwrn7g==
dependencies:
aframe-forcegraph-component "3"
kapsule "1"
"3d-force-graph-vr@2":
version "2.2.2"
resolved "https://registry.npmjs.org/3d-force-graph-vr/-/3d-force-graph-vr-2.2.2.tgz"
integrity sha512-LSI24ugjT7SUE8JCusV5pmFHaFZ51EgGH1B7KMOK4JEgmUvogFwkJcXg7Go2mHI28WXDzFyBvrckWiwv5yLUeA==
dependencies:
accessor-fn "1"
aframe "^1.4"
aframe-extras "^6.1"
aframe-forcegraph-component "3"
kapsule "1"
polished "4"
"3d-force-graph@1":
version "1.71.3"
resolved "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.71.3.tgz"
integrity sha512-cPM2d8Lbf7VJ8SgRE5BDcpy5wOxrsIILwEseD3OdAITcM/3zGbuKzJdDQ98DHM6Ne3iHxl7F4v0A3IJuyqXYXQ==
dependencies:
accessor-fn "1"
kapsule "1"
three ">=0.118 <1"
three-forcegraph "1"
three-render-objects "1"
"@adobe/css-tools@^4.0.1":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11"
@@ -2212,7 +2181,7 @@
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.28.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473"
integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==
@@ -5252,6 +5221,11 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@standard-schema/spec@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
"@stoplight/better-ajv-errors@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz#d74a5c4da5d786c17188d7f4edec505f089885fa"
@@ -5596,10 +5570,10 @@
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@tweenjs/tween.js@18 - 19", "@tweenjs/tween.js@19":
version "19.0.0"
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
integrity sha512-QVbvSlnP7FcjKr1edg460BbUlpdGzmIOfvpsvHCj3JPIVZ9S9KeQLk9mB24VlDzPIl/a/ehAZPE95xFsmqm+pQ==
"@tweenjs/tween.js@18 - 25":
version "25.0.0"
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-25.0.0.tgz#7266baebcc3affe62a3a54318a3ea82d904cd0b9"
integrity sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==
"@types/acorn@^4.0.0":
version "4.0.6"
@@ -6627,11 +6601,6 @@
rehype "~12.0.1"
rehype-prism-plus "~1.6.1"
"@ungap/custom-elements@^1.1.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@ungap/custom-elements/-/custom-elements-1.2.0.tgz"
integrity sha512-zdSuu79stAwVUtzkQU9B5jhGh2LavtkeX4kxd2jtMJmZt7QqRJ1KJW5bukt/vUOaUs3z674GHd+nqYm0bu0Gyg==
"@ungap/structured-clone@^1.0.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@@ -7022,40 +6991,6 @@ acorn@^8.15.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
aframe-extras@^6.1:
version "6.1.1"
resolved "https://registry.npmjs.org/aframe-extras/-/aframe-extras-6.1.1.tgz"
integrity sha512-w3o3sKfQG+cwe1ZoKUxvMLehh0D/MlvFZeg2XuyIto+Nrs/kGLPcb/fsI5DXM4jociZ3wVQfqcA1BVF+0Nq45A==
dependencies:
three-pathfinding "^0.7.0"
aframe-forcegraph-component@3:
version "3.0.8"
resolved "https://registry.npmjs.org/aframe-forcegraph-component/-/aframe-forcegraph-component-3.0.8.tgz"
integrity sha512-ir1SzOYWYVQ4wtG18QwZRR/aVeZlZDXDbcRgFxSIc8A1YB6Mz3Mh0f0zMFKE3jAq481xNgpktxMvsWGZTPi63Q==
dependencies:
accessor-fn "1"
three-forcegraph "1"
aframe@^1.4:
version "1.4.2"
resolved "https://registry.npmjs.org/aframe/-/aframe-1.4.2.tgz"
integrity sha512-/sWCOB3ZNe5dWvMknIIMi5dwfU3rIyCiV+QkfYTDK36rNGivmUrmcdkregLmZk0OGHu9WAXoeUP3n0a23n6D0A==
dependencies:
"@ungap/custom-elements" "^1.1.0"
buffer "^6.0.3"
custom-event-polyfill "^1.0.6"
debug ngokevin/debug#noTimestamp
deep-assign "^2.0.0"
load-bmfont "^1.2.3"
object-assign "^4.0.1"
present "0.0.6"
promise-polyfill "^3.1.0"
super-animejs "^3.1.0"
super-three "^0.147.1"
three-bmfont-text dmarcos/three-bmfont-text#21d017046216e318362c48abd1a48bddfb6e0733
webvr-polyfill "^0.10.12"
agent-base@6:
version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
@@ -7130,11 +7065,6 @@ ajv@^8.11.0, ajv@^8.17.1:
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
an-array@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/an-array/-/an-array-1.0.0.tgz"
integrity sha512-M175GYI7RmsYu24Ok383yZQa3eveDfNnmhTe3OQ3bm70bEovz2gWenH+ST/n32M8lrwLWk74hcPds5CDRPe2wg==
ansi-colors@^4.1.1:
version "4.1.3"
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz"
@@ -7337,11 +7267,6 @@ array-includes@^3.1.5, array-includes@^3.1.6:
get-intrinsic "^1.1.3"
is-string "^1.0.7"
array-shuffle@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/array-shuffle/-/array-shuffle-1.0.1.tgz"
integrity sha512-PBqgo1Y2XWSksBzq3GFPEb798ZrW2snAcmr4drbVeF/6MT/5aBlkGJEvu5A/CzXHf4EjbHOj/ZowatjlIiVidA==
array-tree-filter@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz"
@@ -7429,11 +7354,6 @@ arrify@^1.0.1:
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
as-number@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/as-number/-/as-number-1.0.0.tgz"
integrity sha512-HkI/zLo2AbSRO4fqVkmyf3hms0bJDs3iboHqTrNuwTiCRvdYXM7HFhfhB6Dk51anV2LM/IMB83mtK9mHw4FlAg==
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz"
@@ -8106,21 +8026,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-equal@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz"
integrity sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer-to-arraybuffer@^0.0.5:
version "0.0.5"
resolved "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz"
integrity sha512-3dthu5CYiVB1DEJp61FtApNnNndTckcqe4pFcLdvHtrpG+kcyekCJKg4MRiDcFW7A6AODnXB9U4dwQiCW5kzJQ==
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@@ -8279,22 +8189,13 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz#4adcb443c8b9c8303e04498318f987616b8fea2e"
integrity sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==
canvas-color-tracker@1:
version "1.2.1"
resolved "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.2.1.tgz"
integrity sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==
canvas-color-tracker@^1.3:
version "1.3.2"
resolved "https://registry.yarnpkg.com/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz#b924cf94b33441b82692938fca5b936be971a46d"
integrity sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==
dependencies:
tinycolor2 "^1.6.0"
cardboard-vr-display@^1.0.19:
version "1.0.19"
resolved "https://registry.npmjs.org/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz"
integrity sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ==
dependencies:
gl-preserve-state "^1.0.0"
nosleep.js "^0.7.0"
webvr-polyfill-dpdb "^1.0.17"
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
@@ -9190,11 +9091,6 @@ csstype@^3.1.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
custom-event-polyfill@^1.0.6:
version "1.0.7"
resolved "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.2.3"
resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz"
@@ -9398,13 +9294,6 @@ dargs@^7.0.0:
resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz"
integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==
data-joint@1:
version "1.3.1"
resolved "https://registry.npmjs.org/data-joint/-/data-joint-1.3.1.tgz"
integrity sha512-tMK0m4OVGqiA3zkn8JmO6YAqD8UwJqIAx4AAwFl1SKTtKAqcXePuT+n2aayiX9uITtlN3DFtKKTOxJRUc2+HvQ==
dependencies:
index-array-by "^1.4.0"
data-urls@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz"
@@ -9466,7 +9355,7 @@ debounce@^1.2.1:
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
debug@2.6.9, debug@4, debug@4.3.4, debug@^3.2.6, debug@^3.2.7, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.6, debug@^4.4.3, debug@ngokevin/debug#noTimestamp:
debug@2.6.9, debug@4, debug@4.3.4, debug@^3.2.6, debug@^3.2.7, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.6, debug@^4.4.3:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -9498,18 +9387,6 @@ decode-named-character-reference@^1.0.0:
dependencies:
character-entities "^2.0.0"
decode-uri-component@^0.2.0:
version "0.2.2"
resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz"
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz"
integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==
dependencies:
mimic-response "^1.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@@ -9522,13 +9399,6 @@ dedent@^0.7.0:
resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz"
integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==
deep-assign@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/deep-assign/-/deep-assign-2.0.0.tgz"
integrity sha512-2QhG3Kxulu4XIF3WL5C5x0sc/S17JLgm1SfvDfIRsR/5m7ZGmcejII7fZ2RyWhN0UWIJm0TNM/eKow6LAn3evQ==
dependencies:
is-obj "^1.0.0"
deep-equal@^2.0.5:
version "2.2.1"
resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz"
@@ -9866,11 +9736,6 @@ dotenv@^16.4.2:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
dtype@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz"
integrity sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==
dunder-proto@^1.0.0, dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -10964,12 +10829,14 @@ flatted@^3.1.0:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
flatten-vertex-data@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz"
integrity sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==
float-tooltip@^1.7:
version "1.7.5"
resolved "https://registry.yarnpkg.com/float-tooltip/-/float-tooltip-1.7.5.tgz#7083bf78f0de5a97f9c2d6aa8e90d2139f34047f"
integrity sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==
dependencies:
dtype "^2.0.0"
d3-selection "2 - 3"
kapsule "^1.16"
preact "10"
flubber@^0.4.2:
version "0.4.2"
@@ -11007,15 +10874,15 @@ for-each@^0.3.5:
dependencies:
is-callable "^1.2.7"
force-graph@1:
version "1.43.1"
resolved "https://registry.npmjs.org/force-graph/-/force-graph-1.43.1.tgz"
integrity sha512-JOrmhMYr3uF6zzCXTauEo0KIiSp9OB2fAYYXkOGs6z0GQNSLIpcjCY3lhod4kAbkQCOup+u8JE9mmw7ojQaxTQ==
force-graph@^1.51:
version "1.51.1"
resolved "https://registry.yarnpkg.com/force-graph/-/force-graph-1.51.1.tgz#c967249bf6ad2cb4a3ba89ed4c6d79895bd70fe1"
integrity sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg==
dependencies:
"@tweenjs/tween.js" "19"
"@tweenjs/tween.js" "18 - 25"
accessor-fn "1"
bezier-js "3 - 6"
canvas-color-tracker "1"
canvas-color-tracker "^1.3"
d3-array "1 - 3"
d3-drag "2 - 3"
d3-force-3d "2 - 3"
@@ -11023,8 +10890,9 @@ force-graph@1:
d3-scale-chromatic "1 - 3"
d3-selection "2 - 3"
d3-zoom "2 - 3"
float-tooltip "^1.7"
index-array-by "1"
kapsule "^1.14"
kapsule "^1.16"
lodash-es "4"
form-data@4.0.4, form-data@^3.0.0, form-data@^4.0.4:
@@ -11067,11 +10935,6 @@ fresh@0.5.2:
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
fromentries@^1.3.2:
version "1.3.2"
resolved "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz"
integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==
fs-extra@^10.0.0:
version "10.1.0"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz"
@@ -11284,11 +11147,6 @@ github-slugger@^2.0.0:
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a"
integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==
gl-preserve-state@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz"
integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
@@ -11337,7 +11195,7 @@ global-dirs@^0.1.1:
dependencies:
ini "^1.3.4"
global@^4.3.0, global@~4.4.0:
global@^4.3.0:
version "4.4.0"
resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
@@ -12171,7 +12029,7 @@ indent-string@^4.0.0:
resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
index-array-by@1, index-array-by@^1.4.0:
index-array-by@1:
version "1.4.1"
resolved "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.1.tgz"
integrity sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==
@@ -12400,11 +12258,6 @@ is-boolean-object@^1.2.1:
call-bound "^1.0.3"
has-tostringtag "^1.0.2"
is-buffer@^1.0.2:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
@@ -12497,11 +12350,6 @@ is-fullwidth-code-point@^4.0.0:
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz"
integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==
is-function@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz"
integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==
is-generator-fn@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz"
@@ -12604,11 +12452,6 @@ is-number@^7.0.0:
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz"
integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
is-obj@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz"
@@ -13617,10 +13460,10 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
kapsule@1, kapsule@^1.14:
version "1.14.2"
resolved "https://registry.npmjs.org/kapsule/-/kapsule-1.14.2.tgz"
integrity sha512-6ROjWt7DHJ6pBv8RqpcP5uhP7+IgoaF5XavzcRU0U1/5wT/dZgBDNPrdZMNXaWyWLZvAdJ/ONuVRW0Dr9NbLkA==
kapsule@^1.16:
version "1.16.3"
resolved "https://registry.yarnpkg.com/kapsule/-/kapsule-1.16.3.tgz#5684ed89838b6658b30d0f2cc056dffc3ba68c30"
integrity sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==
dependencies:
lodash-es "4"
@@ -13671,15 +13514,6 @@ launch-editor@^2.6.1:
picocolors "^1.0.0"
shell-quote "^1.8.1"
layout-bmfont-text@^1.2.0:
version "1.3.4"
resolved "https://registry.npmjs.org/layout-bmfont-text/-/layout-bmfont-text-1.3.4.tgz"
integrity sha512-mceomHZ8W7pSKQhTdLvOe1Im4n37u8xa5Gr0J3KPCHRMO/9o7+goWIOzZcUUd+Xgzy3+22bvoIQ0OaN3LRtgaw==
dependencies:
as-number "^1.0.0"
word-wrapper "^1.0.7"
xtend "^4.0.0"
lerc@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz"
@@ -13810,20 +13644,6 @@ listr2@^4.0.5:
through "^2.3.8"
wrap-ansi "^7.0.0"
load-bmfont@^1.2.3:
version "1.4.1"
resolved "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz"
integrity sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==
dependencies:
buffer-equal "0.0.1"
mime "^1.3.4"
parse-bmfont-ascii "^1.0.3"
parse-bmfont-binary "^1.0.5"
parse-bmfont-xml "^1.1.4"
phin "^2.9.1"
xhr "^2.0.1"
xtend "^4.0.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -14122,13 +13942,6 @@ makeerror@1.0.12:
dependencies:
tmpl "1.0.5"
map-limit@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz"
integrity sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==
dependencies:
once "~1.3.0"
map-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz"
@@ -14899,7 +14712,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17,
dependencies:
mime-db "1.52.0"
mime@1.6.0, mime@^1.3.4, mime@^1.4.1:
mime@1.6.0, mime@^1.4.1:
version "1.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -15168,57 +14981,11 @@ neo-async@^2.6.2:
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
new-array@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==
next-themes@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
ngraph.events@^1.0.0, ngraph.events@^1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
ngraph.forcelayout@3:
version "3.3.1"
resolved "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz"
integrity sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==
dependencies:
ngraph.events "^1.0.0"
ngraph.merge "^1.0.0"
ngraph.random "^1.0.0"
ngraph.graph@20:
version "20.0.1"
resolved "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.0.1.tgz"
integrity sha512-VFsQ+EMkT+7lcJO1QP8Ik3w64WbHJl27Q53EO9hiFU9CRyxJ8HfcXtfWz/U8okuoYKDctbciL6pX3vG5dt1rYA==
dependencies:
ngraph.events "^1.2.1"
ngraph.merge@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz"
integrity sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==
ngraph.random@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.1.0.tgz"
integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==
nice-color-palettes@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/nice-color-palettes/-/nice-color-palettes-1.0.1.tgz"
integrity sha512-aHEFYKuGiaga8LqMi0Ttarqzn4tKS7BaIE2MeD9SDjv6yVc7DMIu/Eax4RvUgwR7vS0hXAUEIUx9P0/54O1W0g==
dependencies:
map-limit "0.0.1"
minimist "^1.2.0"
new-array "^1.0.0"
xhr-request "^1.0.1"
nimma@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/nimma/-/nimma-0.2.3.tgz#33cd6244ede857d9c8ac45b9d1aad07091559e45"
@@ -15323,11 +15090,6 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
nosleep.js@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.7.0.tgz"
integrity sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA==
not@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/not/-/not-0.1.0.tgz#c9691c1746c55dcfbe54cbd8bd4ff041bc2b519d"
@@ -15362,6 +15124,13 @@ nth-check@^2.0.0, nth-check@^2.0.1:
dependencies:
boolbase "^1.0.0"
nuqs@2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.8.8.tgz#375be35455b1d7e1104a15ff4d840533d6c55f76"
integrity sha512-LF5sw9nWpHyPWzMMu9oho3r9C5DvkpmBIg4LQN78sexIzGaeRx8DWr0uy3YiFx5i2QGZN1Qqcb+OAtEVRa2bnA==
dependencies:
"@standard-schema/spec" "1.0.0"
nwsapi@^2.2.0:
version "2.2.4"
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz"
@@ -15413,7 +15182,7 @@ oas-validator@^5.0.8:
should "^13.2.1"
yaml "^1.10.0"
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@@ -15573,13 +15342,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
once@~1.3.0:
version "1.3.3"
resolved "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
integrity sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==
dependencies:
wrappy "1"
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz"
@@ -15809,24 +15571,6 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-bmfont-ascii@^1.0.3:
version "1.0.6"
resolved "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz"
integrity sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==
parse-bmfont-binary@^1.0.5:
version "1.0.6"
resolved "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz"
integrity sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==
parse-bmfont-xml@^1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz"
integrity sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==
dependencies:
xml-parse-from-string "^1.0.0"
xml2js "^0.4.5"
parse-entities@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
@@ -15853,7 +15597,7 @@ parse-entities@^4.0.0:
is-decimal "^2.0.0"
is-hexadecimal "^2.0.0"
parse-headers@^2.0.0, parse-headers@^2.0.2:
parse-headers@^2.0.2:
version "2.0.5"
resolved "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz"
integrity sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==
@@ -16000,7 +15744,7 @@ periscopic@^3.0.0:
estree-walker "^3.0.0"
is-reference "^3.0.0"
phin@^2.9.1, phin@^3.7.1:
phin@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/phin/-/phin-3.7.1.tgz#bf841da75ee91286691b10e41522a662aa628fd6"
integrity sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==
@@ -16070,13 +15814,6 @@ playwright@1.55.1:
optionalDependencies:
fsevents "2.3.2"
polished@4:
version "4.2.2"
resolved "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz"
integrity sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==
dependencies:
"@babel/runtime" "^7.17.8"
pony-cause@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pony-cause/-/pony-cause-1.1.1.tgz#f795524f83bebbf1878bd3587b45f69143cbf3f9"
@@ -16410,6 +16147,11 @@ posthog-js@1.298.0:
preact "^10.19.3"
web-vitals "^4.2.4"
preact@10:
version "10.28.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.28.3.tgz#3c2171526b3e29628ad1a6c56a9e3ca867bbdee8"
integrity sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==
preact@^10.19.3:
version "10.22.0"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.22.0.tgz#a50f38006ae438d255e2631cbdaf7488e6dd4e16"
@@ -16425,11 +16167,6 @@ prelude-ls@~1.1.2:
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
present@0.0.6:
version "0.0.6"
resolved "https://registry.npmjs.org/present/-/present-0.0.6.tgz"
integrity sha512-8HGGcsH0xefDkhtWzXhigzieKtervWPQgyX8RtQD3cKr4wU307j8XANVSaZLxbR0+1EBonCJNOdUrQ7hbk3Kiw==
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz"
@@ -16493,11 +16230,6 @@ progress@^2.0.0, progress@^2.0.3:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
promise-polyfill@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-3.1.0.tgz"
integrity sha512-t20OwHJ4ZOUj5fV+qms67oczphAVkRC6Rrjcrne+V1FJkQMym7n69xJmYyXHulm9OUQ0Ie5KSzg0QhOYgaxy+w==
prompts@^2.0.1:
version "2.4.2"
resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz"
@@ -16590,24 +16322,6 @@ qs@6.13.0:
dependencies:
side-channel "^1.0.6"
quad-indices@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/quad-indices/-/quad-indices-2.0.1.tgz"
integrity sha512-6jtmCsEbGAh5npThXrBaubbTjPcF0rMbn57XCJVI7LkW8PUT56V+uIrRCCWCn85PSgJC9v8Pm5tnJDwmOBewvA==
dependencies:
an-array "^1.0.0"
dtype "^2.0.0"
is-buffer "^1.0.2"
query-string@^5.0.1:
version "5.1.1"
resolved "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz"
integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz"
@@ -17155,17 +16869,14 @@ react-fast-compare@^3.2.0:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-force-graph@^1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/react-force-graph/-/react-force-graph-1.43.0.tgz#cf38d053dbce5ce16e16585d3166e95b5324d0d5"
integrity sha512-g59ZWGrR6hkokY8RMO6FQHbltaIZ3+AGf9mrQs+s1+J26Sc2Wc6aro4cLW8PTHMIHgX/zml44yp60gRbzdFSMw==
react-force-graph-2d@^1.29.1:
version "1.29.1"
resolved "https://registry.yarnpkg.com/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz#a0784d4387b12b28e2b552058ec09d092b4e8cda"
integrity sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==
dependencies:
"3d-force-graph" "1"
"3d-force-graph-ar" "1"
"3d-force-graph-vr" "2"
force-graph "1"
force-graph "^1.51"
prop-types "15"
react-kapsule "2"
react-kapsule "^2.5"
react-full-screen@1.1.1:
version "1.1.1"
@@ -17240,12 +16951,11 @@ react-is@^18.0.0, react-is@^18.2.0:
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-kapsule@2:
version "2.4.0"
resolved "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.4.0.tgz"
integrity sha512-w4Yv9CgWdj8kWGQEPNWFGJJ08dYEZHZpiaFR/DgZjCMBNqv9wus2Gy1qvHVJmJbzvAZbq6jdvFC+NYzEqAlNhQ==
react-kapsule@^2.5:
version "2.5.7"
resolved "https://registry.yarnpkg.com/react-kapsule/-/react-kapsule-2.5.7.tgz#dcd957ae8e897ff48055fc8ff48ed04ebe3c5bd2"
integrity sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==
dependencies:
fromentries "^1.3.2"
jerrypick "^1.1.1"
react-lifecycles-compat@^3.0.4:
@@ -18531,11 +18241,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-eval@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-eval/-/simple-eval-1.0.1.tgz#f91fc2b1583b7f5b972cdc088b769880087120a5"
@@ -18543,15 +18248,6 @@ simple-eval@1.0.1:
dependencies:
jsep "^1.3.6"
simple-get@^2.7.0:
version "2.8.2"
resolved "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz"
integrity sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==
dependencies:
decompress-response "^3.3.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz"
@@ -18840,11 +18536,6 @@ strict-event-emitter@^0.4.3:
resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz#ff347c8162b3e931e3ff5f02cfce6772c3b07eb3"
integrity sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz"
integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==
string-argv@^0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz"
@@ -19119,16 +18810,6 @@ stylus@^0.62.0:
sax "~1.3.0"
source-map "^0.7.3"
super-animejs@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/super-animejs/-/super-animejs-3.1.0.tgz"
integrity sha512-6MFAFJDRuvwkovxQZPruuyHinTa4rgj4hNLOndjcYYhZLckoXtVRY9rJPuq8p6c/tgZJrFYEAYAfJ2/hhNtUCA==
super-three@^0.147.1:
version "0.147.1"
resolved "https://registry.npmjs.org/super-three/-/super-three-0.147.1.tgz"
integrity sha512-H8yhlXqjscWpqYLhPQ/h3EfElNBxe3Ktp1tGVp13vBjIxF5sMVkAo2NpIDXkY8+MVhuTrA0ZN42IMNPdOKwKLg==
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
@@ -19319,60 +19000,6 @@ thingies@^1.20.0:
resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1"
integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==
three-bmfont-text@dmarcos/three-bmfont-text#21d017046216e318362c48abd1a48bddfb6e0733:
version "2.4.0"
resolved "https://codeload.github.com/dmarcos/three-bmfont-text/tar.gz/21d017046216e318362c48abd1a48bddfb6e0733"
dependencies:
array-shuffle "^1.0.1"
inherits "^2.0.1"
layout-bmfont-text "^1.2.0"
nice-color-palettes "^1.0.1"
object-assign "^4.0.1"
quad-indices "^2.0.1"
three-buffer-vertex-data dmarcos/three-buffer-vertex-data#69378fc58daf27d3b1d930df9f233473e4a4818c
three-buffer-vertex-data@dmarcos/three-buffer-vertex-data#69378fc58daf27d3b1d930df9f233473e4a4818c:
version "1.1.0"
resolved "https://codeload.github.com/dmarcos/three-buffer-vertex-data/tar.gz/69378fc58daf27d3b1d930df9f233473e4a4818c"
dependencies:
flatten-vertex-data "^1.0.0"
three-forcegraph@1:
version "1.41.8"
resolved "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.41.8.tgz"
integrity sha512-UvD0qgl3wfFQb+2FGuMn2FFL0Ss58WYlfk1g3D3/lbxNqEL1c0C/EbIrJYm9R02CvSzQ0bLnEZbSfD0wlaxmlA==
dependencies:
accessor-fn "1"
d3-array "1 - 3"
d3-force-3d "2 - 3"
d3-scale "1 - 4"
d3-scale-chromatic "1 - 3"
data-joint "1"
kapsule "1"
ngraph.forcelayout "3"
ngraph.graph "20"
tinycolor2 "1"
three-pathfinding@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/three-pathfinding/-/three-pathfinding-0.7.0.tgz"
integrity sha512-UwWvzgio1UFe81n5jKHNzB4B+AG3wfZ54OKp7bTb1MHuC3cy6RTtr0dbbiPQQoqxzr+DRArR2DUwQSEknw5+nw==
three-render-objects@1:
version "1.28.3"
resolved "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.28.3.tgz"
integrity sha512-E4FUEirpNoHwIRn1z2pVioqAaOx8Gyyofb7ukNfuPn/M+MFMIIYszubceKBQmROCW+sF4dALta5W/+UoUCcTkg==
dependencies:
"@tweenjs/tween.js" "18 - 19"
accessor-fn "1"
kapsule "1"
polished "4"
"three@>=0.118 <1":
version "0.152.0"
resolved "https://registry.npmjs.org/three/-/three-0.152.0.tgz"
integrity sha512-uvKoYo4b2bnqzsR4RJFuWecxwMKcgT1nFNmiWooCNr6AxZLCtfkj/xcfFgoi5mFopSVorh7bnvTHPfeW8DINGg==
throat@^6.0.1:
version "6.0.2"
resolved "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz"
@@ -19405,11 +19032,6 @@ thunky@^1.0.2:
resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
timed-out@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz"
integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==
timestamp-nano@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz"
@@ -19425,7 +19047,7 @@ tiny-warning@^1.0.0:
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@1, tinycolor2@1.6.0, tinycolor2@^1.6.0:
tinycolor2@1.6.0, tinycolor2@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
@@ -20078,11 +19700,6 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
url-set-query@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz"
integrity sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg==
use-callback-ref@^1.3.0, use-callback-ref@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
@@ -20503,18 +20120,6 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
webvr-polyfill-dpdb@^1.0.17:
version "1.0.18"
resolved "https://registry.npmjs.org/webvr-polyfill-dpdb/-/webvr-polyfill-dpdb-1.0.18.tgz"
integrity sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw==
webvr-polyfill@^0.10.12:
version "0.10.12"
resolved "https://registry.npmjs.org/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz"
integrity sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA==
dependencies:
cardboard-vr-display "^1.0.19"
whatwg-encoding@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz"
@@ -20658,11 +20263,6 @@ word-wrap@^1.2.3, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
word-wrapper@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/word-wrapper/-/word-wrapper-1.0.7.tgz"
integrity sha512-VOPBFCm9b6FyYKQYfn9AVn2dQvdR/YOVFV6IBRA1TBMJWKffvhEX1af6FMGrttILs2Q9ikCRhLqkbY2weW6dOQ==
wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
@@ -20714,45 +20314,17 @@ ws@^8.18.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
xhr-request@^1.0.1:
version "1.1.0"
resolved "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz"
integrity sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==
dependencies:
buffer-to-arraybuffer "^0.0.5"
object-assign "^4.1.1"
query-string "^5.0.1"
simple-get "^2.7.0"
timed-out "^4.0.1"
url-set-query "^1.0.0"
xhr "^2.0.4"
xhr@^2.0.1, xhr@^2.0.4:
version "2.6.0"
resolved "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz"
integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==
dependencies:
global "~4.4.0"
is-function "^1.0.1"
parse-headers "^2.0.0"
xtend "^4.0.0"
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml-parse-from-string@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz"
integrity sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==
xml-utils@^1.0.2:
version "1.7.0"
resolved "https://registry.npmjs.org/xml-utils/-/xml-utils-1.7.0.tgz"
integrity sha512-bWB489+RQQclC7A9OW8e5BzbT8Tu//jtAOvkYwewFr+Q9T9KDGvfzC1lp0pYPEQPEoPQLDkmxkepSC/2gIAZGw==
xml2js@0.5.0, xml2js@^0.4.5:
xml2js@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7"
integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==
@@ -20864,6 +20436,11 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
zustand@5.0.11:
version "5.0.11"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494"
integrity sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==
zwitch@^2.0.0, zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"

View File

@@ -513,7 +513,7 @@ func (m *module) fetchTimeseriesMetadata(ctx context.Context, orgID valuer.UUID,
"metric_name",
"anyLast(description) AS description",
"anyLast(type) AS metric_type",
"anyLast(unit) AS metric_unit",
"argMax(unit, unix_milli) AS metric_unit",
"anyLast(temporality) AS temporality",
"anyLast(is_monotonic) AS is_monotonic",
)

View File

@@ -22,7 +22,8 @@ type RootConfig struct {
}
type OrgConfig struct {
Name string `mapstructure:"name"`
ID valuer.UUID `mapstructure:"id"`
Name string `mapstructure:"name"`
}
type PasswordConfig struct {

View File

@@ -78,6 +78,43 @@ func (s *service) Stop(ctx context.Context) error {
}
func (s *service) reconcile(ctx context.Context) error {
if !s.config.Org.ID.IsZero() {
return s.reconcileWithOrgID(ctx)
}
return s.reconcileByName(ctx)
}
func (s *service) reconcileWithOrgID(ctx context.Context) error {
org, err := s.orgGetter.Get(ctx, s.config.Org.ID)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return err // something really went wrong
}
// org was not found using id check if we can find an org using name
existingOrgByName, nameErr := s.orgGetter.GetByName(ctx, s.config.Org.Name)
if nameErr != nil && !errors.Ast(nameErr, errors.TypeNotFound) {
return nameErr // something really went wrong
}
// we found an org using name
if existingOrgByName != nil {
// the existing org has the same name as config but org id is different inform user with actionable message
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "organization with name %q already exists with a different ID %s (expected %s)", s.config.Org.Name, existingOrgByName.ID.StringValue(), s.config.Org.ID.StringValue())
}
// default - we did not found any org using id and name both - create a new org
newOrg := types.NewOrganizationWithID(s.config.Org.ID, s.config.Org.Name, s.config.Org.Name)
_, err = s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
return err
}
return s.reconcileRootUser(ctx, org.ID)
}
func (s *service) reconcileByName(ctx context.Context) error {
org, err := s.orgGetter.GetByName(ctx, s.config.Org.Name)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {

View File

@@ -80,11 +80,16 @@ func (q *builderQuery[T]) Fingerprint() string {
case qbtypes.LogAggregation:
aggParts = append(aggParts, a.Expression)
case qbtypes.MetricAggregation:
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s",
var spaceAggParamStr string
if a.ComparisonSpaceAggregationParam != nil {
spaceAggParamStr = a.ComparisonSpaceAggregationParam.StringValue()
}
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s:%s",
a.MetricName,
a.Temporality.StringValue(),
a.TimeAggregation.StringValue(),
a.SpaceAggregation.StringValue(),
spaceAggParamStr,
))
}
}

View File

@@ -276,15 +276,17 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
// Fetch temporality for all metrics at once
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, err = q.metadataStore.FetchTemporalityMulti(ctx, req.Start, req.End, metricNames...)
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
// Continue without temporality - statement builder will handle unspecified
metricTemporality = make(map[string]metrictypes.Temporality)
metricTypes = make(map[string]metrictypes.Type)
}
q.logger.DebugContext(ctx, "fetched metric temporalities", "metric_temporality", metricTemporality)
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
}
queries := make(map[string]qbtypes.Query)
@@ -380,6 +382,12 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
if spec.Aggregations[i].Temporality == metrictypes.Unknown {
spec.Aggregations[i].Temporality = metrictypes.Unspecified
}
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType {
spec.Aggregations[i].Type = foundMetricType
}
}
}
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)

View File

@@ -4308,6 +4308,28 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
}
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for the given host metric names
// from distributed_metadata. When count is 0, minFirstReportedUnixMilli is 0.
func (r *ClickHouseReader) GetMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error) {
if len(metricNames) == 0 {
return 0, 0, nil
}
query := fmt.Sprintf(
`SELECT count(*) AS cnt, min(first_reported_unix_milli) AS min_first_reported
FROM %s.%s
WHERE metric_name IN @metric_names`,
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_METADATA_TABLENAME)
var count, minFirstReported uint64
err := r.db.QueryRow(ctx, query, clickhouse.Named("metric_names", metricNames)).Scan(&count, &minFirstReported)
if err != nil {
zap.L().Error("error getting host metrics existence and earliest time", zap.Error(err))
return 0, 0, err
}
return count, minFirstReported, nil
}
func getPersonalisedError(err error) error {
if err == nil {
return nil
@@ -5419,7 +5441,7 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer.
t.metric_name AS metric_name,
ANY_VALUE(t.description) AS description,
ANY_VALUE(t.type) AS metric_type,
ANY_VALUE(t.unit) AS metric_unit,
argMax(t.unit, unix_milli) AS metric_unit,
uniq(t.fingerprint) AS timeseries,
uniq(metric_name) OVER() AS total
FROM %s.%s AS t

View File

@@ -10,6 +10,7 @@ import (
)
var dotMetricMap = map[string]string{
"system_filesystem_usage": "system.filesystem.usage",
"system_cpu_time": "system.cpu.time",
"system_memory_usage": "system.memory.usage",
"system_cpu_load_average_15m": "system.cpu.load_average.15m",

View File

@@ -67,10 +67,11 @@ var (
GetDotMetrics("os_type"),
}
metricNamesForHosts = map[string]string{
"cpu": GetDotMetrics("system_cpu_time"),
"memory": GetDotMetrics("system_memory_usage"),
"load15": GetDotMetrics("system_cpu_load_average_15m"),
"wait": GetDotMetrics("system_cpu_time"),
"filesystem": GetDotMetrics("system_filesystem_usage"),
"cpu": GetDotMetrics("system_cpu_time"),
"memory": GetDotMetrics("system_memory_usage"),
"load15": GetDotMetrics("system_cpu_load_average_15m"),
"wait": GetDotMetrics("system_cpu_time"),
}
)
@@ -316,24 +317,15 @@ func (h *HostsRepo) getTopHostGroups(ctx context.Context, orgID valuer.UUID, req
return topHostGroups, allHostGroups, nil
}
func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostListRequest) (bool, error) {
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for host metrics
// in distributed_metadata. Uses metricNamesForHosts plus system.filesystem.usage.
func (h *HostsRepo) GetHostMetricsExistenceAndEarliestTime(ctx context.Context, req model.HostListRequest) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForHosts {
names = append(names, metricName)
}
namesStr := "'" + strings.Join(names, "','") + "'"
query := fmt.Sprintf("SELECT count() FROM %s.%s WHERE metric_name IN (%s)",
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME, namesStr)
count, err := h.reader.GetCountOfThings(ctx, query)
if err != nil {
return false, err
}
return count > 0, nil
return h.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) ([]string, []string, error) {
@@ -412,8 +404,25 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
resp.ClusterNames = clusterNames
resp.NodeNames = nodeNames
}
if sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
// 1. Check if any host metrics exist and get earliest retention time
// if no hosts metrics exist, that means we should show the onboarding guide on UI, and return early.
// 2. If host metrics exist, but req.End is earlier than the earliest time of host metrics as read from
// metadata table, then we should convey the same to the user and return early
if count, minFirstReportedUnixMilli, err := h.GetHostMetricsExistenceAndEarliestTime(ctx, req); err == nil {
if count == 0 {
resp.SentAnyHostMetricsData = false
resp.Records = []model.HostListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyHostMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.HostListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))

View File

@@ -125,6 +125,8 @@ const (
SIGNOZ_TIMESERIES_v4_6HRS_TABLENAME = "distributed_time_series_v4_6hrs"
SIGNOZ_ATTRIBUTES_METADATA_TABLENAME = "distributed_attributes_metadata"
SIGNOZ_ATTRIBUTES_METADATA_LOCAL_TABLENAME = "attributes_metadata"
SIGNOZ_METADATA_TABLENAME = "distributed_metadata"
SIGNOZ_METADATA_LOCAL_TABLENAME = "metadata"
)
// alert related constants

View File

@@ -1,86 +0,0 @@
package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)
type dataFormatter struct {
}
func NewDataFormatter() Formatter {
return &dataFormatter{}
}
func (*dataFormatter) Name() string {
return "data"
}
func (f *dataFormatter) Format(value float64, unit string) string {
switch unit {
case "bytes", "By":
return humanize.IBytes(uint64(value))
case "decbytes":
return humanize.Bytes(uint64(value))
case "bits", "bit":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.IBytes(uint64(value / 8))
case "decbits":
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.Bytes(uint64(value / 8))
case "kbytes", "KiBy":
return humanize.IBytes(uint64(value * converter.Kibibit))
case "Kibit":
return humanize.IBytes(uint64(value * converter.Kibibit / 8))
case "decKbytes", "deckbytes", "kBy":
return humanize.Bytes(uint64(value * converter.Kilobit))
case "kbit":
return humanize.Bytes(uint64(value * converter.Kilobit / 8))
case "mbytes", "MiBy":
return humanize.IBytes(uint64(value * converter.Mebibit))
case "Mibit":
return humanize.IBytes(uint64(value * converter.Mebibit / 8))
case "decMbytes", "decmbytes", "MBy":
return humanize.Bytes(uint64(value * converter.Megabit))
case "Mbit":
return humanize.Bytes(uint64(value * converter.Megabit / 8))
case "gbytes", "GiBy":
return humanize.IBytes(uint64(value * converter.Gibibit))
case "Gibit":
return humanize.IBytes(uint64(value * converter.Gibibit / 8))
case "decGbytes", "decgbytes", "GBy":
return humanize.Bytes(uint64(value * converter.Gigabit))
case "Gbit":
return humanize.Bytes(uint64(value * converter.Gigabit / 8))
case "tbytes", "TiBy":
return humanize.IBytes(uint64(value * converter.Tebibit))
case "Tibit":
return humanize.IBytes(uint64(value * converter.Tebibit / 8))
case "decTbytes", "dectbytes", "TBy":
return humanize.Bytes(uint64(value * converter.Terabit))
case "Tbit":
return humanize.Bytes(uint64(value * converter.Terabit / 8))
case "pbytes", "PiBy":
return humanize.IBytes(uint64(value * converter.Pebibit))
case "Pbit":
return humanize.Bytes(uint64(value * converter.Petabit / 8))
case "decPbytes", "decpbytes", "PBy":
return humanize.Bytes(uint64(value * converter.Petabit))
case "EiBy":
return humanize.IBytes(uint64(value * converter.Exbibit))
case "Ebit":
return humanize.Bytes(uint64(value * converter.Exabit / 8))
case "EBy":
return humanize.Bytes(uint64(value * converter.Exabit))
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -1,91 +0,0 @@
package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)
type dataRateFormatter struct {
}
func NewDataRateFormatter() Formatter {
return &dataRateFormatter{}
}
func (*dataRateFormatter) Name() string {
return "data_rate"
}
func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit {
case "binBps":
return humanize.IBytes(uint64(value)) + "/s"
case "Bps", "By/s":
return humanize.Bytes(uint64(value)) + "/s"
case "binbps":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.IBytes(uint64(value/8)) + "/s"
case "bps", "bit/s":
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.Bytes(uint64(value/8)) + "/s"
case "KiBs", "KiBy/s":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond)) + "/s"
case "Kibits", "Kibit/s":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond/8)) + "/s"
case "KBs", "kBy/s":
return humanize.IBytes(uint64(value*converter.KilobitPerSecond)) + "/s"
case "Kbits", "kbit/s":
return humanize.Bytes(uint64(value*converter.KilobitPerSecond/8)) + "/s"
case "MiBs", "MiBy/s":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond)) + "/s"
case "Mibits", "Mibit/s":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond/8)) + "/s"
case "MBs", "MBy/s":
return humanize.IBytes(uint64(value*converter.MegabitPerSecond)) + "/s"
case "Mbits", "Mbit/s":
return humanize.Bytes(uint64(value*converter.MegabitPerSecond/8)) + "/s"
case "GiBs", "GiBy/s":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond)) + "/s"
case "Gibits", "Gibit/s":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond/8)) + "/s"
case "GBs", "GBy/s":
return humanize.IBytes(uint64(value*converter.GigabitPerSecond)) + "/s"
case "Gbits", "Gbit/s":
return humanize.Bytes(uint64(value*converter.GigabitPerSecond/8)) + "/s"
case "TiBs", "TiBy/s":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond)) + "/s"
case "Tibits", "Tibit/s":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond/8)) + "/s"
case "TBs", "TBy/s":
return humanize.IBytes(uint64(value*converter.TerabitPerSecond)) + "/s"
case "Tbits", "Tbit/s":
return humanize.Bytes(uint64(value*converter.TerabitPerSecond/8)) + "/s"
case "PiBs", "PiBy/s":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond)) + "/s"
case "Pibits", "Pibit/s":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond/8)) + "/s"
case "PBs", "PBy/s":
return humanize.IBytes(uint64(value*converter.PetabitPerSecond)) + "/s"
case "Pbits", "Pbit/s":
return humanize.Bytes(uint64(value*converter.PetabitPerSecond/8)) + "/s"
// Exa units
case "EBy/s":
return humanize.Bytes(uint64(value*converter.ExabitPerSecond)) + "/s"
case "Ebit/s":
return humanize.Bytes(uint64(value*converter.ExabitPerSecond/8)) + "/s"
case "EiBy/s":
return humanize.IBytes(uint64(value*converter.ExbibitPerSecond)) + "/s"
case "Eibit/s":
return humanize.IBytes(uint64(value*converter.ExbibitPerSecond/8)) + "/s"
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -100,6 +100,8 @@ type Reader interface {
GetCountOfThings(ctx context.Context, query string) (uint64, error)
GetMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error)
//trace
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError

View File

@@ -44,6 +44,7 @@ type HostListResponse struct {
IsSendingK8SAgentMetrics bool `json:"isSendingK8SAgentMetrics"`
ClusterNames []string `json:"clusterNames"`
NodeNames []string `json:"nodeNames"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *HostListResponse) SortBy(orderBy *v3.OrderBy) {

View File

@@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
@@ -185,7 +185,7 @@ func (r *PromRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletype
func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
// prepare query, run query get data and filter the data based on the threshold
results, err := r.buildAndRunQuery(ctx, ts)

View File

@@ -33,7 +33,7 @@ import (
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
querierV5 "github.com/SigNoz/signoz/pkg/querier"
@@ -571,7 +571,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI
func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
var res ruletypes.Vector
var err error

View File

@@ -1616,40 +1616,52 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, queryTimeRang
}
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
temporalities, _, err := t.FetchTemporalityAndTypeMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
return temporalities, err
}
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
if len(metricNames) == 0 {
return make(map[string]metrictypes.Temporality), nil
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), nil
}
result := make(map[string]metrictypes.Temporality)
metricsTemporality, err := t.fetchMetricsTemporality(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
metricsTemporality, metricTypes, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
if err != nil {
return nil, err
return nil, nil, err
}
// TODO: return error after table migration are run
meterMetricsTemporality, _ := t.fetchMeterSourceMetricsTemporality(ctx, metricNames...)
meterMetricsTemporality, meterMetricsTypes, _ := t.fetchMeterSourceMetricsTemporalityAndType(ctx, metricNames...)
// For metrics not found in the database, set to Unknown
for _, metricName := range metricNames {
if temporality, exists := metricsTemporality[metricName]; exists && len(temporality) > 0 {
if len(temporality) > 1 {
result[metricName] = metrictypes.Multiple
temporalities[metricName] = metrictypes.Multiple
} else {
result[metricName] = temporality[0]
temporalities[metricName] = temporality[0]
}
continue
} else if temporality, exists := meterMetricsTemporality[metricName]; exists {
temporalities[metricName] = temporality
} else {
temporalities[metricName] = metrictypes.Unknown
}
if temporality, exists := meterMetricsTemporality[metricName]; exists {
result[metricName] = temporality
continue
if metricType, exists := metricTypes[metricName]; exists {
types[metricName] = metricType
} else if meterMetricType, exists := meterMetricsTypes[metricName]; exists {
types[metricName] = meterMetricType
} else {
types[metricName] = metrictypes.UnspecifiedType
}
result[metricName] = metrictypes.Unknown
}
return result, nil
return temporalities, types, nil
}
func (t *telemetryMetaStore) fetchMetricsTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, error) {
result := make(map[string][]metrictypes.Temporality)
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string][]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, nil)
@@ -1660,6 +1672,8 @@ func (t *telemetryMetaStore) fetchMetricsTemporality(ctx context.Context, queryT
sb := sqlbuilder.Select(
"metric_name",
"temporality",
"any(type) AS type",
"any(is_monotonic) as is_monotonic",
).
From(t.metricsDBName + "." + tsTableName)
@@ -1678,47 +1692,42 @@ func (t *telemetryMetaStore) fetchMetricsTemporality(ctx context.Context, queryT
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
}
defer rows.Close()
// Process results
for rows.Next() {
var metricName, temporalityStr string
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
// Convert string to Temporality type
var metricName string
var temporality metrictypes.Temporality
switch temporalityStr {
case "Delta":
temporality = metrictypes.Delta
case "Cumulative":
temporality = metrictypes.Cumulative
case "Unspecified":
temporality = metrictypes.Unspecified
default:
// Unknown or empty temporality
temporality = metrictypes.Unknown
var metricType metrictypes.Type
var isMonotonic bool
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
if temporality != metrictypes.Unknown {
result[metricName] = append(result[metricName], temporality)
temporalities[metricName] = append(temporalities[metricName], temporality)
}
if metricType == metrictypes.SumType && !isMonotonic {
metricType = metrictypes.GaugeType
}
types[metricName] = metricType
}
if err := rows.Err(); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
}
return result, nil
return temporalities, types, nil
}
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporality(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
result := make(map[string]metrictypes.Temporality)
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
sb := sqlbuilder.Select(
"metric_name",
"argMax(temporality, unix_milli) as temporality",
"any(type) AS type",
).From(t.meterDBName + "." + t.meterFieldsTblName)
// Filter by metric names (in the temporality column due to data mix-up)
@@ -1733,35 +1742,27 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporality(ctx context.Cont
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
}
defer rows.Close()
// Process results
for rows.Next() {
var metricName, temporalityStr string
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
// Convert string to Temporality type
var metricName string
var temporality metrictypes.Temporality
switch temporalityStr {
case "Delta":
temporality = metrictypes.Delta
case "Cumulative":
temporality = metrictypes.Cumulative
case "Unspecified":
temporality = metrictypes.Unspecified
default:
// Unknown or empty temporality
temporality = metrictypes.Unknown
var metricType metrictypes.Type
var isMonotonic bool
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
}
result[metricName] = temporality
if metricType == metrictypes.SumType && !isMonotonic {
metricType = metrictypes.GaugeType
}
temporalities[metricName] = temporality
types[metricName] = metricType
}
return result, nil
return temporalities, types, nil
}
// chunkSizeFirstSeenMetricMetadata limits the number of tuples per SQL query to avoid hitting the max_query_size limit.

View File

@@ -123,8 +123,7 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
origTimeAgg := query.Aggregations[0].TimeAggregation
origGroupBy := slices.Clone(query.GroupBy)
if query.Aggregations[0].SpaceAggregation.IsPercentile() &&
query.Aggregations[0].Type != metrictypes.ExpHistogramType {
if query.Aggregations[0].Type == metrictypes.HistogramType {
// add le in the group by if doesn't exist
leExists := false
for _, g := range query.GroupBy {
@@ -154,7 +153,11 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
}
// make the time aggregation rate and space aggregation sum
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
} else {
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationIncrease
}
query.Aggregations[0].SpaceAggregation = metrictypes.SpaceAggregationSum
}
@@ -551,6 +554,9 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
cteArgs [][]any,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
) (*qbtypes.Statement, error) {
metricType := query.Aggregations[0].Type
spaceAgg := query.Aggregations[0].SpaceAggregation
combined := querybuilder.CombineCTEs(cteFragments)
var args []any
@@ -560,12 +566,8 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
sb := sqlbuilder.NewSelectBuilder()
var quantile float64
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
quantile = query.Aggregations[0].SpaceAggregation.Percentile()
}
if quantile != 0 && query.Aggregations[0].Type != metrictypes.ExpHistogramType {
if metricType == metrictypes.HistogramType && spaceAgg.IsPercentile() {
quantile := query.Aggregations[0].SpaceAggregation.Percentile()
sb.Select("ts")
for _, g := range query.GroupBy {
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
@@ -577,12 +579,36 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
sb.From("__spatial_aggregation_cte")
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.GroupBy("ts")
if query.Having != nil && query.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
sb.Having(rewrittenExpr)
}
} else if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam != nil {
sb.Select("ts")
for _, g := range query.GroupBy {
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
aggQuery, err := AggregationQueryForHistogramCountWithParams(query.Aggregations[0].ComparisonSpaceAggregationParam)
if err != nil {
return nil, err
}
sb.SelectMore(aggQuery)
sb.From("__spatial_aggregation_cte")
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.GroupBy("ts")
if query.Having != nil && query.Having.Expression != "" {
rewriter := querybuilder.NewHavingExpressionRewriter()
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
sb.Having(rewrittenExpr)
}
} else {
// for count aggregation on histograms with no params, the exact result of spatial aggregation can be sent forward
sb.Select("*")
sb.From("__spatial_aggregation_cte")
if query.Having != nil && query.Having.Expression != "" {
@@ -593,6 +619,9 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
}
sb.OrderBy(querybuilder.GroupByKeys(query.GroupBy)...)
sb.OrderBy("ts")
if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam == nil {
sb.OrderBy("toFloat64(le)")
}
q, a := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return &qbtypes.Statement{Query: combined + q, Args: append(args, a...)}, nil

View File

@@ -1,6 +1,7 @@
package telemetrymetrics
import (
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -308,3 +309,20 @@ func AggregationColumnForSamplesTable(
}
return aggregationColumn, nil
}
func AggregationQueryForHistogramCountWithParams(param *metrictypes.ComparisonSpaceAggregationParam) (string, error) {
if param == nil {
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no aggregation param provided for histogram count")
}
histogramCountThreshold := param.Threshold
switch param.Operater {
case "<=":
return fmt.Sprintf("argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f)) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
case ">":
return fmt.Sprintf("argMax(value, toFloat64(le)) - (argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f))) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
default:
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid space aggregation operator, should be one of the following: [`<=`, `>`]")
}
}

View File

@@ -2,6 +2,7 @@ package metrictypes
import (
"database/sql/driver"
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -256,3 +257,12 @@ type MetricTableHints struct {
type MetricValueFilter struct {
Value float64
}
type ComparisonSpaceAggregationParam struct {
Operater string `json:"operator" required:"true"`
Threshold float64 `json:"threshold" required:"true"`
}
func (param ComparisonSpaceAggregationParam) StringValue() string {
return fmt.Sprintf("operator=%s:threshold=%f", param.Operater, param.Threshold)
}

View File

@@ -41,6 +41,21 @@ func NewOrganization(displayName string, name string) *Organization {
}
}
func NewOrganizationWithID(id valuer.UUID, displayName string, name string) *Organization {
return &Organization{
Identifiable: Identifiable{
ID: id,
},
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Name: name,
DisplayName: displayName,
Key: NewOrganizationKey(id),
}
}
func NewOrganizationKey(orgID valuer.UUID) uint32 {
hasher := fnv.New32a()

View File

@@ -446,6 +446,8 @@ type MetricAggregation struct {
TimeAggregation metrictypes.TimeAggregation `json:"timeAggregation"`
// space aggregation to apply to the query
SpaceAggregation metrictypes.SpaceAggregation `json:"spaceAggregation"`
// param for space aggregation if needed
ComparisonSpaceAggregationParam *metrictypes.ComparisonSpaceAggregationParam `json:"comparisonSpaceAggregationParam"`
// table hints to use for the query
TableHints *metrictypes.MetricTableHints `json:"-"`
// value filter to apply to the query

View File

@@ -7,7 +7,7 @@ import (
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/SigNoz/signoz/pkg/units"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -201,12 +201,12 @@ func sortThresholds(thresholds []BasicRuleThreshold) {
// convertToRuleUnit converts the given value from the target unit to the rule unit
func (b BasicRuleThreshold) convertToRuleUnit(val float64, ruleUnit string) float64 {
unitConverter := converter.FromUnit(converter.Unit(b.TargetUnit))
unitConverter := units.ConverterFromUnit(units.Unit(b.TargetUnit))
// convert the target value to the y-axis unit
value := unitConverter.Convert(converter.Value{
value := unitConverter.Convert(units.Value{
F: val,
U: converter.Unit(b.TargetUnit),
}, converter.Unit(ruleUnit))
U: units.Unit(b.TargetUnit),
}, units.Unit(ruleUnit))
return value.F
}

View File

@@ -32,6 +32,8 @@ type MetadataStore interface {
// FetchTemporalityMulti fetches the temporality for multiple metrics
FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error)
FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error)
// ListLogsJSONIndexes lists the JSON indexes for the logs table.
ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error)

View File

@@ -16,6 +16,7 @@ type MockMetadataStore struct {
RelatedValuesMap map[string][]string
AllValuesMap map[string]*telemetrytypes.TelemetryFieldValues
TemporalityMap map[string]metrictypes.Temporality
TypeMap map[string]metrictypes.Type
PromotedPathsMap map[string]bool
LogsJSONIndexesMap map[string][]schemamigrator.Index
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
@@ -28,6 +29,7 @@ func NewMockMetadataStore() *MockMetadataStore {
RelatedValuesMap: make(map[string][]string),
AllValuesMap: make(map[string]*telemetrytypes.TelemetryFieldValues),
TemporalityMap: make(map[string]metrictypes.Temporality),
TypeMap: make(map[string]metrictypes.Type),
PromotedPathsMap: make(map[string]bool),
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
@@ -287,6 +289,27 @@ func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, queryTime
return result, nil
}
// FetchTemporalityMulti fetches the temporality for multiple metrics
func (m *MockMetadataStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
temporalities := make(map[string]metrictypes.Temporality)
types := make(map[string]metrictypes.Type)
for _, metricName := range metricNames {
if temporality, exists := m.TemporalityMap[metricName]; exists {
temporalities[metricName] = temporality
} else {
temporalities[metricName] = metrictypes.Unknown
}
if metricType, exists := m.TypeMap[metricName]; exists {
types[metricName] = metricType
} else {
types[metricName] = metrictypes.UnspecifiedType
}
}
return temporalities, types, nil
}
// SetTemporality sets the temporality for a metric in the mock store
func (m *MockMetadataStore) SetTemporality(metricName string, temporality metrictypes.Temporality) {
m.TemporalityMap[metricName] = temporality

View File

@@ -1,4 +1,4 @@
package converter
package units
// Unit represents a unit of measurement
type Unit string
@@ -40,8 +40,8 @@ var (
NoneConverter = &noneConverter{}
)
// FromUnit returns a converter for the given unit
func FromUnit(u Unit) Converter {
// ConverterFromUnit returns a converter for the given unit
func ConverterFromUnit(u Unit) Converter {
switch u {
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min", "w", "wk":
return DurationConverter

View File

@@ -1,4 +1,4 @@
package converter
package units
// boolConverter is a Converter implementation for bool
type boolConverter struct{}

View File

@@ -1,4 +1,4 @@
package converter
package units
const (
// base 10 (SI prefixes)

View File

@@ -1,4 +1,4 @@
package converter
package units
const (
// base 10 (SI prefixes)

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package converter
package units
// percentConverter is a converter for percent unit
type percentConverter struct{}

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

Some files were not shown because too many files have changed in this diff Show More