mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-28 03:02:25 +00:00
Compare commits
26 Commits
fix/remove
...
issue_4071
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83cf760a01 | ||
|
|
6e28f4dd84 | ||
|
|
43933f3a33 | ||
|
|
d2e1a24b20 | ||
|
|
887b3c7b3a | ||
|
|
476fe7a29a | ||
|
|
c1d38d86f1 | ||
|
|
4f2594c31d | ||
|
|
c9985b56bc | ||
|
|
f9868e2221 | ||
|
|
72b0398eaf | ||
|
|
5b75a39777 | ||
|
|
6948b69012 | ||
|
|
bc9701397e | ||
|
|
396cf3194e | ||
|
|
8be96a0ded | ||
|
|
82c54b1d36 | ||
|
|
39f5fb7290 | ||
|
|
6ec2989e5c | ||
|
|
016da679b9 | ||
|
|
ff028e366b | ||
|
|
c579614d56 | ||
|
|
78ba2ba356 | ||
|
|
7fd4762e2a | ||
|
|
4e4c9ce5af | ||
|
|
7605775a38 |
@@ -320,3 +320,4 @@ 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.112.1
|
image: signoz/signoz:v0.113.0
|
||||||
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.142.1
|
image: signoz/signoz-otel-collector:v0.144.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:${OTELCOL_TAG:-v0.142.0}
|
image: signoz/signoz-otel-collector:v0.144.1
|
||||||
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.112.1
|
image: signoz/signoz:v0.113.0
|
||||||
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.142.1
|
image: signoz/signoz-otel-collector:v0.144.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:${OTELCOL_TAG:-v0.142.0}
|
image: signoz/signoz-otel-collector:v0.144.1
|
||||||
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,6 +82,12 @@ 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:
|
||||||
@@ -93,19 +99,19 @@ service:
|
|||||||
traces:
|
traces:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [signozspanmetrics/delta, batch]
|
processors: [signozspanmetrics/delta, batch]
|
||||||
exporters: [clickhousetraces, signozmeter]
|
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, signozmeter]
|
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, signozmeter]
|
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter, signozmeter]
|
exporters: [clickhouselogsexporter, metadataexporter, 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.112.1}
|
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||||
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.142.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.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.142.0}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||||
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.112.1}
|
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||||
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.142.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.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.142.0}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||||
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,6 +82,12 @@ 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:
|
||||||
@@ -93,19 +99,19 @@ service:
|
|||||||
traces:
|
traces:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [signozspanmetrics/delta, batch]
|
processors: [signozspanmetrics/delta, batch]
|
||||||
exporters: [clickhousetraces, signozmeter]
|
exporters: [clickhousetraces, metadataexporter, signozmeter]
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, signozmeter]
|
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||||
metrics/prometheus:
|
metrics/prometheus:
|
||||||
receivers: [prometheus]
|
receivers: [prometheus]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [signozclickhousemetrics, signozmeter]
|
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter, signozmeter]
|
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
|
||||||
metrics/meter:
|
metrics/meter:
|
||||||
receivers: [signozmeter]
|
receivers: [signozmeter]
|
||||||
processors: [batch/meter]
|
processors: [batch/meter]
|
||||||
|
|||||||
@@ -842,6 +842,17 @@ 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
|
||||||
@@ -1138,6 +1149,8 @@ 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/query-service/formatter"
|
"github.com/SigNoz/signoz/pkg/units"
|
||||||
|
|
||||||
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 := formatter.FromUnit(r.Unit())
|
valueFormatter := units.FormatterFromUnit(r.Unit())
|
||||||
|
|
||||||
var res ruletypes.Vector
|
var res ruletypes.Vector
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
"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",
|
||||||
@@ -130,7 +131,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": "^1.43.0",
|
"react-force-graph-2d": "^1.29.1",
|
||||||
"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",
|
||||||
@@ -162,7 +163,8 @@
|
|||||||
"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": [
|
||||||
@@ -287,4 +289,4 @@
|
|||||||
"on-headers": "^1.1.0",
|
"on-headers": "^1.1.0",
|
||||||
"tmp": "0.2.4"
|
"tmp": "0.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,6 +1006,18 @@ 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',
|
||||||
@@ -1367,6 +1379,7 @@ export interface Querybuildertypesv5LogAggregationDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Querybuildertypesv5MetricAggregationDTO {
|
export interface Querybuildertypesv5MetricAggregationDTO {
|
||||||
|
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface HostListResponse {
|
|||||||
total: number;
|
total: number;
|
||||||
sentAnyHostMetricsData: boolean;
|
sentAnyHostMetricsData: boolean;
|
||||||
isSendingK8SAgentMetrics: boolean;
|
isSendingK8SAgentMetrics: boolean;
|
||||||
|
endTimeBeforeRetention: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
|
||||||
|
|
||||||
export const getMetricMetadata = async (
|
|
||||||
metricName: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
headers?: Record<string, string>,
|
|
||||||
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
|
|
||||||
try {
|
|
||||||
const encodedMetricName = encodeURIComponent(metricName);
|
|
||||||
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
|
|
||||||
signal,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -248,6 +248,11 @@ declare module 'chart.js' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intlNumberFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
useGrouping: false,
|
||||||
|
maximumFractionDigits: 20,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a number for display, preserving leading zeros after the decimal point
|
* 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.
|
||||||
@@ -270,10 +275,7 @@ 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 = value.toLocaleString('en-US', {
|
const numStr = intlNumberFormatter.format(value);
|
||||||
useGrouping: false,
|
|
||||||
maximumFractionDigits: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [integerPart, decimalPart = ''] = numStr.split('.');
|
const [integerPart, decimalPart = ''] = numStr.split('.');
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,13 @@ 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 explicitly ignored regions
|
// Don't close if clicking on drawer content, overlays, or portal elements
|
||||||
if (target.closest('[data-log-detail-ignore="true"]')) {
|
if (
|
||||||
|
target.closest('[data-log-detail-ignore="true"]') ||
|
||||||
|
target.closest('.cm-tooltip-autocomplete') ||
|
||||||
|
target.closest('.drawer-popover') ||
|
||||||
|
target.closest('.query-status-popover')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +405,11 @@ 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 title={removeEscapeCharacters(log?.body)} placement="left">
|
<Tooltip
|
||||||
|
title={removeEscapeCharacters(log?.body)}
|
||||||
|
placement="left"
|
||||||
|
mouseLeaveDelay={0}
|
||||||
|
>
|
||||||
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@@ -466,6 +475,7 @@ 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"
|
||||||
@@ -481,6 +491,7 @@ 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,7 +27,11 @@ 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 placement="top" content={popOverContent}>
|
<Popover
|
||||||
|
overlayClassName="drawer-popover"
|
||||||
|
placement="top"
|
||||||
|
content={popOverContent}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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: '60rem',
|
maxWidth: '90rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultListViewPanelStyle: CSSProperties = {
|
export const defaultListViewPanelStyle: CSSProperties = {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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';
|
||||||
@@ -86,6 +85,7 @@ 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,6 +96,7 @@ 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[]>([]);
|
||||||
@@ -252,7 +253,8 @@ 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;
|
||||||
@@ -301,6 +303,7 @@ function QuerySearch({
|
|||||||
queryData.aggregateAttribute?.key,
|
queryData.aggregateAttribute?.key,
|
||||||
signalSource,
|
signalSource,
|
||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
|
showFilterSuggestionsWithoutMetric,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -564,15 +567,7 @@ 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((lastPos) => {
|
setCursorPos(newPos);
|
||||||
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) {
|
||||||
@@ -1328,7 +1323,10 @@ function QuerySearch({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="query-where-clause-editor-container">
|
<div className="query-where-clause-editor-container">
|
||||||
<Tooltip title={getTooltipContent()} placement="left">
|
<Tooltip
|
||||||
|
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"
|
||||||
@@ -1562,6 +1560,7 @@ 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,6 +40,7 @@ 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);
|
||||||
@@ -87,7 +88,7 @@ function ValueGraph({
|
|||||||
const {
|
const {
|
||||||
threshold,
|
threshold,
|
||||||
isConflictingThresholds,
|
isConflictingThresholds,
|
||||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -155,6 +156,7 @@ interface ValueGraphProps {
|
|||||||
value: string;
|
value: string;
|
||||||
rawValue: number;
|
rawValue: number;
|
||||||
thresholds: ThresholdProps[];
|
thresholds: ThresholdProps[];
|
||||||
|
yAxisUnit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValueGraph;
|
export default ValueGraph;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
|
||||||
function compareThreshold(
|
function doesValueSatisfyThreshold(
|
||||||
rawValue: number,
|
rawValue: number,
|
||||||
threshold: ThresholdProps,
|
threshold: ThresholdProps,
|
||||||
|
yAxisUnit?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
threshold.thresholdOperator === undefined ||
|
threshold.thresholdOperator === undefined ||
|
||||||
@@ -11,31 +12,14 @@ function compareThreshold(
|
|||||||
) {
|
) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractNumbersFromString(inputString: string): number[] {
|
return evaluateThresholdWithConvertedValue(
|
||||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
rawValue,
|
||||||
const matches = inputString.match(regex);
|
threshold.thresholdValue,
|
||||||
|
threshold.thresholdOperator,
|
||||||
if (matches) {
|
threshold.thresholdUnit,
|
||||||
return matches.map(Number);
|
yAxisUnit,
|
||||||
}
|
);
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHighestPrecedenceThreshold(
|
function getHighestPrecedenceThreshold(
|
||||||
@@ -60,21 +44,32 @@ 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) => {
|
||||||
compareThreshold(
|
const numbers = extractNumbersFromString(rawValue.toString());
|
||||||
extractNumbersFromString(
|
if (numbers.length === 0) {
|
||||||
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
return false;
|
||||||
)[0],
|
}
|
||||||
threshold,
|
return doesValueSatisfyThreshold(numbers[0], threshold, yAxisUnit);
|
||||||
),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingThresholds.length === 0) {
|
if (matchingThresholds.length === 0) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ 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);
|
||||||
|
|
||||||
@@ -66,10 +68,14 @@ function YAxisUnitSelector({
|
|||||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||||
};
|
};
|
||||||
|
|
||||||
const categories = getYAxisCategories(source);
|
const categoriesToRender = useMemo(() => {
|
||||||
|
return categoriesOverride || getYAxisCategories(source);
|
||||||
|
}, [categoriesOverride, source]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="y-axis-unit-selector-component">
|
<div
|
||||||
|
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={universalUnit}
|
value={universalUnit}
|
||||||
@@ -90,7 +96,7 @@ function YAxisUnitSelector({
|
|||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
allowClear
|
allowClear
|
||||||
>
|
>
|
||||||
{categories.map((category) => (
|
{categoriesToRender.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,6 +1,7 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { YAxisSource } from '../types';
|
import { YAxisCategoryNames } from '../constants';
|
||||||
|
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
||||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||||
|
|
||||||
describe('YAxisUnitSelector', () => {
|
describe('YAxisUnitSelector', () => {
|
||||||
@@ -123,4 +124,34 @@ 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,6 +9,8 @@ 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 { latestVersion } = useSelector<AppState, AppReducer>(
|
const { currentVersion } = 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(latestVersion, changelogForTenant),
|
getChangelogByVersion(currentVersion, changelogForTenant),
|
||||||
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
|
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
|
||||||
enabled: isLoggedIn && Boolean(latestVersion),
|
enabled: isLoggedIn && Boolean(currentVersion),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
!changelog &&
|
!changelog &&
|
||||||
!getChangelogByVersionResponse.isLoading &&
|
!getChangelogByVersionResponse.isLoading &&
|
||||||
isLoggedIn &&
|
isLoggedIn &&
|
||||||
Boolean(latestVersion)
|
Boolean(currentVersion)
|
||||||
) {
|
) {
|
||||||
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(latestVersion) &&
|
Boolean(currentVersion) &&
|
||||||
seenChangelogVersion != null &&
|
seenChangelogVersion != null &&
|
||||||
latestVersion !== seenChangelogVersion &&
|
currentVersion !== 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,
|
||||||
latestVersion,
|
currentVersion,
|
||||||
seenChangelogVersion,
|
seenChangelogVersion,
|
||||||
toggleChangelogModal,
|
toggleChangelogModal,
|
||||||
isWorkspaceAccessRestricted,
|
isWorkspaceAccessRestricted,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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,
|
||||||
@@ -31,6 +32,9 @@ 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,
|
||||||
);
|
);
|
||||||
@@ -96,6 +100,28 @@ 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 };
|
||||||
@@ -130,10 +156,12 @@ 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, isString } from 'lodash-es';
|
import { isArray, isEmpty } 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,
|
||||||
applyDefaultIfNeeded,
|
getDefaultValue,
|
||||||
} = useDashboardVariableSelectHelper({
|
} = useDashboardVariableSelectHelper({
|
||||||
variableData,
|
variableData,
|
||||||
optionsData,
|
optionsData,
|
||||||
@@ -68,81 +68,93 @@ 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)
|
||||||
) {
|
) {
|
||||||
const newOptionsData = sortValues(
|
return;
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
|
||||||
|
// 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);
|
||||||
|
|
||||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
// Update tempSelection to maintain ALL state when dropdown is open
|
||||||
let valueNotInList = false;
|
if (tempSelection !== undefined) {
|
||||||
|
setTempSelection(sortedNewOptions.map((option) => option.toString()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isArray(variableData.selectedValue)) {
|
const value = variableData.selectedValue;
|
||||||
variableData.selectedValue.forEach((val) => {
|
let allSelected = false;
|
||||||
if (!newOptionsData.includes(val)) {
|
|
||||||
valueNotInList = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
isString(variableData.selectedValue) &&
|
|
||||||
!newOptionsData.includes(variableData.selectedValue)
|
|
||||||
) {
|
|
||||||
valueNotInList = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variableData.name && (valueNotInList || variableData.allSelected)) {
|
if (variableData.multiSelect) {
|
||||||
if (
|
const { selectedValue } = variableData;
|
||||||
variableData.allSelected &&
|
allSelected =
|
||||||
variableData.multiSelect &&
|
sortedNewOptions.length > 0 &&
|
||||||
variableData.showALLOption
|
Array.isArray(selectedValue) &&
|
||||||
) {
|
sortedNewOptions.every((option) => selectedValue.includes(option));
|
||||||
if (
|
}
|
||||||
variableData.name &&
|
|
||||||
variableData.id &&
|
|
||||||
!isEmpty(variableData.selectedValue)
|
|
||||||
) {
|
|
||||||
onValueUpdate(
|
|
||||||
variableData.name,
|
|
||||||
variableData.id,
|
|
||||||
newOptionsData,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tempSelection to maintain ALL state when dropdown is open
|
if (
|
||||||
if (tempSelection !== undefined) {
|
variableData.name &&
|
||||||
setTempSelection(newOptionsData.map((option) => option.toString()));
|
variableData.id &&
|
||||||
}
|
!isEmpty(variableData.selectedValue)
|
||||||
} else {
|
) {
|
||||||
const value = variableData.selectedValue;
|
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||||
let allSelected = false;
|
} else {
|
||||||
|
const defaultValue = getDefaultValue(sortedNewOptions);
|
||||||
if (variableData.multiSelect) {
|
if (defaultValue !== undefined) {
|
||||||
const { selectedValue } = variableData;
|
onValueUpdate(
|
||||||
allSelected =
|
variableData.name,
|
||||||
newOptionsData.length > 0 &&
|
variableData.id,
|
||||||
Array.isArray(selectedValue) &&
|
defaultValue,
|
||||||
newOptionsData.every((option) => selectedValue.includes(option));
|
allSelected,
|
||||||
}
|
);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -155,7 +167,7 @@ function QueryVariableInput({
|
|||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
tempSelection,
|
tempSelection,
|
||||||
setTempSelection,
|
setTempSelection,
|
||||||
applyDefaultIfNeeded,
|
getDefaultValue,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* 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,
|
||||||
@@ -10,6 +11,7 @@ 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';
|
||||||
@@ -17,6 +19,17 @@ 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();
|
||||||
@@ -56,10 +69,14 @@ jest.mock('react-redux', () => ({
|
|||||||
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock VariableItem to avoid rendering complexity
|
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
|
||||||
jest.mock('../VariableItem', () => ({
|
jest.mock('../VariableItem', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (): JSX.Element => <div data-testid="variable-item" />,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
default: (props: any): JSX.Element => {
|
||||||
|
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
|
||||||
|
return <div data-testid="variable-item" />;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function createVariable(
|
function createVariable(
|
||||||
@@ -200,4 +217,162 @@ 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,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/* 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,6 +46,9 @@ 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
|
||||||
@@ -248,5 +251,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
function evaluateThresholdWithConvertedValue(
|
export function evaluateThresholdWithConvertedValue(
|
||||||
value: number,
|
value: number,
|
||||||
thresholdValue: number,
|
thresholdValue: number,
|
||||||
thresholdOperator?: string,
|
thresholdOperator?: string,
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ 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 { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Skeleton,
|
Skeleton,
|
||||||
@@ -14,12 +14,93 @@ 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,
|
||||||
@@ -46,6 +127,11 @@ 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],
|
||||||
@@ -84,12 +170,6 @@ export default function HostsListTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const showNoFilteredHostsMessage =
|
|
||||||
!isFetching &&
|
|
||||||
!isLoading &&
|
|
||||||
formattedHostMetricsData.length === 0 &&
|
|
||||||
filters.items.length > 0;
|
|
||||||
|
|
||||||
const showHostsEmptyState =
|
const showHostsEmptyState =
|
||||||
!isFetching &&
|
!isFetching &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
@@ -97,63 +177,36 @@ 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;
|
||||||
|
|
||||||
if (isError) {
|
const emptyOrLoadingView = EmptyOrLoadingView({
|
||||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
isError,
|
||||||
}
|
errorMessage: data?.error ?? '',
|
||||||
|
showHostsEmptyState,
|
||||||
|
sentAnyHostMetricsData,
|
||||||
|
isSendingIncorrectK8SAgentMetrics,
|
||||||
|
showEndTimeBeforeRetentionMessage,
|
||||||
|
showNoRecordsInSelectedTimeRangeMessage,
|
||||||
|
showTableLoadingState,
|
||||||
|
});
|
||||||
|
|
||||||
if (showHostsEmptyState) {
|
if (emptyOrLoadingView) {
|
||||||
return (
|
return <>{emptyOrLoadingView}</>;
|
||||||
<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,12 +1,16 @@
|
|||||||
/* 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';
|
||||||
|
|
||||||
describe('HostsListTable', () => {
|
const createMockHost = (): HostData =>
|
||||||
const mockHost = {
|
({
|
||||||
hostName: 'test-host-1',
|
hostName: 'test-host-1',
|
||||||
active: true,
|
active: true,
|
||||||
cpu: 0.75,
|
cpu: 0.75,
|
||||||
@@ -14,20 +18,46 @@ describe('HostsListTable', () => {
|
|||||||
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 mockTableData = {
|
const createMockTableData = (
|
||||||
|
overrides: Partial<HostListResponse['data']> = {},
|
||||||
|
): SuccessResponse<HostListResponse> => {
|
||||||
|
const mockHost = createMockHost();
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
message: 'Success',
|
||||||
|
error: null,
|
||||||
payload: {
|
payload: {
|
||||||
|
status: 'success',
|
||||||
data: {
|
data: {
|
||||||
hosts: [mockHost],
|
type: 'list',
|
||||||
|
records: [mockHost],
|
||||||
|
groups: null,
|
||||||
|
total: 1,
|
||||||
|
sentAnyHostMetricsData: true,
|
||||||
|
isSendingK8SAgentMetrics: false,
|
||||||
|
endTimeBeforeRetention: false,
|
||||||
|
...overrides,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('HostsListTable', () => {
|
||||||
|
const mockHost = createMockHost();
|
||||||
|
const mockTableData = createMockTableData();
|
||||||
|
|
||||||
const mockOnHostClick = jest.fn();
|
const 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,
|
||||||
@@ -43,7 +73,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(
|
||||||
@@ -51,7 +81,7 @@ describe('HostsListTable', () => {
|
|||||||
{...mockProps}
|
{...mockProps}
|
||||||
isLoading
|
isLoading
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={{ payload: { data: { hosts: [] } } }}
|
tableData={createMockTableData({ records: [] })}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||||
@@ -63,7 +93,7 @@ describe('HostsListTable', () => {
|
|||||||
{...mockProps}
|
{...mockProps}
|
||||||
isFetching
|
isFetching
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={{ payload: { data: { hosts: [] } } }}
|
tableData={createMockTableData({ records: [] })}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||||
@@ -74,19 +104,56 @@ 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={{
|
tableData={createMockTableData({
|
||||||
payload: {
|
records: [],
|
||||||
data: { hosts: [] },
|
})}
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
expect(
|
||||||
|
container.querySelector('.no-filtered-hosts-message-container'),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||||
@@ -94,58 +161,114 @@ describe('HostsListTable', () => {
|
|||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={{
|
tableData={createMockTableData({
|
||||||
...mockTableData,
|
sentAnyHostMetricsData: false,
|
||||||
payload: {
|
records: [],
|
||||||
...mockTableData.payload,
|
})}
|
||||||
data: {
|
|
||||||
...mockTableData.payload.data,
|
|
||||||
sentAnyHostMetricsData: false,
|
|
||||||
hosts: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
hostMetricsData={[]}
|
hostMetricsData={[]}
|
||||||
tableData={{
|
tableData={createMockTableData({
|
||||||
...mockTableData,
|
isSendingK8SAgentMetrics: true,
|
||||||
payload: {
|
records: [],
|
||||||
...mockTableData.payload,
|
})}
|
||||||
data: {
|
|
||||||
...mockTableData.payload.data,
|
|
||||||
isSendingIncorrectK8SAgentMetrics: true,
|
|
||||||
hosts: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<HostsListTable
|
||||||
|
{...mockProps}
|
||||||
|
hostMetricsData={[]}
|
||||||
|
tableData={createMockTableData({
|
||||||
|
sentAnyHostMetricsData: true,
|
||||||
|
isSendingK8SAgentMetrics: false,
|
||||||
|
endTimeBeforeRetention: true,
|
||||||
|
records: [],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Your requested end time is earlier than the earliest detected time of host metrics data, please adjust your end time\./,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<HostsListTable
|
||||||
|
{...mockProps}
|
||||||
|
hostMetricsData={[]}
|
||||||
|
tableData={createMockTableData({
|
||||||
|
sentAnyHostMetricsData: true,
|
||||||
|
isSendingK8SAgentMetrics: false,
|
||||||
|
records: [],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector('.no-filtered-hosts-message-container'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/No host metrics in the selected time range and filters/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no filtered hosts message when filters are present and no hosts are found', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<HostsListTable
|
||||||
|
{...mockProps}
|
||||||
|
hostMetricsData={[]}
|
||||||
|
filters={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'host_name',
|
||||||
|
key: {
|
||||||
|
key: 'host_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'tag',
|
||||||
|
isIndexed: true,
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: 'unknown',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
}}
|
||||||
|
tableData={createMockTableData({
|
||||||
|
sentAnyHostMetricsData: true,
|
||||||
|
isSendingK8SAgentMetrics: false,
|
||||||
|
records: [],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(container.querySelector('.no-filtered-hosts-message')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders table data', () => {
|
it('renders table data', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<HostsListTable
|
<HostsListTable
|
||||||
{...mockProps}
|
{...mockProps}
|
||||||
tableData={{
|
tableData={createMockTableData({
|
||||||
...mockTableData,
|
isSendingK8SAgentMetrics: false,
|
||||||
payload: {
|
sentAnyHostMetricsData: true,
|
||||||
...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,6 +107,17 @@ 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: [],
|
||||||
|
|||||||
@@ -121,9 +121,23 @@ function BodyTitleRenderer({
|
|||||||
return (
|
return (
|
||||||
<TitleWrapper onClick={handleNodeClick}>
|
<TitleWrapper onClick={handleNodeClick}>
|
||||||
{typeof value !== 'object' && (
|
{typeof value !== 'object' && (
|
||||||
<Dropdown menu={menu} trigger={['click']}>
|
<span
|
||||||
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
onClick={(e): void => {
|
||||||
</Dropdown>
|
e.stopPropagation();
|
||||||
|
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}>
|
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
|
||||||
<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>(false);
|
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
|
||||||
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">
|
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
onClick={(
|
onClick={(
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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,7 +1,16 @@
|
|||||||
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,
|
||||||
@@ -67,8 +76,13 @@ export function getColorsForSeverityLabels(
|
|||||||
label: string,
|
label: string,
|
||||||
index: number,
|
index: number,
|
||||||
): string {
|
): string {
|
||||||
// Check if we have a direct mapping for this severity variant
|
const trimmed = label.trim();
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -103,5 +117,8 @@ export function getColorsForSeverityLabels(
|
|||||||
return Color.BG_SAKURA_500;
|
return Color.BG_SAKURA_500;
|
||||||
}
|
}
|
||||||
|
|
||||||
return colors[index % colors.length] || themeColors.red;
|
return (
|
||||||
|
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
|
||||||
|
Color.BG_SLATE_400
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,23 +111,19 @@ 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 => (
|
||||||
return (
|
<TableRow
|
||||||
<div key={log.id as string}>
|
tableColumns={tableColumns}
|
||||||
<TableRow
|
index={index}
|
||||||
tableColumns={tableColumns}
|
log={log}
|
||||||
index={index}
|
logs={tableViewProps.logs}
|
||||||
log={log}
|
hasActions
|
||||||
logs={tableViewProps.logs}
|
fontSize={tableViewProps.fontSize}
|
||||||
hasActions
|
onShowLogDetails={onSetActiveLog}
|
||||||
fontSize={tableViewProps.fontSize}
|
isActiveLog={activeLog?.id === log.id}
|
||||||
onShowLogDetails={onSetActiveLog}
|
onClearActiveLog={onCloseActiveLog}
|
||||||
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 && (
|
{isMetricDetailsOpen && selectedMetricName && (
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
metricName={selectedMetricName}
|
metricName={selectedMetricName}
|
||||||
isOpen={isMetricDetailsOpen}
|
isOpen={isMetricDetailsOpen}
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
|
|||||||
dataSource={DataSource.METRICS}
|
dataSource={DataSource.METRICS}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DashboardsAndAlertsPopover
|
<DashboardsAndAlertsPopover metricName={metric.name} />
|
||||||
dashboards={metric.dashboards}
|
|
||||||
alerts={metric.alerts}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries, useQueryClient } 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 { Tooltip, Typography } from 'antd';
|
import { toast } from '@signozhq/sonner';
|
||||||
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
|
import {
|
||||||
|
invalidateGetMetricMetadata,
|
||||||
|
useUpdateMetricMetadata,
|
||||||
|
} from 'api/generated/services/metrics';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||||
@@ -23,7 +28,10 @@ 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 { splitQueryIntoOneChartPerQuery } from './utils';
|
import {
|
||||||
|
buildUpdateMetricYAxisUnitPayload,
|
||||||
|
splitQueryIntoOneChartPerQuery,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function TimeSeries({
|
function TimeSeries({
|
||||||
showOneChartPerQuery,
|
showOneChartPerQuery,
|
||||||
@@ -35,6 +43,7 @@ function TimeSeries({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
setYAxisUnit,
|
setYAxisUnit,
|
||||||
showYAxisUnitSelector,
|
showYAxisUnitSelector,
|
||||||
|
metrics,
|
||||||
}: TimeSeriesProps): JSX.Element {
|
}: TimeSeriesProps): JSX.Element {
|
||||||
const { stagedQuery, currentQuery } = useQueryBuilder();
|
const { stagedQuery, currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@@ -42,6 +51,7 @@ 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[] = [];
|
||||||
@@ -138,54 +148,51 @@ 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 => {
|
||||||
// updateMetricMetadata(
|
if (metrics[0] && yAxisUnit) {
|
||||||
// {
|
updateMetricMetadata(
|
||||||
// metricName: metricNames[0],
|
{
|
||||||
// payload: {
|
pathParams: {
|
||||||
// unit: yAxisUnit,
|
metricName: metricNames[0],
|
||||||
// description: metrics[0]?.description ?? '',
|
},
|
||||||
// metricType: metrics[0]?.type as MetricType,
|
data: buildUpdateMetricYAxisUnitPayload(
|
||||||
// temporality: metrics[0]?.temporality,
|
metricNames[0],
|
||||||
// },
|
metrics[0],
|
||||||
// },
|
yAxisUnit,
|
||||||
// {
|
),
|
||||||
// onSuccess: () => {
|
},
|
||||||
// notifications.success({
|
{
|
||||||
// message: 'Unit saved successfully',
|
onSuccess: () => {
|
||||||
// });
|
toast.success('Unit saved successfully');
|
||||||
// queryClient.invalidateQueries([
|
invalidateGetMetricMetadata(queryClient, {
|
||||||
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
|
metricName: metricNames[0],
|
||||||
// metricNames[0],
|
});
|
||||||
// ]);
|
},
|
||||||
// },
|
onError: () => {
|
||||||
// onError: () => {
|
toast.error('Failed to save unit');
|
||||||
// notifications.error({
|
},
|
||||||
// message: 'Failed to save unit',
|
},
|
||||||
// });
|
);
|
||||||
// },
|
}
|
||||||
// },
|
};
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -198,8 +205,7 @@ function TimeSeries({
|
|||||||
source={YAxisSource.EXPLORER}
|
source={YAxisSource.EXPLORER}
|
||||||
data-testid="y-axis-unit-selector"
|
data-testid="y-axis-unit-selector"
|
||||||
/>
|
/>
|
||||||
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
|
{showSaveUnitButton && (
|
||||||
{/* {showSaveUnitButton && (
|
|
||||||
<div className="save-unit-container">
|
<div className="save-unit-container">
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
Save the selected unit for this metric?
|
Save the selected unit for this metric?
|
||||||
@@ -213,7 +219,7 @@ function TimeSeries({
|
|||||||
<Typography.Paragraph>Yes</Typography.Paragraph>
|
<Typography.Paragraph>Yes</Typography.Paragraph>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ 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 { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import {
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
MetrictypesTemporalityDTO,
|
||||||
|
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';
|
||||||
@@ -14,12 +16,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();
|
||||||
@@ -135,14 +137,6 @@ 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}>
|
||||||
@@ -190,7 +184,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -207,7 +201,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -220,7 +214,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric],
|
metrics: [MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -237,7 +231,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -250,7 +244,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -269,10 +263,10 @@ describe('Explorer', () => {
|
|||||||
isError: false,
|
isError: false,
|
||||||
metrics: [
|
metrics: [
|
||||||
{
|
{
|
||||||
type: MetricType.SUM,
|
type: MetrictypesTypeDTO.sum,
|
||||||
description: 'metric1 description',
|
description: 'metric1 description',
|
||||||
unit: '',
|
unit: '',
|
||||||
temporality: Temporality.CUMULATIVE,
|
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||||
isMonotonic: true,
|
isMonotonic: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -289,7 +283,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric],
|
metrics: [MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
@@ -324,7 +318,7 @@ describe('Explorer', () => {
|
|||||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
});
|
});
|
||||||
|
|
||||||
renderExplorer();
|
renderExplorer();
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
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 { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
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';
|
||||||
|
|
||||||
type MockUpdateMetricMetadata = UseMutationResult<
|
|
||||||
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
|
|
||||||
Error,
|
|
||||||
UseUpdateMetricMetadataProps
|
|
||||||
>;
|
|
||||||
const mockUpdateMetricMetadata = jest.fn();
|
const mockUpdateMetricMetadata = jest.fn();
|
||||||
jest
|
const updateMetricMetadataSpy = jest.spyOn(
|
||||||
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
metricsExplorerHooks,
|
||||||
.mockReturnValue(({
|
'useUpdateMetricMetadata',
|
||||||
mutate: mockUpdateMetricMetadata,
|
);
|
||||||
isLoading: false,
|
type UseUpdateMetricMetadataReturnType = ReturnType<
|
||||||
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
|
typeof metricsExplorerHooks.useUpdateMetricMetadata
|
||||||
|
>;
|
||||||
|
|
||||||
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
|
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -60,14 +50,6 @@ jest.mock('react-redux', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockMetric: MetricMetadata = {
|
|
||||||
type: MetricType.SUM,
|
|
||||||
description: 'metric1 description',
|
|
||||||
unit: 'metric1 unit',
|
|
||||||
temporality: Temporality.CUMULATIVE,
|
|
||||||
isMonotonic: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSetWarning = jest.fn();
|
const mockSetWarning = jest.fn();
|
||||||
const mockSetIsMetricDetailsOpen = jest.fn();
|
const mockSetIsMetricDetailsOpen = jest.fn();
|
||||||
const mockSetYAxisUnit = jest.fn();
|
const mockSetYAxisUnit = jest.fn();
|
||||||
@@ -96,6 +78,13 @@ 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({
|
||||||
@@ -118,7 +107,7 @@ describe('TimeSeries', () => {
|
|||||||
const { container } = renderTimeSeries({
|
const { container } = renderTimeSeries({
|
||||||
metricUnits: ['', 'count'],
|
metricUnits: ['', 'count'],
|
||||||
metricNames: ['metric1', 'metric2'],
|
metricNames: ['metric1', 'metric2'],
|
||||||
metrics: [mockMetric, mockMetric],
|
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,18 +122,17 @@ describe('TimeSeries', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Unskip this test once the save unit button is implemented
|
it('shows Save unit button when metric had no unit but one is selected', async () => {
|
||||||
// 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: [mockMetric],
|
metrics: [MOCK_METRIC_METADATA],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
|
showYAxisUnitSelector: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
findByText('Save the selected unit for this metric?'),
|
await findByText('Save the selected unit for this metric?'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
const yesButton = getByRole('button', { name: 'Yes' });
|
const yesButton = getByRole('button', { name: 'Yes' });
|
||||||
@@ -152,24 +140,25 @@ describe('TimeSeries', () => {
|
|||||||
expect(yesButton).toBeEnabled();
|
expect(yesButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Unskip this test once the save unit button is implemented
|
it('clicking on save unit button shoould upated metric metadata', async () => {
|
||||||
// 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: [mockMetric],
|
metrics: [MOCK_METRIC_METADATA],
|
||||||
yAxisUnit: 'seconds',
|
yAxisUnit: 'seconds',
|
||||||
|
showYAxisUnitSelector: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const yesButton = getByRole('button', { name: /Yes/i });
|
const yesButton = getByRole('button', { name: /Yes/i });
|
||||||
user.click(yesButton);
|
await user.click(yesButton);
|
||||||
|
|
||||||
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
|
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
metricName: 'metric1',
|
pathParams: {
|
||||||
payload: expect.objectContaining({ unit: 'seconds' }),
|
metricName: 'metric1',
|
||||||
|
},
|
||||||
|
data: expect.objectContaining({ unit: 'seconds' }),
|
||||||
},
|
},
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
onSuccess: expect.any(Function),
|
onSuccess: expect.any(Function),
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
MetricsexplorertypesMetricMetadataDTO,
|
||||||
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
|
|
||||||
|
export const MOCK_METRIC_METADATA: MetricsexplorertypesMetricMetadataDTO = {
|
||||||
|
type: MetrictypesTypeDTO.sum,
|
||||||
|
description: 'metric1 description',
|
||||||
|
unit: 'metric1 unit',
|
||||||
|
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||||
|
isMonotonic: true,
|
||||||
|
};
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
|
||||||
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,
|
||||||
@@ -22,6 +16,7 @@ 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],
|
||||||
@@ -91,32 +86,19 @@ describe('splitQueryIntoOneChartPerQuery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const MOCK_METRIC_METADATA: MetricMetadata = {
|
|
||||||
description: 'Metric 1 description',
|
|
||||||
unit: 'unit1',
|
|
||||||
type: MetricType.GAUGE,
|
|
||||||
temporality: Temporality.DELTA,
|
|
||||||
isMonotonic: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('useGetMetrics', () => {
|
describe('useGetMetrics', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest
|
jest
|
||||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||||
.mockReturnValue([
|
.mockReturnValue([
|
||||||
({
|
{
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
data: {
|
data: {
|
||||||
httpStatusCode: 200,
|
data: MOCK_METRIC_METADATA,
|
||||||
data: {
|
status: 'success',
|
||||||
status: 'success',
|
|
||||||
data: MOCK_METRIC_METADATA,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as Partial<
|
} as UseQueryResult<GetMetricMetadata200, Error>,
|
||||||
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
|
||||||
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,12 +115,11 @@ describe('useGetMetrics', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||||
.mockReturnValue([
|
.mockReturnValue([
|
||||||
({
|
{
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isError: false,
|
isError: false,
|
||||||
} as Partial<
|
data: undefined,
|
||||||
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
} as UseQueryResult<GetMetricMetadata200, 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: (MetricMetadata | undefined)[];
|
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
|
||||||
handleOpenMetricDetails: (metricName: string) => void;
|
handleOpenMetricDetails: (metricName: string) => void;
|
||||||
yAxisUnit: string | undefined;
|
yAxisUnit: string | undefined;
|
||||||
setYAxisUnit: (unit: string) => void;
|
setYAxisUnit: (unit: string) => void;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
||||||
|
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
import { 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
|
||||||
@@ -68,16 +71,14 @@ export function useGetMetrics(
|
|||||||
): {
|
): {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
metrics: (MetricMetadata | undefined)[];
|
metrics: (MetricsexplorertypesMetricMetadataDTO | 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
|
metrics: metricsData.map((metric) => metric.data?.data),
|
||||||
.map((metric) => metric.data?.data)
|
|
||||||
.map((data) => data?.data),
|
|
||||||
isError: metricsData.some((metric) => metric.isError),
|
isError: metricsData.some((metric) => metric.isError),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -89,9 +90,24 @@ export function useGetMetrics(
|
|||||||
* @returns The units of the metrics, can be undefined if the metric has no unit
|
* @returns The units of the metrics, can be undefined if the metric has no unit
|
||||||
*/
|
*/
|
||||||
export function getMetricUnits(
|
export function getMetricUnits(
|
||||||
metrics: (MetricMetadata | undefined)[],
|
metrics: (MetricsexplorertypesMetricMetadataDTO | 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,8 +40,10 @@ 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(metricType: MetricType | undefined): boolean {
|
export function isInspectEnabled(
|
||||||
return metricType === MetricType.GAUGE;
|
metricType: MetrictypesTypeDTO | undefined,
|
||||||
|
): boolean {
|
||||||
|
return metricType === MetrictypesTypeDTO.gauge;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllTimestampsOfMetrics(
|
export function getAllTimestampsOfMetrics(
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
Popover,
|
||||||
|
Skeleton,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { 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';
|
||||||
@@ -12,9 +21,33 @@ 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 { AllAttributesProps, AllAttributesValueProps } from './types';
|
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
||||||
|
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,
|
||||||
@@ -110,13 +143,23 @@ 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 | string[]>(
|
const [activeKey, setActiveKey] = useState<string[]>([ALL_ATTRIBUTES_KEY]);
|
||||||
'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();
|
||||||
|
|
||||||
@@ -178,7 +221,7 @@ function AllAttributes({
|
|||||||
attributes.filter(
|
attributes.filter(
|
||||||
(attribute) =>
|
(attribute) =>
|
||||||
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
|
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
|
||||||
attribute.value.some((value) =>
|
attribute.values?.some((value) =>
|
||||||
value.toLowerCase().includes(searchString.toLowerCase()),
|
value.toLowerCase().includes(searchString.toLowerCase()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -195,7 +238,7 @@ function AllAttributes({
|
|||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
key: attribute.key,
|
key: attribute.key,
|
||||||
value: attribute.value,
|
value: attribute.values,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
@@ -270,6 +313,7 @@ function AllAttributes({
|
|||||||
onClick={(e): void => {
|
onClick={(e): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
disabled={isLoadingAttributes || isErrorAttributes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -277,25 +321,49 @@ 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 metrics-metadata-accordion"
|
className="metrics-accordion"
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onChange={(keys): void => setActiveKey(keys)}
|
onChange={(keys): void => setActiveKey(keys as string[])}
|
||||||
items={items}
|
items={items}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,36 +2,84 @@ 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({
|
||||||
alerts,
|
metricName,
|
||||||
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.alert_id,
|
key: alert.alertId,
|
||||||
label: (
|
label: (
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
key={alert.alert_id}
|
key={alert.alertId}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
params.set(QueryParams.ruleId, alert.alert_id);
|
params.set(QueryParams.ruleId, alert.alertId);
|
||||||
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.alert_name || alert.alert_id}
|
{alert.alertName || alert.alertId}
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@@ -39,41 +87,44 @@ 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 (uniqueDashboards && uniqueDashboards.length > 0) {
|
if (dashboards && dashboards.length > 0) {
|
||||||
return uniqueDashboards.map((dashboard) => ({
|
return dashboards.map((dashboard) => ({
|
||||||
key: dashboard.dashboard_id,
|
key: dashboard.dashboardId,
|
||||||
label: (
|
label: (
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
key={dashboard.dashboard_id}
|
key={dashboard.dashboardId}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
safeNavigate(
|
safeNavigate(
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
dashboardId: dashboard.dashboard_id,
|
dashboardId: dashboard.dashboardId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="dashboards-popover-content-item"
|
className="dashboards-popover-content-item"
|
||||||
>
|
>
|
||||||
{dashboard.dashboard_name || dashboard.dashboard_id}
|
{dashboard.dashboardName || dashboard.dashboardId}
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [uniqueDashboards, safeNavigate]);
|
}, [dashboards, safeNavigate]);
|
||||||
|
|
||||||
if (!dashboardsPopoverContent && !alertsPopoverContent) {
|
if (isLoadingAlerts || isLoadingDashboards) {
|
||||||
return null;
|
return (
|
||||||
|
<div className="dashboards-and-alerts-popover-container">
|
||||||
|
<Skeleton title={false} paragraph={{ rows: 1 }} active />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no dashboards or alerts or both have errors, don't show the popover
|
||||||
|
const hidePopover =
|
||||||
|
(!dashboardsPopoverContent && !alertsPopoverContent) ||
|
||||||
|
(isErrorAlerts && isErrorDashboards);
|
||||||
|
if (hidePopover) {
|
||||||
|
return <div className="dashboards-and-alerts-popover-container" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,8 +143,7 @@ function DashboardsAndAlertsPopover({
|
|||||||
>
|
>
|
||||||
<Grid size={12} color={Color.BG_SIENNA_500} />
|
<Grid size={12} color={Color.BG_SIENNA_500} />
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{uniqueDashboards?.length} dashboard
|
{pluralize(dashboards.length, 'dashboard')}
|
||||||
{uniqueDashboards?.length === 1 ? '' : 's'}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -112,7 +162,7 @@ function DashboardsAndAlertsPopover({
|
|||||||
>
|
>
|
||||||
<Bell size={12} color={Color.BG_SAKURA_500} />
|
<Bell size={12} color={Color.BG_SAKURA_500} />
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{alerts?.length} alert {alerts?.length === 1 ? 'rule' : 'rules'}
|
{pluralize(alerts.length, 'alert rule')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Skeleton, Tooltip, Typography } from 'antd';
|
||||||
|
import { useGetMetricHighlights } from 'api/generated/services/metrics';
|
||||||
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||||
|
import { HighlightsProps } from './types';
|
||||||
|
import {
|
||||||
|
formatNumberToCompactFormat,
|
||||||
|
formatTimestampToReadableDate,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
data: metricHighlightsData,
|
||||||
|
isLoading: isLoadingMetricHighlights,
|
||||||
|
isError: isErrorMetricHighlights,
|
||||||
|
refetch: refetchMetricHighlights,
|
||||||
|
} = useGetMetricHighlights(
|
||||||
|
{
|
||||||
|
metricName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
enabled: !!metricName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const metricHighlights = metricHighlightsData?.data;
|
||||||
|
|
||||||
|
const timeSeriesActive = formatNumberToCompactFormat(
|
||||||
|
metricHighlights?.activeTimeSeries,
|
||||||
|
);
|
||||||
|
const timeSeriesTotal = formatNumberToCompactFormat(
|
||||||
|
metricHighlights?.totalTimeSeries,
|
||||||
|
);
|
||||||
|
const lastReceivedText = formatTimestampToReadableDate(
|
||||||
|
metricHighlights?.lastReceived,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoadingMetricHighlights) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="metric-details-content-grid"
|
||||||
|
data-testid="metric-highlights-loading-state"
|
||||||
|
>
|
||||||
|
<Skeleton title={false} paragraph={{ rows: 2 }} active />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isErrorMetricHighlights) {
|
||||||
|
return (
|
||||||
|
<div className="metric-details-content-grid">
|
||||||
|
<div
|
||||||
|
className="metric-highlights-error-state"
|
||||||
|
data-testid="metric-highlights-error-state"
|
||||||
|
>
|
||||||
|
<InfoIcon size={16} color={Color.BG_CHERRY_500} />
|
||||||
|
<Typography.Text>
|
||||||
|
Something went wrong while fetching metric highlights
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="large"
|
||||||
|
onClick={(): void => {
|
||||||
|
refetchMetricHighlights();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry ?
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metric-details-content-grid">
|
||||||
|
<div className="labels-row">
|
||||||
|
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||||
|
SAMPLES
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||||
|
TIME SERIES
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||||
|
LAST RECEIVED
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="values-row">
|
||||||
|
<Typography.Text
|
||||||
|
className="metric-details-grid-value"
|
||||||
|
data-testid="metric-highlights-data-points"
|
||||||
|
>
|
||||||
|
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
|
||||||
|
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
className="metric-details-grid-value"
|
||||||
|
data-testid="metric-highlights-time-series-total"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
title="Active time series are those that have received data points in the last 1
|
||||||
|
hour."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
className="metric-details-grid-value"
|
||||||
|
data-testid="metric-highlights-last-received"
|
||||||
|
>
|
||||||
|
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Highlights;
|
||||||
@@ -1,45 +1,58 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
import { Button, Collapse, Input, Select, Skeleton, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import {
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
invalidateGetMetricMetadata,
|
||||||
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
invalidateListMetrics,
|
||||||
|
useUpdateMetricMetadata,
|
||||||
|
} from 'api/generated/services/metrics';
|
||||||
|
import {
|
||||||
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
RenderErrorResponseDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { 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_TYPE_LABEL_MAP,
|
METRIC_METADATA_KEYS,
|
||||||
METRIC_TYPE_VALUES_MAP,
|
METRIC_METADATA_TEMPORALITY_OPTIONS,
|
||||||
} from '../Summary/constants';
|
METRIC_METADATA_TYPE_OPTIONS,
|
||||||
import { MetricTypeRenderer } from '../Summary/utils';
|
METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
||||||
import { METRIC_METADATA_KEYS } from './constants';
|
} from './constants';
|
||||||
import { MetadataProps } from './types';
|
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
||||||
import { determineIsMonotonic } from './utils';
|
import { MetadataProps, MetricMetadataFormState, TableFields } from './types';
|
||||||
|
import { transformUpdateMetricMetadataRequest } from './utils';
|
||||||
|
|
||||||
function Metadata({
|
function Metadata({
|
||||||
metricName,
|
metricName,
|
||||||
metadata,
|
metadata,
|
||||||
refetchMetricDetails,
|
isErrorMetricMetadata,
|
||||||
|
isLoadingMetricMetadata,
|
||||||
|
refetchMetricMetadata,
|
||||||
}: MetadataProps): JSX.Element {
|
}: MetadataProps): JSX.Element {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
metricMetadata,
|
metricMetadataState,
|
||||||
setMetricMetadata,
|
setMetricMetadataState,
|
||||||
] = useState<UpdateMetricMetadataProps>({
|
] = useState<MetricMetadataFormState>({
|
||||||
metricType: metadata?.metric_type || MetricType.SUM,
|
type: MetrictypesTypeDTO.sum,
|
||||||
description: metadata?.description || '',
|
description: '',
|
||||||
temporality: metadata?.temporality,
|
temporality: MetrictypesTemporalityDTO.unspecified,
|
||||||
unit: metadata?.unit,
|
unit: '',
|
||||||
|
isMonotonic: false,
|
||||||
});
|
});
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const {
|
const {
|
||||||
@@ -51,110 +64,135 @@ 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({
|
? Object.keys(metadata).map((key) => ({
|
||||||
...metadata,
|
key,
|
||||||
temporality: metadata?.temporality,
|
value: {
|
||||||
})
|
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((key: string, value: string) => {
|
const renderUneditableField = useCallback(
|
||||||
if (key === 'metric_type') {
|
(key: keyof MetricMetadataFormState, value: string) => {
|
||||||
return <MetricTypeRenderer type={value as MetricType} />;
|
if (isErrorMetricMetadata) {
|
||||||
}
|
return <FieldRenderer field="-" />;
|
||||||
let fieldValue = value;
|
}
|
||||||
if (key === 'unit') {
|
if (key === TableFields.TYPE) {
|
||||||
fieldValue = getUniversalNameFromMetricUnit(value);
|
return <MetricTypeRendererV2 type={value as MetrictypesTypeDTO} />;
|
||||||
}
|
}
|
||||||
return <FieldRenderer field={fieldValue || '-'} />;
|
if (key === TableFields.IS_MONOTONIC) {
|
||||||
}, []);
|
return <FieldRenderer field={value ? 'Yes' : 'No'} />;
|
||||||
|
}
|
||||||
|
if (key === TableFields.Temporality) {
|
||||||
|
const temporality = METRIC_METADATA_TEMPORALITY_OPTIONS.find(
|
||||||
|
(option) => option.value === value,
|
||||||
|
);
|
||||||
|
return <FieldRenderer field={temporality?.label || '-'} />;
|
||||||
|
}
|
||||||
|
let fieldValue = value;
|
||||||
|
if (key === TableFields.UNIT) {
|
||||||
|
fieldValue = getUniversalNameFromMetricUnit(value);
|
||||||
|
}
|
||||||
|
return <FieldRenderer field={fieldValue || '-'} />;
|
||||||
|
},
|
||||||
|
[isErrorMetricMetadata],
|
||||||
|
);
|
||||||
|
|
||||||
const renderColumnValue = useCallback(
|
const renderColumnValue = useCallback(
|
||||||
(field: { value: string; key: string }): JSX.Element => {
|
(field: {
|
||||||
|
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 = field.key === 'unit' && Boolean(metadata?.unit);
|
const metricUnitAlreadySet =
|
||||||
|
field.key === TableFields.UNIT && Boolean(metadata?.unit);
|
||||||
if (metricUnitAlreadySet) {
|
if (metricUnitAlreadySet) {
|
||||||
return renderUneditableField(field.key, field.value);
|
return renderUneditableField(field.key, field.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.key === 'metric_type') {
|
// Monotonic is not editable
|
||||||
|
if (field.key === TableFields.IS_MONOTONIC) {
|
||||||
|
return renderUneditableField(field.key, field.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.key === TableFields.TYPE) {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data-testid="metric-type-select"
|
data-testid="metric-type-select"
|
||||||
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
|
options={METRIC_METADATA_TYPE_OPTIONS}
|
||||||
value: key,
|
value={metricMetadataState.type}
|
||||||
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
|
||||||
}))}
|
|
||||||
value={metricMetadata.metricType}
|
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadata((prev) => ({
|
setMetricMetadataState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
metricType: value as MetricType,
|
type: value,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === 'unit') {
|
if (field.key === TableFields.UNIT) {
|
||||||
return (
|
return (
|
||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
value={metricMetadata.unit}
|
value={metricMetadataState.unit}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadata((prev) => ({ ...prev, unit: value }));
|
setMetricMetadataState((prev) => ({ ...prev, unit: value }));
|
||||||
}}
|
}}
|
||||||
data-testid="unit-select"
|
data-testid="unit-select"
|
||||||
source={YAxisSource.EXPLORER}
|
source={YAxisSource.EXPLORER}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === 'temporality') {
|
if (field.key === TableFields.Temporality) {
|
||||||
|
const temporalityValue =
|
||||||
|
metricMetadataState.temporality === MetrictypesTemporalityDTO.unspecified
|
||||||
|
? undefined
|
||||||
|
: metricMetadataState.temporality;
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
data-testid="temporality-select"
|
data-testid="temporality-select"
|
||||||
options={Object.values(Temporality).map((key) => ({
|
options={METRIC_METADATA_TEMPORALITY_OPTIONS}
|
||||||
value: key,
|
value={temporalityValue}
|
||||||
label: key,
|
|
||||||
}))}
|
|
||||||
value={metricMetadata.temporality}
|
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadata((prev) => ({
|
setMetricMetadataState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
temporality: value as Temporality,
|
temporality: value,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (field.key === 'description') {
|
if (field.key === TableFields.DESCRIPTION) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-testid="description-input"
|
data-testid="description-input"
|
||||||
name={field.key}
|
name={field.key}
|
||||||
defaultValue={
|
defaultValue={metricMetadataState.description}
|
||||||
metricMetadata[
|
|
||||||
field.key as Exclude<keyof UpdateMetricMetadataProps, 'isMonotonic'>
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
setMetricMetadata((prev) => ({
|
setMetricMetadataState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field.key]: e.target.value,
|
[field.key]: e.target.value,
|
||||||
}));
|
}));
|
||||||
@@ -164,7 +202,7 @@ function Metadata({
|
|||||||
}
|
}
|
||||||
return <FieldRenderer field="-" />;
|
return <FieldRenderer field="-" />;
|
||||||
},
|
},
|
||||||
[isEditing, metadata?.unit, metricMetadata, renderUneditableField],
|
[isEditing, metadata?.unit, metricMetadataState, renderUneditableField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = useMemo(
|
const columns: ColumnsType<DataType> = useMemo(
|
||||||
@@ -201,52 +239,61 @@ function Metadata({
|
|||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
updateMetricMetadata(
|
updateMetricMetadata(
|
||||||
{
|
{
|
||||||
metricName,
|
pathParams: {
|
||||||
payload: {
|
metricName,
|
||||||
...metricMetadata,
|
|
||||||
isMonotonic: determineIsMonotonic(
|
|
||||||
metricMetadata.metricType,
|
|
||||||
metricMetadata.temporality,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (response): void => {
|
onSuccess: (): void => {
|
||||||
if (response?.statusCode === 200) {
|
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
||||||
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
});
|
||||||
});
|
notifications.success({
|
||||||
notifications.success({
|
message: 'Metadata updated successfully',
|
||||||
message: 'Metadata updated successfully',
|
});
|
||||||
});
|
setIsEditing(false);
|
||||||
refetchMetricDetails();
|
invalidateListMetrics(queryClient);
|
||||||
setIsEditing(false);
|
invalidateGetMetricMetadata(queryClient, {
|
||||||
queryClient.invalidateQueries(['metricsList']);
|
metricName,
|
||||||
} else {
|
});
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (): void =>
|
onError: (error): void => {
|
||||||
|
const errorMessage = (error as AxiosError<RenderErrorResponseDTO>).response
|
||||||
|
?.data.error?.message;
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message:
|
message: errorMessage || METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
||||||
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
});
|
||||||
}),
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
updateMetricMetadata,
|
updateMetricMetadata,
|
||||||
metricName,
|
metricName,
|
||||||
metricMetadata,
|
metricMetadataState,
|
||||||
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 (
|
||||||
@@ -254,10 +301,7 @@ function Metadata({
|
|||||||
<Button
|
<Button
|
||||||
className="action-button"
|
className="action-button"
|
||||||
type="text"
|
type="text"
|
||||||
onClick={(e): void => {
|
onClick={cancelEdit}
|
||||||
e.stopPropagation();
|
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
|
||||||
disabled={isUpdatingMetricsMetadata}
|
disabled={isUpdatingMetricsMetadata}
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
@@ -278,6 +322,9 @@ function Metadata({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isErrorMetricMetadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="action-menu">
|
<div className="action-menu">
|
||||||
<Button
|
<Button
|
||||||
@@ -294,7 +341,13 @@ function Metadata({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [handleSave, isEditing, isUpdatingMetricsMetadata]);
|
}, [
|
||||||
|
isEditing,
|
||||||
|
isErrorMetricMetadata,
|
||||||
|
isUpdatingMetricsMetadata,
|
||||||
|
cancelEdit,
|
||||||
|
handleSave,
|
||||||
|
]);
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -306,7 +359,14 @@ function Metadata({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
key: 'metric-metadata',
|
key: 'metric-metadata',
|
||||||
children: (
|
children: isErrorMetricMetadata ? (
|
||||||
|
<div className="metric-metadata-error-state">
|
||||||
|
<MetricDetailsErrorState
|
||||||
|
refetch={refetchMetricMetadata}
|
||||||
|
errorMessage="Something went wrong while fetching metric metadata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
@@ -318,9 +378,23 @@ function Metadata({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[actionButton, columns, tableData],
|
[
|
||||||
|
actionButton,
|
||||||
|
columns,
|
||||||
|
isErrorMetricMetadata,
|
||||||
|
refetchMetricMetadata,
|
||||||
|
tableData,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isLoadingMetricMetadata) {
|
||||||
|
return (
|
||||||
|
<div className="metrics-metadata-skeleton-container">
|
||||||
|
<Skeleton active paragraph={{ rows: 8 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
bordered
|
bordered
|
||||||
|
|||||||
@@ -38,7 +38,12 @@
|
|||||||
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;
|
||||||
@@ -47,6 +52,18 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -72,6 +89,7 @@
|
|||||||
.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;
|
||||||
@@ -102,7 +120,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
@@ -148,7 +178,6 @@
|
|||||||
|
|
||||||
.all-attributes-search-input {
|
.all-attributes-search-input {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
border: 1px solid var(--bg-slate-300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +190,7 @@
|
|||||||
.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 {
|
||||||
@@ -217,6 +247,10 @@
|
|||||||
|
|
||||||
.ant-collapse-content-box {
|
.ant-collapse-content-box {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
.metric-metadata-error-state {
|
||||||
|
height: 267px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-header {
|
.ant-collapse-header {
|
||||||
@@ -237,6 +271,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
@@ -330,18 +365,26 @@
|
|||||||
.metric-details-content {
|
.metric-details-content {
|
||||||
.metrics-accordion {
|
.metrics-accordion {
|
||||||
.metrics-accordion-header {
|
.metrics-accordion-header {
|
||||||
.action-button {
|
.action-menu {
|
||||||
.ant-typography {
|
.action-button {
|
||||||
color: var(--bg-slate-400);
|
.ant-typography {
|
||||||
|
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-slate-400);
|
color: var(--bg-vanilla-200);
|
||||||
background-color: var(--bg-robin-300);
|
background-color: var(--bg-robin-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,3 +438,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-details-error-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
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 {
|
import { Button, Divider, Drawer, Typography } from 'antd';
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Drawer,
|
|
||||||
Empty,
|
|
||||||
Skeleton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
import { useGetMetricMetadata } from 'api/generated/services/metrics';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { Compass, Crosshair, X } from 'lucide-react';
|
import { Compass, Crosshair, X } from 'lucide-react';
|
||||||
|
|
||||||
@@ -19,16 +11,12 @@ 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 {
|
import { getMetricDetailsQuery } from './utils';
|
||||||
formatNumberToCompactFormat,
|
|
||||||
formatTimestampToReadableDate,
|
|
||||||
getMetricDetailsQuery,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
import './MetricDetails.styles.scss';
|
import './MetricDetails.styles.scss';
|
||||||
import '../Summary/Summary.styles.scss';
|
import '../Summary/Summary.styles.scss';
|
||||||
@@ -43,55 +31,49 @@ function MetricDetails({
|
|||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data: metricMetadataResponse,
|
||||||
isLoading,
|
isLoading: isLoadingMetricMetadata,
|
||||||
isFetching,
|
isError: isErrorMetricMetadata,
|
||||||
error: metricDetailsError,
|
refetch: refetchMetricMetadata,
|
||||||
refetch: refetchMetricDetails,
|
} = useGetMetricMetadata(
|
||||||
} = useGetMetricDetails(metricName ?? '', {
|
{
|
||||||
enabled: !!metricName,
|
metricName,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
const metric = data?.payload?.data;
|
query: {
|
||||||
|
enabled: !!metricName,
|
||||||
const lastReceived = useMemo(() => {
|
},
|
||||||
if (!metric) {
|
},
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return formatTimestampToReadableDate(metric.lastReceived);
|
|
||||||
}, [metric]);
|
|
||||||
|
|
||||||
const showInspectFeature = useMemo(
|
|
||||||
() => isInspectEnabled(metric?.metadata?.metric_type),
|
|
||||||
[metric],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMetricDetailsLoading = isLoading || isFetching;
|
const metadata = useMemo(() => {
|
||||||
|
if (!metricMetadataResponse) {
|
||||||
const timeSeries = useMemo(() => {
|
|
||||||
if (!metric) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
|
const {
|
||||||
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
|
type,
|
||||||
|
description,
|
||||||
|
unit,
|
||||||
|
temporality,
|
||||||
|
isMonotonic,
|
||||||
|
} = metricMetadataResponse.data;
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<Tooltip
|
type,
|
||||||
title="Active time series are those that have received data points in the last 1
|
description,
|
||||||
hour."
|
unit,
|
||||||
placement="top"
|
temporality,
|
||||||
>
|
isMonotonic,
|
||||||
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
};
|
||||||
</Tooltip>
|
}, [metricMetadataResponse]);
|
||||||
);
|
|
||||||
}, [metric]);
|
const showInspectFeature = useMemo(() => isInspectEnabled(metadata?.type), [
|
||||||
|
metadata?.type,
|
||||||
|
]);
|
||||||
|
|
||||||
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
||||||
if (metricName) {
|
if (metricName) {
|
||||||
const compositeQuery = getMetricDetailsQuery(
|
const compositeQuery = getMetricDetailsQuery(metricName, metadata?.type);
|
||||||
metricName,
|
|
||||||
metric?.metadata?.metric_type,
|
|
||||||
);
|
|
||||||
handleExplorerTabChange(
|
handleExplorerTabChange(
|
||||||
PANEL_TYPES.TIME_SERIES,
|
PANEL_TYPES.TIME_SERIES,
|
||||||
{
|
{
|
||||||
@@ -107,9 +89,7 @@ function MetricDetails({
|
|||||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
|
}, [metricName, handleExplorerTabChange, metadata?.type]);
|
||||||
|
|
||||||
const isMetricDetailsError = metricDetailsError || !metric;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent(MetricsExplorerEvents.ModalOpened, {
|
logEvent(MetricsExplorerEvents.ModalOpened, {
|
||||||
@@ -117,6 +97,9 @@ function MetricDetails({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isActionButtonDisabled =
|
||||||
|
!metricName || isLoadingMetricMetadata || isErrorMetricMetadata;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
width="60%"
|
||||||
@@ -124,13 +107,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>{metric?.name}</Typography.Text>
|
<Typography.Text>{metricName}</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={!metricName}
|
disabled={isActionButtonDisabled}
|
||||||
data-testid="open-in-explorer-button"
|
data-testid="open-in-explorer-button"
|
||||||
>
|
>
|
||||||
Open in Explorer
|
Open in Explorer
|
||||||
@@ -140,10 +123,11 @@ 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 (metric?.name) {
|
if (metricName) {
|
||||||
openInspectModal(metric.name);
|
openInspectModal(metricName);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-testid="inspect-metric-button"
|
data-testid="inspect-metric-button"
|
||||||
@@ -163,60 +147,18 @@ function MetricDetails({
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
closeIcon={<X size={16} />}
|
closeIcon={<X size={16} />}
|
||||||
>
|
>
|
||||||
{isMetricDetailsLoading && (
|
<div className="metric-details-content">
|
||||||
<div data-testid="metric-details-skeleton">
|
<Highlights metricName={metricName} />
|
||||||
<Skeleton active />
|
<DashboardsAndAlertsPopover metricName={metricName} />
|
||||||
</div>
|
<Metadata
|
||||||
)}
|
metricName={metricName}
|
||||||
{isMetricDetailsError && !isMetricDetailsLoading && (
|
metadata={metadata}
|
||||||
<Empty description="Error fetching metric details" />
|
isErrorMetricMetadata={isErrorMetricMetadata}
|
||||||
)}
|
isLoadingMetricMetadata={isLoadingMetricMetadata}
|
||||||
{!isMetricDetailsLoading && !isMetricDetailsError && (
|
refetchMetricMetadata={refetchMetricMetadata}
|
||||||
<div className="metric-details-content">
|
/>
|
||||||
<div className="metric-details-content-grid">
|
<AllAttributes metricName={metricName} metricType={metadata?.type} />
|
||||||
<div className="labels-row">
|
</div>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import { InfoIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { MetricDetailsErrorStateProps } from './types';
|
||||||
|
|
||||||
|
function MetricDetailsErrorState({
|
||||||
|
refetch,
|
||||||
|
errorMessage,
|
||||||
|
}: MetricDetailsErrorStateProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="metric-details-error-state">
|
||||||
|
<InfoIcon size={20} color={Color.BG_CHERRY_500} />
|
||||||
|
<Typography.Text>{errorMessage}</Typography.Text>
|
||||||
|
{refetch && <Button onClick={refetch}>Retry</Button>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricDetailsErrorState;
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import * as reactUseHooks from 'react-use';
|
import * as reactUseHooks from 'react-use';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
|
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'),
|
||||||
@@ -20,33 +22,28 @@ 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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={mockAttributes}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,9 +53,8 @@ describe('AllAttributes', () => {
|
|||||||
it('renders all attribute keys and values', () => {
|
it('renders all attribute keys and values', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={mockAttributes}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,9 +71,8 @@ describe('AllAttributes', () => {
|
|||||||
it('renders value counts correctly', () => {
|
it('renders value counts correctly', () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={mockAttributes}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,41 +81,44 @@ describe('AllAttributes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty attributes array', () => {
|
it('handles empty attributes array', () => {
|
||||||
|
useGetMetricAttributesMock.mockReturnValue({
|
||||||
|
...getMockMetricAttributesData({
|
||||||
|
data: {
|
||||||
|
attributes: [],
|
||||||
|
totalKeys: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={[]}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('No data')).toBeInTheDocument();
|
expect(screen.getByText('No attributes found')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking on an attribute key opens the explorer with the attribute filter applied', () => {
|
it('clicking on an attribute key opens the explorer with the attribute filter applied', async () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={mockAttributes}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
fireEvent.click(screen.getByText('attribute1'));
|
await userEvent.click(screen.getByText('attribute1'));
|
||||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters attributes based on search input', () => {
|
it('filters attributes based on search input', async () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributes
|
<AllAttributes
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
attributes={mockAttributes}
|
metricType={MetrictypesTypeDTO.gauge}
|
||||||
metricType={mockMetricType}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
fireEvent.change(screen.getByPlaceholderText('Search'), {
|
await userEvent.type(screen.getByPlaceholderText('Search'), 'value1');
|
||||||
target: { value: 'value1' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
@@ -144,7 +142,7 @@ describe('AllAttributesValue', () => {
|
|||||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads more attributes when show more button is clicked', () => {
|
it('loads more attributes when show more button is clicked', async () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -155,7 +153,7 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('value6')).not.toBeInTheDocument();
|
expect(screen.queryByText('value6')).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('Show More'));
|
await userEvent.click(screen.getByText('Show More'));
|
||||||
expect(screen.getByText('value6')).toBeInTheDocument();
|
expect(screen.getByText('value6')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,7 +170,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', () => {
|
it('copy button should copy the attribute value to the clipboard', async () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -183,13 +181,13 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('value1'));
|
await userEvent.click(screen.getByText('value1'));
|
||||||
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
|
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('Copy Attribute'));
|
await userEvent.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', () => {
|
it('explorer button should go to metrics explore with the attribute filter applied', async () => {
|
||||||
render(
|
render(
|
||||||
<AllAttributesValue
|
<AllAttributesValue
|
||||||
filterKey="attribute1"
|
filterKey="attribute1"
|
||||||
@@ -200,10 +198,10 @@ describe('AllAttributesValue', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('value1'));
|
await userEvent.click(screen.getByText('value1'));
|
||||||
|
|
||||||
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByText('Open in Explorer'));
|
await userEvent.click(screen.getByText('Open in Explorer'));
|
||||||
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
|
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
|
||||||
'attribute1',
|
'attribute1',
|
||||||
'value1',
|
'value1',
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { userEvent } from 'tests/test-utils';
|
||||||
|
|
||||||
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
|
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
|
||||||
|
import {
|
||||||
const mockAlert1 = {
|
getMockAlertsData,
|
||||||
alert_id: '1',
|
getMockDashboardsData,
|
||||||
alert_name: 'Alert 1',
|
MOCK_ALERT_1,
|
||||||
};
|
MOCK_ALERT_2,
|
||||||
const mockAlert2 = {
|
MOCK_DASHBOARD_1,
|
||||||
alert_id: '2',
|
MOCK_DASHBOARD_2,
|
||||||
alert_name: 'Alert 2',
|
MOCK_METRIC_NAME,
|
||||||
};
|
} 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', () => ({
|
||||||
@@ -28,7 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
|||||||
safeNavigate: mockSafeNavigate,
|
safeNavigate: mockSafeNavigate,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockSetQuery = jest.fn();
|
const mockSetQuery = jest.fn();
|
||||||
const mockUrlQuery = {
|
const mockUrlQuery = {
|
||||||
set: mockSetQuery,
|
set: mockSetQuery,
|
||||||
@@ -39,125 +30,156 @@ jest.mock('hooks/useUrlQuery', () => ({
|
|||||||
default: jest.fn(() => mockUrlQuery),
|
default: jest.fn(() => mockUrlQuery),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('DashboardsAndAlertsPopover', () => {
|
const useGetMetricAlertsMock = jest.spyOn(
|
||||||
it('renders the popover correctly with multiple dashboards and alerts', () => {
|
metricsExplorerHooks,
|
||||||
render(
|
'useGetMetricAlerts',
|
||||||
<DashboardsAndAlertsPopover
|
);
|
||||||
alerts={mockAlerts}
|
const useGetMetricDashboardsMock = jest.spyOn(
|
||||||
dashboards={mockDashboards}
|
metricsExplorerHooks,
|
||||||
/>,
|
'useGetMetricDashboards',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
describe('DashboardsAndAlertsPopover', () => {
|
||||||
screen.getByText(`${mockDashboards.length} dashboards`),
|
beforeEach(() => {
|
||||||
).toBeInTheDocument();
|
useGetMetricAlertsMock.mockReturnValue(getMockAlertsData());
|
||||||
expect(
|
useGetMetricDashboardsMock.mockReturnValue(getMockDashboardsData());
|
||||||
screen.getByText(`${mockAlerts.length} alert rules`),
|
});
|
||||||
).toBeInTheDocument();
|
|
||||||
|
it('renders the popover correctly with multiple dashboards and alerts', () => {
|
||||||
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(`2 dashboards`)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(`2 alert rules`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders null with no dashboards and alerts', () => {
|
it('renders null with no dashboards and alerts', () => {
|
||||||
const { container } = render(
|
useGetMetricAlertsMock.mockReturnValue(
|
||||||
<DashboardsAndAlertsPopover alerts={[]} dashboards={[]} />,
|
getMockAlertsData({
|
||||||
|
data: {
|
||||||
|
alerts: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(container).toBeEmptyDOMElement();
|
useGetMetricDashboardsMock.mockReturnValue(
|
||||||
|
getMockDashboardsData({
|
||||||
|
data: {
|
||||||
|
dashboards: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector('dashboards-and-alerts-popover-container'),
|
||||||
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders popover with single dashboard and alert', () => {
|
it('renders popover with single dashboard and alert', () => {
|
||||||
render(
|
useGetMetricAlertsMock.mockReturnValue(
|
||||||
<DashboardsAndAlertsPopover
|
getMockAlertsData({
|
||||||
alerts={[mockAlert1]}
|
data: {
|
||||||
dashboards={[mockDashboard1]}
|
alerts: [MOCK_ALERT_1],
|
||||||
/>,
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
useGetMetricDashboardsMock.mockReturnValue(
|
||||||
|
getMockDashboardsData({
|
||||||
|
data: {
|
||||||
|
dashboards: [MOCK_DASHBOARD_1],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
|
|
||||||
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
|
expect(screen.getByText(`1 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', () => {
|
it('renders popover with dashboard id if name is not available', async () => {
|
||||||
render(
|
useGetMetricDashboardsMock.mockReturnValue(
|
||||||
<DashboardsAndAlertsPopover
|
getMockDashboardsData({
|
||||||
alerts={mockAlerts}
|
data: {
|
||||||
dashboards={[{ ...mockDashboard1, dashboard_name: undefined } as any]}
|
dashboards: [{ ...MOCK_DASHBOARD_1, dashboardName: '' }],
|
||||||
/>,
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(`1 dashboard`));
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
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', () => {
|
it('renders popover with alert id if name is not available', async () => {
|
||||||
render(
|
useGetMetricAlertsMock.mockReturnValue(
|
||||||
<DashboardsAndAlertsPopover
|
getMockAlertsData({
|
||||||
alerts={[{ ...mockAlert1, alert_name: undefined } as any]}
|
data: {
|
||||||
dashboards={mockDashboards}
|
alerts: [{ ...MOCK_ALERT_1, alertName: '' }],
|
||||||
/>,
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(`1 alert rule`));
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
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', () => {
|
it('navigates to the dashboard when the dashboard is clicked', async () => {
|
||||||
render(
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
<DashboardsAndAlertsPopover
|
|
||||||
alerts={mockAlerts}
|
|
||||||
dashboards={mockDashboards}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Click on 2 dashboards button
|
// Click on 2 dashboards button
|
||||||
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
await userEvent.click(screen.getByText(`2 dashboards`));
|
||||||
// Popover showing list of 2 dashboards should be visible
|
// Popover showing list of 2 dashboards should be visible
|
||||||
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on the first dashboard
|
// Click on the first dashboard
|
||||||
fireEvent.click(screen.getByText(mockDashboard1.dashboard_name));
|
await userEvent.click(screen.getByText(MOCK_DASHBOARD_1.dashboardName));
|
||||||
|
|
||||||
// Should navigate to the dashboard
|
// Should navigate to the dashboard
|
||||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||||
`/dashboard/${mockDashboard1.dashboard_id}`,
|
`/dashboard/${MOCK_DASHBOARD_1.dashboardId}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigates to the alert when the alert is clicked', () => {
|
it('navigates to the alert when the alert is clicked', async () => {
|
||||||
render(
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
<DashboardsAndAlertsPopover
|
|
||||||
alerts={mockAlerts}
|
|
||||||
dashboards={mockDashboards}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Click on 2 alert rules button
|
// Click on 2 alert rules button
|
||||||
fireEvent.click(screen.getByText(`${mockAlerts.length} alert rules`));
|
await userEvent.click(screen.getByText(`2 alert rules`));
|
||||||
// Popover showing list of 2 alert rules should be visible
|
// Popover showing list of 2 alert rules should be visible
|
||||||
expect(screen.getByText(mockAlert1.alert_name)).toBeInTheDocument();
|
expect(screen.getByText(MOCK_ALERT_1.alertName)).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockAlert2.alert_name)).toBeInTheDocument();
|
expect(screen.getByText(MOCK_ALERT_2.alertName)).toBeInTheDocument();
|
||||||
|
|
||||||
// Click on the first alert rule
|
// Click on the first alert rule
|
||||||
fireEvent.click(screen.getByText(mockAlert1.alert_name));
|
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
|
||||||
|
|
||||||
// Should navigate to the alert rule
|
// Should navigate to the alert rule
|
||||||
expect(mockSetQuery).toHaveBeenCalledWith(
|
expect(mockSetQuery).toHaveBeenCalledWith(
|
||||||
QueryParams.ruleId,
|
QueryParams.ruleId,
|
||||||
mockAlert1.alert_id,
|
MOCK_ALERT_1.alertId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders unique dashboards even when there are duplicates', () => {
|
it('renders unique dashboards even when there are duplicates', async () => {
|
||||||
render(
|
useGetMetricDashboardsMock.mockReturnValue(
|
||||||
<DashboardsAndAlertsPopover
|
getMockDashboardsData({
|
||||||
alerts={mockAlerts}
|
data: {
|
||||||
dashboards={[...mockDashboards, mockDashboard1]}
|
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2, MOCK_DASHBOARD_1],
|
||||||
/>,
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
screen.getByText(`${mockDashboards.length} dashboards`),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||||
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
expect(screen.getByText('2 dashboards')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('2 dashboards'));
|
||||||
|
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
|
|
||||||
|
import Highlights from '../Highlights';
|
||||||
|
import { formatTimestampToReadableDate } from '../utils';
|
||||||
|
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
|
||||||
|
|
||||||
|
const useGetMetricHighlightsMock = jest.spyOn(
|
||||||
|
metricsExplorerHooks,
|
||||||
|
'useGetMetricHighlights',
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Highlights', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetMetricHighlightsMock.mockReturnValue(getMockMetricHighlightsData());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all highlights data correctly', () => {
|
||||||
|
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||||
|
|
||||||
|
const dataPoints = screen.getByTestId('metric-highlights-data-points');
|
||||||
|
const timeSeriesTotal = screen.getByTestId(
|
||||||
|
'metric-highlights-time-series-total',
|
||||||
|
);
|
||||||
|
const lastReceived = screen.getByTestId('metric-highlights-last-received');
|
||||||
|
|
||||||
|
expect(dataPoints.textContent).toBe('1M+');
|
||||||
|
expect(timeSeriesTotal.textContent).toBe('1M total ⎯ 1M active');
|
||||||
|
expect(lastReceived.textContent).toBe(
|
||||||
|
formatTimestampToReadableDate('2026-01-24T00:00:00Z'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error state correctly', () => {
|
||||||
|
useGetMetricHighlightsMock.mockReturnValue(
|
||||||
|
getMockMetricHighlightsData(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('metric-highlights-error-state'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render loading state when data is loading', () => {
|
||||||
|
useGetMetricHighlightsMock.mockReturnValue(
|
||||||
|
getMockMetricHighlightsData(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('metric-highlights-loading-state'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
|
import {
|
||||||
|
GetMetricMetadata200,
|
||||||
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import { 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', () => ({
|
||||||
@@ -72,13 +80,18 @@ 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
|
|
||||||
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
const mockMetricMetadata = transformMetricMetadata(
|
||||||
.mockReturnValue({
|
getMockMetricMetadataData().data as GetMetricMetadata200,
|
||||||
mutate: mockUseUpdateMetricMetadata,
|
) as MetricMetadata;
|
||||||
isLoading: false,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const mockErrorNotification = jest.fn();
|
const mockErrorNotification = jest.fn();
|
||||||
const mockSuccessNotification = jest.fn();
|
const mockSuccessNotification = jest.fn();
|
||||||
@@ -89,47 +102,50 @@ jest.spyOn(useNotificationsHooks, 'useNotifications').mockReturnValue({
|
|||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const mockMetricName = 'test_metric';
|
const mockRefetchMetricMetadata = jest.fn();
|
||||||
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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Metric Type')).toBeInTheDocument();
|
expect(screen.getByText('Metric Type')).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockMetricMetadata.metric_type)).toBeInTheDocument();
|
expect(screen.getByText('Gauge')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
expect(screen.getByText('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(mockMetricMetadata.temporality)).toBeInTheDocument();
|
expect(screen.getByText('Delta')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('editing the metadata should show the form inputs', () => {
|
it('editing the metadata should show the form inputs', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
fireEvent.click(editButton);
|
await userEvent.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();
|
||||||
@@ -139,57 +155,53 @@ 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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={{
|
metadata={{
|
||||||
...mockMetricMetadata,
|
...mockMetricMetadata,
|
||||||
unit: '',
|
unit: '',
|
||||||
}}
|
}}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
expect(metricDescriptionInput).toBeInTheDocument();
|
expect(metricDescriptionInput).toBeInTheDocument();
|
||||||
fireEvent.change(metricDescriptionInput, {
|
await userEvent.clear(metricDescriptionInput);
|
||||||
target: { value: 'Updated description' },
|
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||||
});
|
|
||||||
|
|
||||||
const metricTypeSelect = screen.getByTestId('metric-type-select');
|
const metricTypeSelect = screen.getByTestId('metric-type-select');
|
||||||
expect(metricTypeSelect).toBeInTheDocument();
|
expect(metricTypeSelect).toBeInTheDocument();
|
||||||
fireEvent.change(metricTypeSelect, {
|
await userEvent.selectOptions(metricTypeSelect, MetrictypesTypeDTO.sum);
|
||||||
target: { value: MetricType.SUM },
|
|
||||||
});
|
|
||||||
|
|
||||||
const temporalitySelect = screen.getByTestId('temporality-select');
|
const temporalitySelect = screen.getByTestId('temporality-select');
|
||||||
expect(temporalitySelect).toBeInTheDocument();
|
expect(temporalitySelect).toBeInTheDocument();
|
||||||
fireEvent.change(temporalitySelect, {
|
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
|
||||||
target: { value: Temporality.CUMULATIVE },
|
|
||||||
});
|
|
||||||
|
|
||||||
const unitSelect = screen.getByTestId('unit-select');
|
const unitSelect = screen.getByTestId('unit-select');
|
||||||
expect(unitSelect).toBeInTheDocument();
|
expect(unitSelect).toBeInTheDocument();
|
||||||
fireEvent.change(unitSelect, {
|
await userEvent.selectOptions(unitSelect, 'By');
|
||||||
target: { value: 'By' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
expect(saveButton).toBeInTheDocument();
|
expect(saveButton).toBeInTheDocument();
|
||||||
fireEvent.click(saveButton);
|
await userEvent.click(saveButton);
|
||||||
|
|
||||||
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
metricName: mockMetricName,
|
data: expect.objectContaining({
|
||||||
payload: expect.objectContaining({
|
type: MetrictypesTypeDTO.sum,
|
||||||
description: 'Updated description',
|
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||||
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),
|
||||||
@@ -201,56 +213,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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
fireEvent.change(metricDescriptionInput, {
|
await userEvent.clear(metricDescriptionInput);
|
||||||
target: { value: 'Updated description' },
|
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||||
});
|
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
fireEvent.click(saveButton);
|
await userEvent.click(saveButton);
|
||||||
|
|
||||||
const onSuccessCallback =
|
const onSuccessCallback =
|
||||||
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
||||||
onSuccessCallback({ statusCode: 200 });
|
onSuccessCallback({ status: 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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
fireEvent.change(metricDescriptionInput, {
|
await userEvent.clear(metricDescriptionInput);
|
||||||
target: { value: 'Updated description' },
|
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||||
});
|
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
fireEvent.click(saveButton);
|
await userEvent.click(saveButton);
|
||||||
|
|
||||||
const onSuccessCallback =
|
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
||||||
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
onErrorCallback({ status: 500 });
|
||||||
onSuccessCallback({ statusCode: 500 });
|
|
||||||
|
|
||||||
expect(mockErrorNotification).toHaveBeenCalledWith({
|
expect(mockErrorNotification).toHaveBeenCalledWith({
|
||||||
message:
|
message:
|
||||||
@@ -261,22 +273,23 @@ 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={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||||
fireEvent.change(metricDescriptionInput, {
|
await userEvent.clear(metricDescriptionInput);
|
||||||
target: { value: 'Updated description' },
|
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||||
});
|
|
||||||
|
|
||||||
const saveButton = screen.getByText('Save');
|
const saveButton = screen.getByText('Save');
|
||||||
fireEvent.click(saveButton);
|
await userEvent.click(saveButton);
|
||||||
|
|
||||||
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
||||||
|
|
||||||
@@ -289,39 +302,43 @@ describe('Metadata', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancel button should cancel the edit mode', () => {
|
it('cancel button should cancel the edit mode', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const cancelButton = screen.getByText('Cancel');
|
const cancelButton = screen.getByText('Cancel');
|
||||||
expect(cancelButton).toBeInTheDocument();
|
expect(cancelButton).toBeInTheDocument();
|
||||||
fireEvent.click(cancelButton);
|
await userEvent.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', () => {
|
it('should not allow editing of unit if it is already set', async () => {
|
||||||
render(
|
render(
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={mockMetricName}
|
metricName={MOCK_METRIC_NAME}
|
||||||
metadata={mockMetricMetadata}
|
metadata={mockMetricMetadata}
|
||||||
refetchMetricDetails={mockRefetchMetricDetails}
|
isErrorMetricMetadata={false}
|
||||||
|
isLoadingMetricMetadata={false}
|
||||||
|
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editButton = screen.getByText('Edit');
|
const editButton = screen.getByText('Edit');
|
||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
fireEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
const unitSelect = screen.queryByTestId('unit-select');
|
const unitSelect = screen.queryByTestId('unit-select');
|
||||||
expect(unitSelect).not.toBeInTheDocument();
|
expect(unitSelect).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,68 +1,16 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { MetricDetails as MetricDetailsType } from 'api/metricsExplorer/getMetricDetails';
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
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')
|
||||||
@@ -88,7 +36,50 @@ jest.mock('react-query', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'container/MetricsExplorer/MetricDetails/AllAttributes',
|
||||||
|
() =>
|
||||||
|
function MockAllAttributes(): JSX.Element {
|
||||||
|
return <div data-testid="all-attributes">All Attributes</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jest.mock(
|
||||||
|
'container/MetricsExplorer/MetricDetails/DashboardsAndAlertsPopover',
|
||||||
|
() =>
|
||||||
|
function MockDashboardsAndAlertsPopover(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div data-testid="dashboards-and-alerts-popover">
|
||||||
|
Dashboards and Alerts Popover
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jest.mock(
|
||||||
|
'container/MetricsExplorer/MetricDetails/Highlights',
|
||||||
|
() =>
|
||||||
|
function MockHighlights(): JSX.Element {
|
||||||
|
return <div data-testid="highlights">Highlights</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'container/MetricsExplorer/MetricDetails/Metadata',
|
||||||
|
() =>
|
||||||
|
function MockMetadata(): JSX.Element {
|
||||||
|
return <div data-testid="metadata">Metadata</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const useGetMetricMetadataMock = jest.spyOn(
|
||||||
|
metricsExplorerHooks,
|
||||||
|
'useGetMetricMetadata',
|
||||||
|
);
|
||||||
|
|
||||||
describe('MetricDetails', () => {
|
describe('MetricDetails', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGetMetricMetadataMock.mockReturnValue(getMockMetricMetadataData());
|
||||||
|
});
|
||||||
|
|
||||||
it('renders metric details correctly', () => {
|
it('renders metric details correctly', () => {
|
||||||
render(
|
render(
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
@@ -101,27 +92,15 @@ describe('MetricDetails', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
|
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
|
||||||
expect(screen.getByText(mockMetricDescription)).toBeInTheDocument();
|
expect(screen.getByTestId('all-attributes')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(getUniversalNameFromMetricUnit(mockMetricData.unit)),
|
screen.getByTestId('dashboards-and-alerts-popover'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('highlights')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('metadata')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the "open in explorer" and "inspect" buttons', () => {
|
it('renders the "open in explorer" and "inspect" buttons', async () => {
|
||||||
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}
|
||||||
@@ -135,93 +114,10 @@ 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();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('open-in-explorer-button'));
|
await userEvent.click(screen.getByTestId('open-in-explorer-button'));
|
||||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('inspect-metric-button'));
|
await userEvent.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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||||
|
import {
|
||||||
|
GetMetricAlerts200,
|
||||||
|
GetMetricAttributes200,
|
||||||
|
GetMetricDashboards200,
|
||||||
|
GetMetricHighlights200,
|
||||||
|
GetMetricMetadata200,
|
||||||
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
|
|
||||||
|
export const MOCK_METRIC_NAME = 'test-metric';
|
||||||
|
|
||||||
|
export function getMockMetricHighlightsData(
|
||||||
|
overrides?: Partial<GetMetricHighlights200>,
|
||||||
|
{
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
} = {},
|
||||||
|
): ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights> {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
dataPoints: 1000000,
|
||||||
|
lastReceived: '2026-01-24T00:00:00Z',
|
||||||
|
totalTimeSeries: 1000000,
|
||||||
|
activeTimeSeries: 1000000,
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} as ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MOCK_DASHBOARD_1 = {
|
||||||
|
dashboardName: 'Dashboard 1',
|
||||||
|
dashboardId: '1',
|
||||||
|
widgetId: '1',
|
||||||
|
widgetName: 'Widget 1',
|
||||||
|
};
|
||||||
|
export const MOCK_DASHBOARD_2 = {
|
||||||
|
dashboardName: 'Dashboard 2',
|
||||||
|
dashboardId: '2',
|
||||||
|
widgetId: '2',
|
||||||
|
widgetName: 'Widget 2',
|
||||||
|
};
|
||||||
|
export const MOCK_ALERT_1 = {
|
||||||
|
alertName: 'Alert 1',
|
||||||
|
alertId: '1',
|
||||||
|
};
|
||||||
|
export const MOCK_ALERT_2 = {
|
||||||
|
alertName: 'Alert 2',
|
||||||
|
alertId: '2',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMockDashboardsData(
|
||||||
|
overrides?: Partial<GetMetricDashboards200>,
|
||||||
|
{
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
} = {},
|
||||||
|
): ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards> {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2],
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} as ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMockAlertsData(
|
||||||
|
overrides?: Partial<GetMetricAlerts200>,
|
||||||
|
{
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
} = {},
|
||||||
|
): ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts> {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
alerts: [MOCK_ALERT_1, MOCK_ALERT_2],
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMockMetricAttributesData(
|
||||||
|
overrides?: Partial<GetMetricAttributes200>,
|
||||||
|
{
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
} = {},
|
||||||
|
): ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes> {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
key: 'attribute1',
|
||||||
|
values: ['value1', 'value2'],
|
||||||
|
valueCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attribute2',
|
||||||
|
values: ['value3'],
|
||||||
|
valueCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
totalKeys: 2,
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMockMetricMetadataData(
|
||||||
|
overrides?: Partial<GetMetricMetadata200>,
|
||||||
|
{
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
} = {},
|
||||||
|
): ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata> {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
description: 'test_description',
|
||||||
|
type: MetrictypesTypeDTO.gauge,
|
||||||
|
unit: 'test_unit',
|
||||||
|
temporality: MetrictypesTemporalityDTO.delta,
|
||||||
|
isMonotonic: false,
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} as ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import {
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
determineIsMonotonic,
|
determineIsMonotonic,
|
||||||
@@ -10,35 +12,48 @@ 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(MetricType.HISTOGRAM)).toBe(true);
|
expect(determineIsMonotonic(MetrictypesTypeDTO.histogram)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for exponential histogram metrics', () => {
|
it('should return true for exponential histogram metrics', () => {
|
||||||
expect(determineIsMonotonic(MetricType.EXPONENTIAL_HISTOGRAM)).toBe(true);
|
expect(determineIsMonotonic(MetrictypesTypeDTO.exponentialhistogram)).toBe(
|
||||||
});
|
|
||||||
|
|
||||||
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(determineIsMonotonic(MetricType.SUM, Temporality.DELTA)).toBe(false);
|
expect(
|
||||||
|
determineIsMonotonic(
|
||||||
|
MetrictypesTypeDTO.sum,
|
||||||
|
MetrictypesTemporalityDTO.delta,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false by default', () => {
|
it('should return false by default', () => {
|
||||||
expect(determineIsMonotonic('' as MetricType, '' as Temporality)).toBe(
|
expect(
|
||||||
false,
|
determineIsMonotonic(
|
||||||
);
|
'' as MetrictypesTypeDTO,
|
||||||
|
'' as MetrictypesTemporalityDTO,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,13 +130,16 @@ 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(TEST_METRIC_NAME, MetricType.SUM);
|
const query = getMetricDetailsQuery(
|
||||||
|
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(
|
||||||
MetricType.SUM,
|
MetrictypesTypeDTO.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');
|
||||||
@@ -129,13 +147,16 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for GAUGE metric type', () => {
|
it('should create correct query for GAUGE metric type', () => {
|
||||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.GAUGE);
|
const query = getMetricDetailsQuery(
|
||||||
|
TEST_METRIC_NAME,
|
||||||
|
MetrictypesTypeDTO.gauge,
|
||||||
|
);
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
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(
|
||||||
MetricType.GAUGE,
|
MetrictypesTypeDTO.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');
|
||||||
@@ -143,13 +164,16 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for SUMMARY metric type', () => {
|
it('should create correct query for SUMMARY metric type', () => {
|
||||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUMMARY);
|
const query = getMetricDetailsQuery(
|
||||||
|
TEST_METRIC_NAME,
|
||||||
|
MetrictypesTypeDTO.summary,
|
||||||
|
);
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
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(
|
||||||
MetricType.SUMMARY,
|
MetrictypesTypeDTO.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');
|
||||||
@@ -157,13 +181,16 @@ describe('MetricDetails utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create correct query for HISTOGRAM metric type', () => {
|
it('should create correct query for HISTOGRAM metric type', () => {
|
||||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.HISTOGRAM);
|
const query = getMetricDetailsQuery(
|
||||||
|
TEST_METRIC_NAME,
|
||||||
|
MetrictypesTypeDTO.histogram,
|
||||||
|
);
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
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(
|
||||||
MetricType.HISTOGRAM,
|
MetrictypesTypeDTO.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');
|
||||||
@@ -173,14 +200,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,
|
||||||
MetricType.EXPONENTIAL_HISTOGRAM,
|
MetrictypesTypeDTO.exponentialhistogram,
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
||||||
MetricType.EXPONENTIAL_HISTOGRAM,
|
MetrictypesTypeDTO.exponentialhistogram,
|
||||||
);
|
);
|
||||||
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');
|
||||||
@@ -203,7 +230,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,
|
||||||
MetricType.SUM,
|
MetrictypesTypeDTO.sum,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,7 +248,7 @@ describe('MetricDetails utils', () => {
|
|||||||
const groupBy = 'service';
|
const groupBy = 'service';
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetricType.SUM,
|
MetrictypesTypeDTO.sum,
|
||||||
undefined,
|
undefined,
|
||||||
groupBy,
|
groupBy,
|
||||||
);
|
);
|
||||||
@@ -236,7 +263,7 @@ describe('MetricDetails utils', () => {
|
|||||||
const groupBy = 'endpoint';
|
const groupBy = 'endpoint';
|
||||||
const query = getMetricDetailsQuery(
|
const query = getMetricDetailsQuery(
|
||||||
TEST_METRIC_NAME,
|
TEST_METRIC_NAME,
|
||||||
MetricType.SUM,
|
MetrictypesTypeDTO.sum,
|
||||||
filter,
|
filter,
|
||||||
groupBy,
|
groupBy,
|
||||||
);
|
);
|
||||||
@@ -250,7 +277,10 @@ 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(TEST_METRIC_NAME, MetricType.SUM);
|
const query = getMetricDetailsQuery(
|
||||||
|
TEST_METRIC_NAME,
|
||||||
|
MetrictypesTypeDTO.sum,
|
||||||
|
);
|
||||||
|
|
||||||
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
|
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
|
||||||
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
|
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
|
||||||
|
|||||||
@@ -1,6 +1,55 @@
|
|||||||
|
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',
|
||||||
metric_type: 'Metric Type',
|
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,34 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
MetricDetails,
|
MetricsexplorertypesMetricAlertDTO,
|
||||||
MetricDetailsAlert,
|
MetricsexplorertypesMetricAttributeDTO,
|
||||||
MetricDetailsAttribute,
|
MetricsexplorertypesMetricDashboardDTO,
|
||||||
MetricDetailsDashboard,
|
MetricsexplorertypesMetricHighlightsResponseDTO,
|
||||||
} from 'api/metricsExplorer/getMetricDetails';
|
MetricsexplorertypesMetricMetadataDTO,
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
MetrictypesTemporalityDTO,
|
||||||
|
MetrictypesTypeDTO,
|
||||||
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
|
|
||||||
export interface MetricDetailsProps {
|
export interface MetricDetailsProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
metricName: string | null;
|
metricName: string;
|
||||||
isModalTimeSelection: boolean;
|
isModalTimeSelection: boolean;
|
||||||
openInspectModal?: (metricName: string) => void;
|
openInspectModal?: (metricName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HighlightsProps {
|
||||||
|
metricName: string;
|
||||||
|
}
|
||||||
export interface DashboardsAndAlertsPopoverProps {
|
export interface DashboardsAndAlertsPopoverProps {
|
||||||
dashboards: MetricDetailsDashboard[] | null;
|
metricName: string;
|
||||||
alerts: MetricDetailsAlert[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataProps {
|
export interface MetadataProps {
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metadata: MetricDetails['metadata'] | undefined;
|
metadata: MetricMetadata | null;
|
||||||
refetchMetricDetails: () => void;
|
isErrorMetricMetadata: boolean;
|
||||||
|
isLoadingMetricMetadata: boolean;
|
||||||
|
refetchMetricMetadata: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllAttributesProps {
|
export interface AllAttributesProps {
|
||||||
attributes: MetricDetailsAttribute[];
|
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metricType: MetricType | undefined;
|
metricType: MetrictypesTypeDTO | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllAttributesValueProps {
|
export interface AllAttributesValueProps {
|
||||||
@@ -36,3 +41,38 @@ 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,12 +1,23 @@
|
|||||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import {
|
||||||
|
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';
|
||||||
|
|
||||||
export function formatTimestampToReadableDate(timestamp: string): string {
|
import { MetricMetadata, MetricMetadataFormState } from './types';
|
||||||
|
|
||||||
|
export function formatTimestampToReadableDate(
|
||||||
|
timestamp: number | string | undefined,
|
||||||
|
): string {
|
||||||
|
if (!timestamp) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
const date = new Date(timestamp);
|
const 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);
|
||||||
@@ -39,7 +50,10 @@ export function formatTimestampToReadableDate(timestamp: string): string {
|
|||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumberToCompactFormat(num: number): string {
|
export function formatNumberToCompactFormat(num: number | undefined): string {
|
||||||
|
if (!num) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
@@ -47,27 +61,30 @@ export function formatNumberToCompactFormat(num: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function determineIsMonotonic(
|
export function determineIsMonotonic(
|
||||||
metricType: MetricType,
|
metricType: MetrictypesTypeDTO,
|
||||||
temporality?: Temporality,
|
temporality?: MetrictypesTemporalityDTO,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
if (
|
||||||
metricType === MetricType.HISTOGRAM ||
|
metricType === MetrictypesTypeDTO.histogram ||
|
||||||
metricType === MetricType.EXPONENTIAL_HISTOGRAM
|
metricType === MetrictypesTypeDTO.exponentialhistogram
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
if (
|
||||||
|
metricType === MetrictypesTypeDTO.gauge ||
|
||||||
|
metricType === MetrictypesTypeDTO.summary
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (metricType === MetricType.SUM) {
|
if (metricType === MetrictypesTypeDTO.sum) {
|
||||||
return temporality === Temporality.CUMULATIVE;
|
return temporality === MetrictypesTemporalityDTO.cumulative;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricDetailsQuery(
|
export function getMetricDetailsQuery(
|
||||||
metricName: string,
|
metricName: string,
|
||||||
metricType: MetricType | undefined,
|
metricType: MetrictypesTypeDTO | undefined,
|
||||||
filter?: { key: string; value: string },
|
filter?: { key: string; value: string },
|
||||||
groupBy?: string,
|
groupBy?: string,
|
||||||
): Query {
|
): Query {
|
||||||
@@ -75,23 +92,23 @@ export function getMetricDetailsQuery(
|
|||||||
let spaceAggregation;
|
let spaceAggregation;
|
||||||
let aggregateOperator;
|
let aggregateOperator;
|
||||||
switch (metricType) {
|
switch (metricType) {
|
||||||
case MetricType.SUM:
|
case MetrictypesTypeDTO.sum:
|
||||||
timeAggregation = 'rate';
|
timeAggregation = 'rate';
|
||||||
spaceAggregation = 'sum';
|
spaceAggregation = 'sum';
|
||||||
aggregateOperator = 'rate';
|
aggregateOperator = 'rate';
|
||||||
break;
|
break;
|
||||||
case MetricType.GAUGE:
|
case MetrictypesTypeDTO.gauge:
|
||||||
timeAggregation = 'avg';
|
timeAggregation = 'avg';
|
||||||
spaceAggregation = 'avg';
|
spaceAggregation = 'avg';
|
||||||
aggregateOperator = 'avg';
|
aggregateOperator = 'avg';
|
||||||
break;
|
break;
|
||||||
case MetricType.SUMMARY:
|
case MetrictypesTypeDTO.summary:
|
||||||
timeAggregation = 'noop';
|
timeAggregation = 'noop';
|
||||||
spaceAggregation = 'sum';
|
spaceAggregation = 'sum';
|
||||||
aggregateOperator = 'noop';
|
aggregateOperator = 'noop';
|
||||||
break;
|
break;
|
||||||
case MetricType.HISTOGRAM:
|
case MetrictypesTypeDTO.histogram:
|
||||||
case MetricType.EXPONENTIAL_HISTOGRAM:
|
case MetrictypesTypeDTO.exponentialhistogram:
|
||||||
timeAggregation = 'noop';
|
timeAggregation = 'noop';
|
||||||
spaceAggregation = 'p90';
|
spaceAggregation = 'p90';
|
||||||
aggregateOperator = 'noop';
|
aggregateOperator = 'noop';
|
||||||
@@ -160,3 +177,38 @@ export function getMetricDetailsQuery(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformMetricMetadata(
|
||||||
|
apiData: GetMetricMetadata200 | undefined,
|
||||||
|
): MetricMetadata | null {
|
||||||
|
if (!apiData || !apiData.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { type, description, unit, temporality, isMonotonic } = apiData.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
description,
|
||||||
|
unit,
|
||||||
|
temporality,
|
||||||
|
isMonotonic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformUpdateMetricMetadataRequest(
|
||||||
|
metricName: string,
|
||||||
|
metricMetadata: MetricMetadataFormState,
|
||||||
|
): UpdateMetricMetadataMutationBody {
|
||||||
|
return {
|
||||||
|
metricName: metricName,
|
||||||
|
type: metricMetadata.type,
|
||||||
|
description: metricMetadata.description,
|
||||||
|
unit: metricMetadata.unit,
|
||||||
|
temporality:
|
||||||
|
metricMetadata.temporality ?? MetrictypesTemporalityDTO.unspecified,
|
||||||
|
isMonotonic: determineIsMonotonic(
|
||||||
|
metricMetadata.type,
|
||||||
|
metricMetadata.temporality,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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,
|
||||||
@@ -10,22 +9,24 @@ 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({
|
||||||
queryFilters,
|
queryFilterExpression,
|
||||||
|
onFilterChange,
|
||||||
}: {
|
}: {
|
||||||
queryFilters: TagFilter;
|
queryFilterExpression: Filter;
|
||||||
|
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>('');
|
||||||
@@ -67,9 +68,12 @@ 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.items,
|
...queryFilters,
|
||||||
{
|
{
|
||||||
id: 'metric_name',
|
id: 'metric_name',
|
||||||
op: 'CONTAINS',
|
op: 'CONTAINS',
|
||||||
@@ -83,13 +87,11 @@ function MetricNameSearch({
|
|||||||
],
|
],
|
||||||
op: 'and',
|
op: 'and',
|
||||||
};
|
};
|
||||||
setSearchParams({
|
const newFilterExpression = convertFiltersToExpression(newFilters);
|
||||||
...Object.fromEntries(searchParams.entries()),
|
onFilterChange(newFilterExpression.expression);
|
||||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
|
||||||
});
|
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
},
|
},
|
||||||
[queryFilters.items, setSearchParams, searchParams],
|
[queryFilterExpression, onFilterChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const metricNameFilterValues = useMemo(
|
const metricNameFilterValues = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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,23 +1,19 @@
|
|||||||
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 { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
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 {
|
import { METRIC_TYPE_VIEW_VALUES_MAP } from './constants';
|
||||||
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(
|
||||||
@@ -26,9 +22,9 @@ function MetricTypeSearch({
|
|||||||
key: 'all',
|
key: 'all',
|
||||||
value: 'All',
|
value: 'All',
|
||||||
},
|
},
|
||||||
...Object.keys(METRIC_TYPE_LABEL_MAP).map((key) => ({
|
...Object.keys(METRIC_TYPE_VIEW_VALUES_MAP).map((key) => ({
|
||||||
key: METRIC_TYPE_VALUES_MAP[key as MetricType],
|
key: METRIC_TYPE_VIEW_VALUES_MAP[key as MetrictypesTypeDTO],
|
||||||
value: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
value: METRIC_TYPE_VIEW_VALUES_MAP[key as MetrictypesTypeDTO],
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
@@ -36,16 +32,17 @@ function MetricTypeSearch({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(selectedMetricType: string): void => {
|
(selectedMetricType: string): void => {
|
||||||
|
let newFilters;
|
||||||
if (selectedMetricType !== 'all') {
|
if (selectedMetricType !== 'all') {
|
||||||
const newFilters = {
|
newFilters = {
|
||||||
items: [
|
items: [
|
||||||
...queryFilters.items,
|
...queryFilters.items,
|
||||||
{
|
{
|
||||||
id: 'metric_type',
|
id: 'type',
|
||||||
op: '=',
|
op: '=',
|
||||||
key: {
|
key: {
|
||||||
id: 'metric_type',
|
id: 'type',
|
||||||
key: 'metric_type',
|
key: 'type',
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
},
|
},
|
||||||
value: selectedMetricType,
|
value: selectedMetricType,
|
||||||
@@ -53,23 +50,17 @@ function MetricTypeSearch({
|
|||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
};
|
};
|
||||||
setSearchParams({
|
|
||||||
...Object.fromEntries(searchParams.entries()),
|
|
||||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const newFilters = {
|
newFilters = {
|
||||||
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
|
items: queryFilters.items.filter((item) => item.id !== '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, setSearchParams, searchParams],
|
[queryFilters.items, onFilterChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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,27 +1,58 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||||
|
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { HardHat, Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { MetricsSearchProps } from './types';
|
import { MetricsSearchProps } from './types';
|
||||||
|
|
||||||
function MetricsSearch({ query, onChange }: MetricsSearchProps): JSX.Element {
|
function MetricsSearch({
|
||||||
|
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 className="qb-search-container">
|
<div data-testid="qb-search-container" 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>
|
||||||
<QueryBuilderSearch
|
<QuerySearch
|
||||||
query={query}
|
onChange={handleOnChange}
|
||||||
onChange={onChange}
|
dataSource={DataSource.METRICS}
|
||||||
suffixIcon={<HardHat size={16} />}
|
queryData={{
|
||||||
isMetricsExplorer
|
...query,
|
||||||
|
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,6 +9,7 @@ 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';
|
||||||
@@ -24,7 +25,8 @@ function MetricsTable({
|
|||||||
setOrderBy,
|
setOrderBy,
|
||||||
totalCount,
|
totalCount,
|
||||||
openMetricDetails,
|
openMetricDetails,
|
||||||
queryFilters,
|
queryFilterExpression,
|
||||||
|
onFilterChange,
|
||||||
}: MetricsTableProps): JSX.Element {
|
}: MetricsTableProps): JSX.Element {
|
||||||
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
||||||
(
|
(
|
||||||
@@ -36,13 +38,20 @@ function MetricsTable({
|
|||||||
): void => {
|
): void => {
|
||||||
if ('field' in sorter && sorter.order) {
|
if ('field' in sorter && sorter.order) {
|
||||||
setOrderBy({
|
setOrderBy({
|
||||||
columnName: sorter.field as string,
|
key: {
|
||||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
name: sorter.field as string,
|
||||||
|
},
|
||||||
|
direction:
|
||||||
|
sorter.order === 'ascend'
|
||||||
|
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||||
|
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setOrderBy({
|
setOrderBy({
|
||||||
columnName: 'samples',
|
key: {
|
||||||
order: 'desc',
|
name: 'samples',
|
||||||
|
},
|
||||||
|
direction: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,19 +60,17 @@ function MetricsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-table-container">
|
<div className="metrics-table-container">
|
||||||
{!isError && !isLoading && (
|
<div className="metrics-table-title" data-testid="metrics-table-title">
|
||||||
<div className="metrics-table-title" data-testid="metrics-table-title">
|
<Typography.Title level={4} className="metrics-table-title">
|
||||||
<Typography.Title level={4} className="metrics-table-title">
|
List View
|
||||||
List View
|
</Typography.Title>
|
||||||
</Typography.Title>
|
<Tooltip
|
||||||
<Tooltip
|
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."
|
||||||
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"
|
||||||
placement="right"
|
>
|
||||||
>
|
<Info size={16} />
|
||||||
<Info size={16} />
|
</Tooltip>
|
||||||
</Tooltip>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Table
|
<Table
|
||||||
loading={{
|
loading={{
|
||||||
spinning: isLoading,
|
spinning: isLoading,
|
||||||
@@ -75,7 +82,7 @@ function MetricsTable({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
columns={getMetricsTableColumns(queryFilters)}
|
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: isLoading ? null : (
|
emptyText: isLoading ? null : (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, 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 { stratify, treemapBinary } from 'd3-hierarchy';
|
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import { HierarchyNode, stratify, treemapBinary } from 'd3-hierarchy';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,21 +13,24 @@ import {
|
|||||||
TREEMAP_SQUARE_PADDING,
|
TREEMAP_SQUARE_PADDING,
|
||||||
TREEMAP_VIEW_OPTIONS,
|
TREEMAP_VIEW_OPTIONS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { MetricsTreemapProps, TreemapTile, TreemapViewType } from './types';
|
import {
|
||||||
|
MetricsTreemapInternalProps,
|
||||||
|
MetricsTreemapProps,
|
||||||
|
TreemapTile,
|
||||||
|
} from './types';
|
||||||
import {
|
import {
|
||||||
getTreemapTileStyle,
|
getTreemapTileStyle,
|
||||||
getTreemapTileTextStyle,
|
getTreemapTileTextStyle,
|
||||||
transformTreemapData,
|
transformTreemapData,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function MetricsTreemap({
|
function MetricsTreemapInternal({
|
||||||
viewType,
|
|
||||||
data,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
|
data,
|
||||||
|
viewType,
|
||||||
openMetricDetails,
|
openMetricDetails,
|
||||||
setHeatmapView,
|
}: MetricsTreemapInternalProps): JSX.Element {
|
||||||
}: MetricsTreemapProps): JSX.Element {
|
|
||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
|
||||||
const treemapWidth = useMemo(
|
const treemapWidth = useMemo(
|
||||||
@@ -40,9 +44,9 @@ function MetricsTreemap({
|
|||||||
|
|
||||||
const treemapData = useMemo(() => {
|
const treemapData = useMemo(() => {
|
||||||
const extracedTreemapData =
|
const extracedTreemapData =
|
||||||
(viewType === TreemapViewType.TIMESERIES
|
(viewType === MetricsexplorertypesTreemapModeDTO.timeseries
|
||||||
? data?.data?.[TreemapViewType.TIMESERIES]
|
? data?.timeseries
|
||||||
: data?.data?.[TreemapViewType.SAMPLES]) || [];
|
: data?.samples) || [];
|
||||||
return transformTreemapData(extracedTreemapData, viewType);
|
return transformTreemapData(extracedTreemapData, viewType);
|
||||||
}, [data, viewType]);
|
}, [data, viewType]);
|
||||||
|
|
||||||
@@ -54,41 +58,126 @@ function MetricsTreemap({
|
|||||||
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
|
<Skeleton style={treemapStylesWithoutPadding} active />
|
||||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT + 55 }}
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isError) {
|
||||||
!data ||
|
|
||||||
!data.data ||
|
|
||||||
(data?.status === 'success' && !data?.data?.[viewType])
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<Empty
|
|
||||||
description="No metrics found"
|
|
||||||
data-testid="metrics-treemap-empty-state"
|
|
||||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.status === 'error' || isError) {
|
|
||||||
return (
|
return (
|
||||||
<Empty
|
<Empty
|
||||||
description="Error fetching metrics. If the problem persists, please contact support."
|
description="Error fetching metrics. If the problem persists, please contact support."
|
||||||
data-testid="metrics-treemap-error-state"
|
data-testid="metrics-treemap-error-state"
|
||||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
style={treemapStylesWithPadding}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data || !data?.[viewType]?.length) {
|
||||||
|
return (
|
||||||
|
<Empty
|
||||||
|
description="No metrics found"
|
||||||
|
data-testid="metrics-treemap-empty-state"
|
||||||
|
style={treemapStylesWithPadding}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={treemapWidth} height={TREEMAP_HEIGHT} className="metrics-treemap">
|
||||||
|
<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={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"
|
||||||
@@ -108,72 +197,16 @@ function MetricsTreemap({
|
|||||||
options={TREEMAP_VIEW_OPTIONS}
|
options={TREEMAP_VIEW_OPTIONS}
|
||||||
value={viewType}
|
value={viewType}
|
||||||
onChange={setHeatmapView}
|
onChange={setHeatmapView}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<MetricsTreemapInternal
|
||||||
width={treemapWidth}
|
isLoading={isLoading}
|
||||||
height={TREEMAP_HEIGHT}
|
isError={isError}
|
||||||
className="metrics-treemap"
|
data={data}
|
||||||
>
|
viewType={viewType}
|
||||||
<rect
|
openMetricDetails={openMetricDetails}
|
||||||
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,6 +38,7 @@
|
|||||||
.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,11 +4,24 @@ 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 { initialQueriesMap } from 'constants/queryBuilder';
|
import {
|
||||||
|
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 { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
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';
|
||||||
@@ -23,32 +36,38 @@ 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 { OrderByPayload, TreemapViewType } from './types';
|
import { convertNanoToMilliseconds, formatDataForMetricsTable } from './utils';
|
||||||
import {
|
|
||||||
convertNanoToMilliseconds,
|
|
||||||
formatDataForMetricsTable,
|
|
||||||
getMetricsListQuery,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
import './Summary.styles.scss';
|
import './Summary.styles.scss';
|
||||||
|
|
||||||
const DEFAULT_ORDER_BY: OrderByPayload = {
|
const DEFAULT_ORDER_BY: Querybuildertypesv5OrderByDTO = {
|
||||||
columnName: 'samples',
|
key: {
|
||||||
order: 'desc',
|
name: 'samples',
|
||||||
|
},
|
||||||
|
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<OrderByPayload>(DEFAULT_ORDER_BY);
|
const [orderBy, setOrderBy] = useState<Querybuildertypesv5OrderByDTO>(
|
||||||
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
DEFAULT_ORDER_BY,
|
||||||
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(
|
||||||
@@ -65,16 +84,10 @@ function Summary(): JSX.Element {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryFilters: TagFilter = useMemo(() => {
|
const [
|
||||||
const encodedFilters = searchParams.get(SUMMARY_FILTERS_KEY);
|
currentQueryFilterExpression,
|
||||||
if (encodedFilters) {
|
setCurrentQueryFilterExpression,
|
||||||
return JSON.parse(encodedFilters);
|
] = useState<string>(query?.filter?.expression || '');
|
||||||
}
|
|
||||||
return {
|
|
||||||
items: [],
|
|
||||||
op: 'AND',
|
|
||||||
};
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent(MetricsExplorerEvents.TabChanged, {
|
logEvent(MetricsExplorerEvents.TabChanged, {
|
||||||
@@ -87,105 +100,101 @@ function Summary(): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// This is used to avoid the filters from being serialized with the id
|
const queryFilterExpression = useMemo(() => {
|
||||||
const queryFiltersWithoutId = useMemo(() => {
|
const filters = query.filters || { items: [], op: 'AND' };
|
||||||
const filtersWithoutId = {
|
return convertFiltersToExpression(filters);
|
||||||
...queryFilters,
|
}, [query.filters]);
|
||||||
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
|
|
||||||
};
|
|
||||||
return JSON.stringify(filtersWithoutId);
|
|
||||||
}, [queryFilters]);
|
|
||||||
|
|
||||||
const metricsListQuery = useMemo(() => {
|
const metricsListQuery: MetricsexplorertypesStatsRequestDTO = useMemo(() => {
|
||||||
const baseQuery = getMetricsListQuery();
|
|
||||||
return {
|
return {
|
||||||
...baseQuery,
|
|
||||||
limit: pageSize,
|
|
||||||
offset: (currentPage - 1) * pageSize,
|
|
||||||
filters: queryFilters,
|
|
||||||
start: convertNanoToMilliseconds(minTime),
|
start: convertNanoToMilliseconds(minTime),
|
||||||
end: convertNanoToMilliseconds(maxTime),
|
end: convertNanoToMilliseconds(maxTime),
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
orderBy,
|
orderBy,
|
||||||
|
filter: {
|
||||||
|
expression: queryFilterExpression.expression,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}, [queryFilters, minTime, maxTime, orderBy, pageSize, currentPage]);
|
}, [
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
orderBy,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
queryFilterExpression.expression,
|
||||||
|
]);
|
||||||
|
|
||||||
const metricsTreemapQuery = useMemo(
|
const metricsTreemapQuery: MetricsexplorertypesTreemapRequestDTO = 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,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[queryFilters, heatmapView, minTime, maxTime],
|
[heatmapView, minTime, maxTime, queryFilterExpression.expression],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: metricsData,
|
data: metricsData,
|
||||||
isLoading: isMetricsLoading,
|
mutate: getMetricsStats,
|
||||||
isFetching: isMetricsFetching,
|
isLoading: isGetMetricsStatsLoading,
|
||||||
isError: isMetricsError,
|
isError: isGetMetricsStatsError,
|
||||||
} = useGetMetricsList(metricsListQuery, {
|
} = useGetMetricsStats();
|
||||||
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,
|
||||||
isLoading: isTreeMapLoading,
|
mutate: getMetricsTreemap,
|
||||||
isFetching: isTreeMapFetching,
|
isLoading: isGetMetricsTreemapLoading,
|
||||||
isError: isTreeMapError,
|
isError: isGetMetricsTreemapError,
|
||||||
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
} = useGetMetricsTreemap();
|
||||||
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
|
||||||
queryKey: [
|
|
||||||
'metricsTreemap',
|
|
||||||
queryFiltersWithoutId,
|
|
||||||
heatmapView,
|
|
||||||
minTime,
|
|
||||||
maxTime,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const isProportionViewError = useMemo(
|
useEffect(() => {
|
||||||
() => isTreeMapError || treeMapData?.statusCode !== 200,
|
getMetricsStats({
|
||||||
[isTreeMapError, treeMapData],
|
data: metricsListQuery,
|
||||||
);
|
});
|
||||||
|
}, [metricsListQuery, getMetricsStats]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMetricsTreemap({
|
||||||
|
data: metricsTreemapQuery,
|
||||||
|
});
|
||||||
|
}, [metricsTreemapQuery, getMetricsTreemap]);
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(value: TagFilter) => {
|
(expression: string) => {
|
||||||
setSearchParams({
|
const newFilters: TagFilter = {
|
||||||
...Object.fromEntries(searchParams.entries()),
|
items: convertExpressionToFilters(expression),
|
||||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(value),
|
op: 'AND',
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
filters: newFilters,
|
||||||
|
filter: {
|
||||||
|
expression,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
setCurrentQueryFilterExpression(expression);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
if (value.items.length > 0) {
|
if (expression) {
|
||||||
logEvent(MetricsExplorerEvents.FilterApplied, {
|
logEvent(MetricsExplorerEvents.FilterApplied, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSearchParams, searchParams],
|
[currentQuery, redirectWithQueryBuilderData],
|
||||||
);
|
|
||||||
|
|
||||||
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 => {
|
||||||
@@ -202,7 +211,7 @@ function Summary(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formattedMetricsData = useMemo(
|
const formattedMetricsData = useMemo(
|
||||||
() => formatDataForMetricsTable(metricsData?.payload?.data?.metrics || []),
|
() => formatDataForMetricsTable(metricsData?.data.metrics || []),
|
||||||
[metricsData],
|
[metricsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -254,7 +263,9 @@ function Summary(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetHeatmapView = (view: TreemapViewType): void => {
|
const handleSetHeatmapView = (
|
||||||
|
view: MetricsexplorertypesTreemapModeDTO,
|
||||||
|
): void => {
|
||||||
setHeatmapView(view);
|
setHeatmapView(view);
|
||||||
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
|
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
@@ -262,68 +273,67 @@ function Summary(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetOrderBy = (orderBy: OrderByPayload): void => {
|
const handleSetOrderBy = (orderBy: Querybuildertypesv5OrderByDTO): void => {
|
||||||
setOrderBy(orderBy);
|
setOrderBy(orderBy);
|
||||||
logEvent(MetricsExplorerEvents.OrderByApplied, {
|
logEvent(MetricsExplorerEvents.OrderByApplied, {
|
||||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||||
[MetricsExplorerEventKeys.ColumnName]: orderBy.columnName,
|
[MetricsExplorerEventKeys.ColumnName]: orderBy.key?.name,
|
||||||
[MetricsExplorerEventKeys.Order]: orderBy.order,
|
[MetricsExplorerEventKeys.Order]: orderBy.direction,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMetricsListDataEmpty = useMemo(
|
const isMetricsListDataEmpty =
|
||||||
() =>
|
formattedMetricsData.length === 0 && !isGetMetricsStatsLoading;
|
||||||
formattedMetricsData.length === 0 && !isMetricsLoading && !isMetricsFetching,
|
|
||||||
[formattedMetricsData, isMetricsLoading, isMetricsFetching],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isMetricsTreeMapDataEmpty = useMemo(
|
const isMetricsTreeMapDataEmpty =
|
||||||
() =>
|
!treeMapData?.data[heatmapView]?.length && !isGetMetricsTreemapLoading;
|
||||||
!treeMapData?.payload?.data[heatmapView]?.length &&
|
|
||||||
!isTreeMapLoading &&
|
const showFullScreenLoading =
|
||||||
!isTreeMapFetching,
|
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
|
||||||
[
|
formattedMetricsData.length === 0 &&
|
||||||
treeMapData?.payload?.data,
|
!treeMapData?.data[heatmapView]?.length;
|
||||||
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 query={searchQuery} onChange={handleFilterChange} />
|
<MetricsSearch
|
||||||
{isMetricsLoading || isTreeMapLoading ? (
|
query={query}
|
||||||
|
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?.payload}
|
data={treeMapData?.data}
|
||||||
isLoading={isTreeMapLoading || isTreeMapFetching}
|
isLoading={isGetMetricsTreemapLoading}
|
||||||
isError={isProportionViewError}
|
isError={isGetMetricsTreemapError}
|
||||||
viewType={heatmapView}
|
viewType={heatmapView}
|
||||||
openMetricDetails={openMetricDetails}
|
openMetricDetails={openMetricDetails}
|
||||||
setHeatmapView={handleSetHeatmapView}
|
setHeatmapView={handleSetHeatmapView}
|
||||||
/>
|
/>
|
||||||
<MetricsTable
|
<MetricsTable
|
||||||
isLoading={isMetricsLoading || isMetricsFetching}
|
isLoading={isGetMetricsStatsLoading}
|
||||||
isError={isListViewError}
|
isError={isGetMetricsStatsError}
|
||||||
data={formattedMetricsData}
|
data={formattedMetricsData}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onPaginationChange={onPaginationChange}
|
onPaginationChange={onPaginationChange}
|
||||||
setOrderBy={handleSetOrderBy}
|
setOrderBy={handleSetOrderBy}
|
||||||
totalCount={metricsData?.payload?.data?.total || 0}
|
totalCount={metricsData?.data.total || 0}
|
||||||
openMetricDetails={openMetricDetails}
|
openMetricDetails={openMetricDetails}
|
||||||
queryFilters={queryFilters}
|
queryFilterExpression={queryFilterExpression}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isMetricDetailsOpen && (
|
{isMetricDetailsOpen && selectedMetricName && (
|
||||||
<MetricDetails
|
<MetricDetails
|
||||||
isOpen={isMetricDetailsOpen}
|
isOpen={isMetricDetailsOpen}
|
||||||
onClose={closeMetricDetails}
|
onClose={closeMetricDetails}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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,9 +30,8 @@ const mockData: MetricsListItemRowData[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockQueryFilters: TagFilter = {
|
const mockQueryFilterExpression: Filter = {
|
||||||
items: [],
|
expression: '',
|
||||||
op: 'AND',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('react-router-dom-v5-compat', () => {
|
jest.mock('react-router-dom-v5-compat', () => {
|
||||||
@@ -82,7 +81,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -106,8 +106,9 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
isLoading
|
isLoading
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -130,7 +131,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -158,7 +160,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -187,7 +190,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={jest.fn()}
|
setOrderBy={jest.fn()}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={mockOpenMetricDetails}
|
openMetricDetails={mockOpenMetricDetails}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -212,7 +216,8 @@ describe('MetricsTable', () => {
|
|||||||
setOrderBy={mockSetOrderBy}
|
setOrderBy={mockSetOrderBy}
|
||||||
totalCount={2}
|
totalCount={2}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
queryFilters={mockQueryFilters}
|
queryFilterExpression={mockQueryFilterExpression}
|
||||||
|
onFilterChange={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -222,8 +227,10 @@ describe('MetricsTable', () => {
|
|||||||
fireEvent.click(samplesHeader);
|
fireEvent.click(samplesHeader);
|
||||||
|
|
||||||
expect(mockSetOrderBy).toHaveBeenCalledWith({
|
expect(mockSetOrderBy).toHaveBeenCalledWith({
|
||||||
columnName: 'samples',
|
key: {
|
||||||
order: 'asc',
|
name: 'samples',
|
||||||
|
},
|
||||||
|
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 = [
|
||||||
{
|
{
|
||||||
metric_name: 'Metric 1',
|
metricName: 'Metric 1',
|
||||||
percentage: 0.5,
|
percentage: 0.5,
|
||||||
total_value: 15,
|
totalValue: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metric_name: 'Metric 2',
|
metricName: 'Metric 2',
|
||||||
percentage: 0.6,
|
percentage: 0.6,
|
||||||
total_value: 10,
|
totalValue: 10,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -47,14 +47,11 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError={false}
|
isError={false}
|
||||||
data={{
|
data={{
|
||||||
status: 'success',
|
timeseries: [mockData[0]],
|
||||||
data: {
|
samples: [mockData[1]],
|
||||||
timeseries: [mockData[0]],
|
|
||||||
samples: [mockData[1]],
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={TreemapViewType.SAMPLES}
|
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -72,14 +69,11 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading
|
isLoading
|
||||||
isError={false}
|
isError={false}
|
||||||
data={{
|
data={{
|
||||||
status: 'success',
|
timeseries: [mockData[0]],
|
||||||
data: {
|
samples: [mockData[1]],
|
||||||
timeseries: [mockData[0]],
|
|
||||||
samples: [mockData[1]],
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={TreemapViewType.SAMPLES}
|
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -99,14 +93,11 @@ describe('MetricsTreemap', () => {
|
|||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError
|
isError
|
||||||
data={{
|
data={{
|
||||||
status: 'success',
|
timeseries: [mockData[0]],
|
||||||
data: {
|
samples: [mockData[1]],
|
||||||
timeseries: [mockData[0]],
|
|
||||||
samples: [mockData[1]],
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={TreemapViewType.SAMPLES}
|
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -128,9 +119,9 @@ describe('MetricsTreemap', () => {
|
|||||||
<MetricsTreemap
|
<MetricsTreemap
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError={false}
|
isError={false}
|
||||||
data={null}
|
data={undefined}
|
||||||
openMetricDetails={jest.fn()}
|
openMetricDetails={jest.fn()}
|
||||||
viewType={TreemapViewType.SAMPLES}
|
viewType={MetricsexplorertypesTreemapModeDTO.samples}
|
||||||
setHeatmapView={jest.fn()}
|
setHeatmapView={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,109 +1,81 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
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 {
|
import { formatDataForMetricsTable, getMetricsTableColumns } from '../utils';
|
||||||
formatDataForMetricsTable,
|
|
||||||
getMetricsTableColumns,
|
const mockQueryExpression: Filter = {
|
||||||
MetricTypeRenderer,
|
expression: '',
|
||||||
} 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(getMetricsTableColumns(mockQueryFilters)).toHaveLength(6);
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange),
|
||||||
|
).toHaveLength(6);
|
||||||
|
|
||||||
// Metric Name column
|
// Metric Name column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[0].dataIndex).toBe(
|
expect(
|
||||||
'metric_name',
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].dataIndex,
|
||||||
);
|
).toBe('metric_name');
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[0].width).toBe(400);
|
expect(
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[0].sorter).toBe(false);
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].width,
|
||||||
|
).toBe(400);
|
||||||
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[0].sorter,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
// Description column
|
// Description column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[1].dataIndex).toBe(
|
expect(
|
||||||
'description',
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].dataIndex,
|
||||||
);
|
).toBe('description');
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[1].width).toBe(400);
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[1].width,
|
||||||
|
).toBe(400);
|
||||||
|
|
||||||
// Type column
|
// Type column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[2].dataIndex).toBe(
|
expect(
|
||||||
'metric_type',
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].dataIndex,
|
||||||
);
|
).toBe('metric_type');
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[2].width).toBe(150);
|
expect(
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[2].sorter).toBe(false);
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].width,
|
||||||
|
).toBe(150);
|
||||||
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[2].sorter,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
// Unit column
|
// Unit column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[3].dataIndex).toBe('unit');
|
expect(
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[3].width).toBe(150);
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].dataIndex,
|
||||||
|
).toBe('unit');
|
||||||
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[3].width,
|
||||||
|
).toBe(150);
|
||||||
|
|
||||||
// Samples column
|
// Samples column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[4].dataIndex).toBe(
|
expect(
|
||||||
TreemapViewType.SAMPLES,
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].dataIndex,
|
||||||
);
|
).toBe(TreemapViewType.SAMPLES);
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[4].width).toBe(150);
|
expect(
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[4].sorter).toBe(true);
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].width,
|
||||||
|
).toBe(150);
|
||||||
|
expect(
|
||||||
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[4].sorter,
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
// Time Series column
|
// Time Series column
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[5].dataIndex).toBe(
|
expect(
|
||||||
TreemapViewType.TIMESERIES,
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].dataIndex,
|
||||||
);
|
).toBe(TreemapViewType.TIMESERIES);
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[5].width).toBe(150);
|
expect(
|
||||||
expect(getMetricsTableColumns(mockQueryFilters)[5].sorter).toBe(true);
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].width,
|
||||||
});
|
).toBe(150);
|
||||||
|
expect(
|
||||||
describe('MetricTypeRenderer', () => {
|
getMetricsTableColumns(mockQueryExpression, mockOnChange)[5].sorter,
|
||||||
it('should render correct icon and color for each metric type', () => {
|
).toBe(true);
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,12 +83,12 @@ describe('formatDataForMetricsTable', () => {
|
|||||||
it('should format metrics data correctly', () => {
|
it('should format metrics data correctly', () => {
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
metric_name: 'test_metric',
|
metricName: 'test_metric',
|
||||||
description: 'Test description',
|
description: 'Test description',
|
||||||
type: MetricType.GAUGE,
|
type: MetrictypesTypeDTO.gauge,
|
||||||
unit: 'bytes',
|
unit: 'bytes',
|
||||||
[TreemapViewType.SAMPLES]: 1000,
|
samples: 1000,
|
||||||
[TreemapViewType.TIMESERIES]: 2000,
|
timeseries: 2000,
|
||||||
lastReceived: '2023-01-01T00:00:00Z',
|
lastReceived: '2023-01-01T00:00:00Z',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -163,12 +135,12 @@ describe('formatDataForMetricsTable', () => {
|
|||||||
it('should handle empty/null values', () => {
|
it('should handle empty/null values', () => {
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
metric_name: '',
|
metricName: '',
|
||||||
description: '',
|
description: '',
|
||||||
type: MetricType.GAUGE,
|
type: MetrictypesTypeDTO.gauge,
|
||||||
unit: '',
|
unit: '',
|
||||||
[TreemapViewType.SAMPLES]: 0,
|
samples: 0,
|
||||||
[TreemapViewType.TIMESERIES]: 0,
|
timeseries: 0,
|
||||||
lastReceived: '2023-01-01T00:00:00Z',
|
lastReceived: '2023-01-01T00:00:00Z',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
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: TreemapViewType;
|
value: MetricsexplorertypesTreemapModeDTO;
|
||||||
label: string;
|
label: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ value: TreemapViewType.TIMESERIES, label: 'Time Series' },
|
{ value: MetricsexplorertypesTreemapModeDTO.timeseries, label: 'Time Series' },
|
||||||
{ value: TreemapViewType.SAMPLES, label: 'Samples' },
|
{ value: MetricsexplorertypesTreemapModeDTO.samples, label: 'Samples' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TREEMAP_HEIGHT = 200;
|
export const TREEMAP_HEIGHT = 200;
|
||||||
@@ -17,6 +19,7 @@ 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',
|
||||||
@@ -25,7 +28,16 @@ export const METRIC_TYPE_LABEL_MAP = {
|
|||||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const METRIC_TYPE_VALUES_MAP = {
|
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||||
|
[MetrictypesTypeDTO.sum]: 'Sum',
|
||||||
|
[MetrictypesTypeDTO.gauge]: 'Gauge',
|
||||||
|
[MetrictypesTypeDTO.histogram]: 'Histogram',
|
||||||
|
[MetrictypesTypeDTO.summary]: 'Summary',
|
||||||
|
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(@amlannandy): To remove this once API migration is complete
|
||||||
|
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
|
||||||
[MetricType.SUM]: 'Sum',
|
[MetricType.SUM]: 'Sum',
|
||||||
[MetricType.GAUGE]: 'Gauge',
|
[MetricType.GAUGE]: 'Gauge',
|
||||||
[MetricType.HISTOGRAM]: 'Histogram',
|
[MetricType.HISTOGRAM]: 'Histogram',
|
||||||
@@ -33,7 +45,14 @@ export const METRIC_TYPE_VALUES_MAP = {
|
|||||||
[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,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
|
|
||||||
import {
|
import {
|
||||||
IBuilderQuery,
|
MetricsexplorertypesTreemapModeDTO,
|
||||||
TagFilter,
|
MetricsexplorertypesTreemapResponseDTO,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
Querybuildertypesv5OrderByDTO,
|
||||||
|
} 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;
|
||||||
@@ -12,24 +14,36 @@ 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: OrderByPayload) => void;
|
setOrderBy: (orderBy: Querybuildertypesv5OrderByDTO) => void;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||||
queryFilters: TagFilter;
|
queryFilterExpression: Filter;
|
||||||
|
onFilterChange: (expression: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsSearchProps {
|
export interface MetricsSearchProps {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (value: TagFilter) => void;
|
onChange: (expression: string) => void;
|
||||||
|
currentQueryFilterExpression: string;
|
||||||
|
setCurrentQueryFilterExpression: (expression: string) => void;
|
||||||
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsTreemapProps {
|
export interface MetricsTreemapProps {
|
||||||
data: MetricsTreeMapResponse | null | undefined;
|
data: MetricsexplorertypesTreemapResponseDTO | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
viewType: TreemapViewType;
|
viewType: MetricsexplorertypesTreemapModeDTO;
|
||||||
|
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,39 +1,31 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType } from 'antd/es/table';
|
||||||
import {
|
import {
|
||||||
MetricsListItemData,
|
MetricsexplorertypesStatDTO,
|
||||||
MetricsListPayload,
|
MetricsexplorertypesTreemapEntryDTO,
|
||||||
MetricType,
|
MetricsexplorertypesTreemapModeDTO,
|
||||||
} from 'api/metricsExplorer/getMetricsList';
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
import {
|
import { MetricsListPayload } from 'api/metricsExplorer/getMetricsList';
|
||||||
SamplesData,
|
import { Filter } from 'api/v5/v5';
|
||||||
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 MetricTypeSearch from './MetricTypeSearch';
|
import MetricTypeViewRenderer from './MetricTypeViewRenderer';
|
||||||
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
import { MetricsListItemRowData, TreemapTile } from './types';
|
||||||
|
|
||||||
export const getMetricsTableColumns = (
|
export const getMetricsTableColumns = (
|
||||||
queryFilters: TagFilter,
|
queryFilterExpression: Filter,
|
||||||
|
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 queryFilters={queryFilters} />
|
<MetricNameSearch
|
||||||
|
queryFilterExpression={queryFilterExpression}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
dataIndex: 'metric_name',
|
dataIndex: 'metric_name',
|
||||||
@@ -55,7 +47,10 @@ 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 queryFilters={queryFilters} />
|
{/* <MetricTypeSearch
|
||||||
|
queryFilters={queryFilters}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
dataIndex: 'metric_type',
|
dataIndex: 'metric_type',
|
||||||
@@ -69,13 +64,13 @@ export const getMetricsTableColumns = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SAMPLES',
|
title: 'SAMPLES',
|
||||||
dataIndex: TreemapViewType.SAMPLES,
|
dataIndex: MetricsexplorertypesTreemapModeDTO.samples,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'TIME SERIES',
|
title: 'TIME SERIES',
|
||||||
dataIndex: TreemapViewType.TIMESERIES,
|
dataIndex: MetricsexplorertypesTreemapModeDTO.timeseries,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
@@ -89,60 +84,6 @@ 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,
|
||||||
@@ -160,6 +101,9 @@ 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
|
||||||
@@ -182,13 +126,13 @@ export const formatNumberIntoHumanReadableFormat = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const formatDataForMetricsTable = (
|
export const formatDataForMetricsTable = (
|
||||||
data: MetricsListItemData[],
|
data: MetricsexplorertypesStatDTO[],
|
||||||
): MetricsListItemRowData[] =>
|
): MetricsListItemRowData[] =>
|
||||||
data.map((metric) => ({
|
data.map((metric) => ({
|
||||||
key: metric.metric_name,
|
key: metric.metricName,
|
||||||
metric_name: (
|
metric_name: (
|
||||||
<ValidateRowValueWrapper value={metric.metric_name}>
|
<ValidateRowValueWrapper value={metric.metricName}>
|
||||||
<Tooltip title={metric.metric_name}>{metric.metric_name}</Tooltip>
|
<Tooltip title={metric.metricName}>{metric.metricName}</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
description: (
|
description: (
|
||||||
@@ -198,39 +142,54 @@ export const formatDataForMetricsTable = (
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
metric_type: <MetricTypeRenderer type={metric.type} />,
|
metric_type: <MetricTypeViewRenderer type={metric.type} />,
|
||||||
unit: (
|
unit: (
|
||||||
<ValidateRowValueWrapper value={getUniversalNameFromMetricUnit(metric.unit)}>
|
<ValidateRowValueWrapper value={getUniversalNameFromMetricUnit(metric.unit)}>
|
||||||
{getUniversalNameFromMetricUnit(metric.unit)}
|
{getUniversalNameFromMetricUnit(metric.unit)}
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[TreemapViewType.SAMPLES]: (
|
[MetricsexplorertypesTreemapModeDTO.samples]: (
|
||||||
<ValidateRowValueWrapper value={metric[TreemapViewType.SAMPLES]}>
|
<ValidateRowValueWrapper
|
||||||
<Tooltip title={metric[TreemapViewType.SAMPLES].toLocaleString()}>
|
value={metric[MetricsexplorertypesTreemapModeDTO.samples]}
|
||||||
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.SAMPLES])}
|
>
|
||||||
|
<Tooltip
|
||||||
|
title={metric[MetricsexplorertypesTreemapModeDTO.samples].toLocaleString()}
|
||||||
|
>
|
||||||
|
{formatNumberIntoHumanReadableFormat(
|
||||||
|
metric[MetricsexplorertypesTreemapModeDTO.samples],
|
||||||
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[TreemapViewType.TIMESERIES]: (
|
[MetricsexplorertypesTreemapModeDTO.timeseries]: (
|
||||||
<ValidateRowValueWrapper value={metric[TreemapViewType.TIMESERIES]}>
|
<ValidateRowValueWrapper
|
||||||
<Tooltip title={metric[TreemapViewType.TIMESERIES].toLocaleString()}>
|
value={metric[MetricsexplorertypesTreemapModeDTO.timeseries]}
|
||||||
{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: TimeseriesData[] | SamplesData[],
|
data: MetricsexplorertypesTreemapEntryDTO[],
|
||||||
viewType: TreemapViewType,
|
viewType: MetricsexplorertypesTreemapModeDTO,
|
||||||
): TreemapTile[] => {
|
): TreemapTile[] => {
|
||||||
const totalSize = (data as (TimeseriesData | SamplesData)[]).reduce(
|
const totalSize = data.reduce(
|
||||||
(acc: number, item: TimeseriesData | SamplesData) => acc + item.percentage,
|
(acc: number, item: MetricsexplorertypesTreemapEntryDTO) =>
|
||||||
|
acc + item.percentage,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const children = data.map((item) => ({
|
const children = data.map((item) => ({
|
||||||
id: item.metric_name,
|
id: item.metricName,
|
||||||
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,
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { ColorPickerProps } from 'antd';
|
||||||
|
import { Color } from 'antd/es/color-picker';
|
||||||
|
import { render, screen, userEvent } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import LegendColors from './LegendColors';
|
||||||
|
|
||||||
|
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useQueryBuilder: (): { currentQuery: unknown } => ({
|
||||||
|
currentQuery: {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
queryName: 'A',
|
||||||
|
legend: '{service.name}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useDarkMode', () => ({
|
||||||
|
useIsDarkMode: (): boolean => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('antd', () => {
|
||||||
|
const actual = jest.requireActual('antd');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
ColorPicker: ({ onChange, children }: ColorPickerProps): JSX.Element => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="legend-color-picker"
|
||||||
|
onClick={(): void =>
|
||||||
|
onChange!({ toHexString: (): string => '#ffffff' } as Color, '#ffffff')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('LegendColors', () => {
|
||||||
|
it('renders legend colors panel and items', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={{}}
|
||||||
|
setCustomLegendColors={jest.fn()}
|
||||||
|
queryResponse={undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Legend Colors')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expand the collapse to reveal legend items
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /Legend Colors/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('{service.name}')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setCustomLegendColors when color is changed', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const setCustomLegendColors = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={{}}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expand to render the mocked ColorPicker button
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /Legend Colors/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||||
|
|
||||||
|
await user.click(colorTrigger);
|
||||||
|
|
||||||
|
expect(setCustomLegendColors).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throttles rapid color changes', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||||
|
const setCustomLegendColors = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={{}}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expand panel to render the mocked ColorPicker button
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /Legend Colors/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||||
|
|
||||||
|
// Fire multiple rapid changes
|
||||||
|
await user.click(colorTrigger);
|
||||||
|
await user.click(colorTrigger);
|
||||||
|
await user.click(colorTrigger);
|
||||||
|
await user.click(colorTrigger);
|
||||||
|
|
||||||
|
// Flush pending throttled calls
|
||||||
|
jest.advanceTimersByTime(500);
|
||||||
|
|
||||||
|
// Throttling should ensure we don't invoke the setter once per click
|
||||||
|
expect(setCustomLegendColors).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ 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';
|
||||||
@@ -95,13 +96,24 @@ function LegendColors({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle color change
|
// Handle color change (throttled to avoid excessive updates)
|
||||||
const handleColorChange = (label: string, color: string): void => {
|
const handleColorChange = useMemo(
|
||||||
setCustomLegendColors((prev) => ({
|
() =>
|
||||||
...prev,
|
throttle((label: string, color: string): void => {
|
||||||
[label]: color,
|
setCustomLegendColors((prev) => ({
|
||||||
}));
|
...prev,
|
||||||
};
|
[label]: color,
|
||||||
|
}));
|
||||||
|
}, 200), // 200ms is a good compromise between responsiveness and performance
|
||||||
|
[setCustomLegendColors],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up throttled handler on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return (): void => {
|
||||||
|
handleColorChange.cancel();
|
||||||
|
};
|
||||||
|
}, [handleColorChange]);
|
||||||
|
|
||||||
// Reset to default color
|
// Reset to default color
|
||||||
const resetToDefault = (label: string): void => {
|
const resetToDefault = (label: string): void => {
|
||||||
|
|||||||
@@ -169,6 +169,10 @@
|
|||||||
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,6 +2,9 @@
|
|||||||
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';
|
||||||
@@ -204,6 +207,18 @@ 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}
|
||||||
@@ -313,19 +328,17 @@ function Threshold({
|
|||||||
<ShowCaseValue value={value} className="unit-input" />
|
<ShowCaseValue value={value} className="unit-input" />
|
||||||
)}
|
)}
|
||||||
{isEditMode ? (
|
{isEditMode ? (
|
||||||
<Select
|
<YAxisUnitSelector
|
||||||
defaultValue={unit}
|
value={unit}
|
||||||
options={unitOptions(
|
|
||||||
selectedGraph === PANEL_TYPES.TABLE
|
|
||||||
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
|
|
||||||
: yAxisUnit || '',
|
|
||||||
)}
|
|
||||||
onChange={handleUnitChange}
|
onChange={handleUnitChange}
|
||||||
showSearch
|
placeholder="Select unit"
|
||||||
className="unit-selection"
|
source={YAxisSource.DASHBOARDS}
|
||||||
|
initialValue={unit}
|
||||||
|
categoriesOverride={unitSelectCategories}
|
||||||
|
containerClassName="unit-selection"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ShowCaseValue value={unit} className="unit-selection-prev" />
|
<ShowCaseValue value={unitLabel} className="unit-selection-prev" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="thresholds-color-selector">
|
<div className="thresholds-color-selector">
|
||||||
@@ -356,7 +369,10 @@ function Threshold({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isInvalidUnitComparison && (
|
{isInvalidUnitComparison && (
|
||||||
<Typography.Text className="invalid-unit">
|
<Typography.Text
|
||||||
|
className="invalid-unit"
|
||||||
|
data-testid="invalid-unit-comparison"
|
||||||
|
>
|
||||||
Threshold unit ({unit}) is not valid in comparison with the{' '}
|
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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { render, screen } from 'tests/test-utils';
|
import { render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
@@ -14,12 +16,26 @@ jest.mock('lib/query/createTableColumnsFromQuery', () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the unitOptions function
|
// Mock the unitOptions function to return YAxisCategory-shaped data
|
||||||
jest.mock('container/NewWidget/utils', () => ({
|
jest.mock('container/NewWidget/utils', () => ({
|
||||||
unitOptions: jest.fn(() => [
|
unitOptions: jest.fn(() => [
|
||||||
{ value: 'none', label: 'None' },
|
{
|
||||||
{ value: '%', label: 'Percent (0 - 100)' },
|
name: 'Mock Category',
|
||||||
{ value: 'ms', label: 'Milliseconds (ms)' },
|
units: [
|
||||||
|
{
|
||||||
|
id: UniversalYAxisUnit.NONE,
|
||||||
|
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: UniversalYAxisUnit.PERCENT,
|
||||||
|
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: UniversalYAxisUnit.MILLISECONDS,
|
||||||
|
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -28,7 +44,7 @@ const defaultProps = {
|
|||||||
keyIndex: 0,
|
keyIndex: 0,
|
||||||
thresholdOperator: '>' as const,
|
thresholdOperator: '>' as const,
|
||||||
thresholdValue: 50,
|
thresholdValue: 50,
|
||||||
thresholdUnit: 'none',
|
thresholdUnit: UniversalYAxisUnit.NONE,
|
||||||
thresholdColor: 'Red',
|
thresholdColor: 'Red',
|
||||||
thresholdFormat: 'Text' as const,
|
thresholdFormat: 'Text' as const,
|
||||||
isEditEnabled: true,
|
isEditEnabled: true,
|
||||||
@@ -38,8 +54,11 @@ const defaultProps = {
|
|||||||
{ value: 'memory_usage', label: 'Memory Usage' },
|
{ value: 'memory_usage', label: 'Memory Usage' },
|
||||||
],
|
],
|
||||||
thresholdTableOptions: 'cpu_usage',
|
thresholdTableOptions: 'cpu_usage',
|
||||||
columnUnits: { cpu_usage: 'percent', memory_usage: 'bytes' },
|
columnUnits: {
|
||||||
yAxisUnit: '%',
|
cpu_usage: UniversalYAxisUnit.PERCENT,
|
||||||
|
memory_usage: UniversalYAxisUnit.BYTES,
|
||||||
|
},
|
||||||
|
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||||
moveThreshold: jest.fn(),
|
moveThreshold: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,28 +87,27 @@ describe('Threshold Component Unit Validation', () => {
|
|||||||
it('should show validation error when threshold unit is not "none" and units are incompatible', () => {
|
it('should show validation error when threshold unit is not "none" and units are incompatible', () => {
|
||||||
// Act - Render component with incompatible units (ms vs percent)
|
// Act - Render component with incompatible units (ms vs percent)
|
||||||
renderThreshold({
|
renderThreshold({
|
||||||
thresholdUnit: 'ms',
|
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
|
||||||
thresholdValue: 50,
|
thresholdValue: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||||
// Assert - Validation error should be displayed
|
// Assert - Validation error should be displayed
|
||||||
expect(
|
expect(errorMessage.textContent).toBe(
|
||||||
screen.getByText(
|
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
|
||||||
/Threshold unit \(ms\) is not valid in comparison with the column unit \(percent\)/i,
|
);
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show validation error when threshold unit matches column unit', () => {
|
it('should not show validation error when threshold unit matches column unit', () => {
|
||||||
// Act - Render component with matching units
|
// Act - Render component with matching units
|
||||||
renderThreshold({
|
renderThreshold({
|
||||||
thresholdUnit: 'percent',
|
thresholdUnit: UniversalYAxisUnit.PERCENT,
|
||||||
thresholdValue: 50,
|
thresholdValue: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert - No validation error should be displayed
|
// Assert - No validation error should be displayed
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
screen.queryByTestId('invalid-unit-comparison'),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,17 +115,16 @@ describe('Threshold Component Unit Validation', () => {
|
|||||||
// Act - Render component for time series with incompatible units
|
// Act - Render component for time series with incompatible units
|
||||||
renderThreshold({
|
renderThreshold({
|
||||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||||
thresholdUnit: 'ms',
|
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
|
||||||
thresholdValue: 100,
|
thresholdValue: 100,
|
||||||
yAxisUnit: 'percent',
|
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||||
// Assert - Validation error should be displayed
|
// Assert - Validation error should be displayed
|
||||||
expect(
|
expect(errorMessage.textContent).toBe(
|
||||||
screen.getByText(
|
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the y-axis unit (${UniversalYAxisUnit.PERCENT})`,
|
||||||
/Threshold unit \(ms\) is not valid in comparison with the y-axis unit \(percent\)/i,
|
);
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show validation error for time series graph when threshold unit is "none"', () => {
|
it('should not show validation error for time series graph when threshold unit is "none"', () => {
|
||||||
@@ -116,43 +133,39 @@ describe('Threshold Component Unit Validation', () => {
|
|||||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||||
thresholdUnit: 'none',
|
thresholdUnit: 'none',
|
||||||
thresholdValue: 100,
|
thresholdValue: 100,
|
||||||
yAxisUnit: 'percent',
|
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert - No validation error should be displayed
|
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
screen.queryByTestId('invalid-unit-comparison'),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show validation error when threshold unit is compatible with column unit', () => {
|
it('should not show validation error when threshold unit is compatible with column unit', () => {
|
||||||
// Act - Render component with compatible units (both in same category - Time)
|
// Act - Render component with compatible units (both in same category - Time)
|
||||||
renderThreshold({
|
renderThreshold({
|
||||||
thresholdUnit: 's',
|
thresholdUnit: UniversalYAxisUnit.SECONDS,
|
||||||
thresholdValue: 100,
|
thresholdValue: 100,
|
||||||
columnUnits: { cpu_usage: 'ms' },
|
columnUnits: { cpu_usage: UniversalYAxisUnit.MILLISECONDS },
|
||||||
thresholdTableOptions: 'cpu_usage',
|
thresholdTableOptions: 'cpu_usage',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert - No validation error should be displayed
|
// Assert - No validation error should be displayed
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
screen.queryByTestId('invalid-unit-comparison'),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show validation error when threshold unit is in different category than column unit', () => {
|
it('should show validation error when threshold unit is in different category than column unit', () => {
|
||||||
// Act - Render component with units from different categories
|
// Act - Render component with units from different categories
|
||||||
renderThreshold({
|
renderThreshold({
|
||||||
thresholdUnit: 'bytes',
|
thresholdUnit: UniversalYAxisUnit.BYTES,
|
||||||
thresholdValue: 100,
|
thresholdValue: 100,
|
||||||
yAxisUnit: 'percent',
|
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||||
// Assert - Validation error should be displayed
|
// Assert - Validation error should be displayed
|
||||||
expect(
|
expect(errorMessage.textContent).toBe(
|
||||||
screen.getByText(
|
`Threshold unit (${UniversalYAxisUnit.BYTES}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
|
||||||
/Threshold unit \(bytes\) is not valid in comparison with the column unit \(percent\)/i,
|
);
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { DefaultOptionType } from 'antd/es/select';
|
|
||||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||||
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
|
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
|
||||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
import {
|
||||||
|
UniversalYAxisUnit,
|
||||||
|
YAxisCategory,
|
||||||
|
YAxisSource,
|
||||||
|
} from 'components/YAxisUnitSelector/types';
|
||||||
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
@@ -606,7 +609,7 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
|
|||||||
*/
|
*/
|
||||||
export const getCategorySelectOptionByName = (
|
export const getCategorySelectOptionByName = (
|
||||||
name?: YAxisCategoryNames,
|
name?: YAxisCategoryNames,
|
||||||
): DefaultOptionType[] => {
|
): { name: string; id: UniversalYAxisUnit }[] => {
|
||||||
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
|
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
|
||||||
if (!categories.length) {
|
if (!categories.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -615,8 +618,8 @@ export const getCategorySelectOptionByName = (
|
|||||||
categories
|
categories
|
||||||
.find((category) => category.name === name)
|
.find((category) => category.name === name)
|
||||||
?.units.map((unit) => ({
|
?.units.map((unit) => ({
|
||||||
label: unit.name,
|
name: unit.name,
|
||||||
value: unit.id,
|
id: unit.id,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -628,19 +631,19 @@ export const getCategorySelectOptionByName = (
|
|||||||
* select options. If a valid category is found, it filters the supported categories
|
* select options. If a valid category is found, it filters the supported categories
|
||||||
* to return only the options for the matched category.
|
* to return only the options for the matched category.
|
||||||
*/
|
*/
|
||||||
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
|
export const unitOptions = (columnUnit: string): YAxisCategory[] => {
|
||||||
const category = getCategoryName(columnUnit);
|
const category = getCategoryName(columnUnit);
|
||||||
if (isEmpty(category)) {
|
if (isEmpty(category)) {
|
||||||
return categoryToSupport.map((category) => ({
|
return categoryToSupport.map((category) => ({
|
||||||
label: category,
|
name: category,
|
||||||
options: getCategorySelectOptionByName(category),
|
units: getCategorySelectOptionByName(category),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return categoryToSupport
|
return categoryToSupport
|
||||||
.filter((supportedCategory) => supportedCategory === category)
|
.filter((supportedCategory) => supportedCategory === category)
|
||||||
.map((filteredCategory) => ({
|
.map((filteredCategory) => ({
|
||||||
label: filteredCategory,
|
name: filteredCategory,
|
||||||
options: getCategorySelectOptionByName(filteredCategory),
|
units: getCategorySelectOptionByName(filteredCategory),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ function TagFilterInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilderSearch
|
<QueryBuilderSearchV2
|
||||||
query={query}
|
query={query}
|
||||||
onChange={onQueryChange}
|
onChange={onQueryChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const BASE_URL = ENVIRONMENT.baseURL;
|
const BASE_URL = ENVIRONMENT.baseURL;
|
||||||
const attributeKeysURL = `${BASE_URL}/api/v3/autocomplete/attribute_keys`;
|
const attributeKeysURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -333,26 +333,34 @@ describe('PipelinePage container test', () => {
|
|||||||
ctx.json({
|
ctx.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: {
|
data: {
|
||||||
attributeKeys: [
|
attributes: [
|
||||||
{
|
{
|
||||||
key: 'otelServiceName',
|
key: 'otelServiceName',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'service.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'service.instance.id',
|
key: 'service.instance.id',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'resource',
|
type: 'resource',
|
||||||
},
|
isColumn: false,
|
||||||
{
|
isJSON: false,
|
||||||
key: 'service.name',
|
|
||||||
dataType: DataTypes.String,
|
|
||||||
type: 'resource',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'service.name',
|
key: 'service.name',
|
||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -973,6 +973,7 @@ function QueryBuilderSearchV2(
|
|||||||
return (
|
return (
|
||||||
<div className="query-builder-search-v2">
|
<div className="query-builder-search-v2">
|
||||||
<Select
|
<Select
|
||||||
|
data-testid={'qb-search-select'}
|
||||||
ref={selectRef}
|
ref={selectRef}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}
|
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
|
import { useIsTextSelected } from 'hooks/useIsTextSelected';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
import useLogDetailHandlers from '../useLogDetailHandlers';
|
||||||
|
|
||||||
|
jest.mock('hooks/logs/useActiveLog');
|
||||||
|
jest.mock('hooks/useIsTextSelected');
|
||||||
|
|
||||||
|
const mockOnSetActiveLog = jest.fn();
|
||||||
|
const mockOnClearActiveLog = jest.fn();
|
||||||
|
const mockOnAddToQuery = jest.fn();
|
||||||
|
const mockOnGroupByAttribute = jest.fn();
|
||||||
|
const mockIsTextSelected = jest.fn();
|
||||||
|
|
||||||
|
const mockLog: ILog = {
|
||||||
|
id: 'log-1',
|
||||||
|
timestamp: '2024-01-01T00:00:00Z',
|
||||||
|
date: '2024-01-01',
|
||||||
|
body: 'test log body',
|
||||||
|
severityText: 'INFO',
|
||||||
|
severityNumber: 9,
|
||||||
|
traceFlags: 0,
|
||||||
|
traceId: '',
|
||||||
|
spanID: '',
|
||||||
|
attributesString: {},
|
||||||
|
attributesInt: {},
|
||||||
|
attributesFloat: {},
|
||||||
|
resources_string: {},
|
||||||
|
scope_string: {},
|
||||||
|
attributes_string: {},
|
||||||
|
severity_text: '',
|
||||||
|
severity_number: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
jest.mocked(useIsTextSelected).mockReturnValue(mockIsTextSelected);
|
||||||
|
|
||||||
|
jest.mocked(useActiveLog).mockReturnValue({
|
||||||
|
activeLog: null,
|
||||||
|
onSetActiveLog: mockOnSetActiveLog,
|
||||||
|
onClearActiveLog: mockOnClearActiveLog,
|
||||||
|
onAddToQuery: mockOnAddToQuery,
|
||||||
|
onGroupByAttribute: mockOnGroupByAttribute,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not open log detail when text is selected', () => {
|
||||||
|
mockIsTextSelected.mockReturnValue(true);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogDetailHandlers());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleSetActiveLog(mockLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open log detail when no text is selected', () => {
|
||||||
|
mockIsTextSelected.mockReturnValue(false);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogDetailHandlers());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleSetActiveLog(mockLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnSetActiveLog).toHaveBeenCalledWith(mockLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle off when clicking the same active log', () => {
|
||||||
|
mockIsTextSelected.mockReturnValue(false);
|
||||||
|
|
||||||
|
jest.mocked(useActiveLog).mockReturnValue({
|
||||||
|
activeLog: mockLog,
|
||||||
|
onSetActiveLog: mockOnSetActiveLog,
|
||||||
|
onClearActiveLog: mockOnClearActiveLog,
|
||||||
|
onAddToQuery: mockOnAddToQuery,
|
||||||
|
onGroupByAttribute: mockOnGroupByAttribute,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogDetailHandlers());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleSetActiveLog(mockLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnClearActiveLog).toHaveBeenCalled();
|
||||||
|
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -54,6 +56,20 @@ export const useActiveLog = (): UseActiveLog => {
|
|||||||
|
|
||||||
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||||
|
|
||||||
|
// Close drawer/clear active log when query in URL changes
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const compositeQuery = urlQuery.get(QueryParams.compositeQuery) ?? '';
|
||||||
|
const prevQueryRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
prevQueryRef.current !== null &&
|
||||||
|
prevQueryRef.current !== compositeQuery
|
||||||
|
) {
|
||||||
|
setActiveLog(null);
|
||||||
|
}
|
||||||
|
prevQueryRef.current = compositeQuery;
|
||||||
|
}, [compositeQuery]);
|
||||||
|
|
||||||
const onSetDetailedLogData = useCallback(
|
const onSetDetailedLogData = useCallback(
|
||||||
(logData: ILog) => {
|
(logData: ILog) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import type { UseActiveLog } from 'hooks/logs/types';
|
import type { UseActiveLog } from 'hooks/logs/types';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
|
import { useIsTextSelected } from 'hooks/useIsTextSelected';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
|
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
|
||||||
@@ -28,9 +29,13 @@ function useLogDetailHandlers({
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
|
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
|
||||||
|
const isTextSelected = useIsTextSelected();
|
||||||
|
|
||||||
const handleSetActiveLog = useCallback(
|
const handleSetActiveLog = useCallback(
|
||||||
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
|
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
|
||||||
|
if (isTextSelected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (activeLog?.id === log.id) {
|
if (activeLog?.id === log.id) {
|
||||||
onClearActiveLog();
|
onClearActiveLog();
|
||||||
setSelectedTab(undefined);
|
setSelectedTab(undefined);
|
||||||
@@ -39,7 +44,7 @@ function useLogDetailHandlers({
|
|||||||
onSetActiveLog(log);
|
onSetActiveLog(log);
|
||||||
setSelectedTab(nextTab ?? defaultTab);
|
setSelectedTab(nextTab ?? defaultTab);
|
||||||
},
|
},
|
||||||
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
|
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog, isTextSelected],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCloseLogDetail = useCallback((): void => {
|
const handleCloseLogDetail = useCallback((): void => {
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
|
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
import { getMetricMetadata } from 'api/metricsExplorer/v2/getMetricMetadata';
|
import {
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
getGetMetricMetadataQueryKey,
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
getMetricMetadata,
|
||||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
} from 'api/generated/services/metrics';
|
||||||
|
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
|
||||||
type QueryResult = UseQueryResult<
|
type QueryResult = UseQueryResult<GetMetricMetadata200, Error>;
|
||||||
SuccessResponseV2<MetricMetadataResponse>,
|
|
||||||
Error
|
|
||||||
>;
|
|
||||||
|
|
||||||
type UseGetMultipleMetrics = (
|
type UseGetMultipleMetrics = (
|
||||||
metricNames: string[],
|
metricNames: string[],
|
||||||
options?: UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
options?: UseQueryOptions<GetMetricMetadata200, Error>,
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
) => QueryResult[];
|
) => QueryResult[];
|
||||||
|
|
||||||
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
|
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
|
||||||
metricNames,
|
metricNames,
|
||||||
options,
|
options,
|
||||||
headers,
|
|
||||||
) =>
|
) =>
|
||||||
useQueries(
|
useQueries(
|
||||||
metricNames.map(
|
metricNames.map(
|
||||||
(metricName) =>
|
(metricName) =>
|
||||||
({
|
({
|
||||||
queryKey: [REACT_QUERY_KEY.GET_METRIC_METADATA, metricName],
|
queryKey: getGetMetricMetadataQueryKey({
|
||||||
queryFn: ({ signal }) => getMetricMetadata(metricName, signal, headers),
|
metricName,
|
||||||
|
}),
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
getMetricMetadata(
|
||||||
|
{
|
||||||
|
metricName,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
),
|
||||||
...options,
|
...options,
|
||||||
} as UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>),
|
} as UseQueryOptions<GetMetricMetadata200, Error>),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
import { useGetMetrics } from 'container/MetricsExplorer/Explorer/utils';
|
import { useGetMetrics } from 'container/MetricsExplorer/Explorer/utils';
|
||||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||||
@@ -24,13 +24,13 @@ const mockUseGetMetrics = useGetMetrics as jest.MockedFunction<
|
|||||||
|
|
||||||
const MOCK_METRIC_1 = {
|
const MOCK_METRIC_1 = {
|
||||||
unit: UniversalYAxisUnit.BYTES,
|
unit: UniversalYAxisUnit.BYTES,
|
||||||
} as MetricMetadata;
|
} as MetricsexplorertypesMetricMetadataDTO;
|
||||||
const MOCK_METRIC_2 = {
|
const MOCK_METRIC_2 = {
|
||||||
unit: UniversalYAxisUnit.SECONDS,
|
unit: UniversalYAxisUnit.SECONDS,
|
||||||
} as MetricMetadata;
|
} as MetricsexplorertypesMetricMetadataDTO;
|
||||||
const MOCK_METRIC_3 = {
|
const MOCK_METRIC_3 = {
|
||||||
unit: '',
|
unit: '',
|
||||||
} as MetricMetadata;
|
} as MetricsexplorertypesMetricMetadataDTO;
|
||||||
|
|
||||||
function createMockCurrentQuery(
|
function createMockCurrentQuery(
|
||||||
queryType: EQueryType,
|
queryType: EQueryType,
|
||||||
|
|||||||
10
frontend/src/hooks/useIsTextSelected.ts
Normal file
10
frontend/src/hooks/useIsTextSelected.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useIsTextSelected(): () => boolean {
|
||||||
|
return useCallback((): boolean => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
return (
|
||||||
|
!!selection && !selection.isCollapsed && selection.toString().length > 0
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user