mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-27 18:52:53 +00:00
Compare commits
4 Commits
issue_4071
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77b5b80fd | ||
|
|
43509681fa | ||
|
|
ff5fcc0e98 | ||
|
|
122d88c4d2 |
@@ -320,4 +320,3 @@ user:
|
|||||||
# The name of the organization to create or look up for the root user.
|
# The name of the organization to create or look up for the root user.
|
||||||
org:
|
org:
|
||||||
name: default
|
name: default
|
||||||
id: 00000000-0000-0000-0000-000000000000
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.113.0
|
image: signoz/signoz:v0.112.1
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@@ -213,7 +213,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.144.1
|
image: signoz/signoz-otel-collector:v0.142.1
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
command:
|
command:
|
||||||
@@ -241,7 +241,7 @@ services:
|
|||||||
replicas: 3
|
replicas: 3
|
||||||
signoz-telemetrystore-migrator:
|
signoz-telemetrystore-migrator:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.144.1
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||||
environment:
|
environment:
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.113.0
|
image: signoz/signoz:v0.112.1
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
volumes:
|
volumes:
|
||||||
@@ -139,7 +139,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.144.1
|
image: signoz/signoz-otel-collector:v0.142.1
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
command:
|
command:
|
||||||
@@ -167,7 +167,7 @@ services:
|
|||||||
replicas: 3
|
replicas: 3
|
||||||
signoz-telemetrystore-migrator:
|
signoz-telemetrystore-migrator:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:v0.144.1
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||||
environment:
|
environment:
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||||
|
|||||||
@@ -82,12 +82,6 @@ exporters:
|
|||||||
timeout: 45s
|
timeout: 45s
|
||||||
sending_queue:
|
sending_queue:
|
||||||
enabled: false
|
enabled: false
|
||||||
metadataexporter:
|
|
||||||
cache:
|
|
||||||
provider: in_memory
|
|
||||||
dsn: tcp://clickhouse:9000/signoz_metadata
|
|
||||||
enabled: true
|
|
||||||
timeout: 45s
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
logs:
|
logs:
|
||||||
@@ -99,19 +93,19 @@ service:
|
|||||||
traces:
|
traces:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [signozspanmetrics/delta, batch]
|
processors: [signozspanmetrics/delta, batch]
|
||||||
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
exporters: [clickhousetraces, signozmeter]
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
exporters: [signozclickhousemetrics, signozmeter]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
exporters: [signozclickhousemetrics, signozmeter]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
|
exporters: [clickhouselogsexporter, signozmeter]
|
||||||
metrics/meter:
|
metrics/meter:
|
||||||
receivers: [signozmeter]
|
receivers: [signozmeter]
|
||||||
processors: [batch/meter]
|
processors: [batch/meter]
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
@@ -204,7 +204,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
@@ -229,7 +229,7 @@ services:
|
|||||||
- "4318:4318" # OTLP HTTP receiver
|
- "4318:4318" # OTLP HTTP receiver
|
||||||
signoz-telemetrystore-migrator:
|
signoz-telemetrystore-migrator:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||||
container_name: signoz-telemetrystore-migrator
|
container_name: signoz-telemetrystore-migrator
|
||||||
environment:
|
environment:
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # signoz port
|
- "8080:8080" # signoz port
|
||||||
@@ -132,7 +132,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
otel-collector:
|
otel-collector:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
@@ -157,7 +157,7 @@ services:
|
|||||||
- "4318:4318" # OTLP HTTP receiver
|
- "4318:4318" # OTLP HTTP receiver
|
||||||
signoz-telemetrystore-migrator:
|
signoz-telemetrystore-migrator:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||||
container_name: signoz-telemetrystore-migrator
|
container_name: signoz-telemetrystore-migrator
|
||||||
environment:
|
environment:
|
||||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||||
|
|||||||
@@ -82,12 +82,6 @@ exporters:
|
|||||||
timeout: 45s
|
timeout: 45s
|
||||||
sending_queue:
|
sending_queue:
|
||||||
enabled: false
|
enabled: false
|
||||||
metadataexporter:
|
|
||||||
cache:
|
|
||||||
provider: in_memory
|
|
||||||
dsn: tcp://clickhouse:9000/signoz_metadata
|
|
||||||
enabled: true
|
|
||||||
timeout: 45s
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
logs:
|
logs:
|
||||||
@@ -99,19 +93,19 @@ service:
|
|||||||
traces:
|
traces:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [signozspanmetrics/delta, batch]
|
processors: [signozspanmetrics/delta, batch]
|
||||||
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
exporters: [clickhousetraces, signozmeter]
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
exporters: [signozclickhousemetrics, signozmeter]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
exporters: [signozclickhousemetrics, signozmeter]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
|
exporters: [clickhouselogsexporter, signozmeter]
|
||||||
metrics/meter:
|
metrics/meter:
|
||||||
receivers: [signozmeter]
|
receivers: [signozmeter]
|
||||||
processors: [batch/meter]
|
processors: [batch/meter]
|
||||||
|
|||||||
@@ -842,17 +842,6 @@ components:
|
|||||||
- temporality
|
- temporality
|
||||||
- isMonotonic
|
- isMonotonic
|
||||||
type: object
|
type: object
|
||||||
MetrictypesComparisonSpaceAggregationParam:
|
|
||||||
properties:
|
|
||||||
operator:
|
|
||||||
type: string
|
|
||||||
threshold:
|
|
||||||
format: double
|
|
||||||
type: number
|
|
||||||
required:
|
|
||||||
- operator
|
|
||||||
- threshold
|
|
||||||
type: object
|
|
||||||
MetrictypesSpaceAggregation:
|
MetrictypesSpaceAggregation:
|
||||||
enum:
|
enum:
|
||||||
- sum
|
- sum
|
||||||
@@ -1149,8 +1138,6 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
Querybuildertypesv5MetricAggregation:
|
Querybuildertypesv5MetricAggregation:
|
||||||
properties:
|
properties:
|
||||||
comparisonSpaceAggregationParam:
|
|
||||||
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
|
|
||||||
metricName:
|
metricName:
|
||||||
type: string
|
type: string
|
||||||
reduceTo:
|
reduceTo:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
|
"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/utils/timestamp"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/units"
|
"github.com/SigNoz/signoz/pkg/query-service/formatter"
|
||||||
|
|
||||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
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()
|
prevState := r.State()
|
||||||
|
|
||||||
valueFormatter := units.FormatterFromUnit(r.Unit())
|
valueFormatter := formatter.FromUnit(r.Unit())
|
||||||
|
|
||||||
var res ruletypes.Vector
|
var res ruletypes.Vector
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -117,7 +117,6 @@
|
|||||||
"lucide-react": "0.498.0",
|
"lucide-react": "0.498.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"motion": "12.4.13",
|
"motion": "12.4.13",
|
||||||
"nuqs": "2.8.8",
|
|
||||||
"overlayscrollbars": "^2.8.1",
|
"overlayscrollbars": "^2.8.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
@@ -131,7 +130,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-drag-listview": "2.0.0",
|
"react-drag-listview": "2.0.0",
|
||||||
"react-error-boundary": "4.0.11",
|
"react-error-boundary": "4.0.11",
|
||||||
"react-force-graph-2d": "^1.29.1",
|
"react-force-graph": "^1.43.0",
|
||||||
"react-full-screen": "1.1.1",
|
"react-full-screen": "1.1.1",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-helmet-async": "1.3.0",
|
"react-helmet-async": "1.3.0",
|
||||||
@@ -163,8 +162,7 @@
|
|||||||
"webpack": "5.94.0",
|
"webpack": "5.94.0",
|
||||||
"webpack-dev-server": "^5.2.1",
|
"webpack-dev-server": "^5.2.1",
|
||||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||||
"xstate": "^4.31.0",
|
"xstate": "^4.31.0"
|
||||||
"zustand": "5.0.11"
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -289,4 +287,4 @@
|
|||||||
"on-headers": "^1.1.0",
|
"on-headers": "^1.1.0",
|
||||||
"tmp": "0.2.4"
|
"tmp": "0.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1006,18 +1006,6 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
|||||||
unit: string;
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetrictypesComparisonSpaceAggregationParamDTO {
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
operator: string;
|
|
||||||
/**
|
|
||||||
* @type number
|
|
||||||
* @format double
|
|
||||||
*/
|
|
||||||
threshold: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MetrictypesSpaceAggregationDTO {
|
export enum MetrictypesSpaceAggregationDTO {
|
||||||
sum = 'sum',
|
sum = 'sum',
|
||||||
avg = 'avg',
|
avg = 'avg',
|
||||||
@@ -1379,7 +1367,6 @@ export interface Querybuildertypesv5LogAggregationDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Querybuildertypesv5MetricAggregationDTO {
|
export interface Querybuildertypesv5MetricAggregationDTO {
|
||||||
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
|
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export interface HostListResponse {
|
|||||||
total: number;
|
total: number;
|
||||||
sentAnyHostMetricsData: boolean;
|
sentAnyHostMetricsData: boolean;
|
||||||
isSendingK8SAgentMetrics: boolean;
|
isSendingK8SAgentMetrics: boolean;
|
||||||
endTimeBeforeRetention: boolean;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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>);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -248,11 +248,6 @@ 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
|
* 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.
|
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
|
||||||
@@ -275,7 +270,10 @@ export const formatDecimalWithLeadingZeros = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use toLocaleString to get a full decimal representation without scientific notation.
|
// Use toLocaleString to get a full decimal representation without scientific notation.
|
||||||
const numStr = intlNumberFormatter.format(value);
|
const numStr = value.toLocaleString('en-US', {
|
||||||
|
useGrouping: false,
|
||||||
|
maximumFractionDigits: 20,
|
||||||
|
});
|
||||||
|
|
||||||
const [integerPart, decimalPart = ''] = numStr.split('.');
|
const [integerPart, decimalPart = ''] = numStr.split('.');
|
||||||
|
|
||||||
|
|||||||
@@ -86,13 +86,8 @@ function LogDetailInner({
|
|||||||
const handleClickOutside = (e: MouseEvent): void => {
|
const handleClickOutside = (e: MouseEvent): void => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
// Don't close if clicking on drawer content, overlays, or portal elements
|
// Don't close if clicking on explicitly ignored regions
|
||||||
if (
|
if (target.closest('[data-log-detail-ignore="true"]')) {
|
||||||
target.closest('[data-log-detail-ignore="true"]') ||
|
|
||||||
target.closest('.cm-tooltip-autocomplete') ||
|
|
||||||
target.closest('.drawer-popover') ||
|
|
||||||
target.closest('.query-status-popover')
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,11 +400,7 @@ function LogDetailInner({
|
|||||||
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
|
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
|
||||||
<div className="log-detail-drawer__log">
|
<div className="log-detail-drawer__log">
|
||||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||||
<Tooltip
|
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||||
title={removeEscapeCharacters(log?.body)}
|
|
||||||
placement="left"
|
|
||||||
mouseLeaveDelay={0}
|
|
||||||
>
|
|
||||||
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@@ -475,7 +466,6 @@ function LogDetailInner({
|
|||||||
title="Show Filters"
|
title="Show Filters"
|
||||||
placement="topLeft"
|
placement="topLeft"
|
||||||
aria-label="Show Filters"
|
aria-label="Show Filters"
|
||||||
mouseLeaveDelay={0}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
@@ -491,7 +481,6 @@ function LogDetailInner({
|
|||||||
aria-label={
|
aria-label={
|
||||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||||
}
|
}
|
||||||
mouseLeaveDelay={0}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ function AddToQueryHOC({
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||||
<Popover
|
<Popover placement="top" content={popOverContent}>
|
||||||
overlayClassName="drawer-popover"
|
|
||||||
placement="top"
|
|
||||||
content={popOverContent}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ function CopyClipboardHOC({
|
|||||||
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
||||||
<Popover
|
<Popover
|
||||||
placement="top"
|
placement="top"
|
||||||
overlayClassName="drawer-popover"
|
|
||||||
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
|
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
|||||||
|
|
||||||
export const defaultTableStyle: CSSProperties = {
|
export const defaultTableStyle: CSSProperties = {
|
||||||
minWidth: '40rem',
|
minWidth: '40rem',
|
||||||
maxWidth: '90rem',
|
maxWidth: '60rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultListViewPanelStyle: CSSProperties = {
|
export const defaultListViewPanelStyle: CSSProperties = {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
startCompletion,
|
startCompletion,
|
||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||||
@@ -85,7 +86,6 @@ interface QuerySearchProps {
|
|||||||
signalSource?: string;
|
signalSource?: string;
|
||||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||||
onRun?: (query: string) => void;
|
onRun?: (query: string) => void;
|
||||||
showFilterSuggestionsWithoutMetric?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function QuerySearch({
|
function QuerySearch({
|
||||||
@@ -96,7 +96,6 @@ function QuerySearch({
|
|||||||
onRun,
|
onRun,
|
||||||
signalSource,
|
signalSource,
|
||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
showFilterSuggestionsWithoutMetric,
|
|
||||||
}: QuerySearchProps): JSX.Element {
|
}: QuerySearchProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||||
@@ -253,8 +252,7 @@ function QuerySearch({
|
|||||||
async (searchText?: string): Promise<void> => {
|
async (searchText?: string): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
dataSource === DataSource.METRICS &&
|
dataSource === DataSource.METRICS &&
|
||||||
!queryData.aggregateAttribute?.key &&
|
!queryData.aggregateAttribute?.key
|
||||||
!showFilterSuggestionsWithoutMetric
|
|
||||||
) {
|
) {
|
||||||
setKeySuggestions([]);
|
setKeySuggestions([]);
|
||||||
return;
|
return;
|
||||||
@@ -303,7 +301,6 @@ function QuerySearch({
|
|||||||
queryData.aggregateAttribute?.key,
|
queryData.aggregateAttribute?.key,
|
||||||
signalSource,
|
signalSource,
|
||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
showFilterSuggestionsWithoutMetric,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -567,7 +564,15 @@ function QuerySearch({
|
|||||||
const lastPos = lastPosRef.current;
|
const lastPos = lastPosRef.current;
|
||||||
|
|
||||||
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
||||||
setCursorPos(newPos);
|
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;
|
||||||
|
});
|
||||||
lastPosRef.current = newPos;
|
lastPosRef.current = newPos;
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
@@ -1323,10 +1328,7 @@ function QuerySearch({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="query-where-clause-editor-container">
|
<div className="query-where-clause-editor-container">
|
||||||
<Tooltip
|
<Tooltip title={getTooltipContent()} placement="left">
|
||||||
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
|
|
||||||
placement="left"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href="https://signoz.io/docs/userguide/search-syntax/"
|
href="https://signoz.io/docs/userguide/search-syntax/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1560,7 +1562,6 @@ QuerySearch.defaultProps = {
|
|||||||
hardcodedAttributeKeys: undefined,
|
hardcodedAttributeKeys: undefined,
|
||||||
placeholder:
|
placeholder:
|
||||||
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
|
||||||
showFilterSuggestionsWithoutMetric: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QuerySearch;
|
export default QuerySearch;
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ function ValueGraph({
|
|||||||
value,
|
value,
|
||||||
rawValue,
|
rawValue,
|
||||||
thresholds,
|
thresholds,
|
||||||
yAxisUnit,
|
|
||||||
}: ValueGraphProps): JSX.Element {
|
}: ValueGraphProps): JSX.Element {
|
||||||
const { t } = useTranslation(['valueGraph']);
|
const { t } = useTranslation(['valueGraph']);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -88,7 +87,7 @@ function ValueGraph({
|
|||||||
const {
|
const {
|
||||||
threshold,
|
threshold,
|
||||||
isConflictingThresholds,
|
isConflictingThresholds,
|
||||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
|
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -156,7 +155,6 @@ interface ValueGraphProps {
|
|||||||
value: string;
|
value: string;
|
||||||
rawValue: number;
|
rawValue: number;
|
||||||
thresholds: ThresholdProps[];
|
thresholds: ThresholdProps[];
|
||||||
yAxisUnit?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValueGraph;
|
export default ValueGraph;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
|
||||||
function doesValueSatisfyThreshold(
|
function compareThreshold(
|
||||||
rawValue: number,
|
rawValue: number,
|
||||||
threshold: ThresholdProps,
|
threshold: ThresholdProps,
|
||||||
yAxisUnit?: string,
|
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
threshold.thresholdOperator === undefined ||
|
threshold.thresholdOperator === undefined ||
|
||||||
@@ -12,14 +11,31 @@ function doesValueSatisfyThreshold(
|
|||||||
) {
|
) {
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return evaluateThresholdWithConvertedValue(
|
function extractNumbersFromString(inputString: string): number[] {
|
||||||
rawValue,
|
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||||
threshold.thresholdValue,
|
const matches = inputString.match(regex);
|
||||||
threshold.thresholdOperator,
|
|
||||||
threshold.thresholdUnit,
|
if (matches) {
|
||||||
yAxisUnit,
|
return matches.map(Number);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHighestPrecedenceThreshold(
|
function getHighestPrecedenceThreshold(
|
||||||
@@ -44,32 +60,21 @@ function getHighestPrecedenceThreshold(
|
|||||||
return highestPrecedenceThreshold;
|
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(
|
export function getBackgroundColorAndThresholdCheck(
|
||||||
thresholds: ThresholdProps[],
|
thresholds: ThresholdProps[],
|
||||||
rawValue: number,
|
rawValue: number,
|
||||||
yAxisUnit?: string,
|
|
||||||
): {
|
): {
|
||||||
threshold: ThresholdProps;
|
threshold: ThresholdProps;
|
||||||
isConflictingThresholds: boolean;
|
isConflictingThresholds: boolean;
|
||||||
} {
|
} {
|
||||||
const matchingThresholds = thresholds.filter((threshold) => {
|
const matchingThresholds = thresholds.filter((threshold) =>
|
||||||
const numbers = extractNumbersFromString(rawValue.toString());
|
compareThreshold(
|
||||||
if (numbers.length === 0) {
|
extractNumbersFromString(
|
||||||
return false;
|
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
||||||
}
|
)[0],
|
||||||
return doesValueSatisfyThreshold(numbers[0], threshold, yAxisUnit);
|
threshold,
|
||||||
});
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (matchingThresholds.length === 0) {
|
if (matchingThresholds.length === 0) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ function YAxisUnitSelector({
|
|||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
source,
|
source,
|
||||||
initialValue,
|
initialValue,
|
||||||
categoriesOverride,
|
|
||||||
containerClassName,
|
|
||||||
}: YAxisUnitSelectorProps): JSX.Element {
|
}: YAxisUnitSelectorProps): JSX.Element {
|
||||||
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
||||||
|
|
||||||
@@ -68,14 +66,10 @@ function YAxisUnitSelector({
|
|||||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoriesToRender = useMemo(() => {
|
const categories = getYAxisCategories(source);
|
||||||
return categoriesOverride || getYAxisCategories(source);
|
|
||||||
}, [categoriesOverride, source]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="y-axis-unit-selector-component">
|
||||||
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={universalUnit}
|
value={universalUnit}
|
||||||
@@ -96,7 +90,7 @@ function YAxisUnitSelector({
|
|||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
allowClear
|
allowClear
|
||||||
>
|
>
|
||||||
{categoriesToRender.map((category) => (
|
{categories.map((category) => (
|
||||||
<Select.OptGroup key={category.name} label={category.name}>
|
<Select.OptGroup key={category.name} label={category.name}>
|
||||||
{category.units.map((unit) => (
|
{category.units.map((unit) => (
|
||||||
<Select.Option key={unit.id} value={unit.id}>
|
<Select.Option key={unit.id} value={unit.id}>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { YAxisCategoryNames } from '../constants';
|
import { YAxisSource } from '../types';
|
||||||
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
|
||||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||||
|
|
||||||
describe('YAxisUnitSelector', () => {
|
describe('YAxisUnitSelector', () => {
|
||||||
@@ -124,34 +123,4 @@ describe('YAxisUnitSelector', () => {
|
|||||||
const warningIcon = screen.queryByLabelText('warning');
|
const warningIcon = screen.queryByLabelText('warning');
|
||||||
expect(warningIcon).not.toBeInTheDocument();
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export interface YAxisUnitSelectorProps {
|
|||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
source: YAxisSource;
|
source: YAxisSource;
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
categoriesOverride?: YAxisCategory[];
|
|
||||||
containerClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UniversalYAxisUnit {
|
export enum UniversalYAxisUnit {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||||
|
|
||||||
const { currentVersion } = useSelector<AppState, AppReducer>(
|
const { latestVersion } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -213,9 +213,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||||
getChangelogByVersion(currentVersion, changelogForTenant),
|
getChangelogByVersion(latestVersion, changelogForTenant),
|
||||||
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
|
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
|
||||||
enabled: isLoggedIn && Boolean(currentVersion),
|
enabled: isLoggedIn && Boolean(latestVersion),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
!changelog &&
|
!changelog &&
|
||||||
!getChangelogByVersionResponse.isLoading &&
|
!getChangelogByVersionResponse.isLoading &&
|
||||||
isLoggedIn &&
|
isLoggedIn &&
|
||||||
Boolean(currentVersion)
|
Boolean(latestVersion)
|
||||||
) {
|
) {
|
||||||
getChangelogByVersionResponse.refetch();
|
getChangelogByVersionResponse.refetch();
|
||||||
}
|
}
|
||||||
@@ -237,9 +237,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
let timer: ReturnType<typeof setTimeout>;
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
if (
|
if (
|
||||||
isCloudUserVal &&
|
isCloudUserVal &&
|
||||||
Boolean(currentVersion) &&
|
Boolean(latestVersion) &&
|
||||||
seenChangelogVersion != null &&
|
seenChangelogVersion != null &&
|
||||||
currentVersion !== seenChangelogVersion &&
|
latestVersion !== seenChangelogVersion &&
|
||||||
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
|
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
|
||||||
!isWorkspaceAccessRestricted
|
!isWorkspaceAccessRestricted
|
||||||
) {
|
) {
|
||||||
@@ -255,7 +255,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
isCloudUserVal,
|
isCloudUserVal,
|
||||||
currentVersion,
|
latestVersion,
|
||||||
seenChangelogVersion,
|
seenChangelogVersion,
|
||||||
toggleChangelogModal,
|
toggleChangelogModal,
|
||||||
isWorkspaceAccessRestricted,
|
isWorkspaceAccessRestricted,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
enqueueDescendantsOfVariable,
|
||||||
enqueueFetchOfAllVariables,
|
enqueueFetchOfAllVariables,
|
||||||
@@ -32,9 +31,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||||
|
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const dashboardId = useDashboardVariablesSelector(
|
|
||||||
(state) => state.dashboardId,
|
|
||||||
);
|
|
||||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||||
(state) => state.sortedVariablesArray,
|
(state) => state.sortedVariablesArray,
|
||||||
);
|
);
|
||||||
@@ -100,28 +96,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
updateUrlVariable(name || id, value);
|
updateUrlVariable(name || id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronously update the external store with the new variable value so that
|
|
||||||
// child variables see the updated parent value when they refetch, rather than
|
|
||||||
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
|
|
||||||
const updatedVariables = { ...dashboardVariables };
|
|
||||||
if (updatedVariables[id]) {
|
|
||||||
updatedVariables[id] = {
|
|
||||||
...updatedVariables[id],
|
|
||||||
selectedValue: value,
|
|
||||||
allSelected,
|
|
||||||
haveCustomValuesSelected,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (updatedVariables[name]) {
|
|
||||||
updatedVariables[name] = {
|
|
||||||
...updatedVariables[name],
|
|
||||||
selectedValue: value,
|
|
||||||
allSelected,
|
|
||||||
haveCustomValuesSelected,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
|
|
||||||
|
|
||||||
setSelectedDashboard((prev) => {
|
setSelectedDashboard((prev) => {
|
||||||
if (prev) {
|
if (prev) {
|
||||||
const oldVariables = { ...prev?.data.variables };
|
const oldVariables = { ...prev?.data.variables };
|
||||||
@@ -156,12 +130,10 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cascade: enqueue query-type descendants for refetching.
|
// Cascade: enqueue query-type descendants for refetching
|
||||||
// Safe to call synchronously now that the store already has the updated value.
|
|
||||||
enqueueDescendantsOfVariable(name);
|
enqueueDescendantsOfVariable(name);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
dashboardId,
|
|
||||||
dashboardVariables,
|
dashboardVariables,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
updateUrlVariable,
|
updateUrlVariable,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
|
|||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
|
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
|
||||||
import sortValues from 'lib/dashboardVariables/sortVariableValues';
|
import sortValues from 'lib/dashboardVariables/sortVariableValues';
|
||||||
import { isArray, isEmpty } from 'lodash-es';
|
import { isArray, isEmpty, isString } from 'lodash-es';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@@ -54,7 +54,7 @@ function QueryVariableInput({
|
|||||||
onChange,
|
onChange,
|
||||||
onDropdownVisibleChange,
|
onDropdownVisibleChange,
|
||||||
handleClear,
|
handleClear,
|
||||||
getDefaultValue,
|
applyDefaultIfNeeded,
|
||||||
} = useDashboardVariableSelectHelper({
|
} = useDashboardVariableSelectHelper({
|
||||||
variableData,
|
variableData,
|
||||||
optionsData,
|
optionsData,
|
||||||
@@ -68,93 +68,81 @@ function QueryVariableInput({
|
|||||||
try {
|
try {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
// This is just a check given the previously undefined typed name prop. Not significant
|
|
||||||
// This will be changed when we change the schema
|
|
||||||
// TODO: @AshwinBhatkal Perses
|
|
||||||
if (!variableData.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the response is not an array, premature return
|
|
||||||
if (
|
if (
|
||||||
!variablesRes?.variableValues ||
|
variablesRes?.variableValues &&
|
||||||
!Array.isArray(variablesRes?.variableValues)
|
Array.isArray(variablesRes?.variableValues)
|
||||||
) {
|
) {
|
||||||
return;
|
const newOptionsData = sortValues(
|
||||||
}
|
variablesRes?.variableValues,
|
||||||
|
variableData.sort,
|
||||||
const sortedNewOptions = sortValues(
|
|
||||||
variablesRes.variableValues,
|
|
||||||
variableData.sort,
|
|
||||||
);
|
|
||||||
const sortedOldOptions = sortValues(optionsData, variableData.sort);
|
|
||||||
|
|
||||||
// if options are the same as before, no need to update state or check for selected value validity
|
|
||||||
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
|
|
||||||
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
|
|
||||||
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOptionsData(sortedNewOptions);
|
|
||||||
|
|
||||||
let isSelectedValueMissingInNewOptions = false;
|
|
||||||
|
|
||||||
// Check if currently selected value(s) are present in the new options list
|
|
||||||
if (isArray(variableData.selectedValue)) {
|
|
||||||
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
|
|
||||||
(val) => !sortedNewOptions.includes(val),
|
|
||||||
);
|
);
|
||||||
} else if (
|
|
||||||
variableData.selectedValue &&
|
|
||||||
!sortedNewOptions.includes(variableData.selectedValue)
|
|
||||||
) {
|
|
||||||
isSelectedValueMissingInNewOptions = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
|
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||||
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
|
|
||||||
if (
|
|
||||||
variableData.multiSelect &&
|
|
||||||
variableData.showALLOption &&
|
|
||||||
variableData.allSelected &&
|
|
||||||
isSelectedValueMissingInNewOptions
|
|
||||||
) {
|
|
||||||
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
|
|
||||||
|
|
||||||
// Update tempSelection to maintain ALL state when dropdown is open
|
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||||
if (tempSelection !== undefined) {
|
let valueNotInList = false;
|
||||||
setTempSelection(sortedNewOptions.map((option) => option.toString()));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = variableData.selectedValue;
|
if (isArray(variableData.selectedValue)) {
|
||||||
let allSelected = false;
|
variableData.selectedValue.forEach((val) => {
|
||||||
|
if (!newOptionsData.includes(val)) {
|
||||||
|
valueNotInList = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
isString(variableData.selectedValue) &&
|
||||||
|
!newOptionsData.includes(variableData.selectedValue)
|
||||||
|
) {
|
||||||
|
valueNotInList = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (variableData.multiSelect) {
|
if (variableData.name && (valueNotInList || variableData.allSelected)) {
|
||||||
const { selectedValue } = variableData;
|
if (
|
||||||
allSelected =
|
variableData.allSelected &&
|
||||||
sortedNewOptions.length > 0 &&
|
variableData.multiSelect &&
|
||||||
Array.isArray(selectedValue) &&
|
variableData.showALLOption
|
||||||
sortedNewOptions.every((option) => selectedValue.includes(option));
|
) {
|
||||||
}
|
if (
|
||||||
|
variableData.name &&
|
||||||
|
variableData.id &&
|
||||||
|
!isEmpty(variableData.selectedValue)
|
||||||
|
) {
|
||||||
|
onValueUpdate(
|
||||||
|
variableData.name,
|
||||||
|
variableData.id,
|
||||||
|
newOptionsData,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
// Update tempSelection to maintain ALL state when dropdown is open
|
||||||
variableData.name &&
|
if (tempSelection !== undefined) {
|
||||||
variableData.id &&
|
setTempSelection(newOptionsData.map((option) => option.toString()));
|
||||||
!isEmpty(variableData.selectedValue)
|
}
|
||||||
) {
|
} else {
|
||||||
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
const value = variableData.selectedValue;
|
||||||
} else {
|
let allSelected = false;
|
||||||
const defaultValue = getDefaultValue(sortedNewOptions);
|
|
||||||
if (defaultValue !== undefined) {
|
if (variableData.multiSelect) {
|
||||||
onValueUpdate(
|
const { selectedValue } = variableData;
|
||||||
variableData.name,
|
allSelected =
|
||||||
variableData.id,
|
newOptionsData.length > 0 &&
|
||||||
defaultValue,
|
Array.isArray(selectedValue) &&
|
||||||
allSelected,
|
newOptionsData.every((option) => selectedValue.includes(option));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
variableData.name &&
|
||||||
|
variableData.id &&
|
||||||
|
!isEmpty(variableData.selectedValue)
|
||||||
|
) {
|
||||||
|
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptionsData(newOptionsData);
|
||||||
|
// Apply default if no value is selected (e.g., new variable, first load)
|
||||||
|
applyDefaultIfNeeded(newOptionsData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -167,7 +155,7 @@ function QueryVariableInput({
|
|||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
tempSelection,
|
tempSelection,
|
||||||
setTempSelection,
|
setTempSelection,
|
||||||
getDefaultValue,
|
applyDefaultIfNeeded,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { act, render } from '@testing-library/react';
|
import { act, render } from '@testing-library/react';
|
||||||
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
|
||||||
import {
|
import {
|
||||||
dashboardVariablesStore,
|
dashboardVariablesStore,
|
||||||
setDashboardVariablesStore,
|
setDashboardVariablesStore,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
IDashboardVariablesStoreState,
|
IDashboardVariablesStoreState,
|
||||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
|
||||||
enqueueFetchOfAllVariables,
|
enqueueFetchOfAllVariables,
|
||||||
initializeVariableFetchStore,
|
initializeVariableFetchStore,
|
||||||
} from 'providers/Dashboard/store/variableFetchStore';
|
} from 'providers/Dashboard/store/variableFetchStore';
|
||||||
@@ -19,17 +17,6 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
|||||||
|
|
||||||
import DashboardVariableSelection from '../DashboardVariableSelection';
|
import DashboardVariableSelection from '../DashboardVariableSelection';
|
||||||
|
|
||||||
// Mutable container to capture the onValueUpdate callback from VariableItem
|
|
||||||
const mockVariableItemCallbacks: {
|
|
||||||
onValueUpdate?: (
|
|
||||||
name: string,
|
|
||||||
id: string,
|
|
||||||
value: IDashboardVariable['selectedValue'],
|
|
||||||
allSelected: boolean,
|
|
||||||
haveCustomValuesSelected?: boolean,
|
|
||||||
) => void;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
// Mock providers/Dashboard/Dashboard
|
// Mock providers/Dashboard/Dashboard
|
||||||
const mockSetSelectedDashboard = jest.fn();
|
const mockSetSelectedDashboard = jest.fn();
|
||||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||||
@@ -69,14 +56,10 @@ jest.mock('react-redux', () => ({
|
|||||||
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
|
// Mock VariableItem to avoid rendering complexity
|
||||||
jest.mock('../VariableItem', () => ({
|
jest.mock('../VariableItem', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
default: (): JSX.Element => <div data-testid="variable-item" />,
|
||||||
default: (props: any): JSX.Element => {
|
|
||||||
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
|
|
||||||
return <div data-testid="variable-item" />;
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function createVariable(
|
function createVariable(
|
||||||
@@ -217,162 +200,4 @@ describe('DashboardVariableSelection', () => {
|
|||||||
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
|
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
|
||||||
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
|
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onValueUpdate', () => {
|
|
||||||
let updateStoreSpy: jest.SpyInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
resetStore();
|
|
||||||
jest.clearAllMocks();
|
|
||||||
// Real implementation pass-through — we just want to observe calls
|
|
||||||
updateStoreSpy = jest.spyOn(
|
|
||||||
dashboardVariablesStoreModule,
|
|
||||||
'updateDashboardVariablesStore',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
updateStoreSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
|
|
||||||
setDashboardVariablesStore({
|
|
||||||
dashboardId: 'dash-1',
|
|
||||||
variables: {
|
|
||||||
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DashboardVariableSelection />);
|
|
||||||
|
|
||||||
const callOrder: string[] = [];
|
|
||||||
updateStoreSpy.mockImplementation(() => {
|
|
||||||
callOrder.push('updateDashboardVariablesStore');
|
|
||||||
});
|
|
||||||
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
|
|
||||||
callOrder.push('enqueueDescendantsOfVariable');
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mockVariableItemCallbacks.onValueUpdate?.(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
'production',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callOrder).toEqual([
|
|
||||||
'updateDashboardVariablesStore',
|
|
||||||
'enqueueDescendantsOfVariable',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes updated variable value to dashboardVariablesStore', () => {
|
|
||||||
setDashboardVariablesStore({
|
|
||||||
dashboardId: 'dash-1',
|
|
||||||
variables: {
|
|
||||||
env: createVariable({
|
|
||||||
name: 'env',
|
|
||||||
id: 'env-id',
|
|
||||||
order: 0,
|
|
||||||
selectedValue: 'staging',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DashboardVariableSelection />);
|
|
||||||
|
|
||||||
// Clear spy calls that happened during setup/render
|
|
||||||
updateStoreSpy.mockClear();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mockVariableItemCallbacks.onValueUpdate?.(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
'production',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateStoreSpy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
dashboardId: 'dash-1',
|
|
||||||
variables: expect.objectContaining({
|
|
||||||
env: expect.objectContaining({
|
|
||||||
selectedValue: 'production',
|
|
||||||
allSelected: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
|
|
||||||
setDashboardVariablesStore({
|
|
||||||
dashboardId: 'dash-1',
|
|
||||||
variables: {
|
|
||||||
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DashboardVariableSelection />);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mockVariableItemCallbacks.onValueUpdate?.(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
'production',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Must be called immediately — no timer advancement needed
|
|
||||||
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
|
|
||||||
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
|
|
||||||
setDashboardVariablesStore({
|
|
||||||
dashboardId: 'dash-1',
|
|
||||||
variables: {
|
|
||||||
env: createVariable({
|
|
||||||
name: 'env',
|
|
||||||
id: 'env-id',
|
|
||||||
order: 0,
|
|
||||||
multiSelect: true,
|
|
||||||
showALLOption: true,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<DashboardVariableSelection />);
|
|
||||||
updateStoreSpy.mockClear();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
mockVariableItemCallbacks.onValueUpdate?.(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
['production', 'staging'],
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateStoreSpy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
variables: expect.objectContaining({
|
|
||||||
env: expect.objectContaining({
|
|
||||||
selectedValue: ['production', 'staging'],
|
|
||||||
allSelected: true,
|
|
||||||
haveCustomValuesSelected: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
||||||
import { act, render, waitFor } from '@testing-library/react';
|
|
||||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
|
||||||
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
|
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
|
|
||||||
import QueryVariableInput from '../QueryVariableInput';
|
|
||||||
|
|
||||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
|
|
||||||
|
|
||||||
jest.mock('react-redux', () => ({
|
|
||||||
...jest.requireActual('react-redux'),
|
|
||||||
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function createTestQueryClient(): QueryClient {
|
|
||||||
return new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: { retry: false, refetchOnWindowFocus: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function Wrapper({
|
|
||||||
children,
|
|
||||||
queryClient,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
queryClient: QueryClient;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVariable(
|
|
||||||
overrides: Partial<IDashboardVariable> = {},
|
|
||||||
): IDashboardVariable {
|
|
||||||
return {
|
|
||||||
id: 'env-id',
|
|
||||||
name: 'env',
|
|
||||||
description: '',
|
|
||||||
type: 'QUERY',
|
|
||||||
sort: 'DISABLED',
|
|
||||||
showALLOption: false,
|
|
||||||
multiSelect: false,
|
|
||||||
order: 0,
|
|
||||||
queryValue: 'SELECT env FROM table',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Put the named variable into 'loading' state so useQuery fires on mount */
|
|
||||||
function setVariableLoading(name: string): void {
|
|
||||||
variableFetchStore.update((draft) => {
|
|
||||||
draft.states[name] = 'loading';
|
|
||||||
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFetchStore(): void {
|
|
||||||
variableFetchStore.set(() => ({
|
|
||||||
states: {},
|
|
||||||
lastUpdated: {},
|
|
||||||
cycleIds: {},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('QueryVariableInput - getOptions logic', () => {
|
|
||||||
const mockOnValueUpdate = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
resetFetchStore();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
resetFetchStore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies default value (first option) when selectedValue is empty on first load', async () => {
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: ['production', 'staging', 'dev'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const variable = createVariable({ selectedValue: undefined });
|
|
||||||
setVariableLoading('env');
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
'production', // first option by default
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps existing selectedValue when it is present in new options', async () => {
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: ['production', 'staging'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const variable = createVariable({ selectedValue: 'staging' });
|
|
||||||
setVariableLoading('env');
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
'staging',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selects all new options when allSelected=true and value is missing from new options', async () => {
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: ['production', 'staging'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const variable = createVariable({
|
|
||||||
selectedValue: ['old-env'],
|
|
||||||
allSelected: true,
|
|
||||||
multiSelect: true,
|
|
||||||
showALLOption: true,
|
|
||||||
});
|
|
||||||
setVariableLoading('env');
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
|
||||||
'env',
|
|
||||||
'env-id',
|
|
||||||
['production', 'staging'],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call onValueUpdate a second time when options have not changed', async () => {
|
|
||||||
const mockQueryFn = jest.fn().mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: ['production', 'staging'] },
|
|
||||||
});
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
|
|
||||||
|
|
||||||
const variable = createVariable({ selectedValue: 'production' });
|
|
||||||
setVariableLoading('env');
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for first fetch and onValueUpdate call
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
mockOnValueUpdate.mockClear();
|
|
||||||
|
|
||||||
// Trigger a second fetch cycle with the same API response
|
|
||||||
act(() => {
|
|
||||||
variableFetchStore.update((draft) => {
|
|
||||||
draft.states['env'] = 'revalidating';
|
|
||||||
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for second query to fire
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockQueryFn).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Options are unchanged, so onValueUpdate must not fire again
|
|
||||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call onValueUpdate when API returns a non-array response', async () => {
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
const variable = createVariable({ selectedValue: 'production' });
|
|
||||||
setVariableLoading('env');
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dashboardVariablesQuery).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not fire the query when variableData.name is empty', () => {
|
|
||||||
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
|
|
||||||
statusCode: 200,
|
|
||||||
payload: { variableValues: ['production'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Variable with no name — useVariableFetchState will be called with ''
|
|
||||||
// and the query key will have an empty name, leaving it disabled
|
|
||||||
const variable = createVariable({ name: '' });
|
|
||||||
// Note: we do NOT put it in 'loading' state since name is empty
|
|
||||||
// (no variableFetchStore entry for '' means isVariableFetching=false)
|
|
||||||
|
|
||||||
const queryClient = createTestQueryClient();
|
|
||||||
render(
|
|
||||||
<Wrapper queryClient={queryClient}>
|
|
||||||
<QueryVariableInput
|
|
||||||
variableData={variable}
|
|
||||||
existingVariables={{ 'env-id': variable }}
|
|
||||||
onValueUpdate={mockOnValueUpdate}
|
|
||||||
/>
|
|
||||||
</Wrapper>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
|
|
||||||
expect(mockOnValueUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -46,9 +46,6 @@ interface UseDashboardVariableSelectHelperReturn {
|
|||||||
applyDefaultIfNeeded: (
|
applyDefaultIfNeeded: (
|
||||||
overrideOptions?: (string | number | boolean)[],
|
overrideOptions?: (string | number | boolean)[],
|
||||||
) => void;
|
) => void;
|
||||||
getDefaultValue: (
|
|
||||||
overrideOptions?: (string | number | boolean)[],
|
|
||||||
) => string | string[] | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@@ -251,6 +248,5 @@ export function useDashboardVariableSelectHelper({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
applyDefaultIfNeeded,
|
applyDefaultIfNeeded,
|
||||||
getDefaultValue,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function evaluateCondition(
|
|||||||
* @param columnUnit - The current unit of the value.
|
* @param columnUnit - The current unit of the value.
|
||||||
* @returns A boolean indicating whether the value meets the threshold condition.
|
* @returns A boolean indicating whether the value meets the threshold condition.
|
||||||
*/
|
*/
|
||||||
export function evaluateThresholdWithConvertedValue(
|
function evaluateThresholdWithConvertedValue(
|
||||||
value: number,
|
value: number,
|
||||||
thresholdValue: number,
|
thresholdValue: number,
|
||||||
thresholdOperator?: string,
|
thresholdOperator?: string,
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ function GridValueComponent({
|
|||||||
<ValueGraph
|
<ValueGraph
|
||||||
thresholds={thresholds || []}
|
thresholds={thresholds || []}
|
||||||
rawValue={value}
|
rawValue={value}
|
||||||
yAxisUnit={yAxisUnit}
|
|
||||||
value={
|
value={
|
||||||
yAxisUnit
|
yAxisUnit
|
||||||
? getYAxisFormattedValue(
|
? getYAxisFormattedValue(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Skeleton,
|
Skeleton,
|
||||||
@@ -14,93 +14,12 @@ import { InfraMonitoringEvents } from 'constants/events';
|
|||||||
|
|
||||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||||
import {
|
import {
|
||||||
EmptyOrLoadingViewProps,
|
|
||||||
formatDataForTable,
|
formatDataForTable,
|
||||||
getHostsListColumns,
|
getHostsListColumns,
|
||||||
HostRowData,
|
HostRowData,
|
||||||
HostsListTableProps,
|
HostsListTableProps,
|
||||||
} from './utils';
|
} 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({
|
export default function HostsListTable({
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -127,11 +46,6 @@ export default function HostsListTable({
|
|||||||
[data],
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
const endTimeBeforeRetention = useMemo(
|
|
||||||
() => data?.payload?.data?.endTimeBeforeRetention || false,
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedHostMetricsData = useMemo(
|
const formattedHostMetricsData = useMemo(
|
||||||
() => formatDataForTable(hostMetricsData),
|
() => formatDataForTable(hostMetricsData),
|
||||||
[hostMetricsData],
|
[hostMetricsData],
|
||||||
@@ -170,6 +84,12 @@ export default function HostsListTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showNoFilteredHostsMessage =
|
||||||
|
!isFetching &&
|
||||||
|
!isLoading &&
|
||||||
|
formattedHostMetricsData.length === 0 &&
|
||||||
|
filters.items.length > 0;
|
||||||
|
|
||||||
const showHostsEmptyState =
|
const showHostsEmptyState =
|
||||||
!isFetching &&
|
!isFetching &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
@@ -177,36 +97,63 @@ export default function HostsListTable({
|
|||||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||||
!filters.items.length;
|
!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 =
|
const showTableLoadingState =
|
||||||
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
||||||
|
|
||||||
const emptyOrLoadingView = EmptyOrLoadingView({
|
if (isError) {
|
||||||
isError,
|
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||||
errorMessage: data?.error ?? '',
|
}
|
||||||
showHostsEmptyState,
|
|
||||||
sentAnyHostMetricsData,
|
|
||||||
isSendingIncorrectK8SAgentMetrics,
|
|
||||||
showEndTimeBeforeRetentionMessage,
|
|
||||||
showNoRecordsInSelectedTimeRangeMessage,
|
|
||||||
showTableLoadingState,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emptyOrLoadingView) {
|
if (showHostsEmptyState) {
|
||||||
return <>{emptyOrLoadingView}</>;
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import { render, screen } from '@testing-library/react';
|
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 HostsListTable from '../HostsListTable';
|
||||||
import { HostsListTableProps } from '../utils';
|
|
||||||
|
|
||||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||||
|
|
||||||
const createMockHost = (): HostData =>
|
describe('HostsListTable', () => {
|
||||||
({
|
const mockHost = {
|
||||||
hostName: 'test-host-1',
|
hostName: 'test-host-1',
|
||||||
active: true,
|
active: true,
|
||||||
cpu: 0.75,
|
cpu: 0.75,
|
||||||
@@ -18,46 +14,20 @@ const createMockHost = (): HostData =>
|
|||||||
wait: 0.03,
|
wait: 0.03,
|
||||||
load15: 1.5,
|
load15: 1.5,
|
||||||
os: 'linux',
|
os: 'linux',
|
||||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
};
|
||||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
|
||||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
|
||||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
|
||||||
} as HostData);
|
|
||||||
|
|
||||||
const createMockTableData = (
|
const mockTableData = {
|
||||||
overrides: Partial<HostListResponse['data']> = {},
|
|
||||||
): SuccessResponse<HostListResponse> => {
|
|
||||||
const mockHost = createMockHost();
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
message: 'Success',
|
|
||||||
error: null,
|
|
||||||
payload: {
|
payload: {
|
||||||
status: 'success',
|
|
||||||
data: {
|
data: {
|
||||||
type: 'list',
|
hosts: [mockHost],
|
||||||
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 mockOnHostClick = jest.fn();
|
||||||
const mockSetCurrentPage = jest.fn();
|
const mockSetCurrentPage = jest.fn();
|
||||||
const mockSetOrderBy = jest.fn();
|
const mockSetOrderBy = jest.fn();
|
||||||
const mockSetPageSize = jest.fn();
|
const mockSetPageSize = jest.fn();
|
||||||
|
const mockProps = {
|
||||||
const mockProps: HostsListTableProps = {
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
@@ -73,7 +43,7 @@ describe('HostsListTable', () => {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
setOrderBy: mockSetOrderBy,
|
setOrderBy: mockSetOrderBy,
|
||||||
setPageSize: mockSetPageSize,
|
setPageSize: mockSetPageSize,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@@ -81,7 +51,7 @@ describe('HostsListTable', () => {
|
|||||||
{...mockProps}
|
{...mockProps}
|
||||||
isLoading
|
isLoading
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={createMockTableData({ records: [] })}
|
tableData={{ payload: { data: { hosts: [] } } }}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||||
@@ -93,7 +63,7 @@ describe('HostsListTable', () => {
|
|||||||
{...mockProps}
|
{...mockProps}
|
||||||
isFetching
|
isFetching
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={createMockTableData({ records: [] })}
|
tableData={{ payload: { data: { hosts: [] } } }}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||||
@@ -104,56 +74,19 @@ describe('HostsListTable', () => {
|
|||||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
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', () => {
|
it('renders empty state if no hosts are found', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={createMockTableData({
|
tableData={{
|
||||||
records: [],
|
payload: {
|
||||||
})}
|
data: { hosts: [] },
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(
|
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
container.querySelector('.no-filtered-hosts-message-container'),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||||
@@ -161,114 +94,58 @@ describe('HostsListTable', () => {
|
|||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={createMockTableData({
|
tableData={{
|
||||||
sentAnyHostMetricsData: false,
|
...mockTableData,
|
||||||
records: [],
|
payload: {
|
||||||
})}
|
...mockTableData.payload,
|
||||||
/>,
|
data: {
|
||||||
);
|
...mockTableData.payload.data,
|
||||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
sentAnyHostMetricsData: false,
|
||||||
});
|
hosts: [],
|
||||||
|
|
||||||
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<HostsListTable
|
|
||||||
{...mockProps}
|
|
||||||
hostMetricsData={[]}
|
|
||||||
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(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
expect(
|
});
|
||||||
screen.getByText(
|
|
||||||
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
|
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
||||||
),
|
const { container } = render(
|
||||||
).toBeInTheDocument();
|
<HostsListTable
|
||||||
|
{...mockProps}
|
||||||
|
hostMetricsData={[]}
|
||||||
|
tableData={{
|
||||||
|
...mockTableData,
|
||||||
|
payload: {
|
||||||
|
...mockTableData.payload,
|
||||||
|
data: {
|
||||||
|
...mockTableData.payload.data,
|
||||||
|
isSendingIncorrectK8SAgentMetrics: true,
|
||||||
|
hosts: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders table data', () => {
|
it('renders table data', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
tableData={createMockTableData({
|
tableData={{
|
||||||
isSendingK8SAgentMetrics: false,
|
...mockTableData,
|
||||||
sentAnyHostMetricsData: true,
|
payload: {
|
||||||
})}
|
...mockTableData.payload,
|
||||||
|
data: {
|
||||||
|
...mockTableData.payload.data,
|
||||||
|
isSendingIncorrectK8SAgentMetrics: false,
|
||||||
|
sentAnyHostMetricsData: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||||
|
|||||||
@@ -107,17 +107,6 @@ export interface HostsListTableProps {
|
|||||||
setPageSize: (pageSize: number) => void;
|
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 => ({
|
export const getHostListsQuery = (): HostListPayload => ({
|
||||||
filters: {
|
filters: {
|
||||||
items: [],
|
items: [],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -103,9 +103,10 @@ function K8sClustersList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -101,9 +101,10 @@ function K8sJobsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { safeParseJSON } from './commonUtils';
|
||||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
||||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||||
import { IEntityColumn } from './utils';
|
import { IEntityColumn } from './utils';
|
||||||
@@ -58,9 +59,10 @@ function K8sHeader({
|
|||||||
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
||||||
let { filters } = currentQuery.builder.queryData[0];
|
let { filters } = currentQuery.builder.queryData[0];
|
||||||
if (urlFilters) {
|
if (urlFilters) {
|
||||||
const decoded = decodeURIComponent(urlFilters);
|
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
filters = parsed;
|
filters = parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -104,9 +104,10 @@ function K8sNamespacesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -99,9 +99,10 @@ function K8sNodesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -92,9 +92,10 @@ function K8sPodsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sVolumesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Table, Tooltip, Typography } from 'antd';
|
import { Table, Tooltip, Typography } from 'antd';
|
||||||
import { Progress } from 'antd/lib';
|
import { Progress } from 'antd/lib';
|
||||||
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
|
|||||||
return uniqueFilters;
|
return uniqueFilters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const safeParseJSON = <T,>(value: string): T | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing JSON from URL parameter:', e);
|
||||||
|
// TODO: Should we capture this error in Sentry?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getOrderByFromParams = (
|
export const getOrderByFromParams = (
|
||||||
searchParams: URLSearchParams,
|
searchParams: URLSearchParams,
|
||||||
returnNullAsDefault = false,
|
returnNullAsDefault = false,
|
||||||
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
|
|||||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||||
);
|
);
|
||||||
if (orderByFromParams) {
|
if (orderByFromParams) {
|
||||||
const decoded = decodeURIComponent(orderByFromParams);
|
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
|
||||||
const parsed = JSON.parse(decoded);
|
orderByFromParams,
|
||||||
return parsed as { columnName: string; order: 'asc' | 'desc' };
|
);
|
||||||
|
if (parsed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (returnNullAsDefault) {
|
if (returnNullAsDefault) {
|
||||||
return null;
|
return null;
|
||||||
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
|
|||||||
): IBuilderQuery['filters'] | null => {
|
): IBuilderQuery['filters'] | null => {
|
||||||
const filtersFromParams = searchParams.get(queryKey);
|
const filtersFromParams = searchParams.get(queryKey);
|
||||||
if (filtersFromParams) {
|
if (filtersFromParams) {
|
||||||
try {
|
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
|
||||||
const decoded = decodeURIComponent(filtersFromParams);
|
|
||||||
const parsed = JSON.parse(decoded);
|
|
||||||
return parsed as IBuilderQuery['filters'];
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,23 +121,9 @@ function BodyTitleRenderer({
|
|||||||
return (
|
return (
|
||||||
<TitleWrapper onClick={handleNodeClick}>
|
<TitleWrapper onClick={handleNodeClick}>
|
||||||
{typeof value !== 'object' && (
|
{typeof value !== 'object' && (
|
||||||
<span
|
<Dropdown menu={menu} trigger={['click']}>
|
||||||
onClick={(e): void => {
|
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
||||||
e.stopPropagation();
|
</Dropdown>
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
onMouseDown={(e): void => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<Dropdown
|
|
||||||
menu={menu}
|
|
||||||
trigger={['click']}
|
|
||||||
dropdownRender={(originNode): React.ReactNode => (
|
|
||||||
<div data-log-detail-ignore="true">{originNode}</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
|
||||||
</Dropdown>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{title.toString()}{' '}
|
{title.toString()}{' '}
|
||||||
{!parentIsArray && typeof value !== 'object' && (
|
{!parentIsArray && typeof value !== 'object' && (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
|||||||
<span className="field-renderer-container">
|
<span className="field-renderer-container">
|
||||||
{dataType && newField && logType ? (
|
{dataType && newField && logType ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
|
<Tooltip placement="left" title={newField}>
|
||||||
<Typography.Text ellipsis className="label">
|
<Typography.Text ellipsis className="label">
|
||||||
{newField}{' '}
|
{newField}{' '}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function Overview({
|
|||||||
handleChangeSelectedView,
|
handleChangeSelectedView,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
||||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
|
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
||||||
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
|
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ function TableView({
|
|||||||
<Typography.Text>{renderedField}</Typography.Text>
|
<Typography.Text>{renderedField}</Typography.Text>
|
||||||
|
|
||||||
{traceId && (
|
{traceId && (
|
||||||
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
|
<Tooltip title="Inspect in Trace">
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
onClick={(
|
onClick={(
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
|
|
||||||
import { getColorsForSeverityLabels, isRedLike } from '../utils';
|
|
||||||
|
|
||||||
describe('getColorsForSeverityLabels', () => {
|
|
||||||
it('should return slate for blank labels', () => {
|
|
||||||
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
|
|
||||||
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct colors for known severity variants', () => {
|
|
||||||
expect(getColorsForSeverityLabels('INFO', 0)).toBe(Color.BG_ROBIN_600);
|
|
||||||
expect(getColorsForSeverityLabels('ERROR', 0)).toBe(Color.BG_CHERRY_600);
|
|
||||||
expect(getColorsForSeverityLabels('WARN', 0)).toBe(Color.BG_AMBER_600);
|
|
||||||
expect(getColorsForSeverityLabels('DEBUG', 0)).toBe(Color.BG_AQUA_600);
|
|
||||||
expect(getColorsForSeverityLabels('TRACE', 0)).toBe(Color.BG_FOREST_600);
|
|
||||||
expect(getColorsForSeverityLabels('FATAL', 0)).toBe(Color.BG_SAKURA_600);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return non-red colors for unrecognized labels at any index', () => {
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
const color = getColorsForSeverityLabels('4', i);
|
|
||||||
expect(isRedLike(color)).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return non-red colors for numeric severity text', () => {
|
|
||||||
const numericLabels = ['1', '2', '4', '9', '13', '17', '21'];
|
|
||||||
numericLabels.forEach((label) => {
|
|
||||||
const color = getColorsForSeverityLabels(label, 0);
|
|
||||||
expect(isRedLike(color)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +1,7 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
import { colors } from 'lib/getRandomColor';
|
import { colors } from 'lib/getRandomColor';
|
||||||
|
|
||||||
// Function to determine if a color is "red-like" based on its RGB values
|
|
||||||
export function isRedLike(hex: string): boolean {
|
|
||||||
const r = parseInt(hex.slice(1, 3), 16);
|
|
||||||
const g = parseInt(hex.slice(3, 5), 16);
|
|
||||||
const b = parseInt(hex.slice(5, 7), 16);
|
|
||||||
return r > 180 && r > g * 1.4 && r > b * 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAFE_FALLBACK_COLORS = colors.filter((c) => !isRedLike(c));
|
|
||||||
|
|
||||||
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
|
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
|
||||||
TRACE: Color.BG_FOREST_600,
|
TRACE: Color.BG_FOREST_600,
|
||||||
Trace: Color.BG_FOREST_500,
|
Trace: Color.BG_FOREST_500,
|
||||||
@@ -76,13 +67,8 @@ export function getColorsForSeverityLabels(
|
|||||||
label: string,
|
label: string,
|
||||||
index: number,
|
index: number,
|
||||||
): string {
|
): string {
|
||||||
const trimmed = label.trim();
|
// Check if we have a direct mapping for this severity variant
|
||||||
|
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
|
||||||
if (!trimmed) {
|
|
||||||
return Color.BG_SLATE_300;
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
|
|
||||||
if (variantColor) {
|
if (variantColor) {
|
||||||
return variantColor;
|
return variantColor;
|
||||||
}
|
}
|
||||||
@@ -117,8 +103,5 @@ export function getColorsForSeverityLabels(
|
|||||||
return Color.BG_SAKURA_500;
|
return Color.BG_SAKURA_500;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return colors[index % colors.length] || themeColors.red;
|
||||||
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
|
|
||||||
Color.BG_SLATE_400
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,19 +111,23 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const itemContent = useCallback(
|
const itemContent = useCallback(
|
||||||
(index: number, log: Record<string, unknown>): JSX.Element => (
|
(index: number, log: Record<string, unknown>): JSX.Element => {
|
||||||
<TableRow
|
return (
|
||||||
tableColumns={tableColumns}
|
<div key={log.id as string}>
|
||||||
index={index}
|
<TableRow
|
||||||
log={log}
|
tableColumns={tableColumns}
|
||||||
logs={tableViewProps.logs}
|
index={index}
|
||||||
hasActions
|
log={log}
|
||||||
fontSize={tableViewProps.fontSize}
|
logs={tableViewProps.logs}
|
||||||
onShowLogDetails={onSetActiveLog}
|
hasActions
|
||||||
isActiveLog={activeLog?.id === log.id}
|
fontSize={tableViewProps.fontSize}
|
||||||
onClearActiveLog={onCloseActiveLog}
|
onShowLogDetails={onSetActiveLog}
|
||||||
/>
|
isActiveLog={activeLog?.id === log.id}
|
||||||
),
|
onClearActiveLog={onCloseActiveLog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
[
|
[
|
||||||
tableColumns,
|
tableColumns,
|
||||||
onSetActiveLog,
|
onSetActiveLog,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function Explorer(): JSX.Element {
|
|||||||
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
|
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
|
||||||
|
|
||||||
const unitsLength = useMemo(() => units.length, [units]);
|
const unitsLength = useMemo(() => units.length, [units]);
|
||||||
const firstUnit = useMemo(() => units[0], [units]);
|
const firstUnit = useMemo(() => units?.[0], [units]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set the y axis unit to the first metric unit if
|
// Set the y axis unit to the first metric unit if
|
||||||
@@ -349,7 +349,7 @@ function Explorer(): JSX.Element {
|
|||||||
isOneChartPerQuery={showOneChartPerQuery}
|
isOneChartPerQuery={showOneChartPerQuery}
|
||||||
splitedQueries={splitedQueries}
|
splitedQueries={splitedQueries}
|
||||||
/>
|
/>
|
||||||
{isMetricDetailsOpen && selectedMetricName && (
|
{isMetricDetailsOpen && (
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
metricName={selectedMetricName}
|
metricName={selectedMetricName}
|
||||||
isOpen={isMetricDetailsOpen}
|
isOpen={isMetricDetailsOpen}
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
|
|||||||
dataSource={DataSource.METRICS}
|
dataSource={DataSource.METRICS}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DashboardsAndAlertsPopover metricName={metric.name} />
|
<DashboardsAndAlertsPopover
|
||||||
|
dashboards={metric.dashboards}
|
||||||
|
alerts={metric.alerts}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQueries, useQueryClient } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { toast } from '@signozhq/sonner';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import { Button, Tooltip, Typography } from 'antd';
|
|
||||||
import {
|
|
||||||
invalidateGetMetricMetadata,
|
|
||||||
useUpdateMetricMetadata,
|
|
||||||
} from 'api/generated/services/metrics';
|
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||||
@@ -28,10 +23,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { TimeSeriesProps } from './types';
|
import { TimeSeriesProps } from './types';
|
||||||
import {
|
import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||||
buildUpdateMetricYAxisUnitPayload,
|
|
||||||
splitQueryIntoOneChartPerQuery,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
function TimeSeries({
|
function TimeSeries({
|
||||||
showOneChartPerQuery,
|
showOneChartPerQuery,
|
||||||
@@ -43,7 +35,6 @@ function TimeSeries({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
setYAxisUnit,
|
setYAxisUnit,
|
||||||
showYAxisUnitSelector,
|
showYAxisUnitSelector,
|
||||||
metrics,
|
|
||||||
}: TimeSeriesProps): JSX.Element {
|
}: TimeSeriesProps): JSX.Element {
|
||||||
const { stagedQuery, currentQuery } = useQueryBuilder();
|
const { stagedQuery, currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -51,7 +42,6 @@ function TimeSeries({
|
|||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const isValidToConvertToMs = useMemo(() => {
|
const isValidToConvertToMs = useMemo(() => {
|
||||||
const isValid: boolean[] = [];
|
const isValid: boolean[] = [];
|
||||||
@@ -148,51 +138,54 @@ function TimeSeries({
|
|||||||
setYAxisUnit(value);
|
setYAxisUnit(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Enable once we have resolved all related metrics v2 api issues
|
||||||
// Show the save unit button if
|
// Show the save unit button if
|
||||||
// 1. There is only one metric
|
// 1. There is only one metric
|
||||||
// 2. The metric has no saved unit
|
// 2. The metric has no saved unit
|
||||||
// 3. The user has selected a unit
|
// 3. The user has selected a unit
|
||||||
const showSaveUnitButton = useMemo(
|
// const showSaveUnitButton = useMemo(
|
||||||
() =>
|
// () =>
|
||||||
metricUnits.length === 1 &&
|
// metricUnits.length === 1 &&
|
||||||
Boolean(metrics[0]) &&
|
// Boolean(metrics?.[0]) &&
|
||||||
!metricUnits[0] &&
|
// !metricUnits[0] &&
|
||||||
yAxisUnit,
|
// yAxisUnit,
|
||||||
[metricUnits, metrics, yAxisUnit],
|
// [metricUnits, metrics, yAxisUnit],
|
||||||
);
|
// );
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
mutate: updateMetricMetadata,
|
// mutate: updateMetricMetadata,
|
||||||
isLoading: isUpdatingMetricMetadata,
|
// isLoading: isUpdatingMetricMetadata,
|
||||||
} = useUpdateMetricMetadata();
|
// } = useUpdateMetricMetadata();
|
||||||
|
|
||||||
const handleSaveUnit = (): void => {
|
// const handleSaveUnit = (): void => {
|
||||||
if (metrics[0] && yAxisUnit) {
|
// updateMetricMetadata(
|
||||||
updateMetricMetadata(
|
// {
|
||||||
{
|
// metricName: metricNames[0],
|
||||||
pathParams: {
|
// payload: {
|
||||||
metricName: metricNames[0],
|
// unit: yAxisUnit,
|
||||||
},
|
// description: metrics[0]?.description ?? '',
|
||||||
data: buildUpdateMetricYAxisUnitPayload(
|
// metricType: metrics[0]?.type as MetricType,
|
||||||
metricNames[0],
|
// temporality: metrics[0]?.temporality,
|
||||||
metrics[0],
|
// },
|
||||||
yAxisUnit,
|
// },
|
||||||
),
|
// {
|
||||||
},
|
// onSuccess: () => {
|
||||||
{
|
// notifications.success({
|
||||||
onSuccess: () => {
|
// message: 'Unit saved successfully',
|
||||||
toast.success('Unit saved successfully');
|
// });
|
||||||
invalidateGetMetricMetadata(queryClient, {
|
// queryClient.invalidateQueries([
|
||||||
metricName: metricNames[0],
|
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
|
||||||
});
|
// metricNames[0],
|
||||||
},
|
// ]);
|
||||||
onError: () => {
|
// },
|
||||||
toast.error('Failed to save unit');
|
// onError: () => {
|
||||||
},
|
// notifications.error({
|
||||||
},
|
// message: 'Failed to save unit',
|
||||||
);
|
// });
|
||||||
}
|
// },
|
||||||
};
|
// },
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -205,7 +198,8 @@ function TimeSeries({
|
|||||||
source={YAxisSource.EXPLORER}
|
source={YAxisSource.EXPLORER}
|
||||||
data-testid="y-axis-unit-selector"
|
data-testid="y-axis-unit-selector"
|
||||||
/>
|
/>
|
||||||
{showSaveUnitButton && (
|
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
|
||||||
|
{/* {showSaveUnitButton && (
|
||||||
<div className="save-unit-container">
|
<div className="save-unit-container">
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
Save the selected unit for this metric?
|
Save the selected unit for this metric?
|
||||||
@@ -219,7 +213,7 @@ function TimeSeries({
|
|||||||
<Typography.Paragraph>Yes</Typography.Paragraph>
|
<Typography.Paragraph>Yes</Typography.Paragraph>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import { Provider } from 'react-redux';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
MetrictypesTemporalityDTO,
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
||||||
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
||||||
@@ -16,12 +14,12 @@ import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
|||||||
import * as timezoneHooks from 'providers/Timezone';
|
import * as timezoneHooks from 'providers/Timezone';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||||
|
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import Explorer from '../Explorer';
|
import Explorer from '../Explorer';
|
||||||
import * as useGetMetricsHooks from '../utils';
|
import * as useGetMetricsHooks from '../utils';
|
||||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
|
||||||
|
|
||||||
const mockSetSearchParams = jest.fn();
|
const mockSetSearchParams = jest.fn();
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -137,6 +135,14 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
|||||||
|
|
||||||
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
|
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 {
|
function renderExplorer(): void {
|
||||||
render(
|
render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
@@ -184,7 +190,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -201,7 +207,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -214,7 +220,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA],
|
metrics: [mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -231,7 +237,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -244,7 +250,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -263,10 +269,10 @@ describe('Explorer', () => {
|
|||||||
isError: false,
|
isError: false,
|
||||||
metrics: [
|
metrics: [
|
||||||
{
|
{
|
||||||
type: MetrictypesTypeDTO.sum,
|
type: MetricType.SUM,
|
||||||
description: 'metric1 description',
|
description: 'metric1 description',
|
||||||
unit: '',
|
unit: '',
|
||||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
temporality: Temporality.CUMULATIVE,
|
||||||
isMonotonic: true,
|
isMonotonic: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -283,7 +289,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA],
|
metrics: [mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -318,7 +324,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
|
import { UseMutationResult } from 'react-query';
|
||||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
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 TimeSeries from '../TimeSeries';
|
import TimeSeries from '../TimeSeries';
|
||||||
import { TimeSeriesProps } from '../types';
|
import { TimeSeriesProps } from '../types';
|
||||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
|
||||||
|
|
||||||
const mockUpdateMetricMetadata = jest.fn();
|
type MockUpdateMetricMetadata = UseMutationResult<
|
||||||
const updateMetricMetadataSpy = jest.spyOn(
|
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
|
||||||
metricsExplorerHooks,
|
Error,
|
||||||
'useUpdateMetricMetadata',
|
UseUpdateMetricMetadataProps
|
||||||
);
|
|
||||||
type UseUpdateMetricMetadataReturnType = ReturnType<
|
|
||||||
typeof metricsExplorerHooks.useUpdateMetricMetadata
|
|
||||||
>;
|
>;
|
||||||
|
const mockUpdateMetricMetadata = jest.fn();
|
||||||
|
jest
|
||||||
|
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
||||||
|
.mockReturnValue(({
|
||||||
|
mutate: mockUpdateMetricMetadata,
|
||||||
|
isLoading: false,
|
||||||
|
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
|
||||||
|
|
||||||
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
|
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -50,6 +60,14 @@ 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 mockSetWarning = jest.fn();
|
||||||
const mockSetIsMetricDetailsOpen = jest.fn();
|
const mockSetIsMetricDetailsOpen = jest.fn();
|
||||||
const mockSetYAxisUnit = jest.fn();
|
const mockSetYAxisUnit = jest.fn();
|
||||||
@@ -78,13 +96,6 @@ function renderTimeSeries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('TimeSeries', () => {
|
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', () => {
|
it('should render a warning icon when a metric has no unit among multiple metrics', () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { container } = renderTimeSeries({
|
const { container } = renderTimeSeries({
|
||||||
@@ -107,7 +118,7 @@ describe('TimeSeries', () => {
|
|||||||
const { container } = renderTimeSeries({
|
const { container } = renderTimeSeries({
|
||||||
metricUnits: ['', 'count'],
|
metricUnits: ['', 'count'],
|
||||||
metricNames: ['metric1', 'metric2'],
|
metricNames: ['metric1', 'metric2'],
|
||||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
metrics: [mockMetric, mockMetric],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,17 +133,18 @@ describe('TimeSeries', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows Save unit button when metric had no unit but one is selected', async () => {
|
// 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', () => {
|
||||||
const { findByText, getByRole } = renderTimeSeries({
|
const { findByText, getByRole } = renderTimeSeries({
|
||||||
metricUnits: [undefined],
|
metricUnits: [undefined],
|
||||||
metricNames: ['metric1'],
|
metricNames: ['metric1'],
|
||||||
metrics: [MOCK_METRIC_METADATA],
|
metrics: [mockMetric],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
showYAxisUnitSelector: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await findByText('Save the selected unit for this metric?'),
|
findByText('Save the selected unit for this metric?'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
const yesButton = getByRole('button', { name: 'Yes' });
|
const yesButton = getByRole('button', { name: 'Yes' });
|
||||||
@@ -140,25 +152,24 @@ describe('TimeSeries', () => {
|
|||||||
expect(yesButton).toBeEnabled();
|
expect(yesButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking on save unit button shoould upated metric metadata', async () => {
|
// 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', () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { getByRole } = renderTimeSeries({
|
const { getByRole } = renderTimeSeries({
|
||||||
metricUnits: [''],
|
metricUnits: [''],
|
||||||
metricNames: ['metric1'],
|
metricNames: ['metric1'],
|
||||||
metrics: [MOCK_METRIC_METADATA],
|
metrics: [mockMetric],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
showYAxisUnitSelector: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const yesButton = getByRole('button', { name: /Yes/i });
|
const yesButton = getByRole('button', { name: /Yes/i });
|
||||||
await user.click(yesButton);
|
user.click(yesButton);
|
||||||
|
|
||||||
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
|
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
pathParams: {
|
metricName: 'metric1',
|
||||||
metricName: 'metric1',
|
payload: expect.objectContaining({ unit: 'seconds' }),
|
||||||
},
|
|
||||||
data: expect.objectContaining({ unit: 'seconds' }),
|
|
||||||
},
|
},
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
onSuccess: expect.any(Function),
|
onSuccess: expect.any(Function),
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import * as useGetMultipleMetricsHook from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
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 { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import {
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
@@ -16,7 +22,6 @@ import {
|
|||||||
splitQueryIntoOneChartPerQuery,
|
splitQueryIntoOneChartPerQuery,
|
||||||
useGetMetrics,
|
useGetMetrics,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
|
||||||
|
|
||||||
const MOCK_QUERY_DATA_1: IBuilderQuery = {
|
const MOCK_QUERY_DATA_1: IBuilderQuery = {
|
||||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||||
@@ -86,19 +91,32 @@ describe('splitQueryIntoOneChartPerQuery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MOCK_METRIC_METADATA: MetricMetadata = {
|
||||||
|
description: 'Metric 1 description',
|
||||||
|
unit: 'unit1',
|
||||||
|
type: MetricType.GAUGE,
|
||||||
|
temporality: Temporality.DELTA,
|
||||||
|
isMonotonic: true,
|
||||||
|
};
|
||||||
|
|
||||||
describe('useGetMetrics', () => {
|
describe('useGetMetrics', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest
|
jest
|
||||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||||
.mockReturnValue([
|
.mockReturnValue([
|
||||||
{
|
({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
data: {
|
data: {
|
||||||
data: MOCK_METRIC_METADATA,
|
httpStatusCode: 200,
|
||||||
status: 'success',
|
data: {
|
||||||
|
status: 'success',
|
||||||
|
data: MOCK_METRIC_METADATA,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as UseQueryResult<GetMetricMetadata200, Error>,
|
} as Partial<
|
||||||
|
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
||||||
|
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,11 +133,12 @@ describe('useGetMetrics', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||||
.mockReturnValue([
|
.mockReturnValue([
|
||||||
{
|
({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isError: false,
|
isError: false,
|
||||||
data: undefined,
|
} as Partial<
|
||||||
} as UseQueryResult<GetMetricMetadata200, Error>,
|
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
||||||
|
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
||||||
]);
|
]);
|
||||||
const { result } = renderHook(() => useGetMetrics(['metric1']));
|
const { result } = renderHook(() => useGetMetrics(['metric1']));
|
||||||
expect(result.current.metrics).toHaveLength(1);
|
expect(result.current.metrics).toHaveLength(1);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
|
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
|
||||||
import { SuccessResponse, Warning } from 'types/api';
|
import { SuccessResponse, Warning } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||||
|
|
||||||
export enum ExplorerTabs {
|
export enum ExplorerTabs {
|
||||||
TIME_SERIES = 'time-series',
|
TIME_SERIES = 'time-series',
|
||||||
@@ -18,7 +18,7 @@ export interface TimeSeriesProps {
|
|||||||
isMetricUnitsError: boolean;
|
isMetricUnitsError: boolean;
|
||||||
metricUnits: (string | undefined)[];
|
metricUnits: (string | undefined)[];
|
||||||
metricNames: string[];
|
metricNames: string[];
|
||||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
|
metrics: (MetricMetadata | undefined)[];
|
||||||
handleOpenMetricDetails: (metricName: string) => void;
|
handleOpenMetricDetails: (metricName: string) => void;
|
||||||
yAxisUnit: string | undefined;
|
yAxisUnit: string | undefined;
|
||||||
setYAxisUnit: (unit: string) => void;
|
setYAxisUnit: (unit: string) => void;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
|
||||||
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
||||||
|
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { v4 as uuid } from 'uuid';
|
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.
|
* Split a query with multiple queryData to multiple distinct queries, each with a single queryData.
|
||||||
* @param query - The query to split
|
* @param query - The query to split
|
||||||
@@ -71,14 +68,16 @@ export function useGetMetrics(
|
|||||||
): {
|
): {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
|
metrics: (MetricMetadata | undefined)[];
|
||||||
} {
|
} {
|
||||||
const metricsData = useGetMultipleMetrics(metricNames, {
|
const metricsData = useGetMultipleMetrics(metricNames, {
|
||||||
enabled: metricNames.length > 0 && isEnabled,
|
enabled: metricNames.length > 0 && isEnabled,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
isLoading: metricsData.some((metric) => metric.isLoading),
|
isLoading: metricsData.some((metric) => metric.isLoading),
|
||||||
metrics: metricsData.map((metric) => metric.data?.data),
|
metrics: metricsData
|
||||||
|
.map((metric) => metric.data?.data)
|
||||||
|
.map((data) => data?.data),
|
||||||
isError: metricsData.some((metric) => metric.isError),
|
isError: metricsData.some((metric) => metric.isError),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,24 +89,9 @@ export function useGetMetrics(
|
|||||||
* @returns The units of the metrics, can be undefined if the metric has no unit
|
* @returns The units of the metrics, can be undefined if the metric has no unit
|
||||||
*/
|
*/
|
||||||
export function getMetricUnits(
|
export function getMetricUnits(
|
||||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[],
|
metrics: (MetricMetadata | undefined)[],
|
||||||
): (string | undefined)[] {
|
): (string | undefined)[] {
|
||||||
return metrics
|
return metrics
|
||||||
.map((metric) => metric?.unit)
|
.map((metric) => metric?.unit)
|
||||||
.map((unit) => mapMetricUnitToUniversalUnit(unit) || undefined);
|
.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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Card, Input, Select, Typography } from 'antd';
|
import { Card, Input, Select, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||||
@@ -40,10 +40,8 @@ import {
|
|||||||
* returns true if the feature flag is enabled, false otherwise
|
* returns true if the feature flag is enabled, false otherwise
|
||||||
* Show the inspect button in metrics explorer if the feature flag is enabled
|
* Show the inspect button in metrics explorer if the feature flag is enabled
|
||||||
*/
|
*/
|
||||||
export function isInspectEnabled(
|
export function isInspectEnabled(metricType: MetricType | undefined): boolean {
|
||||||
metricType: MetrictypesTypeDTO | undefined,
|
return metricType === MetricType.GAUGE;
|
||||||
): boolean {
|
|
||||||
return metricType === MetrictypesTypeDTO.gauge;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllTimestampsOfMetrics(
|
export function getAllTimestampsOfMetrics(
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import {
|
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
|
||||||
Button,
|
|
||||||
Collapse,
|
|
||||||
Input,
|
|
||||||
Menu,
|
|
||||||
Popover,
|
|
||||||
Skeleton,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { useGetMetricAttributes } from 'api/generated/services/metrics';
|
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { DataType } from 'container/LogDetailedView/TableView';
|
import { DataType } from 'container/LogDetailedView/TableView';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@@ -21,33 +12,9 @@ import { PANEL_TYPES } from '../../../constants/queryBuilder';
|
|||||||
import ROUTES from '../../../constants/routes';
|
import ROUTES from '../../../constants/routes';
|
||||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||||
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
import { AllAttributesProps, AllAttributesValueProps } from './types';
|
||||||
import {
|
|
||||||
AllAttributesEmptyTextProps,
|
|
||||||
AllAttributesProps,
|
|
||||||
AllAttributesValueProps,
|
|
||||||
} from './types';
|
|
||||||
import { getMetricDetailsQuery } from './utils';
|
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({
|
export function AllAttributesValue({
|
||||||
filterKey,
|
filterKey,
|
||||||
filterValue,
|
filterValue,
|
||||||
@@ -143,23 +110,13 @@ export function AllAttributesValue({
|
|||||||
|
|
||||||
function AllAttributes({
|
function AllAttributes({
|
||||||
metricName,
|
metricName,
|
||||||
|
attributes,
|
||||||
metricType,
|
metricType,
|
||||||
}: AllAttributesProps): JSX.Element {
|
}: AllAttributesProps): JSX.Element {
|
||||||
const [searchString, setSearchString] = useState('');
|
const [searchString, setSearchString] = useState('');
|
||||||
const [activeKey, setActiveKey] = useState<string[]>([ALL_ATTRIBUTES_KEY]);
|
const [activeKey, setActiveKey] = useState<string | string[]>(
|
||||||
|
'all-attributes',
|
||||||
const {
|
);
|
||||||
data: attributesData,
|
|
||||||
isLoading: isLoadingAttributes,
|
|
||||||
isError: isErrorAttributes,
|
|
||||||
refetch: refetchAttributes,
|
|
||||||
} = useGetMetricAttributes({
|
|
||||||
metricName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const attributes = useMemo(() => attributesData?.data.attributes ?? [], [
|
|
||||||
attributesData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
@@ -221,7 +178,7 @@ function AllAttributes({
|
|||||||
attributes.filter(
|
attributes.filter(
|
||||||
(attribute) =>
|
(attribute) =>
|
||||||
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
|
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
|
||||||
attribute.values?.some((value) =>
|
attribute.value.some((value) =>
|
||||||
value.toLowerCase().includes(searchString.toLowerCase()),
|
value.toLowerCase().includes(searchString.toLowerCase()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -238,7 +195,7 @@ function AllAttributes({
|
|||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
key: attribute.key,
|
key: attribute.key,
|
||||||
value: attribute.values,
|
value: attribute.value,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
@@ -313,7 +270,6 @@ function AllAttributes({
|
|||||||
onClick={(e): void => {
|
onClick={(e): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
disabled={isLoadingAttributes || isErrorAttributes}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -321,49 +277,25 @@ function AllAttributes({
|
|||||||
children: (
|
children: (
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
loading={isLoadingAttributes}
|
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
dataSource={tableData}
|
dataSource={tableData}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
className="metrics-accordion-content all-attributes-content"
|
className="metrics-accordion-content all-attributes-content"
|
||||||
scroll={{ y: 600 }}
|
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 (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
bordered
|
bordered
|
||||||
className="metrics-accordion"
|
className="metrics-accordion metrics-metadata-accordion"
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onChange={(keys): void => setActiveKey(keys as string[])}
|
onChange={(keys): void => setActiveKey(keys)}
|
||||||
items={items}
|
items={items}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,84 +2,36 @@ import { useMemo } from 'react';
|
|||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Dropdown, Typography } from 'antd';
|
import { Dropdown, Typography } from 'antd';
|
||||||
import { Skeleton } from 'antd/lib';
|
|
||||||
import {
|
|
||||||
useGetMetricAlerts,
|
|
||||||
useGetMetricDashboards,
|
|
||||||
} from 'api/generated/services/metrics';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { Bell, Grid } from 'lucide-react';
|
import { Bell, Grid } from 'lucide-react';
|
||||||
import { pluralize } from 'utils/pluralize';
|
|
||||||
|
|
||||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||||
|
|
||||||
function DashboardsAndAlertsPopover({
|
function DashboardsAndAlertsPopover({
|
||||||
metricName,
|
alerts,
|
||||||
|
dashboards,
|
||||||
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
|
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const params = useUrlQuery();
|
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(() => {
|
const alertsPopoverContent = useMemo(() => {
|
||||||
if (alerts && alerts.length > 0) {
|
if (alerts && alerts.length > 0) {
|
||||||
return alerts.map((alert) => ({
|
return alerts.map((alert) => ({
|
||||||
key: alert.alertId,
|
key: alert.alert_id,
|
||||||
label: (
|
label: (
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
key={alert.alertId}
|
key={alert.alert_id}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
params.set(QueryParams.ruleId, alert.alertId);
|
params.set(QueryParams.ruleId, alert.alert_id);
|
||||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||||
}}
|
}}
|
||||||
className="dashboards-popover-content-item"
|
className="dashboards-popover-content-item"
|
||||||
>
|
>
|
||||||
{alert.alertName || alert.alertId}
|
{alert.alert_name || alert.alert_id}
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@@ -87,44 +39,41 @@ function DashboardsAndAlertsPopover({
|
|||||||
return null;
|
return null;
|
||||||
}, [alerts, params]);
|
}, [alerts, params]);
|
||||||
|
|
||||||
|
const uniqueDashboards = useMemo(
|
||||||
|
() =>
|
||||||
|
dashboards?.filter(
|
||||||
|
(item, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.dashboard_id === item.dashboard_id),
|
||||||
|
),
|
||||||
|
[dashboards],
|
||||||
|
);
|
||||||
|
|
||||||
const dashboardsPopoverContent = useMemo(() => {
|
const dashboardsPopoverContent = useMemo(() => {
|
||||||
if (dashboards && dashboards.length > 0) {
|
if (uniqueDashboards && uniqueDashboards.length > 0) {
|
||||||
return dashboards.map((dashboard) => ({
|
return uniqueDashboards.map((dashboard) => ({
|
||||||
key: dashboard.dashboardId,
|
key: dashboard.dashboard_id,
|
||||||
label: (
|
label: (
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
key={dashboard.dashboardId}
|
key={dashboard.dashboard_id}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
safeNavigate(
|
safeNavigate(
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
dashboardId: dashboard.dashboardId,
|
dashboardId: dashboard.dashboard_id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="dashboards-popover-content-item"
|
className="dashboards-popover-content-item"
|
||||||
>
|
>
|
||||||
{dashboard.dashboardName || dashboard.dashboardId}
|
{dashboard.dashboard_name || dashboard.dashboard_id}
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [dashboards, safeNavigate]);
|
}, [uniqueDashboards, safeNavigate]);
|
||||||
|
|
||||||
if (isLoadingAlerts || isLoadingDashboards) {
|
if (!dashboardsPopoverContent && !alertsPopoverContent) {
|
||||||
return (
|
return null;
|
||||||
<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 (
|
return (
|
||||||
@@ -143,7 +92,8 @@ function DashboardsAndAlertsPopover({
|
|||||||
>
|
>
|
||||||
<Grid size={12} color={Color.BG_SIENNA_500} />
|
<Grid size={12} color={Color.BG_SIENNA_500} />
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{pluralize(dashboards.length, 'dashboard')}
|
{uniqueDashboards?.length} dashboard
|
||||||
|
{uniqueDashboards?.length === 1 ? '' : 's'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -162,7 +112,7 @@ function DashboardsAndAlertsPopover({
|
|||||||
>
|
>
|
||||||
<Bell size={12} color={Color.BG_SAKURA_500} />
|
<Bell size={12} color={Color.BG_SAKURA_500} />
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{pluralize(alerts.length, 'alert rule')}
|
{alerts?.length} alert {alerts?.length === 1 ? 'rule' : 'rules'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,58 +1,45 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { Button, Collapse, Input, Select, Skeleton, Typography } from 'antd';
|
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import {
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
invalidateGetMetricMetadata,
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
invalidateListMetrics,
|
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
||||||
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 { ResizeTable } from 'components/ResizeTable';
|
||||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||||
import { DataType } from 'container/LogDetailedView/TableView';
|
import { DataType } from 'container/LogDetailedView/TableView';
|
||||||
|
import { useUpdateMetricMetadata } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { Edit2, Save, X } from 'lucide-react';
|
import { Edit2, Save, X } from 'lucide-react';
|
||||||
|
|
||||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||||
import MetricTypeRendererV2 from '../Summary/MetricTypeViewRenderer';
|
|
||||||
import {
|
import {
|
||||||
METRIC_METADATA_KEYS,
|
METRIC_TYPE_LABEL_MAP,
|
||||||
METRIC_METADATA_TEMPORALITY_OPTIONS,
|
METRIC_TYPE_VALUES_MAP,
|
||||||
METRIC_METADATA_TYPE_OPTIONS,
|
} from '../Summary/constants';
|
||||||
METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
import { MetricTypeRenderer } from '../Summary/utils';
|
||||||
} from './constants';
|
import { METRIC_METADATA_KEYS } from './constants';
|
||||||
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
import { MetadataProps } from './types';
|
||||||
import { MetadataProps, MetricMetadataFormState, TableFields } from './types';
|
import { determineIsMonotonic } from './utils';
|
||||||
import { transformUpdateMetricMetadataRequest } from './utils';
|
|
||||||
|
|
||||||
function Metadata({
|
function Metadata({
|
||||||
metricName,
|
metricName,
|
||||||
metadata,
|
metadata,
|
||||||
isErrorMetricMetadata,
|
refetchMetricDetails,
|
||||||
isLoadingMetricMetadata,
|
|
||||||
refetchMetricMetadata,
|
|
||||||
}: MetadataProps): JSX.Element {
|
}: MetadataProps): JSX.Element {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
metricMetadataState,
|
metricMetadata,
|
||||||
setMetricMetadataState,
|
setMetricMetadata,
|
||||||
] = useState<MetricMetadataFormState>({
|
] = useState<UpdateMetricMetadataProps>({
|
||||||
type: MetrictypesTypeDTO.sum,
|
metricType: metadata?.metric_type || MetricType.SUM,
|
||||||
description: '',
|
description: metadata?.description || '',
|
||||||
temporality: MetrictypesTemporalityDTO.unspecified,
|
temporality: metadata?.temporality,
|
||||||
unit: '',
|
unit: metadata?.unit,
|
||||||
isMonotonic: false,
|
|
||||||
});
|
});
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const {
|
const {
|
||||||
@@ -64,135 +51,110 @@ function Metadata({
|
|||||||
);
|
);
|
||||||
const queryClient = useQueryClient();
|
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(
|
const tableData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
metadata
|
metadata
|
||||||
? Object.keys(metadata).map((key) => ({
|
? Object.keys({
|
||||||
key,
|
...metadata,
|
||||||
value: {
|
temporality: metadata?.temporality,
|
||||||
value: metadata[key as keyof typeof metadata],
|
})
|
||||||
|
// Filter out monotonic as user input is not required
|
||||||
|
.filter((key) => key !== 'monotonic')
|
||||||
|
.map((key) => ({
|
||||||
key,
|
key,
|
||||||
},
|
value: {
|
||||||
}))
|
value: metadata[key as keyof typeof metadata],
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
}))
|
||||||
: [],
|
: [],
|
||||||
[metadata],
|
[metadata],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render un-editable field value
|
// Render un-editable field value
|
||||||
const renderUneditableField = useCallback(
|
const renderUneditableField = useCallback((key: string, value: string) => {
|
||||||
(key: keyof MetricMetadataFormState, value: string) => {
|
if (key === 'metric_type') {
|
||||||
if (isErrorMetricMetadata) {
|
return <MetricTypeRenderer type={value as MetricType} />;
|
||||||
return <FieldRenderer field="-" />;
|
}
|
||||||
}
|
let fieldValue = value;
|
||||||
if (key === TableFields.TYPE) {
|
if (key === 'unit') {
|
||||||
return <MetricTypeRendererV2 type={value as MetrictypesTypeDTO} />;
|
fieldValue = getUniversalNameFromMetricUnit(value);
|
||||||
}
|
}
|
||||||
if (key === TableFields.IS_MONOTONIC) {
|
return <FieldRenderer field={fieldValue || '-'} />;
|
||||||
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(
|
const renderColumnValue = useCallback(
|
||||||
(field: {
|
(field: { value: string; key: string }): JSX.Element => {
|
||||||
value: string;
|
|
||||||
key: keyof MetricMetadataFormState;
|
|
||||||
}): JSX.Element => {
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
return renderUneditableField(field.key, field.value);
|
return renderUneditableField(field.key, field.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow editing of unit if it's already set
|
// Don't allow editing of unit if it's already set
|
||||||
const metricUnitAlreadySet =
|
const metricUnitAlreadySet = field.key === 'unit' && Boolean(metadata?.unit);
|
||||||
field.key === TableFields.UNIT && Boolean(metadata?.unit);
|
|
||||||
if (metricUnitAlreadySet) {
|
if (metricUnitAlreadySet) {
|
||||||
return renderUneditableField(field.key, field.value);
|
return renderUneditableField(field.key, field.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monotonic is not editable
|
if (field.key === 'metric_type') {
|
||||||
if (field.key === TableFields.IS_MONOTONIC) {
|
|
||||||
return renderUneditableField(field.key, field.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.key === TableFields.TYPE) {
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data-testid="metric-type-select"
|
data-testid="metric-type-select"
|
||||||
options={METRIC_METADATA_TYPE_OPTIONS}
|
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
|
||||||
value={metricMetadataState.type}
|
value: key,
|
||||||
|
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
||||||
|
}))}
|
||||||
|
value={metricMetadata.metricType}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadataState((prev) => ({
|
setMetricMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
type: value,
|
metricType: value as MetricType,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === TableFields.UNIT) {
|
if (field.key === 'unit') {
|
||||||
return (
|
return (
|
||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
value={metricMetadataState.unit}
|
value={metricMetadata.unit}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadataState((prev) => ({ ...prev, unit: value }));
|
setMetricMetadata((prev) => ({ ...prev, unit: value }));
|
||||||
}}
|
}}
|
||||||
data-testid="unit-select"
|
data-testid="unit-select"
|
||||||
source={YAxisSource.EXPLORER}
|
source={YAxisSource.EXPLORER}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === TableFields.Temporality) {
|
if (field.key === 'temporality') {
|
||||||
const temporalityValue =
|
|
||||||
metricMetadataState.temporality === MetrictypesTemporalityDTO.unspecified
|
|
||||||
? undefined
|
|
||||||
: metricMetadataState.temporality;
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data-testid="temporality-select"
|
data-testid="temporality-select"
|
||||||
options={METRIC_METADATA_TEMPORALITY_OPTIONS}
|
options={Object.values(Temporality).map((key) => ({
|
||||||
value={temporalityValue}
|
value: key,
|
||||||
|
label: key,
|
||||||
|
}))}
|
||||||
|
value={metricMetadata.temporality}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadataState((prev) => ({
|
setMetricMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
temporality: value,
|
temporality: value as Temporality,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === TableFields.DESCRIPTION) {
|
if (field.key === 'description') {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-testid="description-input"
|
data-testid="description-input"
|
||||||
name={field.key}
|
name={field.key}
|
||||||
defaultValue={metricMetadataState.description}
|
defaultValue={
|
||||||
|
metricMetadata[
|
||||||
|
field.key as Exclude<keyof UpdateMetricMetadataProps, 'isMonotonic'>
|
||||||
|
]
|
||||||
|
}
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
setMetricMetadataState((prev) => ({
|
setMetricMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field.key]: e.target.value,
|
[field.key]: e.target.value,
|
||||||
}));
|
}));
|
||||||
@@ -202,7 +164,7 @@ function Metadata({
|
|||||||
}
|
}
|
||||||
return <FieldRenderer field="-" />;
|
return <FieldRenderer field="-" />;
|
||||||
},
|
},
|
||||||
[isEditing, metadata?.unit, metricMetadataState, renderUneditableField],
|
[isEditing, metadata?.unit, metricMetadata, renderUneditableField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = useMemo(
|
const columns: ColumnsType<DataType> = useMemo(
|
||||||
@@ -239,61 +201,52 @@ function Metadata({
|
|||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
updateMetricMetadata(
|
updateMetricMetadata(
|
||||||
{
|
{
|
||||||
pathParams: {
|
metricName,
|
||||||
metricName,
|
payload: {
|
||||||
|
...metricMetadata,
|
||||||
|
isMonotonic: determineIsMonotonic(
|
||||||
|
metricMetadata.metricType,
|
||||||
|
metricMetadata.temporality,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (): void => {
|
onSuccess: (response): void => {
|
||||||
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
if (response?.statusCode === 200) {
|
||||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
});
|
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||||
notifications.success({
|
});
|
||||||
message: 'Metadata updated successfully',
|
notifications.success({
|
||||||
});
|
message: 'Metadata updated successfully',
|
||||||
setIsEditing(false);
|
});
|
||||||
invalidateListMetrics(queryClient);
|
refetchMetricDetails();
|
||||||
invalidateGetMetricMetadata(queryClient, {
|
setIsEditing(false);
|
||||||
metricName,
|
queryClient.invalidateQueries(['metricsList']);
|
||||||
});
|
} else {
|
||||||
|
notifications.error({
|
||||||
|
message:
|
||||||
|
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error): void => {
|
onError: (): void =>
|
||||||
const errorMessage = (error as AxiosError<RenderErrorResponseDTO>).response
|
|
||||||
?.data.error?.message;
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: errorMessage || METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
message:
|
||||||
});
|
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
updateMetricMetadata,
|
updateMetricMetadata,
|
||||||
metricName,
|
metricName,
|
||||||
metricMetadataState,
|
metricMetadata,
|
||||||
notifications,
|
notifications,
|
||||||
|
refetchMetricDetails,
|
||||||
queryClient,
|
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(() => {
|
const actionButton = useMemo(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
@@ -301,7 +254,10 @@ function Metadata({
|
|||||||
<Button
|
<Button
|
||||||
className="action-button"
|
className="action-button"
|
||||||
type="text"
|
type="text"
|
||||||
onClick={cancelEdit}
|
onClick={(e): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
disabled={isUpdatingMetricsMetadata}
|
disabled={isUpdatingMetricsMetadata}
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
@@ -322,9 +278,6 @@ function Metadata({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isErrorMetricMetadata) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="action-menu">
|
<div className="action-menu">
|
||||||
<Button
|
<Button
|
||||||
@@ -341,13 +294,7 @@ function Metadata({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [
|
}, [handleSave, isEditing, isUpdatingMetricsMetadata]);
|
||||||
isEditing,
|
|
||||||
isErrorMetricMetadata,
|
|
||||||
isUpdatingMetricsMetadata,
|
|
||||||
cancelEdit,
|
|
||||||
handleSave,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -359,14 +306,7 @@ function Metadata({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
key: 'metric-metadata',
|
key: 'metric-metadata',
|
||||||
children: isErrorMetricMetadata ? (
|
children: (
|
||||||
<div className="metric-metadata-error-state">
|
|
||||||
<MetricDetailsErrorState
|
|
||||||
refetch={refetchMetricMetadata}
|
|
||||||
errorMessage="Something went wrong while fetching metric metadata"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
@@ -378,23 +318,9 @@ 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 (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
bordered
|
bordered
|
||||||
|
|||||||
@@ -38,12 +38,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
.metrics-metadata-error {
|
|
||||||
padding: 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-details-content-grid {
|
.metric-details-content-grid {
|
||||||
height: 50px;
|
|
||||||
.labels-row,
|
.labels-row,
|
||||||
.values-row {
|
.values-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -52,18 +47,6 @@
|
|||||||
align-items: center;
|
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 {
|
.labels-row {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
@@ -89,7 +72,6 @@
|
|||||||
.dashboards-and-alerts-popover-container {
|
.dashboards-and-alerts-popover-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
.dashboards-and-alerts-popover {
|
.dashboards-and-alerts-popover {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -120,19 +102,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics-metadata-skeleton-container {
|
|
||||||
height: 330px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-attributes-skeleton-container {
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metrics-accordion {
|
.metrics-accordion {
|
||||||
.all-attributes-error-state {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-body {
|
.ant-table-body {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
@@ -178,6 +148,7 @@
|
|||||||
|
|
||||||
.all-attributes-search-input {
|
.all-attributes-search-input {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +161,6 @@
|
|||||||
.ant-typography:first-child {
|
.ant-typography:first-child {
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
color: var(--bg-robin-400);
|
color: var(--bg-robin-400);
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.all-attributes-contribution {
|
.all-attributes-contribution {
|
||||||
@@ -247,10 +217,6 @@
|
|||||||
|
|
||||||
.ant-collapse-content-box {
|
.ant-collapse-content-box {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.metric-metadata-error-state {
|
|
||||||
height: 267px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-header {
|
.ant-collapse-header {
|
||||||
@@ -271,7 +237,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metric-metadata-value {
|
.metric-metadata-value {
|
||||||
height: 67px;
|
|
||||||
background: rgba(22, 25, 34, 0.4);
|
background: rgba(22, 25, 34, 0.4);
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
.field-renderer-container {
|
.field-renderer-container {
|
||||||
@@ -365,26 +330,18 @@
|
|||||||
.metric-details-content {
|
.metric-details-content {
|
||||||
.metrics-accordion {
|
.metrics-accordion {
|
||||||
.metrics-accordion-header {
|
.metrics-accordion-header {
|
||||||
.action-menu {
|
.action-button {
|
||||||
.action-button {
|
.ant-typography {
|
||||||
.ant-typography {
|
color: var(--bg-slate-400);
|
||||||
color: var(--bg-slate-400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics-accordion-content {
|
.metrics-accordion-content {
|
||||||
.metric-metadata-key {
|
.metric-metadata-key {
|
||||||
.field-renderer-container {
|
|
||||||
.label {
|
|
||||||
color: var(--bg-slate-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-attributes-key {
|
.all-attributes-key {
|
||||||
.ant-typography:last-child {
|
.ant-typography:last-child {
|
||||||
color: var(--bg-vanilla-200);
|
color: var(--bg-slate-400);
|
||||||
background-color: var(--bg-robin-300);
|
background-color: var(--bg-robin-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,13 +395,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-details-error-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Divider, Drawer, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
|
Skeleton,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { useGetMetricMetadata } from 'api/generated/services/metrics';
|
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { Compass, Crosshair, X } from 'lucide-react';
|
import { Compass, Crosshair, X } from 'lucide-react';
|
||||||
|
|
||||||
@@ -11,12 +19,16 @@ import ROUTES from '../../../constants/routes';
|
|||||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||||
import { isInspectEnabled } from '../Inspect/utils';
|
import { isInspectEnabled } from '../Inspect/utils';
|
||||||
|
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||||
import AllAttributes from './AllAttributes';
|
import AllAttributes from './AllAttributes';
|
||||||
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
|
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
|
||||||
import Highlights from './Highlights';
|
|
||||||
import Metadata from './Metadata';
|
import Metadata from './Metadata';
|
||||||
import { MetricDetailsProps } from './types';
|
import { MetricDetailsProps } from './types';
|
||||||
import { getMetricDetailsQuery } from './utils';
|
import {
|
||||||
|
formatNumberToCompactFormat,
|
||||||
|
formatTimestampToReadableDate,
|
||||||
|
getMetricDetailsQuery,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
import './MetricDetails.styles.scss';
|
import './MetricDetails.styles.scss';
|
||||||
import '../Summary/Summary.styles.scss';
|
import '../Summary/Summary.styles.scss';
|
||||||
@@ -31,49 +43,55 @@ function MetricDetails({
|
|||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: metricMetadataResponse,
|
data,
|
||||||
isLoading: isLoadingMetricMetadata,
|
isLoading,
|
||||||
isError: isErrorMetricMetadata,
|
isFetching,
|
||||||
refetch: refetchMetricMetadata,
|
error: metricDetailsError,
|
||||||
} = useGetMetricMetadata(
|
refetch: refetchMetricDetails,
|
||||||
{
|
} = useGetMetricDetails(metricName ?? '', {
|
||||||
metricName,
|
enabled: !!metricName,
|
||||||
},
|
});
|
||||||
{
|
|
||||||
query: {
|
|
||||||
enabled: !!metricName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const metadata = useMemo(() => {
|
const metric = data?.payload?.data;
|
||||||
if (!metricMetadataResponse) {
|
|
||||||
|
const lastReceived = useMemo(() => {
|
||||||
|
if (!metric) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const {
|
return formatTimestampToReadableDate(metric.lastReceived);
|
||||||
type,
|
}, [metric]);
|
||||||
description,
|
|
||||||
unit,
|
|
||||||
temporality,
|
|
||||||
isMonotonic,
|
|
||||||
} = metricMetadataResponse.data;
|
|
||||||
|
|
||||||
return {
|
const showInspectFeature = useMemo(
|
||||||
type,
|
() => isInspectEnabled(metric?.metadata?.metric_type),
|
||||||
description,
|
[metric],
|
||||||
unit,
|
);
|
||||||
temporality,
|
|
||||||
isMonotonic,
|
|
||||||
};
|
|
||||||
}, [metricMetadataResponse]);
|
|
||||||
|
|
||||||
const showInspectFeature = useMemo(() => isInspectEnabled(metadata?.type), [
|
const isMetricDetailsLoading = isLoading || isFetching;
|
||||||
metadata?.type,
|
|
||||||
]);
|
const timeSeries = useMemo(() => {
|
||||||
|
if (!metric) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
|
||||||
|
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
||||||
if (metricName) {
|
if (metricName) {
|
||||||
const compositeQuery = getMetricDetailsQuery(metricName, metadata?.type);
|
const compositeQuery = getMetricDetailsQuery(
|
||||||
|
metricName,
|
||||||
|
metric?.metadata?.metric_type,
|
||||||
|
);
|
||||||
handleExplorerTabChange(
|
handleExplorerTabChange(
|
||||||
PANEL_TYPES.TIME_SERIES,
|
PANEL_TYPES.TIME_SERIES,
|
||||||
{
|
{
|
||||||
@@ -89,7 +107,9 @@ function MetricDetails({
|
|||||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [metricName, handleExplorerTabChange, metadata?.type]);
|
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
|
||||||
|
|
||||||
|
const isMetricDetailsError = metricDetailsError || !metric;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent(MetricsExplorerEvents.ModalOpened, {
|
logEvent(MetricsExplorerEvents.ModalOpened, {
|
||||||
@@ -97,9 +117,6 @@ function MetricDetails({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isActionButtonDisabled =
|
|
||||||
!metricName || isLoadingMetricMetadata || isErrorMetricMetadata;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
width="60%"
|
||||||
@@ -107,13 +124,13 @@ function MetricDetails({
|
|||||||
<div className="metric-details-header">
|
<div className="metric-details-header">
|
||||||
<div className="metric-details-title">
|
<div className="metric-details-title">
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Typography.Text>{metricName}</Typography.Text>
|
<Typography.Text>{metric?.name}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-details-header-buttons">
|
<div className="metric-details-header-buttons">
|
||||||
<Button
|
<Button
|
||||||
onClick={goToMetricsExplorerwithSelectedMetric}
|
onClick={goToMetricsExplorerwithSelectedMetric}
|
||||||
icon={<Compass size={16} />}
|
icon={<Compass size={16} />}
|
||||||
disabled={isActionButtonDisabled}
|
disabled={!metricName}
|
||||||
data-testid="open-in-explorer-button"
|
data-testid="open-in-explorer-button"
|
||||||
>
|
>
|
||||||
Open in Explorer
|
Open in Explorer
|
||||||
@@ -123,11 +140,10 @@ function MetricDetails({
|
|||||||
<Button
|
<Button
|
||||||
className="inspect-metrics-button"
|
className="inspect-metrics-button"
|
||||||
aria-label="Inspect Metric"
|
aria-label="Inspect Metric"
|
||||||
disabled={isActionButtonDisabled}
|
|
||||||
icon={<Crosshair size={18} />}
|
icon={<Crosshair size={18} />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (metricName) {
|
if (metric?.name) {
|
||||||
openInspectModal(metricName);
|
openInspectModal(metric.name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-testid="inspect-metric-button"
|
data-testid="inspect-metric-button"
|
||||||
@@ -147,18 +163,60 @@ function MetricDetails({
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
closeIcon={<X size={16} />}
|
closeIcon={<X size={16} />}
|
||||||
>
|
>
|
||||||
<div className="metric-details-content">
|
{isMetricDetailsLoading && (
|
||||||
<Highlights metricName={metricName} />
|
<div data-testid="metric-details-skeleton">
|
||||||
<DashboardsAndAlertsPopover metricName={metricName} />
|
<Skeleton active />
|
||||||
<Metadata
|
</div>
|
||||||
metricName={metricName}
|
)}
|
||||||
metadata={metadata}
|
{isMetricDetailsError && !isMetricDetailsLoading && (
|
||||||
isErrorMetricMetadata={isErrorMetricMetadata}
|
<Empty description="Error fetching metric details" />
|
||||||
isLoadingMetricMetadata={isLoadingMetricMetadata}
|
)}
|
||||||
refetchMetricMetadata={refetchMetricMetadata}
|
{!isMetricDetailsLoading && !isMetricDetailsError && (
|
||||||
/>
|
<div className="metric-details-content">
|
||||||
<AllAttributes metricName={metricName} metricType={metadata?.type} />
|
<div className="metric-details-content-grid">
|
||||||
</div>
|
<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>
|
||||||
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import * as reactUseHooks from 'react-use';
|
import * as reactUseHooks from 'react-use';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
|
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
|
||||||
import { userEvent } from 'tests/test-utils';
|
|
||||||
|
|
||||||
|
import { MetricDetailsAttribute } from '../../../../api/metricsExplorer/getMetricDetails';
|
||||||
import ROUTES from '../../../../constants/routes';
|
import ROUTES from '../../../../constants/routes';
|
||||||
import AllAttributes, { AllAttributesValue } from '../AllAttributes';
|
import AllAttributes, { AllAttributesValue } from '../AllAttributes';
|
||||||
import { getMockMetricAttributesData, MOCK_METRIC_NAME } from './testUtlls';
|
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
@@ -22,28 +20,33 @@ jest
|
|||||||
handleExplorerTabChange: mockHandleExplorerTabChange,
|
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();
|
const mockUseCopyToClipboard = jest.fn();
|
||||||
jest
|
jest
|
||||||
.spyOn(reactUseHooks, 'useCopyToClipboard')
|
.spyOn(reactUseHooks, 'useCopyToClipboard')
|
||||||
.mockReturnValue([{ value: 'value1' }, mockUseCopyToClipboard] as any);
|
.mockReturnValue([{ value: 'value1' }, mockUseCopyToClipboard] as any);
|
||||||
|
|
||||||
const useGetMetricAttributesMock = jest.spyOn(
|
|
||||||
metricsExplorerHooks,
|
|
||||||
'useGetMetricAttributes',
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('AllAttributes', () => {
|
describe('AllAttributes', () => {
|
||||||
beforeEach(() => {
|
|
||||||
useGetMetricAttributesMock.mockReturnValue({
|
|
||||||
...getMockMetricAttributesData(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders attributes section with title', () => {
|
it('renders attributes section with title', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={mockAttributes}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,8 +56,9 @@ describe('AllAttributes', () => {
|
|||||||
it('renders all attribute keys and values', () => {
|
it('renders all attribute keys and values', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={mockAttributes}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,8 +75,9 @@ describe('AllAttributes', () => {
|
|||||||
it('renders value counts correctly', () => {
|
it('renders value counts correctly', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={mockAttributes}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,44 +86,41 @@ describe('AllAttributes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty attributes array', () => {
|
it('handles empty attributes array', () => {
|
||||||
useGetMetricAttributesMock.mockReturnValue({
|
|
||||||
...getMockMetricAttributesData({
|
|
||||||
data: {
|
|
||||||
attributes: [],
|
|
||||||
totalKeys: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={[]}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
||||||
expect(screen.getByText('No attributes found')).toBeInTheDocument();
|
expect(screen.queryByText('No data')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking on an attribute key opens the explorer with the attribute filter applied', async () => {
|
it('clicking on an attribute key opens the explorer with the attribute filter applied', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={mockAttributes}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await userEvent.click(screen.getByText('attribute1'));
|
fireEvent.click(screen.getByText('attribute1'));
|
||||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters attributes based on search input', async () => {
|
it('filters attributes based on search input', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metricType={MetrictypesTypeDTO.gauge}
|
attributes={mockAttributes}
|
||||||
|
metricType={mockMetricType}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
await userEvent.type(screen.getByPlaceholderText('Search'), 'value1');
|
fireEvent.change(screen.getByPlaceholderText('Search'), {
|
||||||
|
target: { value: 'value1' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
@@ -142,7 +144,7 @@ describe('AllAttributesValue', () => {
|
|||||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads more attributes when show more button is clicked', async () => {
|
it('loads more attributes when show more button is clicked', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -153,7 +155,7 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('value6')).not.toBeInTheDocument();
|
expect(screen.queryByText('value6')).not.toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('Show More'));
|
fireEvent.click(screen.getByText('Show More'));
|
||||||
expect(screen.getByText('value6')).toBeInTheDocument();
|
expect(screen.getByText('value6')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ describe('AllAttributesValue', () => {
|
|||||||
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
|
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copy button should copy the attribute value to the clipboard', async () => {
|
it('copy button should copy the attribute value to the clipboard', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -181,13 +183,13 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('value1'));
|
fireEvent.click(screen.getByText('value1'));
|
||||||
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
|
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('Copy Attribute'));
|
fireEvent.click(screen.getByText('Copy Attribute'));
|
||||||
expect(mockUseCopyToClipboard).toHaveBeenCalledWith('value1');
|
expect(mockUseCopyToClipboard).toHaveBeenCalledWith('value1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('explorer button should go to metrics explore with the attribute filter applied', async () => {
|
it('explorer button should go to metrics explore with the attribute filter applied', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -198,10 +200,10 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('value1'));
|
fireEvent.click(screen.getByText('value1'));
|
||||||
|
|
||||||
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByText('Open in Explorer'));
|
fireEvent.click(screen.getByText('Open in Explorer'));
|
||||||
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
|
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
|
||||||
'attribute1',
|
'attribute1',
|
||||||
'value1',
|
'value1',
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { userEvent } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
|
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
|
||||||
import {
|
|
||||||
getMockAlertsData,
|
const mockAlert1 = {
|
||||||
getMockDashboardsData,
|
alert_id: '1',
|
||||||
MOCK_ALERT_1,
|
alert_name: 'Alert 1',
|
||||||
MOCK_ALERT_2,
|
};
|
||||||
MOCK_DASHBOARD_1,
|
const mockAlert2 = {
|
||||||
MOCK_DASHBOARD_2,
|
alert_id: '2',
|
||||||
MOCK_METRIC_NAME,
|
alert_name: 'Alert 2',
|
||||||
} from './testUtlls';
|
};
|
||||||
|
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];
|
||||||
|
|
||||||
const mockSafeNavigate = jest.fn();
|
const mockSafeNavigate = jest.fn();
|
||||||
jest.mock('hooks/useSafeNavigate', () => ({
|
jest.mock('hooks/useSafeNavigate', () => ({
|
||||||
@@ -20,6 +28,7 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
|||||||
safeNavigate: mockSafeNavigate,
|
safeNavigate: mockSafeNavigate,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockSetQuery = jest.fn();
|
const mockSetQuery = jest.fn();
|
||||||
const mockUrlQuery = {
|
const mockUrlQuery = {
|
||||||
set: mockSetQuery,
|
set: mockSetQuery,
|
||||||
@@ -30,156 +39,125 @@ jest.mock('hooks/useUrlQuery', () => ({
|
|||||||
default: jest.fn(() => mockUrlQuery),
|
default: jest.fn(() => mockUrlQuery),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const useGetMetricAlertsMock = jest.spyOn(
|
|
||||||
metricsExplorerHooks,
|
|
||||||
'useGetMetricAlerts',
|
|
||||||
);
|
|
||||||
const useGetMetricDashboardsMock = jest.spyOn(
|
|
||||||
metricsExplorerHooks,
|
|
||||||
'useGetMetricDashboards',
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('DashboardsAndAlertsPopover', () => {
|
describe('DashboardsAndAlertsPopover', () => {
|
||||||
beforeEach(() => {
|
|
||||||
useGetMetricAlertsMock.mockReturnValue(getMockAlertsData());
|
|
||||||
useGetMetricDashboardsMock.mockReturnValue(getMockDashboardsData());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the popover correctly with multiple dashboards and alerts', () => {
|
it('renders the popover correctly with multiple dashboards and alerts', () => {
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
render(
|
||||||
|
<DashboardsAndAlertsPopover
|
||||||
|
alerts={mockAlerts}
|
||||||
|
dashboards={mockDashboards}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.getByText(`2 dashboards`)).toBeInTheDocument();
|
expect(
|
||||||
expect(screen.getByText(`2 alert rules`)).toBeInTheDocument();
|
screen.getByText(`${mockDashboards.length} dashboards`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(`${mockAlerts.length} alert rules`),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders null with no dashboards and alerts', () => {
|
it('renders null with no dashboards and alerts', () => {
|
||||||
useGetMetricAlertsMock.mockReturnValue(
|
|
||||||
getMockAlertsData({
|
|
||||||
data: {
|
|
||||||
alerts: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
useGetMetricDashboardsMock.mockReturnValue(
|
|
||||||
getMockDashboardsData({
|
|
||||||
data: {
|
|
||||||
dashboards: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />,
|
<DashboardsAndAlertsPopover alerts={[]} dashboards={[]} />,
|
||||||
);
|
);
|
||||||
expect(
|
expect(container).toBeEmptyDOMElement();
|
||||||
container.querySelector('dashboards-and-alerts-popover-container'),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders popover with single dashboard and alert', () => {
|
it('renders popover with single dashboard and alert', () => {
|
||||||
useGetMetricAlertsMock.mockReturnValue(
|
render(
|
||||||
getMockAlertsData({
|
<DashboardsAndAlertsPopover
|
||||||
data: {
|
alerts={[mockAlert1]}
|
||||||
alerts: [MOCK_ALERT_1],
|
dashboards={[mockDashboard1]}
|
||||||
},
|
/>,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
useGetMetricDashboardsMock.mockReturnValue(
|
|
||||||
getMockDashboardsData({
|
|
||||||
data: {
|
|
||||||
dashboards: [MOCK_DASHBOARD_1],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
|
||||||
|
|
||||||
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
|
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
|
||||||
expect(screen.getByText(`1 alert rule`)).toBeInTheDocument();
|
expect(screen.getByText(`1 alert rule`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders popover with dashboard id if name is not available', async () => {
|
it('renders popover with dashboard id if name is not available', () => {
|
||||||
useGetMetricDashboardsMock.mockReturnValue(
|
render(
|
||||||
getMockDashboardsData({
|
<DashboardsAndAlertsPopover
|
||||||
data: {
|
alerts={mockAlerts}
|
||||||
dashboards: [{ ...MOCK_DASHBOARD_1, dashboardName: '' }],
|
dashboards={[{ ...mockDashboard1, dashboard_name: undefined } as any]}
|
||||||
},
|
/>,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
fireEvent.click(screen.getByText(`1 dashboard`));
|
||||||
|
expect(screen.getByText(mockDashboard1.dashboard_id)).toBeInTheDocument();
|
||||||
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', async () => {
|
it('renders popover with alert id if name is not available', () => {
|
||||||
useGetMetricAlertsMock.mockReturnValue(
|
render(
|
||||||
getMockAlertsData({
|
<DashboardsAndAlertsPopover
|
||||||
data: {
|
alerts={[{ ...mockAlert1, alert_name: undefined } as any]}
|
||||||
alerts: [{ ...MOCK_ALERT_1, alertName: '' }],
|
dashboards={mockDashboards}
|
||||||
},
|
/>,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
fireEvent.click(screen.getByText(`1 alert rule`));
|
||||||
|
expect(screen.getByText(mockAlert1.alert_id)).toBeInTheDocument();
|
||||||
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', async () => {
|
it('navigates to the dashboard when the dashboard is clicked', () => {
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
render(
|
||||||
|
<DashboardsAndAlertsPopover
|
||||||
|
alerts={mockAlerts}
|
||||||
|
dashboards={mockDashboards}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
// Click on 2 dashboards button
|
// Click on 2 dashboards button
|
||||||
await userEvent.click(screen.getByText(`2 dashboards`));
|
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
||||||
// Popover showing list of 2 dashboards should be visible
|
// Popover showing list of 2 dashboards should be visible
|
||||||
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
||||||
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on the first dashboard
|
// Click on the first dashboard
|
||||||
await userEvent.click(screen.getByText(MOCK_DASHBOARD_1.dashboardName));
|
fireEvent.click(screen.getByText(mockDashboard1.dashboard_name));
|
||||||
|
|
||||||
// Should navigate to the dashboard
|
// Should navigate to the dashboard
|
||||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||||
`/dashboard/${MOCK_DASHBOARD_1.dashboardId}`,
|
`/dashboard/${mockDashboard1.dashboard_id}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to the alert when the alert is clicked', async () => {
|
it('navigates to the alert when the alert is clicked', () => {
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
render(
|
||||||
|
<DashboardsAndAlertsPopover
|
||||||
|
alerts={mockAlerts}
|
||||||
|
dashboards={mockDashboards}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
// Click on 2 alert rules button
|
// Click on 2 alert rules button
|
||||||
await userEvent.click(screen.getByText(`2 alert rules`));
|
fireEvent.click(screen.getByText(`${mockAlerts.length} alert rules`));
|
||||||
// Popover showing list of 2 alert rules should be visible
|
// Popover showing list of 2 alert rules should be visible
|
||||||
expect(screen.getByText(MOCK_ALERT_1.alertName)).toBeInTheDocument();
|
expect(screen.getByText(mockAlert1.alert_name)).toBeInTheDocument();
|
||||||
expect(screen.getByText(MOCK_ALERT_2.alertName)).toBeInTheDocument();
|
expect(screen.getByText(mockAlert2.alert_name)).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on the first alert rule
|
// Click on the first alert rule
|
||||||
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
|
fireEvent.click(screen.getByText(mockAlert1.alert_name));
|
||||||
|
|
||||||
// Should navigate to the alert rule
|
// Should navigate to the alert rule
|
||||||
expect(mockSetQuery).toHaveBeenCalledWith(
|
expect(mockSetQuery).toHaveBeenCalledWith(
|
||||||
QueryParams.ruleId,
|
QueryParams.ruleId,
|
||||||
MOCK_ALERT_1.alertId,
|
mockAlert1.alert_id,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders unique dashboards even when there are duplicates', async () => {
|
it('renders unique dashboards even when there are duplicates', () => {
|
||||||
useGetMetricDashboardsMock.mockReturnValue(
|
render(
|
||||||
getMockDashboardsData({
|
<DashboardsAndAlertsPopover
|
||||||
data: {
|
alerts={mockAlerts}
|
||||||
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2, MOCK_DASHBOARD_1],
|
dashboards={[...mockDashboards, mockDashboard1]}
|
||||||
},
|
/>,
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(`${mockDashboards.length} dashboards`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
||||||
|
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
||||||
expect(screen.getByText('2 dashboards')).toBeInTheDocument();
|
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(screen.getByText('2 dashboards'));
|
|
||||||
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, 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 { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import {
|
import {
|
||||||
UniversalYAxisUnit,
|
UniversalYAxisUnit,
|
||||||
YAxisUnitSelectorProps,
|
YAxisUnitSelectorProps,
|
||||||
} from 'components/YAxisUnitSelector/types';
|
} from 'components/YAxisUnitSelector/types';
|
||||||
|
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||||
import * as useNotificationsHooks from 'hooks/useNotifications';
|
import * as useNotificationsHooks from 'hooks/useNotifications';
|
||||||
import { userEvent } from 'tests/test-utils';
|
|
||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
import Metadata from '../Metadata';
|
import Metadata from '../Metadata';
|
||||||
import { MetricMetadata } from '../types';
|
|
||||||
import { transformMetricMetadata } from '../utils';
|
|
||||||
import { getMockMetricMetadataData, MOCK_METRIC_NAME } from './testUtlls';
|
|
||||||
|
|
||||||
// Mock antd select for testing
|
// Mock antd select for testing
|
||||||
jest.mock('antd', () => ({
|
jest.mock('antd', () => ({
|
||||||
@@ -80,18 +72,13 @@ jest.mock('react-query', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockUseUpdateMetricMetadataHook = jest.spyOn(
|
|
||||||
metricsExplorerHooks,
|
|
||||||
'useUpdateMetricMetadata',
|
|
||||||
);
|
|
||||||
type UseUpdateMetricMetadataResult = ReturnType<
|
|
||||||
typeof metricsExplorerHooks.useUpdateMetricMetadata
|
|
||||||
>;
|
|
||||||
const mockUseUpdateMetricMetadata = jest.fn();
|
const mockUseUpdateMetricMetadata = jest.fn();
|
||||||
|
jest
|
||||||
const mockMetricMetadata = transformMetricMetadata(
|
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
||||||
getMockMetricMetadataData().data as GetMetricMetadata200,
|
.mockReturnValue({
|
||||||
) as MetricMetadata;
|
mutate: mockUseUpdateMetricMetadata,
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const mockErrorNotification = jest.fn();
|
const mockErrorNotification = jest.fn();
|
||||||
const mockSuccessNotification = jest.fn();
|
const mockSuccessNotification = jest.fn();
|
||||||
@@ -102,50 +89,47 @@ jest.spyOn(useNotificationsHooks, 'useNotifications').mockReturnValue({
|
|||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const mockRefetchMetricMetadata = jest.fn();
|
const mockMetricName = 'test_metric';
|
||||||
|
const mockMetricMetadata = {
|
||||||
|
metric_type: MetricType.GAUGE,
|
||||||
|
description: 'test_description',
|
||||||
|
unit: 'test_unit',
|
||||||
|
temporality: Temporality.DELTA,
|
||||||
|
};
|
||||||
|
const mockRefetchMetricDetails = jest.fn();
|
||||||
|
|
||||||
describe('Metadata', () => {
|
describe('Metadata', () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockUseUpdateMetricMetadataHook.mockReturnValue(({
|
|
||||||
mutate: mockUseUpdateMetricMetadata,
|
|
||||||
} as Partial<UseUpdateMetricMetadataResult>) as UseUpdateMetricMetadataResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the metadata properly', () => {
|
it('should render the metadata properly', () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Metric Type')).toBeInTheDocument();
|
expect(screen.getByText('Metric Type')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Gauge')).toBeInTheDocument();
|
expect(screen.getByText(mockMetricMetadata.metric_type)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockMetricMetadata.description)).toBeInTheDocument();
|
expect(screen.getByText(mockMetricMetadata.description)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Unit')).toBeInTheDocument();
|
expect(screen.getByText('Unit')).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockMetricMetadata.unit)).toBeInTheDocument();
|
expect(screen.getByText(mockMetricMetadata.unit)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Temporality')).toBeInTheDocument();
|
expect(screen.getByText('Temporality')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Delta')).toBeInTheDocument();
|
expect(screen.getByText(mockMetricMetadata.temporality)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('editing the metadata should show the form inputs', async () => {
|
it('editing the metadata should show the form inputs', () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
expect(screen.getByTestId('metric-type-select')).toBeInTheDocument();
|
expect(screen.getByTestId('metric-type-select')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('temporality-select')).toBeInTheDocument();
|
expect(screen.getByTestId('temporality-select')).toBeInTheDocument();
|
||||||
@@ -155,53 +139,57 @@ describe('Metadata', () => {
|
|||||||
it('should update the metadata when the form is submitted', async () => {
|
it('should update the metadata when the form is submitted', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={{
|
metadata={{
|
||||||
...mockMetricMetadata,
|
...mockMetricMetadata,
|
||||||
unit: '',
|
unit: '',
|
||||||
}}
|
}}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
expect(metricDescriptionInput).toBeInTheDocument();
|
expect(metricDescriptionInput).toBeInTheDocument();
|
||||||
await userEvent.clear(metricDescriptionInput);
|
fireEvent.change(metricDescriptionInput, {
|
||||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
target: { value: 'Updated description' },
|
||||||
|
});
|
||||||
|
|
||||||
const metricTypeSelect = screen.getByTestId('metric-type-select');
|
const metricTypeSelect = screen.getByTestId('metric-type-select');
|
||||||
expect(metricTypeSelect).toBeInTheDocument();
|
expect(metricTypeSelect).toBeInTheDocument();
|
||||||
await userEvent.selectOptions(metricTypeSelect, MetrictypesTypeDTO.sum);
|
fireEvent.change(metricTypeSelect, {
|
||||||
|
target: { value: MetricType.SUM },
|
||||||
|
});
|
||||||
|
|
||||||
const temporalitySelect = screen.getByTestId('temporality-select');
|
const temporalitySelect = screen.getByTestId('temporality-select');
|
||||||
expect(temporalitySelect).toBeInTheDocument();
|
expect(temporalitySelect).toBeInTheDocument();
|
||||||
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
|
fireEvent.change(temporalitySelect, {
|
||||||
|
target: { value: Temporality.CUMULATIVE },
|
||||||
|
});
|
||||||
|
|
||||||
const unitSelect = screen.getByTestId('unit-select');
|
const unitSelect = screen.getByTestId('unit-select');
|
||||||
expect(unitSelect).toBeInTheDocument();
|
expect(unitSelect).toBeInTheDocument();
|
||||||
await userEvent.selectOptions(unitSelect, 'By');
|
fireEvent.change(unitSelect, {
|
||||||
|
target: { value: 'By' },
|
||||||
|
});
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
expect(saveButton).toBeInTheDocument();
|
expect(saveButton).toBeInTheDocument();
|
||||||
await userEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
metricName: mockMetricName,
|
||||||
type: MetrictypesTypeDTO.sum,
|
payload: expect.objectContaining({
|
||||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
description: 'Updated description',
|
||||||
|
metricType: MetricType.SUM,
|
||||||
|
temporality: Temporality.CUMULATIVE,
|
||||||
unit: 'By',
|
unit: 'By',
|
||||||
isMonotonic: true,
|
isMonotonic: true,
|
||||||
}),
|
}),
|
||||||
pathParams: {
|
|
||||||
metricName: MOCK_METRIC_NAME,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
onSuccess: expect.any(Function),
|
onSuccess: expect.any(Function),
|
||||||
@@ -213,56 +201,56 @@ describe('Metadata', () => {
|
|||||||
it('should show success notification when metadata is updated successfully', async () => {
|
it('should show success notification when metadata is updated successfully', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
await userEvent.clear(metricDescriptionInput);
|
fireEvent.change(metricDescriptionInput, {
|
||||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
target: { value: 'Updated description' },
|
||||||
|
});
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
await userEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
const onSuccessCallback =
|
const onSuccessCallback =
|
||||||
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
||||||
onSuccessCallback({ status: 200 });
|
onSuccessCallback({ statusCode: 200 });
|
||||||
|
|
||||||
expect(mockSuccessNotification).toHaveBeenCalledWith({
|
expect(mockSuccessNotification).toHaveBeenCalledWith({
|
||||||
message: 'Metadata updated successfully',
|
message: 'Metadata updated successfully',
|
||||||
});
|
});
|
||||||
|
expect(mockRefetchMetricDetails).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error notification when metadata update fails with non-200 response', async () => {
|
it('should show error notification when metadata update fails with non-200 response', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
await userEvent.clear(metricDescriptionInput);
|
fireEvent.change(metricDescriptionInput, {
|
||||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
target: { value: 'Updated description' },
|
||||||
|
});
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
await userEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
const onSuccessCallback =
|
||||||
onErrorCallback({ status: 500 });
|
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
||||||
|
onSuccessCallback({ statusCode: 500 });
|
||||||
|
|
||||||
expect(mockErrorNotification).toHaveBeenCalledWith({
|
expect(mockErrorNotification).toHaveBeenCalledWith({
|
||||||
message:
|
message:
|
||||||
@@ -273,23 +261,22 @@ describe('Metadata', () => {
|
|||||||
it('should show error notification when metadata update fails', async () => {
|
it('should show error notification when metadata update fails', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
await userEvent.clear(metricDescriptionInput);
|
fireEvent.change(metricDescriptionInput, {
|
||||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
target: { value: 'Updated description' },
|
||||||
|
});
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
await userEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
||||||
|
|
||||||
@@ -302,43 +289,39 @@ describe('Metadata', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancel button should cancel the edit mode', async () => {
|
it('cancel button should cancel the edit mode', () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const cancelButton = screen.getByText('Cancel');
|
const cancelButton = screen.getByText('Cancel');
|
||||||
expect(cancelButton).toBeInTheDocument();
|
expect(cancelButton).toBeInTheDocument();
|
||||||
await userEvent.click(cancelButton);
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
const editButton2 = screen.getByText('Edit');
|
const editButton2 = screen.getByText('Edit');
|
||||||
expect(editButton2).toBeInTheDocument();
|
expect(editButton2).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow editing of unit if it is already set', async () => {
|
it('should not allow editing of unit if it is already set', () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={MOCK_METRIC_NAME}
|
metricName={mockMetricName}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
isErrorMetricMetadata={false}
|
refetchMetricDetails={mockRefetchMetricDetails}
|
||||||
isLoadingMetricMetadata={false}
|
|
||||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
const unitSelect = screen.queryByTestId('unit-select');
|
const unitSelect = screen.queryByTestId('unit-select');
|
||||||
expect(unitSelect).not.toBeInTheDocument();
|
expect(unitSelect).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,16 +1,68 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
import { MetricDetails as MetricDetailsType } from 'api/metricsExplorer/getMetricDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
import ROUTES from 'constants/routes';
|
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 * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
|
||||||
import { userEvent } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import MetricDetails from '../MetricDetails';
|
import MetricDetails from '../MetricDetails';
|
||||||
import { getMockMetricMetadataData } from './testUtlls';
|
|
||||||
|
|
||||||
const mockMetricName = 'test-metric';
|
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 mockOpenInspectModal = jest.fn();
|
||||||
const mockOnClose = 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();
|
const mockHandleExplorerTabChange = jest.fn();
|
||||||
jest
|
jest
|
||||||
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
|
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
|
||||||
@@ -36,50 +88,7 @@ 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', () => {
|
describe('MetricDetails', () => {
|
||||||
beforeEach(() => {
|
|
||||||
useGetMetricMetadataMock.mockReturnValue(getMockMetricMetadataData());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders metric details correctly', () => {
|
it('renders metric details correctly', () => {
|
||||||
render(
|
render(
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
@@ -92,15 +101,27 @@ describe('MetricDetails', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
|
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('all-attributes')).toBeInTheDocument();
|
expect(screen.getByText(mockMetricDescription)).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId('dashboards-and-alerts-popover'),
|
screen.getByText(getUniversalNameFromMetricUnit(mockMetricData.unit)),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('highlights')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('metadata')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the "open in explorer" and "inspect" buttons', async () => {
|
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);
|
||||||
render(
|
render(
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
@@ -114,10 +135,93 @@ describe('MetricDetails', () => {
|
|||||||
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
|
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('inspect-metric-button')).toBeInTheDocument();
|
expect(screen.getByTestId('inspect-metric-button')).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId('open-in-explorer-button'));
|
fireEvent.click(screen.getByTestId('open-in-explorer-button'));
|
||||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId('inspect-metric-button'));
|
fireEvent.click(screen.getByTestId('inspect-metric-button'));
|
||||||
expect(mockOpenInspectModal).toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
MetrictypesTemporalityDTO,
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
determineIsMonotonic,
|
determineIsMonotonic,
|
||||||
@@ -12,48 +10,35 @@ import {
|
|||||||
describe('MetricDetails utils', () => {
|
describe('MetricDetails utils', () => {
|
||||||
describe('determineIsMonotonic', () => {
|
describe('determineIsMonotonic', () => {
|
||||||
it('should return true for histogram metrics', () => {
|
it('should return true for histogram metrics', () => {
|
||||||
expect(determineIsMonotonic(MetrictypesTypeDTO.histogram)).toBe(true);
|
expect(determineIsMonotonic(MetricType.HISTOGRAM)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for exponential histogram metrics', () => {
|
it('should return true for exponential histogram metrics', () => {
|
||||||
expect(determineIsMonotonic(MetrictypesTypeDTO.exponentialhistogram)).toBe(
|
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(
|
||||||
true,
|
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', () => {
|
it('should return false for sum metrics with delta temporality', () => {
|
||||||
expect(
|
expect(determineIsMonotonic(MetricType.SUM, Temporality.DELTA)).toBe(false);
|
||||||
determineIsMonotonic(
|
|
||||||
MetrictypesTypeDTO.sum,
|
|
||||||
MetrictypesTemporalityDTO.delta,
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false by default', () => {
|
it('should return false by default', () => {
|
||||||
expect(
|
expect(determineIsMonotonic('' as MetricType, '' as Temporality)).toBe(
|
||||||
determineIsMonotonic(
|
false,
|
||||||
'' as MetrictypesTypeDTO,
|
);
|
||||||
'' as MetrictypesTemporalityDTO,
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,16 +115,13 @@ describe('MetricDetails utils', () => {
|
|||||||
const API_GATEWAY = 'api-gateway';
|
const API_GATEWAY = 'api-gateway';
|
||||||
|
|
||||||
it('should create correct query for SUM metric type', () => {
|
it('should create correct query for SUM metric type', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
|
||||||
TEST_METRIC_NAME,
|
|
||||||
MetrictypesTypeDTO.sum,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||||
MetrictypesTypeDTO.sum,
|
MetricType.SUM,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
|
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
|
||||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
|
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
|
||||||
@@ -147,16 +129,13 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for GAUGE metric type', () => {
|
it('should create correct query for GAUGE metric type', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.GAUGE);
|
||||||
TEST_METRIC_NAME,
|
|
||||||
MetrictypesTypeDTO.gauge,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||||
MetrictypesTypeDTO.gauge,
|
MetricType.GAUGE,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
|
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
|
||||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
|
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
|
||||||
@@ -164,16 +143,13 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for SUMMARY metric type', () => {
|
it('should create correct query for SUMMARY metric type', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUMMARY);
|
||||||
TEST_METRIC_NAME,
|
|
||||||
MetrictypesTypeDTO.summary,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||||
MetrictypesTypeDTO.summary,
|
MetricType.SUMMARY,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||||
@@ -181,16 +157,13 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for HISTOGRAM metric type', () => {
|
it('should create correct query for HISTOGRAM metric type', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.HISTOGRAM);
|
||||||
TEST_METRIC_NAME,
|
|
||||||
MetrictypesTypeDTO.histogram,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||||
MetrictypesTypeDTO.histogram,
|
MetricType.HISTOGRAM,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||||
@@ -200,14 +173,14 @@ describe('MetricDetails utils', () => {
|
|||||||
it('should create correct query for EXPONENTIAL_HISTOGRAM metric type', () => {
|
it('should create correct query for EXPONENTIAL_HISTOGRAM metric type', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetrictypesTypeDTO.exponentialhistogram,
|
MetricType.EXPONENTIAL_HISTOGRAM,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||||
MetrictypesTypeDTO.exponentialhistogram,
|
MetricType.EXPONENTIAL_HISTOGRAM,
|
||||||
);
|
);
|
||||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||||
@@ -230,7 +203,7 @@ describe('MetricDetails utils', () => {
|
|||||||
const filter = { key: 'service', value: API_GATEWAY };
|
const filter = { key: 'service', value: API_GATEWAY };
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetrictypesTypeDTO.sum,
|
MetricType.SUM,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -248,7 +221,7 @@ describe('MetricDetails utils', () => {
|
|||||||
const groupBy = 'service';
|
const groupBy = 'service';
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetrictypesTypeDTO.sum,
|
MetricType.SUM,
|
||||||
undefined,
|
undefined,
|
||||||
groupBy,
|
groupBy,
|
||||||
);
|
);
|
||||||
@@ -263,7 +236,7 @@ describe('MetricDetails utils', () => {
|
|||||||
const groupBy = 'endpoint';
|
const groupBy = 'endpoint';
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetrictypesTypeDTO.sum,
|
MetricType.SUM,
|
||||||
filter,
|
filter,
|
||||||
groupBy,
|
groupBy,
|
||||||
);
|
);
|
||||||
@@ -277,10 +250,7 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not include filters or groupBy when not provided', () => {
|
it('should not include filters or groupBy when not provided', () => {
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
|
||||||
TEST_METRIC_NAME,
|
|
||||||
MetrictypesTypeDTO.sum,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
|
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
|
||||||
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
|
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
|
||||||
|
|||||||
@@ -1,55 +1,6 @@
|
|||||||
import {
|
|
||||||
MetrictypesTemporalityDTO,
|
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
|
|
||||||
export const METRIC_METADATA_KEYS = {
|
export const METRIC_METADATA_KEYS = {
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
unit: 'Unit',
|
unit: 'Unit',
|
||||||
type: 'Metric Type',
|
metric_type: 'Metric Type',
|
||||||
temporality: 'Temporality',
|
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.';
|
|
||||||
|
|||||||
@@ -1,39 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
MetricsexplorertypesMetricAlertDTO,
|
MetricDetails,
|
||||||
MetricsexplorertypesMetricAttributeDTO,
|
MetricDetailsAlert,
|
||||||
MetricsexplorertypesMetricDashboardDTO,
|
MetricDetailsAttribute,
|
||||||
MetricsexplorertypesMetricHighlightsResponseDTO,
|
MetricDetailsDashboard,
|
||||||
MetricsexplorertypesMetricMetadataDTO,
|
} from 'api/metricsExplorer/getMetricDetails';
|
||||||
MetrictypesTemporalityDTO,
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
|
|
||||||
export interface MetricDetailsProps {
|
export interface MetricDetailsProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
metricName: string;
|
metricName: string | null;
|
||||||
isModalTimeSelection: boolean;
|
isModalTimeSelection: boolean;
|
||||||
openInspectModal?: (metricName: string) => void;
|
openInspectModal?: (metricName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HighlightsProps {
|
|
||||||
metricName: string;
|
|
||||||
}
|
|
||||||
export interface DashboardsAndAlertsPopoverProps {
|
export interface DashboardsAndAlertsPopoverProps {
|
||||||
metricName: string;
|
dashboards: MetricDetailsDashboard[] | null;
|
||||||
|
alerts: MetricDetailsAlert[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataProps {
|
export interface MetadataProps {
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metadata: MetricMetadata | null;
|
metadata: MetricDetails['metadata'] | undefined;
|
||||||
isErrorMetricMetadata: boolean;
|
refetchMetricDetails: () => void;
|
||||||
isLoadingMetricMetadata: boolean;
|
|
||||||
refetchMetricMetadata: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllAttributesProps {
|
export interface AllAttributesProps {
|
||||||
|
attributes: MetricDetailsAttribute[];
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metricType: MetrictypesTypeDTO | undefined;
|
metricType: MetricType | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllAttributesValueProps {
|
export interface AllAttributesValueProps {
|
||||||
@@ -41,38 +36,3 @@ export interface AllAttributesValueProps {
|
|||||||
filterValue: string[];
|
filterValue: string[];
|
||||||
goToMetricsExploreWithAppliedAttribute: (key: string, value: string) => void;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
import {
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
GetMetricMetadata200,
|
|
||||||
MetrictypesTemporalityDTO,
|
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
|
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { MetricMetadata, MetricMetadataFormState } from './types';
|
export function formatTimestampToReadableDate(timestamp: string): string {
|
||||||
|
|
||||||
export function formatTimestampToReadableDate(
|
|
||||||
timestamp: number | string | undefined,
|
|
||||||
): string {
|
|
||||||
if (!timestamp) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
@@ -50,10 +39,7 @@ export function formatTimestampToReadableDate(
|
|||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumberToCompactFormat(num: number | undefined): string {
|
export function formatNumberToCompactFormat(num: number): string {
|
||||||
if (!num) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
@@ -61,30 +47,27 @@ export function formatNumberToCompactFormat(num: number | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function determineIsMonotonic(
|
export function determineIsMonotonic(
|
||||||
metricType: MetrictypesTypeDTO,
|
metricType: MetricType,
|
||||||
temporality?: MetrictypesTemporalityDTO,
|
temporality?: Temporality,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
metricType === MetrictypesTypeDTO.histogram ||
|
metricType === MetricType.HISTOGRAM ||
|
||||||
metricType === MetrictypesTypeDTO.exponentialhistogram
|
metricType === MetricType.EXPONENTIAL_HISTOGRAM
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (
|
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
||||||
metricType === MetrictypesTypeDTO.gauge ||
|
|
||||||
metricType === MetrictypesTypeDTO.summary
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (metricType === MetrictypesTypeDTO.sum) {
|
if (metricType === MetricType.SUM) {
|
||||||
return temporality === MetrictypesTemporalityDTO.cumulative;
|
return temporality === Temporality.CUMULATIVE;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricDetailsQuery(
|
export function getMetricDetailsQuery(
|
||||||
metricName: string,
|
metricName: string,
|
||||||
metricType: MetrictypesTypeDTO | undefined,
|
metricType: MetricType | undefined,
|
||||||
filter?: { key: string; value: string },
|
filter?: { key: string; value: string },
|
||||||
groupBy?: string,
|
groupBy?: string,
|
||||||
): Query {
|
): Query {
|
||||||
@@ -92,23 +75,23 @@ export function getMetricDetailsQuery(
|
|||||||
let spaceAggregation;
|
let spaceAggregation;
|
||||||
let aggregateOperator;
|
let aggregateOperator;
|
||||||
switch (metricType) {
|
switch (metricType) {
|
||||||
case MetrictypesTypeDTO.sum:
|
case MetricType.SUM:
|
||||||
timeAggregation = 'rate';
|
timeAggregation = 'rate';
|
||||||
spaceAggregation = 'sum';
|
spaceAggregation = 'sum';
|
||||||
aggregateOperator = 'rate';
|
aggregateOperator = 'rate';
|
||||||
break;
|
break;
|
||||||
case MetrictypesTypeDTO.gauge:
|
case MetricType.GAUGE:
|
||||||
timeAggregation = 'avg';
|
timeAggregation = 'avg';
|
||||||
spaceAggregation = 'avg';
|
spaceAggregation = 'avg';
|
||||||
aggregateOperator = 'avg';
|
aggregateOperator = 'avg';
|
||||||
break;
|
break;
|
||||||
case MetrictypesTypeDTO.summary:
|
case MetricType.SUMMARY:
|
||||||
timeAggregation = 'noop';
|
timeAggregation = 'noop';
|
||||||
spaceAggregation = 'sum';
|
spaceAggregation = 'sum';
|
||||||
aggregateOperator = 'noop';
|
aggregateOperator = 'noop';
|
||||||
break;
|
break;
|
||||||
case MetrictypesTypeDTO.histogram:
|
case MetricType.HISTOGRAM:
|
||||||
case MetrictypesTypeDTO.exponentialhistogram:
|
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||||
timeAggregation = 'noop';
|
timeAggregation = 'noop';
|
||||||
spaceAggregation = 'p90';
|
spaceAggregation = 'p90';
|
||||||
aggregateOperator = 'noop';
|
aggregateOperator = 'noop';
|
||||||
@@ -177,38 +160,3 @@ 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,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Empty,
|
Empty,
|
||||||
@@ -9,24 +10,22 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Spin,
|
Spin,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { Filter } from 'api/v5/v5';
|
|
||||||
import {
|
|
||||||
convertExpressionToFilters,
|
|
||||||
convertFiltersToExpression,
|
|
||||||
} from 'components/QueryBuilderV2/utils';
|
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { SUMMARY_FILTERS_KEY } from './constants';
|
||||||
|
|
||||||
function MetricNameSearch({
|
function MetricNameSearch({
|
||||||
queryFilterExpression,
|
queryFilters,
|
||||||
onFilterChange,
|
|
||||||
}: {
|
}: {
|
||||||
queryFilterExpression: Filter;
|
queryFilters: TagFilter;
|
||||||
onFilterChange: (value: string) => void;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||||
const [searchString, setSearchString] = useState<string>('');
|
const [searchString, setSearchString] = useState<string>('');
|
||||||
const [debouncedSearchString, setDebouncedSearchString] = useState<string>('');
|
const [debouncedSearchString, setDebouncedSearchString] = useState<string>('');
|
||||||
@@ -68,12 +67,9 @@ function MetricNameSearch({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(selectedMetricName: string): void => {
|
(selectedMetricName: string): void => {
|
||||||
const queryFilters = convertExpressionToFilters(
|
|
||||||
queryFilterExpression.expression,
|
|
||||||
);
|
|
||||||
const newFilters = {
|
const newFilters = {
|
||||||
items: [
|
items: [
|
||||||
...queryFilters,
|
...queryFilters.items,
|
||||||
{
|
{
|
||||||
id: 'metric_name',
|
id: 'metric_name',
|
||||||
op: 'CONTAINS',
|
op: 'CONTAINS',
|
||||||
@@ -87,11 +83,13 @@ function MetricNameSearch({
|
|||||||
],
|
],
|
||||||
op: 'and',
|
op: 'and',
|
||||||
};
|
};
|
||||||
const newFilterExpression = convertFiltersToExpression(newFilters);
|
setSearchParams({
|
||||||
onFilterChange(newFilterExpression.expression);
|
...Object.fromEntries(searchParams.entries()),
|
||||||
|
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||||
|
});
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
},
|
},
|
||||||
[queryFilterExpression, onFilterChange],
|
[queryFilters.items, setSearchParams, searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const metricNameFilterValues = useMemo(
|
const metricNameFilterValues = useMemo(
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
BarChart2,
|
|
||||||
BarChartHorizontal,
|
|
||||||
Diff,
|
|
||||||
Gauge,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
|
||||||
|
|
||||||
// TODO: @amlannandy Delete this component after API migration is complete
|
|
||||||
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
|
|
||||||
const [icon, color] = useMemo(() => {
|
|
||||||
switch (type) {
|
|
||||||
case MetricType.SUM:
|
|
||||||
return [
|
|
||||||
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
|
||||||
Color.BG_ROBIN_500,
|
|
||||||
];
|
|
||||||
case MetricType.GAUGE:
|
|
||||||
return [
|
|
||||||
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
|
||||||
Color.BG_SAKURA_500,
|
|
||||||
];
|
|
||||||
case MetricType.HISTOGRAM:
|
|
||||||
return [
|
|
||||||
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
|
||||||
Color.BG_SIENNA_500,
|
|
||||||
];
|
|
||||||
case MetricType.SUMMARY:
|
|
||||||
return [
|
|
||||||
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
|
||||||
Color.BG_FOREST_500,
|
|
||||||
];
|
|
||||||
case MetricType.EXPONENTIAL_HISTOGRAM:
|
|
||||||
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_LABEL_MAP[type]}
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetricTypeRenderer;
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { Button, Menu, Popover, Tooltip } from 'antd';
|
import { Button, Menu, Popover, Tooltip } from 'antd';
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { METRIC_TYPE_VIEW_VALUES_MAP } from './constants';
|
import {
|
||||||
|
METRIC_TYPE_LABEL_MAP,
|
||||||
|
METRIC_TYPE_VALUES_MAP,
|
||||||
|
SUMMARY_FILTERS_KEY,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
function MetricTypeSearch({
|
function MetricTypeSearch({
|
||||||
queryFilters,
|
queryFilters,
|
||||||
onFilterChange,
|
|
||||||
}: {
|
}: {
|
||||||
queryFilters: TagFilter;
|
queryFilters: TagFilter;
|
||||||
onFilterChange: (expression: string) => void;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
@@ -22,9 +26,9 @@ function MetricTypeSearch({
|
|||||||
key: 'all',
|
key: 'all',
|
||||||
value: 'All',
|
value: 'All',
|
||||||
},
|
},
|
||||||
...Object.keys(METRIC_TYPE_VIEW_VALUES_MAP).map((key) => ({
|
...Object.keys(METRIC_TYPE_LABEL_MAP).map((key) => ({
|
||||||
key: METRIC_TYPE_VIEW_VALUES_MAP[key as MetrictypesTypeDTO],
|
key: METRIC_TYPE_VALUES_MAP[key as MetricType],
|
||||||
value: METRIC_TYPE_VIEW_VALUES_MAP[key as MetrictypesTypeDTO],
|
value: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
@@ -32,17 +36,16 @@ function MetricTypeSearch({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(selectedMetricType: string): void => {
|
(selectedMetricType: string): void => {
|
||||||
let newFilters;
|
|
||||||
if (selectedMetricType !== 'all') {
|
if (selectedMetricType !== 'all') {
|
||||||
newFilters = {
|
const newFilters = {
|
||||||
items: [
|
items: [
|
||||||
...queryFilters.items,
|
...queryFilters.items,
|
||||||
{
|
{
|
||||||
id: 'type',
|
id: 'metric_type',
|
||||||
op: '=',
|
op: '=',
|
||||||
key: {
|
key: {
|
||||||
id: 'type',
|
id: 'metric_type',
|
||||||
key: 'type',
|
key: 'metric_type',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
value: selectedMetricType,
|
value: selectedMetricType,
|
||||||
@@ -50,17 +53,23 @@ function MetricTypeSearch({
|
|||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
};
|
};
|
||||||
|
setSearchParams({
|
||||||
|
...Object.fromEntries(searchParams.entries()),
|
||||||
|
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
newFilters = {
|
const newFilters = {
|
||||||
items: queryFilters.items.filter((item) => item.id !== 'type'),
|
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
};
|
};
|
||||||
|
setSearchParams({
|
||||||
|
...Object.fromEntries(searchParams.entries()),
|
||||||
|
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const newFilterExpression = convertFiltersToExpression(newFilters);
|
|
||||||
onFilterChange(newFilterExpression.expression);
|
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
},
|
},
|
||||||
[queryFilters.items, onFilterChange],
|
[queryFilters.items, setSearchParams, searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
BarChart2,
|
|
||||||
BarChartHorizontal,
|
|
||||||
Diff,
|
|
||||||
Gauge,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import { METRIC_TYPE_VIEW_VALUES_MAP } from './constants';
|
|
||||||
|
|
||||||
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 {
|
|
||||||
metricTypeViewRendererStyle,
|
|
||||||
metricTypeViewRendererTextStyle,
|
|
||||||
} = useMemo(() => {
|
|
||||||
return {
|
|
||||||
metricTypeViewRendererStyle: {
|
|
||||||
backgroundColor: `${color}33`,
|
|
||||||
border: `1px solid ${color}`,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
metricTypeViewRendererTextStyle: {
|
|
||||||
color,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [color]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="metric-type-renderer" style={metricTypeViewRendererStyle}>
|
|
||||||
{icon}
|
|
||||||
<Typography.Text style={metricTypeViewRendererTextStyle}>
|
|
||||||
{METRIC_TYPE_VIEW_VALUES_MAP[type]}
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MetricTypeViewRenderer;
|
|
||||||
@@ -1,58 +1,27 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { Info } from 'lucide-react';
|
import { HardHat, Info } from 'lucide-react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
import { MetricsSearchProps } from './types';
|
import { MetricsSearchProps } from './types';
|
||||||
|
|
||||||
function MetricsSearch({
|
function MetricsSearch({ query, onChange }: MetricsSearchProps): JSX.Element {
|
||||||
query,
|
|
||||||
onChange,
|
|
||||||
currentQueryFilterExpression,
|
|
||||||
setCurrentQueryFilterExpression,
|
|
||||||
isLoading,
|
|
||||||
}: MetricsSearchProps): JSX.Element {
|
|
||||||
const handleOnChange = useCallback(
|
|
||||||
(expression: string): void => {
|
|
||||||
setCurrentQueryFilterExpression(expression);
|
|
||||||
},
|
|
||||||
[setCurrentQueryFilterExpression],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleStageAndRunQuery = useCallback(() => {
|
|
||||||
onChange(currentQueryFilterExpression);
|
|
||||||
}, [currentQueryFilterExpression, onChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-search-container">
|
<div className="metrics-search-container">
|
||||||
<div data-testid="qb-search-container" className="qb-search-container">
|
<div className="qb-search-container">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Use filters to refine metrics based on attributes. Example: service_name=api - Shows all metrics associated with the API service"
|
title="Use filters to refine metrics based on attributes. Example: service_name=api - Shows all metrics associated with the API service"
|
||||||
placement="right"
|
placement="right"
|
||||||
>
|
>
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<QuerySearch
|
<QueryBuilderSearch
|
||||||
onChange={handleOnChange}
|
query={query}
|
||||||
dataSource={DataSource.METRICS}
|
onChange={onChange}
|
||||||
queryData={{
|
suffixIcon={<HardHat size={16} />}
|
||||||
...query,
|
isMetricsExplorer
|
||||||
filter: {
|
|
||||||
...query?.filter,
|
|
||||||
expression: currentQueryFilterExpression,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onRun={handleOnChange}
|
|
||||||
showFilterSuggestionsWithoutMetric
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<RunQueryBtn
|
|
||||||
onStageRunQuery={handleStageAndRunQuery}
|
|
||||||
isLoadingQueries={isLoading}
|
|
||||||
/>
|
|
||||||
<div className="metrics-search-options">
|
<div className="metrics-search-options">
|
||||||
<DateTimeSelectionV2
|
<DateTimeSelectionV2
|
||||||
showAutoRefresh={false}
|
showAutoRefresh={false}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { SorterResult } from 'antd/es/table/interface';
|
import { SorterResult } from 'antd/es/table/interface';
|
||||||
import { Querybuildertypesv5OrderDirectionDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||||
@@ -25,8 +24,7 @@ function MetricsTable({
|
|||||||
setOrderBy,
|
setOrderBy,
|
||||||
totalCount,
|
totalCount,
|
||||||
openMetricDetails,
|
openMetricDetails,
|
||||||
queryFilterExpression,
|
queryFilters,
|
||||||
onFilterChange,
|
|
||||||
}: MetricsTableProps): JSX.Element {
|
}: MetricsTableProps): JSX.Element {
|
||||||
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
||||||
(
|
(
|
||||||
@@ -38,20 +36,13 @@ function MetricsTable({
|
|||||||
): void => {
|
): void => {
|
||||||
if ('field' in sorter && sorter.order) {
|
if ('field' in sorter && sorter.order) {
|
||||||
setOrderBy({
|
setOrderBy({
|
||||||
key: {
|
columnName: sorter.field as string,
|
||||||
name: sorter.field as string,
|
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||||
},
|
|
||||||
direction:
|
|
||||||
sorter.order === 'ascend'
|
|
||||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
|
||||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setOrderBy({
|
setOrderBy({
|
||||||
key: {
|
columnName: 'samples',
|
||||||
name: 'samples',
|
order: 'desc',
|
||||||
},
|
|
||||||
direction: Querybuildertypesv5OrderDirectionDTO.desc,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,17 +51,19 @@ function MetricsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-table-container">
|
<div className="metrics-table-container">
|
||||||
<div className="metrics-table-title" data-testid="metrics-table-title">
|
{!isError && !isLoading && (
|
||||||
<Typography.Title level={4} className="metrics-table-title">
|
<div className="metrics-table-title" data-testid="metrics-table-title">
|
||||||
List View
|
<Typography.Title level={4} className="metrics-table-title">
|
||||||
</Typography.Title>
|
List View
|
||||||
<Tooltip
|
</Typography.Title>
|
||||||
title="The table displays all metrics in the selected time range. Each row represents a unique metric, and its metric name, and metadata like description, type, unit, and samples/timeseries cardinality observed in the selected time range."
|
<Tooltip
|
||||||
placement="right"
|
title="The table displays all metrics in the selected time range. Each row represents a unique metric, and its metric name, and metadata like description, type, unit, and samples/timeseries cardinality observed in the selected time range."
|
||||||
>
|
placement="right"
|
||||||
<Info size={16} />
|
>
|
||||||
</Tooltip>
|
<Info size={16} />
|
||||||
</div>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
loading={{
|
loading={{
|
||||||
spinning: isLoading,
|
spinning: isLoading,
|
||||||
@@ -82,7 +75,7 @@ function MetricsTable({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
|
columns={getMetricsTableColumns(queryFilters)}
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: isLoading ? null : (
|
emptyText: isLoading ? null : (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
import { useWindowSize } from 'react-use';
|
||||||
import { Group } from '@visx/group';
|
import { Group } from '@visx/group';
|
||||||
import { Treemap } from '@visx/hierarchy';
|
import { Treemap } from '@visx/hierarchy';
|
||||||
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
|
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
|
||||||
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { stratify, treemapBinary } from 'd3-hierarchy';
|
||||||
import { HierarchyNode, stratify, treemapBinary } from 'd3-hierarchy';
|
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,24 +12,21 @@ import {
|
|||||||
TREEMAP_SQUARE_PADDING,
|
TREEMAP_SQUARE_PADDING,
|
||||||
TREEMAP_VIEW_OPTIONS,
|
TREEMAP_VIEW_OPTIONS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import {
|
import { MetricsTreemapProps, TreemapTile, TreemapViewType } from './types';
|
||||||
MetricsTreemapInternalProps,
|
|
||||||
MetricsTreemapProps,
|
|
||||||
TreemapTile,
|
|
||||||
} from './types';
|
|
||||||
import {
|
import {
|
||||||
getTreemapTileStyle,
|
getTreemapTileStyle,
|
||||||
getTreemapTileTextStyle,
|
getTreemapTileTextStyle,
|
||||||
transformTreemapData,
|
transformTreemapData,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function MetricsTreemapInternal({
|
function MetricsTreemap({
|
||||||
|
viewType,
|
||||||
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
data,
|
|
||||||
viewType,
|
|
||||||
openMetricDetails,
|
openMetricDetails,
|
||||||
}: MetricsTreemapInternalProps): JSX.Element {
|
setHeatmapView,
|
||||||
|
}: MetricsTreemapProps): JSX.Element {
|
||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
|
||||||
const treemapWidth = useMemo(
|
const treemapWidth = useMemo(
|
||||||
@@ -44,9 +40,9 @@ function MetricsTreemapInternal({
|
|||||||
|
|
||||||
const treemapData = useMemo(() => {
|
const treemapData = useMemo(() => {
|
||||||
const extracedTreemapData =
|
const extracedTreemapData =
|
||||||
(viewType === MetricsexplorertypesTreemapModeDTO.timeseries
|
(viewType === TreemapViewType.TIMESERIES
|
||||||
? data?.timeseries
|
? data?.data?.[TreemapViewType.TIMESERIES]
|
||||||
: data?.samples) || [];
|
: data?.data?.[TreemapViewType.SAMPLES]) || [];
|
||||||
return transformTreemapData(extracedTreemapData, viewType);
|
return transformTreemapData(extracedTreemapData, viewType);
|
||||||
}, [data, viewType]);
|
}, [data, viewType]);
|
||||||
|
|
||||||
@@ -58,126 +54,41 @@ function MetricsTreemapInternal({
|
|||||||
const xMax = treemapWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT;
|
const xMax = treemapWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT;
|
||||||
const yMax = TREEMAP_HEIGHT - TREEMAP_MARGINS.TOP - TREEMAP_MARGINS.BOTTOM;
|
const yMax = TREEMAP_HEIGHT - TREEMAP_MARGINS.TOP - TREEMAP_MARGINS.BOTTOM;
|
||||||
|
|
||||||
const treemapStylesWithoutPadding = useMemo(
|
|
||||||
() => ({
|
|
||||||
width: treemapWidth,
|
|
||||||
height: TREEMAP_HEIGHT,
|
|
||||||
}),
|
|
||||||
[treemapWidth],
|
|
||||||
);
|
|
||||||
|
|
||||||
const treemapStylesWithPadding = useMemo(
|
|
||||||
() => ({
|
|
||||||
width: treemapWidth,
|
|
||||||
height: TREEMAP_HEIGHT,
|
|
||||||
paddingTop: 30,
|
|
||||||
}),
|
|
||||||
[treemapWidth],
|
|
||||||
);
|
|
||||||
|
|
||||||
const treemapTileStyle = useCallback(
|
|
||||||
(node: HierarchyNode<TreemapTile>) => ({
|
|
||||||
...getTreemapTileStyle(node.data),
|
|
||||||
...getTreemapTileTextStyle(),
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div data-testid="metrics-treemap-loading-state">
|
<div data-testid="metrics-treemap-loading-state">
|
||||||
<Skeleton style={treemapStylesWithoutPadding} active />
|
<Skeleton
|
||||||
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT + 55 }}
|
||||||
|
active
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (
|
||||||
return (
|
!data ||
|
||||||
<Empty
|
!data.data ||
|
||||||
description="Error fetching metrics. If the problem persists, please contact support."
|
(data?.status === 'success' && !data?.data?.[viewType])
|
||||||
data-testid="metrics-treemap-error-state"
|
) {
|
||||||
style={treemapStylesWithPadding}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || !data?.[viewType]?.length) {
|
|
||||||
return (
|
return (
|
||||||
<Empty
|
<Empty
|
||||||
description="No metrics found"
|
description="No metrics found"
|
||||||
data-testid="metrics-treemap-empty-state"
|
data-testid="metrics-treemap-empty-state"
|
||||||
style={treemapStylesWithPadding}
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (data?.status === 'error' || isError) {
|
||||||
<svg width={treemapWidth} height={TREEMAP_HEIGHT} className="metrics-treemap">
|
return (
|
||||||
<rect
|
<Empty
|
||||||
width={treemapWidth}
|
description="Error fetching metrics. If the problem persists, please contact support."
|
||||||
height={TREEMAP_HEIGHT}
|
data-testid="metrics-treemap-error-state"
|
||||||
rx={14}
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||||
fill="transparent"
|
|
||||||
/>
|
/>
|
||||||
<Treemap<TreemapTile>
|
);
|
||||||
top={TREEMAP_MARGINS.TOP}
|
}
|
||||||
root={transformedTreemapData}
|
|
||||||
size={[xMax, yMax]}
|
|
||||||
tile={treemapBinary}
|
|
||||||
round
|
|
||||||
>
|
|
||||||
{(treemap): JSX.Element => (
|
|
||||||
<Group>
|
|
||||||
{treemap
|
|
||||||
.descendants()
|
|
||||||
.reverse()
|
|
||||||
.map((node, i) => {
|
|
||||||
const nodeWidth = node.x1 - node.x0 - TREEMAP_SQUARE_PADDING;
|
|
||||||
const nodeHeight = node.y1 - node.y0 - TREEMAP_SQUARE_PADDING;
|
|
||||||
if (nodeWidth < 0 || nodeHeight < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
key={node.data.id || `node-${i}`}
|
|
||||||
top={node.y0 + TREEMAP_MARGINS.TOP}
|
|
||||||
left={node.x0 + TREEMAP_MARGINS.LEFT}
|
|
||||||
>
|
|
||||||
{node.depth > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
title={`${node.data.id}: ${node.data.displayValue}%`}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<foreignObject
|
|
||||||
width={nodeWidth}
|
|
||||||
height={nodeHeight}
|
|
||||||
onClick={(): void => openMetricDetails(node.data.id, 'treemap')}
|
|
||||||
>
|
|
||||||
<div style={treemapTileStyle(node)}>
|
|
||||||
{`${node.data.displayValue}%`}
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Treemap>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetricsTreemap({
|
|
||||||
viewType,
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
openMetricDetails,
|
|
||||||
setHeatmapView,
|
|
||||||
}: MetricsTreemapProps): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="metrics-treemap-container"
|
className="metrics-treemap-container"
|
||||||
@@ -197,16 +108,72 @@ function MetricsTreemap({
|
|||||||
options={TREEMAP_VIEW_OPTIONS}
|
options={TREEMAP_VIEW_OPTIONS}
|
||||||
value={viewType}
|
value={viewType}
|
||||||
onChange={setHeatmapView}
|
onChange={setHeatmapView}
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MetricsTreemapInternal
|
<svg
|
||||||
isLoading={isLoading}
|
width={treemapWidth}
|
||||||
isError={isError}
|
height={TREEMAP_HEIGHT}
|
||||||
data={data}
|
className="metrics-treemap"
|
||||||
viewType={viewType}
|
>
|
||||||
openMetricDetails={openMetricDetails}
|
<rect
|
||||||
/>
|
width={treemapWidth}
|
||||||
|
height={TREEMAP_HEIGHT}
|
||||||
|
rx={14}
|
||||||
|
fill="transparent"
|
||||||
|
/>
|
||||||
|
<Treemap<TreemapTile>
|
||||||
|
top={TREEMAP_MARGINS.TOP}
|
||||||
|
root={transformedTreemapData}
|
||||||
|
size={[xMax, yMax]}
|
||||||
|
tile={treemapBinary}
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{(treemap): JSX.Element => (
|
||||||
|
<Group>
|
||||||
|
{treemap
|
||||||
|
.descendants()
|
||||||
|
.reverse()
|
||||||
|
.map((node, i) => {
|
||||||
|
const nodeWidth = node.x1 - node.x0 - TREEMAP_SQUARE_PADDING;
|
||||||
|
const nodeHeight = node.y1 - node.y0 - TREEMAP_SQUARE_PADDING;
|
||||||
|
if (nodeWidth < 0 || nodeHeight < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={node.data.id || `node-${i}`}
|
||||||
|
top={node.y0 + TREEMAP_MARGINS.TOP}
|
||||||
|
left={node.x0 + TREEMAP_MARGINS.LEFT}
|
||||||
|
>
|
||||||
|
{node.depth > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
title={`${node.data.id}: ${node.data.displayValue}%`}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<foreignObject
|
||||||
|
width={nodeWidth}
|
||||||
|
height={nodeHeight}
|
||||||
|
onClick={(): void => openMetricDetails(node.data.id, 'treemap')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...getTreemapTileStyle(node.data),
|
||||||
|
...getTreemapTileTextStyle(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${node.data.displayValue}%`}
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Treemap>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
.metrics-search-container {
|
.metrics-search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.metrics-search-options {
|
.metrics-search-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,24 +4,11 @@ import { useSelector } from 'react-redux';
|
|||||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import {
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
useGetMetricsStats,
|
|
||||||
useGetMetricsTreemap,
|
|
||||||
} from 'api/generated/services/metrics';
|
|
||||||
import {
|
|
||||||
MetricsexplorertypesStatsRequestDTO,
|
|
||||||
MetricsexplorertypesTreemapModeDTO,
|
|
||||||
MetricsexplorertypesTreemapRequestDTO,
|
|
||||||
Querybuildertypesv5OrderByDTO,
|
|
||||||
Querybuildertypesv5OrderDirectionDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import {
|
|
||||||
convertExpressionToFilters,
|
|
||||||
convertFiltersToExpression,
|
|
||||||
} from 'components/QueryBuilderV2/utils';
|
|
||||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||||
import NoLogs from 'container/NoLogs/NoLogs';
|
import NoLogs from 'container/NoLogs/NoLogs';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
|
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@@ -36,38 +23,32 @@ import {
|
|||||||
IS_INSPECT_MODAL_OPEN_KEY,
|
IS_INSPECT_MODAL_OPEN_KEY,
|
||||||
IS_METRIC_DETAILS_OPEN_KEY,
|
IS_METRIC_DETAILS_OPEN_KEY,
|
||||||
SELECTED_METRIC_NAME_KEY,
|
SELECTED_METRIC_NAME_KEY,
|
||||||
|
SUMMARY_FILTERS_KEY,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import MetricsSearch from './MetricsSearch';
|
import MetricsSearch from './MetricsSearch';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import MetricsTreemap from './MetricsTreemap';
|
import MetricsTreemap from './MetricsTreemap';
|
||||||
import { convertNanoToMilliseconds, formatDataForMetricsTable } from './utils';
|
import { OrderByPayload, TreemapViewType } from './types';
|
||||||
|
import {
|
||||||
|
convertNanoToMilliseconds,
|
||||||
|
formatDataForMetricsTable,
|
||||||
|
getMetricsListQuery,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
import './Summary.styles.scss';
|
import './Summary.styles.scss';
|
||||||
|
|
||||||
const DEFAULT_ORDER_BY: Querybuildertypesv5OrderByDTO = {
|
const DEFAULT_ORDER_BY: OrderByPayload = {
|
||||||
key: {
|
columnName: 'samples',
|
||||||
name: 'samples',
|
order: 'desc',
|
||||||
},
|
|
||||||
direction: Querybuildertypesv5OrderDirectionDTO.desc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Summary(): JSX.Element {
|
function Summary(): JSX.Element {
|
||||||
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [orderBy, setOrderBy] = useState<Querybuildertypesv5OrderByDTO>(
|
const [orderBy, setOrderBy] = useState<OrderByPayload>(DEFAULT_ORDER_BY);
|
||||||
DEFAULT_ORDER_BY,
|
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
||||||
|
TreemapViewType.TIMESERIES,
|
||||||
);
|
);
|
||||||
const [
|
|
||||||
heatmapView,
|
|
||||||
setHeatmapView,
|
|
||||||
] = useState<MetricsexplorertypesTreemapModeDTO>(
|
|
||||||
MetricsexplorertypesTreemapModeDTO.timeseries,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
|
||||||
const query = useMemo(() => currentQuery?.builder?.queryData[0], [
|
|
||||||
currentQuery,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
|
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
|
||||||
@@ -84,10 +65,16 @@ function Summary(): JSX.Element {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const queryFilters: TagFilter = useMemo(() => {
|
||||||
currentQueryFilterExpression,
|
const encodedFilters = searchParams.get(SUMMARY_FILTERS_KEY);
|
||||||
setCurrentQueryFilterExpression,
|
if (encodedFilters) {
|
||||||
] = useState<string>(query?.filter?.expression || '');
|
return JSON.parse(encodedFilters);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent(MetricsExplorerEvents.TabChanged, {
|
logEvent(MetricsExplorerEvents.TabChanged, {
|
||||||
@@ -100,101 +87,105 @@ function Summary(): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const queryFilterExpression = useMemo(() => {
|
// This is used to avoid the filters from being serialized with the id
|
||||||
const filters = query.filters || { items: [], op: 'AND' };
|
const queryFiltersWithoutId = useMemo(() => {
|
||||||
return convertFiltersToExpression(filters);
|
const filtersWithoutId = {
|
||||||
}, [query.filters]);
|
...queryFilters,
|
||||||
|
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
|
||||||
|
};
|
||||||
|
return JSON.stringify(filtersWithoutId);
|
||||||
|
}, [queryFilters]);
|
||||||
|
|
||||||
const metricsListQuery: MetricsexplorertypesStatsRequestDTO = useMemo(() => {
|
const metricsListQuery = useMemo(() => {
|
||||||
|
const baseQuery = getMetricsListQuery();
|
||||||
return {
|
return {
|
||||||
start: convertNanoToMilliseconds(minTime),
|
...baseQuery,
|
||||||
end: convertNanoToMilliseconds(maxTime),
|
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (currentPage - 1) * pageSize,
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
filters: queryFilters,
|
||||||
|
start: convertNanoToMilliseconds(minTime),
|
||||||
|
end: convertNanoToMilliseconds(maxTime),
|
||||||
orderBy,
|
orderBy,
|
||||||
filter: {
|
|
||||||
expression: queryFilterExpression.expression,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [queryFilters, minTime, maxTime, orderBy, pageSize, currentPage]);
|
||||||
minTime,
|
|
||||||
maxTime,
|
|
||||||
orderBy,
|
|
||||||
pageSize,
|
|
||||||
currentPage,
|
|
||||||
queryFilterExpression.expression,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const metricsTreemapQuery: MetricsexplorertypesTreemapRequestDTO = useMemo(
|
const metricsTreemapQuery = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
filters: queryFilters,
|
||||||
treemap: heatmapView,
|
treemap: heatmapView,
|
||||||
start: convertNanoToMilliseconds(minTime),
|
start: convertNanoToMilliseconds(minTime),
|
||||||
end: convertNanoToMilliseconds(maxTime),
|
end: convertNanoToMilliseconds(maxTime),
|
||||||
mode: heatmapView,
|
|
||||||
filter: {
|
|
||||||
expression: queryFilterExpression.expression,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
[heatmapView, minTime, maxTime, queryFilterExpression.expression],
|
[queryFilters, heatmapView, minTime, maxTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: metricsData,
|
data: metricsData,
|
||||||
mutate: getMetricsStats,
|
isLoading: isMetricsLoading,
|
||||||
isLoading: isGetMetricsStatsLoading,
|
isFetching: isMetricsFetching,
|
||||||
isError: isGetMetricsStatsError,
|
isError: isMetricsError,
|
||||||
} = useGetMetricsStats();
|
} = useGetMetricsList(metricsListQuery, {
|
||||||
|
enabled: !!metricsListQuery && !isInspectModalOpen,
|
||||||
|
queryKey: [
|
||||||
|
'metricsList',
|
||||||
|
queryFiltersWithoutId,
|
||||||
|
orderBy,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const isListViewError = useMemo(
|
||||||
|
() => isMetricsError || !!(metricsData && metricsData.statusCode !== 200),
|
||||||
|
[isMetricsError, metricsData],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: treeMapData,
|
data: treeMapData,
|
||||||
mutate: getMetricsTreemap,
|
isLoading: isTreeMapLoading,
|
||||||
isLoading: isGetMetricsTreemapLoading,
|
isFetching: isTreeMapFetching,
|
||||||
isError: isGetMetricsTreemapError,
|
isError: isTreeMapError,
|
||||||
} = useGetMetricsTreemap();
|
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
||||||
|
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
||||||
|
queryKey: [
|
||||||
|
'metricsTreemap',
|
||||||
|
queryFiltersWithoutId,
|
||||||
|
heatmapView,
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const isProportionViewError = useMemo(
|
||||||
getMetricsStats({
|
() => isTreeMapError || treeMapData?.statusCode !== 200,
|
||||||
data: metricsListQuery,
|
[isTreeMapError, treeMapData],
|
||||||
});
|
);
|
||||||
}, [metricsListQuery, getMetricsStats]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMetricsTreemap({
|
|
||||||
data: metricsTreemapQuery,
|
|
||||||
});
|
|
||||||
}, [metricsTreemapQuery, getMetricsTreemap]);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(expression: string) => {
|
(value: TagFilter) => {
|
||||||
const newFilters: TagFilter = {
|
setSearchParams({
|
||||||
items: convertExpressionToFilters(expression),
|
...Object.fromEntries(searchParams.entries()),
|
||||||
op: 'AND',
|
[SUMMARY_FILTERS_KEY]: JSON.stringify(value),
|
||||||
};
|
|
||||||
redirectWithQueryBuilderData({
|
|
||||||
...currentQuery,
|
|
||||||
builder: {
|
|
||||||
...currentQuery.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...currentQuery.builder.queryData[0],
|
|
||||||
filters: newFilters,
|
|
||||||
filter: {
|
|
||||||
expression,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setCurrentQueryFilterExpression(expression);
|
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
if (expression) {
|
if (value.items.length > 0) {
|
||||||
logEvent(MetricsExplorerEvents.FilterApplied, {
|
logEvent(MetricsExplorerEvents.FilterApplied, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentQuery, redirectWithQueryBuilderData],
|
[setSearchParams, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...initialQueriesMap.metrics.builder.queryData[0],
|
||||||
|
filters: queryFilters,
|
||||||
|
}),
|
||||||
|
[queryFilters],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||||
@@ -211,7 +202,7 @@ function Summary(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formattedMetricsData = useMemo(
|
const formattedMetricsData = useMemo(
|
||||||
() => formatDataForMetricsTable(metricsData?.data.metrics || []),
|
() => formatDataForMetricsTable(metricsData?.payload?.data?.metrics || []),
|
||||||
[metricsData],
|
[metricsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -263,9 +254,7 @@ function Summary(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetHeatmapView = (
|
const handleSetHeatmapView = (view: TreemapViewType): void => {
|
||||||
view: MetricsexplorertypesTreemapModeDTO,
|
|
||||||
): void => {
|
|
||||||
setHeatmapView(view);
|
setHeatmapView(view);
|
||||||
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
|
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
@@ -273,67 +262,68 @@ function Summary(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetOrderBy = (orderBy: Querybuildertypesv5OrderByDTO): void => {
|
const handleSetOrderBy = (orderBy: OrderByPayload): void => {
|
||||||
setOrderBy(orderBy);
|
setOrderBy(orderBy);
|
||||||
logEvent(MetricsExplorerEvents.OrderByApplied, {
|
logEvent(MetricsExplorerEvents.OrderByApplied, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
[MetricsExplorerEventKeys.ColumnName]: orderBy.key?.name,
|
[MetricsExplorerEventKeys.ColumnName]: orderBy.columnName,
|
||||||
[MetricsExplorerEventKeys.Order]: orderBy.direction,
|
[MetricsExplorerEventKeys.Order]: orderBy.order,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMetricsListDataEmpty =
|
const isMetricsListDataEmpty = useMemo(
|
||||||
formattedMetricsData.length === 0 && !isGetMetricsStatsLoading;
|
() =>
|
||||||
|
formattedMetricsData.length === 0 && !isMetricsLoading && !isMetricsFetching,
|
||||||
|
[formattedMetricsData, isMetricsLoading, isMetricsFetching],
|
||||||
|
);
|
||||||
|
|
||||||
const isMetricsTreeMapDataEmpty =
|
const isMetricsTreeMapDataEmpty = useMemo(
|
||||||
!treeMapData?.data[heatmapView]?.length && !isGetMetricsTreemapLoading;
|
() =>
|
||||||
|
!treeMapData?.payload?.data[heatmapView]?.length &&
|
||||||
const showFullScreenLoading =
|
!isTreeMapLoading &&
|
||||||
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
|
!isTreeMapFetching,
|
||||||
formattedMetricsData.length === 0 &&
|
[
|
||||||
!treeMapData?.data[heatmapView]?.length;
|
treeMapData?.payload?.data,
|
||||||
|
heatmapView,
|
||||||
|
isTreeMapLoading,
|
||||||
|
isTreeMapFetching,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<div className="metrics-explorer-summary-tab">
|
<div className="metrics-explorer-summary-tab">
|
||||||
<MetricsSearch
|
<MetricsSearch query={searchQuery} onChange={handleFilterChange} />
|
||||||
query={query}
|
{isMetricsLoading || isTreeMapLoading ? (
|
||||||
onChange={handleFilterChange}
|
|
||||||
currentQueryFilterExpression={currentQueryFilterExpression}
|
|
||||||
setCurrentQueryFilterExpression={setCurrentQueryFilterExpression}
|
|
||||||
isLoading={isGetMetricsStatsLoading || isGetMetricsTreemapLoading}
|
|
||||||
/>
|
|
||||||
{showFullScreenLoading ? (
|
|
||||||
<MetricsLoading />
|
<MetricsLoading />
|
||||||
) : isMetricsListDataEmpty && isMetricsTreeMapDataEmpty ? (
|
) : isMetricsListDataEmpty && isMetricsTreeMapDataEmpty ? (
|
||||||
<NoLogs dataSource={DataSource.METRICS} />
|
<NoLogs dataSource={DataSource.METRICS} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<MetricsTreemap
|
<MetricsTreemap
|
||||||
data={treeMapData?.data}
|
data={treeMapData?.payload}
|
||||||
isLoading={isGetMetricsTreemapLoading}
|
isLoading={isTreeMapLoading || isTreeMapFetching}
|
||||||
isError={isGetMetricsTreemapError}
|
isError={isProportionViewError}
|
||||||
viewType={heatmapView}
|
viewType={heatmapView}
|
||||||
openMetricDetails={openMetricDetails}
|
openMetricDetails={openMetricDetails}
|
||||||
setHeatmapView={handleSetHeatmapView}
|
setHeatmapView={handleSetHeatmapView}
|
||||||
/>
|
/>
|
||||||
<MetricsTable
|
<MetricsTable
|
||||||
isLoading={isGetMetricsStatsLoading}
|
isLoading={isMetricsLoading || isMetricsFetching}
|
||||||
isError={isGetMetricsStatsError}
|
isError={isListViewError}
|
||||||
data={formattedMetricsData}
|
data={formattedMetricsData}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onPaginationChange={onPaginationChange}
|
onPaginationChange={onPaginationChange}
|
||||||
setOrderBy={handleSetOrderBy}
|
setOrderBy={handleSetOrderBy}
|
||||||
totalCount={metricsData?.data.total || 0}
|
totalCount={metricsData?.payload?.data?.total || 0}
|
||||||
openMetricDetails={openMetricDetails}
|
openMetricDetails={openMetricDetails}
|
||||||
queryFilterExpression={queryFilterExpression}
|
queryFilters={queryFilters}
|
||||||
onFilterChange={handleFilterChange}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isMetricDetailsOpen && selectedMetricName && (
|
{isMetricDetailsOpen && (
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
isOpen={isMetricDetailsOpen}
|
isOpen={isMetricDetailsOpen}
|
||||||
onClose={closeMetricDetails}
|
onClose={closeMetricDetails}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
|
||||||
|
|
||||||
import MetricTypeRenderer from '../MetricTypeRenderer';
|
|
||||||
|
|
||||||
jest.mock('lucide-react', () => {
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
Diff: (): JSX.Element => <svg data-testid="diff-icon" />,
|
|
||||||
Gauge: (): JSX.Element => <svg data-testid="gauge-icon" />,
|
|
||||||
BarChart2: (): JSX.Element => <svg data-testid="bar-chart-2-icon" />,
|
|
||||||
BarChartHorizontal: (): JSX.Element => (
|
|
||||||
<svg data-testid="bar-chart-horizontal-icon" />
|
|
||||||
),
|
|
||||||
BarChart: (): JSX.Element => <svg data-testid="bar-chart-icon" />,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('MetricTypeRenderer', () => {
|
|
||||||
it('should render correct icon and color for each metric type', () => {
|
|
||||||
const types = [
|
|
||||||
{
|
|
||||||
type: MetricType.SUM,
|
|
||||||
color: Color.BG_ROBIN_500,
|
|
||||||
iconTestId: 'diff-icon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: MetricType.GAUGE,
|
|
||||||
color: Color.BG_SAKURA_500,
|
|
||||||
iconTestId: 'gauge-icon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: MetricType.HISTOGRAM,
|
|
||||||
color: Color.BG_SIENNA_500,
|
|
||||||
iconTestId: 'bar-chart-2-icon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: MetricType.SUMMARY,
|
|
||||||
color: Color.BG_FOREST_500,
|
|
||||||
iconTestId: 'bar-chart-horizontal-icon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: MetricType.EXPONENTIAL_HISTOGRAM,
|
|
||||||
color: Color.BG_AQUA_500,
|
|
||||||
iconTestId: 'bar-chart-icon',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
types.forEach(({ type, color, iconTestId }) => {
|
|
||||||
const { container } = render(<MetricTypeRenderer type={type} />);
|
|
||||||
const rendererDiv = container.firstChild as HTMLElement;
|
|
||||||
|
|
||||||
expect(rendererDiv).toHaveStyle({
|
|
||||||
backgroundColor: `${color}33`,
|
|
||||||
border: `1px solid ${color}`,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId(iconTestId)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { Filter } from 'api/v5/v5';
|
|
||||||
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
||||||
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import MetricsTable from '../MetricsTable';
|
import MetricsTable from '../MetricsTable';
|
||||||
import { MetricsListItemRowData } from '../types';
|
import { MetricsListItemRowData } from '../types';
|
||||||
@@ -30,8 +30,9 @@ const mockData: MetricsListItemRowData[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockQueryFilterExpression: Filter = {
|
const mockQueryFilters: TagFilter = {
|
||||||
expression: '',
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('react-router-dom-v5-compat', () => {
|
jest.mock('react-router-dom-v5-compat', () => {
|
||||||
@@ -81,8 +82,7 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -106,9 +106,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
isLoading
|
isLoading
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -131,8 +130,7 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -160,8 +158,7 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -190,8 +187,7 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={mockOpenMetricDetails}
|
openMetricDetails={mockOpenMetricDetails}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -216,8 +212,7 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={mockSetOrderBy}
|
setOrderBy={mockSetOrderBy}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilterExpression={mockQueryFilterExpression}
|
queryFilters={mockQueryFilters}
|
||||||
onFilterChange={jest.fn()}
|
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -227,10 +222,8 @@ describe('MetricsTable', () => {
|
|||||||
fireEvent.click(samplesHeader);
|
fireEvent.click(samplesHeader);
|
||||||
|
|
||||||
expect(mockSetOrderBy).toHaveBeenCalledWith({
|
expect(mockSetOrderBy).toHaveBeenCalledWith({
|
||||||
key: {
|
columnName: 'samples',
|
||||||
name: 'samples',
|
order: 'asc',
|
||||||
},
|
|
||||||
direction: 'asc',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
import MetricsTreemap from '../MetricsTreemap';
|
import MetricsTreemap from '../MetricsTreemap';
|
||||||
|
import { TreemapViewType } from '../types';
|
||||||
|
|
||||||
jest.mock('d3-hierarchy', () => ({
|
jest.mock('d3-hierarchy', () => ({
|
||||||
stratify: jest.fn().mockReturnValue({
|
stratify: jest.fn().mockReturnValue({
|
||||||
@@ -27,14 +27,14 @@ jest.mock('react-use', () => ({
|
|||||||
|
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
metricName: 'Metric 1',
|
metric_name: 'Metric 1',
|
||||||
percentage: 0.5,
|
percentage: 0.5,
|
||||||
totalValue: 15,
|
total_value: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metricName: 'Metric 2',
|
metric_name: 'Metric 2',
|
||||||
percentage: 0.6,
|
percentage: 0.6,
|
||||||
totalValue: 10,
|
total_value: 10,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -47,11 +47,14 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError={false}
|
isError={false}
|
||||||
data={{
|
data={{
|
||||||
timeseries: [mockData[0]],
|
status: 'success',
|
||||||
samples: [mockData[1]],
|
data: {
|
||||||
|
timeseries: [mockData[0]],
|
||||||
|
samples: [mockData[1]],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
viewType={TreemapViewType.SAMPLES}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -69,11 +72,14 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading
|
isLoading
|
||||||
isError={false}
|
isError={false}
|
||||||
data={{
|
data={{
|
||||||
timeseries: [mockData[0]],
|
status: 'success',
|
||||||
samples: [mockData[1]],
|
data: {
|
||||||
|
timeseries: [mockData[0]],
|
||||||
|
samples: [mockData[1]],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
viewType={TreemapViewType.SAMPLES}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -93,11 +99,14 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError
|
isError
|
||||||
data={{
|
data={{
|
||||||
timeseries: [mockData[0]],
|
status: 'success',
|
||||||
samples: [mockData[1]],
|
data: {
|
||||||
|
timeseries: [mockData[0]],
|
||||||
|
samples: [mockData[1]],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
viewType={TreemapViewType.SAMPLES}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -119,9 +128,9 @@ describe('MetricsTreemap', () => {
|
|||||||
<MetricsTreemap
|
<MetricsTreemap
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError={false}
|
isError={false}
|
||||||
data={undefined}
|
data={null}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
viewType={TreemapViewType.SAMPLES}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,81 +1,109 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import { Filter } from 'api/v5/v5';
|
|
||||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { TreemapViewType } from '../types';
|
import { TreemapViewType } from '../types';
|
||||||
import { formatDataForMetricsTable, getMetricsTableColumns } from '../utils';
|
import {
|
||||||
|
formatDataForMetricsTable,
|
||||||
const mockQueryExpression: Filter = {
|
getMetricsTableColumns,
|
||||||
expression: '',
|
MetricTypeRenderer,
|
||||||
};
|
} from '../utils';
|
||||||
const mockOnChange = jest.fn();
|
|
||||||
|
|
||||||
describe('metricsTableColumns', () => {
|
describe('metricsTableColumns', () => {
|
||||||
|
const mockQueryFilters: TagFilter = {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
it('should have correct column definitions', () => {
|
it('should have correct column definitions', () => {
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)).toHaveLength(6);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange),
|
|
||||||
).toHaveLength(6);
|
|
||||||
|
|
||||||
// Metric Name column
|
// Metric Name column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[0].dataIndex).toBe(
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].dataIndex,
|
'metric_name',
|
||||||
).toBe('metric_name');
|
);
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[0].width).toBe(400);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].width,
|
expect(getMetricsTableColumns(mockQueryFilters)[0].sorter).toBe(false);
|
||||||
).toBe(400);
|
|
||||||
expect(
|
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].sorter,
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
// Description column
|
// Description column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[1].dataIndex).toBe(
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].dataIndex,
|
'description',
|
||||||
).toBe('description');
|
);
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[1].width).toBe(400);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].width,
|
|
||||||
).toBe(400);
|
|
||||||
|
|
||||||
// Type column
|
// Type column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[2].dataIndex).toBe(
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].dataIndex,
|
'metric_type',
|
||||||
).toBe('metric_type');
|
);
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[2].width).toBe(150);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].width,
|
expect(getMetricsTableColumns(mockQueryFilters)[2].sorter).toBe(false);
|
||||||
).toBe(150);
|
|
||||||
expect(
|
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].sorter,
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
// Unit column
|
// Unit column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[3].dataIndex).toBe('unit');
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].dataIndex,
|
expect(getMetricsTableColumns(mockQueryFilters)[3].width).toBe(150);
|
||||||
).toBe('unit');
|
|
||||||
expect(
|
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].width,
|
|
||||||
).toBe(150);
|
|
||||||
|
|
||||||
// Samples column
|
// Samples column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[4].dataIndex).toBe(
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].dataIndex,
|
TreemapViewType.SAMPLES,
|
||||||
).toBe(TreemapViewType.SAMPLES);
|
);
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[4].width).toBe(150);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].width,
|
expect(getMetricsTableColumns(mockQueryFilters)[4].sorter).toBe(true);
|
||||||
).toBe(150);
|
|
||||||
expect(
|
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].sorter,
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
// Time Series column
|
// Time Series column
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[5].dataIndex).toBe(
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].dataIndex,
|
TreemapViewType.TIMESERIES,
|
||||||
).toBe(TreemapViewType.TIMESERIES);
|
);
|
||||||
expect(
|
expect(getMetricsTableColumns(mockQueryFilters)[5].width).toBe(150);
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].width,
|
expect(getMetricsTableColumns(mockQueryFilters)[5].sorter).toBe(true);
|
||||||
).toBe(150);
|
});
|
||||||
expect(
|
|
||||||
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].sorter,
|
describe('MetricTypeRenderer', () => {
|
||||||
).toBe(true);
|
it('should render correct icon and color for each metric type', () => {
|
||||||
|
const types = [
|
||||||
|
{
|
||||||
|
type: MetricType.SUM,
|
||||||
|
color: Color.BG_ROBIN_500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetricType.GAUGE,
|
||||||
|
color: Color.BG_SAKURA_500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetricType.HISTOGRAM,
|
||||||
|
color: Color.BG_SIENNA_500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetricType.SUMMARY,
|
||||||
|
color: Color.BG_FOREST_500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetricType.EXPONENTIAL_HISTOGRAM,
|
||||||
|
color: Color.BG_AQUA_500,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
types.forEach(({ type, color }) => {
|
||||||
|
const { container } = render(<MetricTypeRenderer type={type} />);
|
||||||
|
const rendererDiv = container.firstChild as HTMLElement;
|
||||||
|
|
||||||
|
expect(rendererDiv).toHaveStyle({
|
||||||
|
backgroundColor: `${color}33`,
|
||||||
|
border: `1px solid ${color}`,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty icon and color for unknown metric type', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MetricTypeRenderer type={'UNKNOWN' as MetricType} />,
|
||||||
|
);
|
||||||
|
const rendererDiv = container.firstChild as HTMLElement;
|
||||||
|
|
||||||
|
expect(rendererDiv.querySelector('svg')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,12 +111,12 @@ describe('formatDataForMetricsTable', () => {
|
|||||||
it('should format metrics data correctly', () => {
|
it('should format metrics data correctly', () => {
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
metricName: 'test_metric',
|
metric_name: 'test_metric',
|
||||||
description: 'Test description',
|
description: 'Test description',
|
||||||
type: MetrictypesTypeDTO.gauge,
|
type: MetricType.GAUGE,
|
||||||
unit: 'bytes',
|
unit: 'bytes',
|
||||||
samples: 1000,
|
[TreemapViewType.SAMPLES]: 1000,
|
||||||
timeseries: 2000,
|
[TreemapViewType.TIMESERIES]: 2000,
|
||||||
lastReceived: '2023-01-01T00:00:00Z',
|
lastReceived: '2023-01-01T00:00:00Z',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -135,12 +163,12 @@ describe('formatDataForMetricsTable', () => {
|
|||||||
it('should handle empty/null values', () => {
|
it('should handle empty/null values', () => {
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
metricName: '',
|
metric_name: '',
|
||||||
description: '',
|
description: '',
|
||||||
type: MetrictypesTypeDTO.gauge,
|
type: MetricType.GAUGE,
|
||||||
unit: '',
|
unit: '',
|
||||||
samples: 0,
|
[TreemapViewType.SAMPLES]: 0,
|
||||||
timeseries: 0,
|
[TreemapViewType.TIMESERIES]: 0,
|
||||||
lastReceived: '2023-01-01T00:00:00Z',
|
lastReceived: '2023-01-01T00:00:00Z',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import {
|
|
||||||
MetricsexplorertypesTreemapModeDTO,
|
|
||||||
MetrictypesTypeDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
|
||||||
|
import { TreemapViewType } from './types';
|
||||||
|
|
||||||
export const METRICS_TABLE_PAGE_SIZE = 10;
|
export const METRICS_TABLE_PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const TREEMAP_VIEW_OPTIONS: {
|
export const TREEMAP_VIEW_OPTIONS: {
|
||||||
value: MetricsexplorertypesTreemapModeDTO;
|
value: TreemapViewType;
|
||||||
label: string;
|
label: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ value: MetricsexplorertypesTreemapModeDTO.timeseries, label: 'Time Series' },
|
{ value: TreemapViewType.TIMESERIES, label: 'Time Series' },
|
||||||
{ value: MetricsexplorertypesTreemapModeDTO.samples, label: 'Samples' },
|
{ value: TreemapViewType.SAMPLES, label: 'Samples' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TREEMAP_HEIGHT = 200;
|
export const TREEMAP_HEIGHT = 200;
|
||||||
@@ -19,7 +17,6 @@ export const TREEMAP_SQUARE_PADDING = 5;
|
|||||||
|
|
||||||
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
||||||
|
|
||||||
// TODO: Remove this once API migration is complete
|
|
||||||
export const METRIC_TYPE_LABEL_MAP = {
|
export const METRIC_TYPE_LABEL_MAP = {
|
||||||
[MetricType.SUM]: 'Sum',
|
[MetricType.SUM]: 'Sum',
|
||||||
[MetricType.GAUGE]: 'Gauge',
|
[MetricType.GAUGE]: 'Gauge',
|
||||||
@@ -28,16 +25,7 @@ export const METRIC_TYPE_LABEL_MAP = {
|
|||||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
|
export const METRIC_TYPE_VALUES_MAP = {
|
||||||
[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.SUM]: 'Sum',
|
||||||
[MetricType.GAUGE]: 'Gauge',
|
[MetricType.GAUGE]: 'Gauge',
|
||||||
[MetricType.HISTOGRAM]: 'Histogram',
|
[MetricType.HISTOGRAM]: 'Histogram',
|
||||||
@@ -45,14 +33,7 @@ export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
|
|||||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
|
[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_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
|
||||||
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
|
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
|
||||||
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
|
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
|
||||||
|
export const SUMMARY_FILTERS_KEY = 'summaryFilters';
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
import {
|
import {
|
||||||
MetricsexplorertypesTreemapModeDTO,
|
IBuilderQuery,
|
||||||
MetricsexplorertypesTreemapResponseDTO,
|
TagFilter,
|
||||||
Querybuildertypesv5OrderByDTO,
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
|
||||||
import { Filter } from 'api/v5/v5';
|
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
|
|
||||||
export interface MetricsTableProps {
|
export interface MetricsTableProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -14,36 +12,24 @@ export interface MetricsTableProps {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
onPaginationChange: (page: number, pageSize: number) => void;
|
onPaginationChange: (page: number, pageSize: number) => void;
|
||||||
setOrderBy: (orderBy: Querybuildertypesv5OrderByDTO) => void;
|
setOrderBy: (orderBy: OrderByPayload) => void;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||||
queryFilterExpression: Filter;
|
queryFilters: TagFilter;
|
||||||
onFilterChange: (expression: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsSearchProps {
|
export interface MetricsSearchProps {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (expression: string) => void;
|
onChange: (value: TagFilter) => void;
|
||||||
currentQueryFilterExpression: string;
|
|
||||||
setCurrentQueryFilterExpression: (expression: string) => void;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsTreemapProps {
|
export interface MetricsTreemapProps {
|
||||||
data: MetricsexplorertypesTreemapResponseDTO | undefined;
|
data: MetricsTreeMapResponse | null | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
viewType: MetricsexplorertypesTreemapModeDTO;
|
viewType: TreemapViewType;
|
||||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
|
||||||
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetricsTreemapInternalProps {
|
|
||||||
isLoading: boolean;
|
|
||||||
isError: boolean;
|
|
||||||
data: MetricsexplorertypesTreemapResponseDTO | undefined;
|
|
||||||
viewType: MetricsexplorertypesTreemapModeDTO;
|
|
||||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||||
|
setHeatmapView: (value: TreemapViewType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderByPayload {
|
export interface OrderByPayload {
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType } from 'antd/es/table';
|
||||||
import {
|
import {
|
||||||
MetricsexplorertypesStatDTO,
|
MetricsListItemData,
|
||||||
MetricsexplorertypesTreemapEntryDTO,
|
MetricsListPayload,
|
||||||
MetricsexplorertypesTreemapModeDTO,
|
MetricType,
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
} from 'api/metricsExplorer/getMetricsList';
|
||||||
import { MetricsListPayload } from 'api/metricsExplorer/getMetricsList';
|
import {
|
||||||
import { Filter } from 'api/v5/v5';
|
SamplesData,
|
||||||
|
TimeseriesData,
|
||||||
|
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
BarChart2,
|
||||||
|
BarChartHorizontal,
|
||||||
|
Diff,
|
||||||
|
Gauge,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||||
import MetricNameSearch from './MetricNameSearch';
|
import MetricNameSearch from './MetricNameSearch';
|
||||||
import MetricTypeViewRenderer from './MetricTypeViewRenderer';
|
import MetricTypeSearch from './MetricTypeSearch';
|
||||||
import { MetricsListItemRowData, TreemapTile } from './types';
|
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
||||||
|
|
||||||
export const getMetricsTableColumns = (
|
export const getMetricsTableColumns = (
|
||||||
queryFilterExpression: Filter,
|
queryFilters: TagFilter,
|
||||||
onFilterChange: (expression: string) => void,
|
|
||||||
): ColumnType<MetricsListItemRowData>[] => [
|
): ColumnType<MetricsListItemRowData>[] => [
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
<div className="metric-name-column-header">
|
<div className="metric-name-column-header">
|
||||||
<span className="metric-name-column-header-text">METRIC</span>
|
<span className="metric-name-column-header-text">METRIC</span>
|
||||||
<MetricNameSearch
|
<MetricNameSearch queryFilters={queryFilters} />
|
||||||
queryFilterExpression={queryFilterExpression}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
dataIndex: 'metric_name',
|
dataIndex: 'metric_name',
|
||||||
@@ -47,10 +55,7 @@ export const getMetricsTableColumns = (
|
|||||||
title: (
|
title: (
|
||||||
<div className="metric-type-column-header">
|
<div className="metric-type-column-header">
|
||||||
<span className="metric-type-column-header-text">TYPE</span>
|
<span className="metric-type-column-header-text">TYPE</span>
|
||||||
{/* <MetricTypeSearch
|
<MetricTypeSearch queryFilters={queryFilters} />
|
||||||
queryFilters={queryFilters}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
dataIndex: 'metric_type',
|
dataIndex: 'metric_type',
|
||||||
@@ -64,13 +69,13 @@ export const getMetricsTableColumns = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SAMPLES',
|
title: 'SAMPLES',
|
||||||
dataIndex: MetricsexplorertypesTreemapModeDTO.samples,
|
dataIndex: TreemapViewType.SAMPLES,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'TIME SERIES',
|
title: 'TIME SERIES',
|
||||||
dataIndex: MetricsexplorertypesTreemapModeDTO.timeseries,
|
dataIndex: TreemapViewType.TIMESERIES,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
@@ -84,6 +89,60 @@ export const getMetricsListQuery = (): MetricsListPayload => ({
|
|||||||
orderBy: { columnName: 'metric_name', order: 'asc' },
|
orderBy: { columnName: 'metric_name', order: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function MetricTypeRenderer({
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
type: MetricType;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [icon, color] = useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case MetricType.SUM:
|
||||||
|
return [
|
||||||
|
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
||||||
|
Color.BG_ROBIN_500,
|
||||||
|
];
|
||||||
|
case MetricType.GAUGE:
|
||||||
|
return [
|
||||||
|
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
||||||
|
Color.BG_SAKURA_500,
|
||||||
|
];
|
||||||
|
case MetricType.HISTOGRAM:
|
||||||
|
return [
|
||||||
|
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
||||||
|
Color.BG_SIENNA_500,
|
||||||
|
];
|
||||||
|
case MetricType.SUMMARY:
|
||||||
|
return [
|
||||||
|
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
||||||
|
Color.BG_FOREST_500,
|
||||||
|
];
|
||||||
|
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||||
|
return [
|
||||||
|
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
|
||||||
|
Color.BG_AQUA_500,
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [null, ''];
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="metric-type-renderer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${color}33`,
|
||||||
|
border: `1px solid ${color}`,
|
||||||
|
color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<Typography.Text style={{ color, fontSize: 12 }}>
|
||||||
|
{METRIC_TYPE_LABEL_MAP[type]}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ValidateRowValueWrapper({
|
function ValidateRowValueWrapper({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
@@ -101,9 +160,6 @@ export const formatNumberIntoHumanReadableFormat = (
|
|||||||
num: number,
|
num: number,
|
||||||
addPlusSign = true,
|
addPlusSign = true,
|
||||||
): string => {
|
): string => {
|
||||||
if (!num) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
function format(num: number, divisor: number, suffix: string): string {
|
function format(num: number, divisor: number, suffix: string): string {
|
||||||
const value = num / divisor;
|
const value = num / divisor;
|
||||||
return value % 1 === 0
|
return value % 1 === 0
|
||||||
@@ -126,13 +182,13 @@ export const formatNumberIntoHumanReadableFormat = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const formatDataForMetricsTable = (
|
export const formatDataForMetricsTable = (
|
||||||
data: MetricsexplorertypesStatDTO[],
|
data: MetricsListItemData[],
|
||||||
): MetricsListItemRowData[] =>
|
): MetricsListItemRowData[] =>
|
||||||
data.map((metric) => ({
|
data.map((metric) => ({
|
||||||
key: metric.metricName,
|
key: metric.metric_name,
|
||||||
metric_name: (
|
metric_name: (
|
||||||
<ValidateRowValueWrapper value={metric.metricName}>
|
<ValidateRowValueWrapper value={metric.metric_name}>
|
||||||
<Tooltip title={metric.metricName}>{metric.metricName}</Tooltip>
|
<Tooltip title={metric.metric_name}>{metric.metric_name}</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
description: (
|
description: (
|
||||||
@@ -142,54 +198,39 @@ export const formatDataForMetricsTable = (
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
metric_type: <MetricTypeViewRenderer type={metric.type} />,
|
metric_type: <MetricTypeRenderer type={metric.type} />,
|
||||||
unit: (
|
unit: (
|
||||||
<ValidateRowValueWrapper value={getUniversalNameFromMetricUnit(metric.unit)}>
|
<ValidateRowValueWrapper value={getUniversalNameFromMetricUnit(metric.unit)}>
|
||||||
{getUniversalNameFromMetricUnit(metric.unit)}
|
{getUniversalNameFromMetricUnit(metric.unit)}
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[MetricsexplorertypesTreemapModeDTO.samples]: (
|
[TreemapViewType.SAMPLES]: (
|
||||||
<ValidateRowValueWrapper
|
<ValidateRowValueWrapper value={metric[TreemapViewType.SAMPLES]}>
|
||||||
value={metric[MetricsexplorertypesTreemapModeDTO.samples]}
|
<Tooltip title={metric[TreemapViewType.SAMPLES].toLocaleString()}>
|
||||||
>
|
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.SAMPLES])}
|
||||||
<Tooltip
|
|
||||||
title={metric[MetricsexplorertypesTreemapModeDTO.samples].toLocaleString()}
|
|
||||||
>
|
|
||||||
{formatNumberIntoHumanReadableFormat(
|
|
||||||
metric[MetricsexplorertypesTreemapModeDTO.samples],
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[MetricsexplorertypesTreemapModeDTO.timeseries]: (
|
[TreemapViewType.TIMESERIES]: (
|
||||||
<ValidateRowValueWrapper
|
<ValidateRowValueWrapper value={metric[TreemapViewType.TIMESERIES]}>
|
||||||
value={metric[MetricsexplorertypesTreemapModeDTO.timeseries]}
|
<Tooltip title={metric[TreemapViewType.TIMESERIES].toLocaleString()}>
|
||||||
>
|
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.TIMESERIES])}
|
||||||
<Tooltip
|
|
||||||
title={metric[
|
|
||||||
MetricsexplorertypesTreemapModeDTO.timeseries
|
|
||||||
].toLocaleString()}
|
|
||||||
>
|
|
||||||
{formatNumberIntoHumanReadableFormat(
|
|
||||||
metric[MetricsexplorertypesTreemapModeDTO.timeseries],
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const transformTreemapData = (
|
export const transformTreemapData = (
|
||||||
data: MetricsexplorertypesTreemapEntryDTO[],
|
data: TimeseriesData[] | SamplesData[],
|
||||||
viewType: MetricsexplorertypesTreemapModeDTO,
|
viewType: TreemapViewType,
|
||||||
): TreemapTile[] => {
|
): TreemapTile[] => {
|
||||||
const totalSize = data.reduce(
|
const totalSize = (data as (TimeseriesData | SamplesData)[]).reduce(
|
||||||
(acc: number, item: MetricsexplorertypesTreemapEntryDTO) =>
|
(acc: number, item: TimeseriesData | SamplesData) => acc + item.percentage,
|
||||||
acc + item.percentage,
|
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const children = data.map((item) => ({
|
const children = data.map((item) => ({
|
||||||
id: item.metricName,
|
id: item.metric_name,
|
||||||
size: totalSize > 0 ? Number((item.percentage / totalSize).toFixed(2)) : 0,
|
size: totalSize > 0 ? Number((item.percentage / totalSize).toFixed(2)) : 0,
|
||||||
displayValue: Number(item.percentage).toFixed(2),
|
displayValue: Number(item.percentage).toFixed(2),
|
||||||
parent: viewType,
|
parent: viewType,
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -14,7 +14,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||||
import getLabelName from 'lib/getLabelName';
|
import getLabelName from 'lib/getLabelName';
|
||||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
import throttle from 'lodash-es/throttle';
|
|
||||||
import { Palette } from 'lucide-react';
|
import { Palette } from 'lucide-react';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
@@ -96,24 +95,13 @@ function LegendColors({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle color change (throttled to avoid excessive updates)
|
// Handle color change
|
||||||
const handleColorChange = useMemo(
|
const handleColorChange = (label: string, color: string): void => {
|
||||||
() =>
|
setCustomLegendColors((prev) => ({
|
||||||
throttle((label: string, color: string): void => {
|
...prev,
|
||||||
setCustomLegendColors((prev) => ({
|
[label]: color,
|
||||||
...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
|
// Reset to default color
|
||||||
const resetToDefault = (label: string): void => {
|
const resetToDefault = (label: string): void => {
|
||||||
|
|||||||
@@ -169,10 +169,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 16px; /* 133.333% */
|
line-height: 16px; /* 133.333% */
|
||||||
|
|
||||||
.ant-select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: none;
|
border: none;
|
||||||
height: unset;
|
height: unset;
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||||
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
|
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 { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { unitOptions } from 'container/NewWidget/utils';
|
import { unitOptions } from 'container/NewWidget/utils';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
@@ -207,18 +204,6 @@ function Threshold({
|
|||||||
return unit !== 'none' && convertUnit(value, unit, toUnitId) === null;
|
return unit !== 'none' && convertUnit(value, unit, toUnitId) === null;
|
||||||
}, [selectedGraph, yAxisUnit, tableSelectedOption, columnUnits, unit, value]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={allowDragAndDrop ? ref : null}
|
ref={allowDragAndDrop ? ref : null}
|
||||||
@@ -328,17 +313,19 @@ function Threshold({
|
|||||||
<ShowCaseValue value={value} className="unit-input" />
|
<ShowCaseValue value={value} className="unit-input" />
|
||||||
)}
|
)}
|
||||||
{isEditMode ? (
|
{isEditMode ? (
|
||||||
<YAxisUnitSelector
|
<Select
|
||||||
value={unit}
|
defaultValue={unit}
|
||||||
|
options={unitOptions(
|
||||||
|
selectedGraph === PANEL_TYPES.TABLE
|
||||||
|
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
|
||||||
|
: yAxisUnit || '',
|
||||||
|
)}
|
||||||
onChange={handleUnitChange}
|
onChange={handleUnitChange}
|
||||||
placeholder="Select unit"
|
showSearch
|
||||||
source={YAxisSource.DASHBOARDS}
|
className="unit-selection"
|
||||||
initialValue={unit}
|
|
||||||
categoriesOverride={unitSelectCategories}
|
|
||||||
containerClassName="unit-selection"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ShowCaseValue value={unitLabel} className="unit-selection-prev" />
|
<ShowCaseValue value={unit} className="unit-selection-prev" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="thresholds-color-selector">
|
<div className="thresholds-color-selector">
|
||||||
@@ -369,10 +356,7 @@ function Threshold({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isInvalidUnitComparison && (
|
{isInvalidUnitComparison && (
|
||||||
<Typography.Text
|
<Typography.Text className="invalid-unit">
|
||||||
className="invalid-unit"
|
|
||||||
data-testid="invalid-unit-comparison"
|
|
||||||
>
|
|
||||||
Threshold unit ({unit}) is not valid in comparison with the{' '}
|
Threshold unit ({unit}) is not valid in comparison with the{' '}
|
||||||
{selectedGraph === PANEL_TYPES.TABLE ? 'column' : 'y-axis'} unit (
|
{selectedGraph === PANEL_TYPES.TABLE ? 'column' : 'y-axis'} unit (
|
||||||
{selectedGraph === PANEL_TYPES.TABLE
|
{selectedGraph === PANEL_TYPES.TABLE
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user