Compare commits

..

32 Commits

Author SHA1 Message Date
Yunus M
abcfa880bb chore: address review comments 2025-08-06 13:38:58 +05:30
Yunus M
7c7e496f2f chore: fix type errors 2025-08-06 13:25:28 +05:30
Yunus M
b3551bf140 feat: handle save views 2025-08-06 13:23:15 +05:30
Yunus M
7363cf43b1 feat: update attribute keys api to accept meter as datasource 2025-08-06 13:23:15 +05:30
Yunus M
f515b077bf feat: update attribute keys api to accept signal source 2025-08-06 13:23:15 +05:30
Yunus M
07659f91c3 feat: integrate meter api changes 2025-08-06 13:23:11 +05:30
Yunus M
0595c526a7 feat: remove quick filters 2025-08-06 13:19:57 +05:30
Yunus M
8509146589 feat: meter explorer - initial commit 2025-08-06 13:18:24 +05:30
Vikrant Gupta
aedc9353d2 Merge branch 'main' into feat/telemetry-meter 2025-08-06 13:03:21 +05:30
vikrantgupta25
436fcb7453 feat(telemetrymeter): use meter aggregate keys 2025-08-06 13:02:45 +05:30
vikrantgupta25
c3238d761a feat(telemetrymeter): add the statement builder for the ranged cache queries 2025-08-05 18:03:04 +05:30
vikrantgupta25
855b381160 feat(telemetrymeter): incorporate the new changes to stmnt builder 2025-08-05 15:02:49 +05:30
Vikrant Gupta
0b97a6764e Merge branch 'main' into feat/telemetry-meter 2025-08-05 14:59:54 +05:30
Vikrant Gupta
f069adb195 Merge branch 'main' into feat/telemetry-meter 2025-08-04 16:54:18 +05:30
vikrantgupta25
e642ec9427 feat(telemetrymeter): added quick filters for meter explorer 2025-08-04 16:49:22 +05:30
vikrantgupta25
26e2e14a7f feat(telemetrymeter): better naming for source in metadata 2025-08-04 03:00:06 +05:30
vikrantgupta25
e522817df5 feat(telemetrymeter): introduce source for query 2025-08-04 02:49:09 +05:30
Vikrant Gupta
8370deaf35 Merge branch 'main' into feat/telemetry-meter 2025-08-03 18:02:30 +05:30
vikrantgupta25
0a71e0f0b4 feat(telemetrymeter): cleanup the types 2025-08-03 01:56:09 +05:30
vikrantgupta25
3e41397ef7 feat(telemetrymeter): deprecate the signal and use aggregation instead 2025-08-03 01:38:04 +05:30
vikrantgupta25
a193794403 feat(telemetrymeter): deprecate the signal and use aggregation instead 2025-08-03 01:36:50 +05:30
vikrantgupta25
9158b25d4d feat(telemetrymeter): deprecate the signal and use aggregation instead 2025-08-03 01:32:23 +05:30
vikrantgupta25
254c7f8396 feat(telemetrymeter): metadata changes and aggregate attribute changes 2025-08-02 16:36:08 +05:30
vikrantgupta25
240ce72c9a feat(telemetrymeter): metadata changes and aggregate attribute changes 2025-08-02 14:41:01 +05:30
vikrantgupta25
712fa3e041 feat(telemetrymeter): step interval improvements 2025-07-31 19:02:21 +05:30
vikrantgupta25
27305e6cc6 feat(telemetry/meter): improve error messages 2025-07-31 17:25:57 +05:30
vikrantgupta25
bd762d02e3 feat(telemetry/meter): test query range API fixes 2025-07-31 16:45:48 +05:30
Vikrant Gupta
73bc95a56a Merge branch 'main' into feat/telemetry-meter 2025-07-31 16:04:24 +05:30
vikrantgupta25
540ad965db feat(telemetry/meter): fix stmnt builder tests 2025-07-31 13:05:35 +05:30
Vikrant Gupta
f4cf7eb92e Merge branch 'main' into feat/telemetry-meter 2025-07-31 13:04:40 +05:30
vikrantgupta25
e7a5266cd3 feat(telemetry/meter): added metadata setup for meter 2025-07-31 01:41:55 +05:30
vikrantgupta25
71e17a760c feat(telemetry/meter): added base setup for telemetry meter signal 2025-07-31 00:10:10 +05:30
238 changed files with 1564 additions and 12529 deletions

View File

@@ -24,7 +24,7 @@ services:
depends_on:
- zookeeper
zookeeper:
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
container_name: schema-migrator-async
command:
- async

View File

@@ -1,29 +0,0 @@
services:
signoz-otel-collector:
image: signoz/signoz-otel-collector:v0.128.2
container_name: signoz-otel-collector-dev
command:
- --config=/etc/otel-collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "13133:13133" # health check extension
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:13133
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -1,96 +0,0 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://host.docker.internal:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://host.docker.internal:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://host.docker.internal:9000/signoz_logs
timeout: 10s
use_new_schema: true
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]

View File

@@ -61,17 +61,6 @@ devenv-postgres: ## Run postgres in devenv
@cd .devenv/docker/postgres; \
docker compose -f compose.yaml up -d
.PHONY: devenv-signoz-otel-collector
devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
@cd .devenv/docker/signoz-otel-collector; \
docker compose -f compose.yaml up -d
.PHONY: devenv-up
devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
@echo "Development environment is ready!"
@echo " - ClickHouse: http://localhost:8123"
@echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
##############################################################
# go commands
##############################################################

View File

@@ -121,8 +121,6 @@ telemetrystore:
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:

View File

@@ -39,7 +39,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.92.1
image: signoz/signoz:v0.91.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
deploy:
labels:
@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.92.1
image: signoz/signoz:v0.91.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.129.0
image: signoz/signoz-otel-collector:v0.128.2
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.129.0
image: signoz/signoz-schema-migrator:v0.128.2
deploy:
restart_policy:
condition: on-failure

View File

@@ -42,7 +42,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.92.1}
image: signoz/signoz:${VERSION:-v0.91.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -38,7 +38,7 @@ x-clickhouse-defaults: &clickhouse-defaults
hard: 262144
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
image: bitnami/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.92.1}
image: signoz/signoz:${VERSION:-v0.91.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
container_name: schema-migrator-async
command:
- async

View File

@@ -44,35 +44,20 @@ Before diving in, make sure you have these tools installed:
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
### 1. Setting up ClickHouse
### 1. Setting up Clickhouse
First, we need to get ClickHouse running:
First, we need to get Clickhouse running:
```bash
make devenv-clickhouse
```
This command:
- Starts ClickHouse in a single-shard, single-replica cluster
- Starts Clickhouse in a single-shard, single-replica cluster
- Sets up Zookeeper
- Runs the latest schema migrations
### 2. Setting up SigNoz OpenTelemetry Collector
Next, start the OpenTelemetry Collector to receive telemetry data:
```bash
make devenv-signoz-otel-collector
```
This command:
- Starts the SigNoz OpenTelemetry Collector
- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
- Forwards data to ClickHouse for storage
> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
### 3. Starting the Backend
### 2. Starting the Backend
1. Run the backend server:
```bash
@@ -88,7 +73,7 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
### 4. Setting up the Frontend
### 3. Setting up the Frontend
1. Navigate to the frontend directory:
```bash
@@ -113,25 +98,3 @@ This command:
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
Now you're all set to start developing! Happy coding! 🎉
## Verifying Your Setup
To verify everything is working correctly:
1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
4. **Check Frontend**: Open `http://localhost:3301` in your browser
## How to send test data?
You can now send telemetry data to your local SigNoz instance:
- **OTLP gRPC**: `localhost:4317`
- **OTLP HTTP**: `localhost:4318`
For example, using `curl` to send a test trace:
```bash
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
```

View File

@@ -48,6 +48,6 @@
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter"
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
}

View File

@@ -71,6 +71,6 @@
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | External APIs",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter"
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
}

View File

@@ -437,10 +437,10 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.METER,
path: ROUTES.METER_EXPLORER_BASE,
exact: true,
component: MeterExplorer,
key: 'METER',
key: 'METER_EXPLORER_BASE',
isPrivate: true,
},
{

View File

@@ -1,34 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
/**
* Get field keys for a given signal type
* @param signal Type of signal (traces, logs, metrics)
* @param name Optional search text
*/
export const getFieldKeys = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
): Promise<SuccessResponse<FieldKeyResponse> | ErrorResponse> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = signal;
}
if (name) {
params.name = name;
}
const response = await ApiBaseInstance.get('/fields/keys', { params });
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default getFieldKeys;

View File

@@ -1,63 +0,0 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
/**
* Get field values for a given signal type and field name
* @param signal Type of signal (traces, logs, metrics)
* @param name Name of the attribute for which values are being fetched
* @param value Optional search text
*/
export const getFieldValues = async (
signal?: 'traces' | 'logs' | 'metrics',
name?: string,
value?: string,
startUnixMilli?: number,
endUnixMilli?: number,
): Promise<SuccessResponse<FieldValueResponse> | ErrorResponse> => {
const params: Record<string, string> = {};
if (signal) {
params.signal = signal;
}
if (name) {
params.name = name;
}
if (value) {
params.value = value;
}
if (startUnixMilli) {
params.startUnixMilli = Math.floor(startUnixMilli / 1000000).toString();
}
if (endUnixMilli) {
params.endUnixMilli = Math.floor(endUnixMilli / 1000000).toString();
}
const response = await ApiBaseInstance.get('/fields/values', { params });
// Normalize values from different types (stringValues, boolValues, etc.)
if (response.data?.data?.values) {
const allValues: string[] = [];
Object.values(response.data.data.values).forEach((valueArray: any) => {
if (Array.isArray(valueArray)) {
allValues.push(...valueArray.map(String));
}
});
// Add a normalized values array to the response
response.data.data.normalizedValues = allValues;
}
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default getFieldValues;

View File

@@ -8,15 +8,14 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText, signalSource, metricName } = props;
const { signal, key, searchText, signalSource } = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
const encodedMetricName = encodeURIComponent(metricName || '');
const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&source=${encodedSource}`,
);
};

View File

@@ -385,7 +385,7 @@ export function convertV5ResponseToLegacy(
data: {
resultType: 'scalar',
result: webTables,
warnings: v5Data?.data?.warning || [],
warnings: v5Data?.data?.warnings || [],
},
warning: v5Data?.warning || undefined,
},

View File

@@ -137,11 +137,5 @@
h6 {
color: var(--text-ink-500);
}
code {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
color: var(--text-ink-500);
}
}
}

View File

@@ -28,7 +28,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForMultiSelect,
SPACEKEY,
} from './utils';
@@ -38,7 +37,7 @@ enum ToggleTagValue {
All = 'All',
}
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
placeholder = 'Search...',
@@ -63,8 +62,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
allowClear = false,
onRetry,
maxTagTextLength,
onDropdownVisibleChange,
showIncompleteDataMessage = false,
...rest
}) => {
// ===== State & Refs =====
@@ -81,8 +78,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
const [visibleOptions, setVisibleOptions] = useState<OptionData[]>([]);
const isClickInsideDropdownRef = useRef(false);
const justOpenedRef = useRef<boolean>(false);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
// Convert single string value to array for consistency
const selectedValues = useMemo(
@@ -129,12 +124,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return allAvailableValues.every((val) => selectedValues.includes(val));
}, [selectedValues, allAvailableValues, enableAllSelection]);
// Define allOptionShown earlier in the code
const allOptionShown = useMemo(
() => value === ALL_SELECTED_VALUE || value === 'ALL',
[value],
);
// Value passed to the underlying Ant Select component
const displayValue = useMemo(
() => (isAllSelected ? [ALL_SELECTED_VALUE] : selectedValues),
@@ -143,18 +132,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// ===== Internal onChange Handler =====
const handleInternalChange = useCallback(
(newValue: string | string[], directCaller?: boolean): void => {
(newValue: string | string[]): void => {
// Ensure newValue is an array
const currentNewValue = Array.isArray(newValue) ? newValue : [];
if (
(allOptionShown || isAllSelected) &&
!directCaller &&
currentNewValue.length === 0
) {
return;
}
if (!onChange) return;
// Case 1: Cleared (empty array or undefined)
@@ -163,7 +144,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return;
}
// Case 2: "__ALL__" is selected (means select all actual values)
// Case 2: "__all__" is selected (means select all actual values)
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
const allActualOptions = allAvailableValues.map(
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
@@ -194,14 +175,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
}
},
[
allOptionShown,
isAllSelected,
onChange,
allAvailableValues,
options,
enableAllSelection,
],
[onChange, allAvailableValues, options, enableAllSelection],
);
// ===== Existing Callbacks (potentially needing adjustment later) =====
@@ -536,19 +510,11 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
// Normal single value handling
const trimmedValue = value.trim();
setSearchText(trimmedValue);
setSearchText(value.trim());
if (!isOpen) {
setIsOpen(true);
justOpenedRef.current = true;
}
// Reset active index when search changes if dropdown is open
if (isOpen && trimmedValue) {
setActiveIndex(0);
}
if (onSearch) onSearch(trimmedValue);
if (onSearch) onSearch(value.trim());
},
[onSearch, isOpen, selectedValues, onChange],
);
@@ -562,34 +528,28 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
// If regex fails, return the original text without highlighting
console.error('Error in text highlighting:', error);
return text;
}
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
},
[highlightSearch],
);
@@ -600,10 +560,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (isAllSelected) {
// If all are selected, deselect all
handleInternalChange([], true);
handleInternalChange([]);
} else {
// Otherwise, select all
handleInternalChange([ALL_SELECTED_VALUE], true);
handleInternalChange([ALL_SELECTED_VALUE]);
}
}, [options, isAllSelected, handleInternalChange]);
@@ -778,26 +738,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Enhanced keyboard navigation with support for maxTagCount
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLElement>): void => {
// Simple early return if ALL is selected - block all possible keyboard interactions
// that could remove the ALL tag, but still allow dropdown navigation and search
if (
(allOptionShown || isAllSelected) &&
(e.key === 'Backspace' || e.key === 'Delete')
) {
// Only prevent default if the input is empty or cursor is at start position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
const isInputEmpty = isInputActive && !activeElement?.value;
const isCursorAtStart =
isInputActive && activeElement?.selectionStart === 0;
if (isInputEmpty || isCursorAtStart) {
e.preventDefault();
e.stopPropagation();
return;
}
}
// Get flattened list of all selectable options
const getFlatOptions = (): OptionData[] => {
if (!visibleOptions) return [];
@@ -812,7 +752,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
if (hasAll) {
flatList.push({
label: 'ALL',
value: ALL_SELECTED_VALUE, // Special value for the ALL option
value: '__all__', // Special value for the ALL option
type: 'defined',
});
}
@@ -844,17 +784,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const flatOptions = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && flatOptions.length > 0) {
setActiveIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options and dropdown is open, activate the first one
if (isOpen && activeIndex === -1 && flatOptions.length > 0) {
setActiveIndex(0);
}
// Get the active input element to check cursor position
const activeElement = document.activeElement as HTMLInputElement;
const isInputActive = activeElement?.tagName === 'INPUT';
@@ -1200,7 +1129,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// If there's an active option in the dropdown, prioritize selecting it
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
const selectedOption = flatOptions[activeIndex];
if (selectedOption.value === ALL_SELECTED_VALUE) {
if (selectedOption.value === '__all__') {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1230,10 +1159,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveIndex(-1);
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
if (onDropdownVisibleChange) {
onDropdownVisibleChange(false);
}
break;
case SPACEKEY:
@@ -1243,7 +1168,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const selectedOption = flatOptions[activeIndex];
// Check if it's the ALL option
if (selectedOption.value === ALL_SELECTED_VALUE) {
if (selectedOption.value === '__all__') {
handleSelectAll();
} else if (selectedOption.value && onChange) {
const newValues = selectedValues.includes(selectedOption.value)
@@ -1289,7 +1214,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
e.stopPropagation();
e.preventDefault();
setIsOpen(true);
justOpenedRef.current = true; // Set flag to initialize active option on next render
setActiveIndex(0);
setActiveChipIndex(-1);
break;
@@ -1335,14 +1260,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
},
[
allOptionShown,
isAllSelected,
isOpen,
activeIndex,
getVisibleChipIndices,
getLastVisibleChipIndex,
selectedChips,
isSelectionMode,
isOpen,
activeChipIndex,
selectedValues,
visibleOptions,
@@ -1358,8 +1278,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
startSelection,
selectionEnd,
extendSelection,
onDropdownVisibleChange,
activeIndex,
handleSelectAll,
getVisibleChipIndices,
getLastVisibleChipIndex,
],
);
@@ -1384,14 +1306,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
setIsOpen(false);
}, []);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// Custom dropdown render with sections support
const customDropdownRender = useCallback((): React.ReactElement => {
// Process options based on current search
@@ -1468,7 +1382,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
onMouseDown={handleDropdownMouseDown}
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
onBlur={handleBlur}
role="listbox"
aria-multiselectable="true"
@@ -1547,18 +1460,15 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<ArrowLeft size={8} className="icons" />
<ArrowRight size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
@@ -1584,19 +1494,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Use search for more options
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
</div>
</div>
);
@@ -1613,7 +1513,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
handleDropdownMouseDown,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
handleBlur,
activeIndex,
loading,
@@ -1623,31 +1522,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
renderOptionWithIndex,
handleSelectAll,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
]);
// Custom handler for dropdown visibility changes
const handleDropdownVisibleChange = useCallback(
(visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveIndex(0);
setActiveChipIndex(-1);
} else {
setSearchText('');
setActiveIndex(-1);
// Don't clear activeChipIndex when dropdown closes to maintain tag focus
}
// Pass through to the parent component's handler if provided
if (onDropdownVisibleChange) {
onDropdownVisibleChange(visible);
}
},
[onDropdownVisibleChange],
);
// ===== Side Effects =====
// Clear search when dropdown closes
@@ -1712,9 +1588,52 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const { label, value, closable, onClose } = props;
// If the display value is the special ALL value, render the ALL tag
if (allOptionShown) {
// Don't render a visible tag - will be shown as placeholder
return <div style={{ display: 'none' }} />;
if (value === ALL_SELECTED_VALUE && isAllSelected) {
const handleAllTagClose = (
e: React.MouseEvent | React.KeyboardEvent,
): void => {
e.stopPropagation();
e.preventDefault();
handleInternalChange([]); // Clear selection when ALL tag is closed
};
const handleAllTagKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter' || e.key === SPACEKEY) {
handleAllTagClose(e);
}
// Prevent Backspace/Delete propagation if needed, handle in main keydown handler
};
return (
<div
className={cx('ant-select-selection-item', {
'ant-select-selection-item-active': activeChipIndex === 0, // Treat ALL tag as index 0 when active
'ant-select-selection-item-selected': selectedChips.includes(0),
})}
style={
activeChipIndex === 0 || selectedChips.includes(0)
? {
borderColor: Color.BG_ROBIN_500,
backgroundColor: Color.BG_SLATE_400,
}
: undefined
}
>
<span className="ant-select-selection-item-content">ALL</span>
{closable && (
<span
className="ant-select-selection-item-remove"
onClick={handleAllTagClose}
onKeyDown={handleAllTagKeyDown}
role="button"
tabIndex={0}
aria-label="Remove ALL tag (deselect all)"
>
×
</span>
)}
</div>
);
}
// If not isAllSelected, render individual tags using previous logic
@@ -1794,69 +1713,52 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// Fallback for safety, should not be reached
return <div />;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isAllSelected, activeChipIndex, selectedChips, selectedValues, maxTagCount],
[
isAllSelected,
handleInternalChange,
activeChipIndex,
selectedChips,
selectedValues,
maxTagCount,
],
);
// Simple onClear handler to prevent clearing ALL
const onClearHandler = useCallback((): void => {
// Skip clearing if ALL is selected
if (allOptionShown || isAllSelected) {
return;
}
// Normal clear behavior
handleInternalChange([], true);
if (onClear) onClear();
}, [onClear, handleInternalChange, allOptionShown, isAllSelected]);
// ===== Component Rendering =====
return (
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={handleInternalChange}
onClear={(): void => handleInternalChange([])}
onDropdownVisibleChange={setIsOpen}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? 1 : maxTagCount}
{...rest}
/>
);
};

View File

@@ -29,7 +29,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { CustomSelectProps, OptionData } from './types';
import {
filterOptionsBySearch,
handleScrollToBottom,
prioritizeOrAddOptionForSingleSelect,
SPACEKEY,
} from './utils';
@@ -58,29 +57,17 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
errorMessage,
allowClear = false,
onRetry,
showIncompleteDataMessage = false,
...rest
}) => {
// ===== State & Refs =====
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState('');
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
// Refs for element access and scroll behavior
const selectRef = useRef<BaseSelectRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
// Flag to track if dropdown just opened
const justOpenedRef = useRef<boolean>(false);
// Add a scroll handler for the dropdown
const handleDropdownScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>): void => {
setIsScrolledToBottom(handleScrollToBottom(e));
},
[],
);
// ===== Option Filtering & Processing Utilities =====
@@ -143,33 +130,23 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
(text: string, searchQuery: string): React.ReactNode => {
if (!searchQuery || !highlightSearch) return text;
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
return (
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
} catch (error) {
console.error('Error in text highlighting:', error);
return text;
}
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
{part}
</span>
) : (
part
);
})}
</>
);
},
[highlightSearch],
);
@@ -269,14 +246,9 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const trimmedValue = value.trim();
setSearchText(trimmedValue);
// Reset active option index when search changes
if (isOpen) {
setActiveOptionIndex(0);
}
if (onSearch) onSearch(trimmedValue);
},
[onSearch, isOpen],
[onSearch],
);
/**
@@ -300,23 +272,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const flatList: OptionData[] = [];
// Process options
let processedOptions = isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
if (!isEmpty(searchText)) {
processedOptions = filterOptionsBySearch(processedOptions, searchText);
}
const { sectionOptions, nonSectionOptions } = splitOptions(
processedOptions,
isEmpty(value)
? filteredOptions
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
);
// Add custom option if needed
if (
!isEmpty(searchText) &&
!isLabelPresent(processedOptions, searchText)
) {
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
flatList.push({
label: searchText,
value: searchText,
@@ -337,52 +300,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const options = getFlatOptions();
// If we just opened the dropdown and have options, set first option as active
if (justOpenedRef.current && options.length > 0) {
setActiveOptionIndex(0);
justOpenedRef.current = false;
}
// If no option is active but we have options, activate the first one
if (activeOptionIndex === -1 && options.length > 0) {
setActiveOptionIndex(0);
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
break;
case 'Tab':
// Tab navigation with Shift key support
if (e.shiftKey) {
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
}
setActiveOptionIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1,
);
} else {
e.preventDefault();
if (options.length > 0) {
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
setActiveOptionIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0,
);
}
break;
@@ -395,7 +339,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
} else if (!isEmpty(searchText)) {
// Add custom value when no option is focused
@@ -408,7 +351,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(customOption.value, customOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -417,7 +359,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
e.preventDefault();
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
break;
case ' ': // Space key
@@ -428,7 +369,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onChange(selectedOption.value, selectedOption);
setIsOpen(false);
setActiveOptionIndex(-1);
setSearchText('');
}
}
break;
@@ -439,7 +379,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
// Open dropdown when Down or Tab is pressed while closed
e.preventDefault();
setIsOpen(true);
justOpenedRef.current = true; // Set flag to initialize active option on next render
setActiveOptionIndex(0);
}
},
[
@@ -504,7 +444,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
className="custom-select-dropdown"
onClick={handleDropdownClick}
onKeyDown={handleKeyDown}
onScroll={handleDropdownScroll}
role="listbox"
tabIndex={-1}
aria-activedescendant={
@@ -515,6 +454,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<div className="no-section-options">
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
</div>
{/* Section options */}
{sectionOptions.length > 0 &&
sectionOptions.map((section) =>
@@ -532,16 +472,13 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
{/* Navigation help footer */}
<div className="navigation-footer" role="note">
{!loading &&
!errorMessage &&
!noDataMessage &&
!(showIncompleteDataMessage && isScrolledToBottom) && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{!loading && !errorMessage && !noDataMessage && (
<section className="navigate">
<ArrowDown size={8} className="icons" />
<ArrowUp size={8} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
)}
{loading && (
<div className="navigation-loading">
<div className="navigation-icons">
@@ -567,19 +504,9 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
</div>
)}
{showIncompleteDataMessage &&
isScrolledToBottom &&
!loading &&
!errorMessage && (
<div className="navigation-text-incomplete">
Use search for more options
</div>
)}
{noDataMessage &&
!loading &&
!(showIncompleteDataMessage && isScrolledToBottom) &&
!errorMessage && <div className="navigation-text">{noDataMessage}</div>}
{noDataMessage && !loading && (
<div className="navigation-text">{noDataMessage}</div>
)}
</div>
</div>
);
@@ -593,7 +520,6 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
isLabelPresent,
handleDropdownClick,
handleKeyDown,
handleDropdownScroll,
activeOptionIndex,
loading,
errorMessage,
@@ -601,22 +527,8 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
dropdownRender,
renderOptionWithIndex,
onRetry,
showIncompleteDataMessage,
isScrolledToBottom,
]);
// Handle dropdown visibility changes
const handleDropdownVisibleChange = useCallback((visible: boolean): void => {
setIsOpen(visible);
if (visible) {
justOpenedRef.current = true;
setActiveOptionIndex(0);
} else {
setSearchText('');
setActiveOptionIndex(-1);
}
}, []);
// ===== Side Effects =====
// Clear search text when dropdown closes
@@ -670,7 +582,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
onSearch={handleSearch}
value={value}
onChange={onChange}
onDropdownVisibleChange={handleDropdownVisibleChange}
onDropdownVisibleChange={setIsOpen}
open={isOpen}
options={optionsWithHighlight}
defaultActiveFirstOption={defaultActiveFirstOption}

View File

@@ -35,43 +35,6 @@ $custom-border-color: #2c3044;
width: 100%;
position: relative;
&.is-all-selected {
.ant-select-selection-search-input {
caret-color: transparent;
}
.ant-select-selection-placeholder {
opacity: 1 !important;
color: var(--bg-vanilla-400) !important;
font-weight: 500;
visibility: visible !important;
pointer-events: none;
z-index: 2;
.lightMode & {
color: rgba(0, 0, 0, 0.85) !important;
}
}
&.ant-select-focused .ant-select-selection-placeholder {
opacity: 0.45 !important;
}
}
.all-selected-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
z-index: 1;
pointer-events: none;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
.ant-select-selector {
max-height: 200px;
overflow: auto;
@@ -195,7 +158,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for single select
.custom-select-dropdown {
padding: 8px 0 0 0;
max-height: 300px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -313,10 +276,6 @@ $custom-border-color: #2c3044;
font-size: 12px;
}
.navigation-text-incomplete {
color: var(--bg-amber-600) !important;
}
.navigation-error {
.navigation-text,
.navigation-icons {
@@ -363,7 +322,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
max-height: 350px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -697,10 +656,6 @@ $custom-border-color: #2c3044;
border: 1px solid #e8e8e8;
color: rgba(0, 0, 0, 0.85);
font-size: 12px !important;
height: 20px;
line-height: 18px;
.ant-select-selection-item-content {
color: rgba(0, 0, 0, 0.85);
}
@@ -881,38 +836,3 @@ $custom-border-color: #2c3044;
}
}
}
.custom-multiselect-wrapper {
position: relative;
width: 100%;
&.all-selected {
.all-text {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--bg-vanilla-400);
font-weight: 500;
z-index: 2;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
.lightMode & {
color: rgba(0, 0, 0, 0.85);
}
}
&:focus-within .all-text {
opacity: 0.45;
}
.ant-select-selection-search-input {
caret-color: auto;
}
.ant-select-selection-placeholder {
display: none;
}
}
}

View File

@@ -24,10 +24,9 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
highlightSearch?: boolean;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
popupMatchSelectWidth?: boolean;
errorMessage?: string | null;
errorMessage?: string;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
showIncompleteDataMessage?: boolean;
}
export interface CustomTagProps {
@@ -52,12 +51,10 @@ export interface CustomMultiSelectProps
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
highlightSearch?: boolean;
errorMessage?: string | null;
errorMessage?: string;
popupClassName?: string;
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
maxTagCount?: number;
allowClear?: SelectProps['allowClear'];
onRetry?: () => void;
maxTagTextLength?: number;
showIncompleteDataMessage?: boolean;
}

View File

@@ -133,15 +133,3 @@ export const filterOptionsBySearch = (
})
.filter(Boolean) as OptionData[];
};
/**
* Utility function to handle dropdown scroll and detect when scrolled to bottom
* Returns true when scrolled to within 20px of the bottom
*/
export const handleScrollToBottom = (
e: React.UIEvent<HTMLDivElement>,
): boolean => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
// Consider "scrolled to bottom" when within 20px of the bottom or at the bottom
return scrollHeight - scrollTop - clientHeight < 20;
};

View File

@@ -44,14 +44,13 @@
.lightMode {
.metrics-select-container {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
border: 1px solid var(--bg-slate-300) !important;
background: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300) !important;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: none;

View File

@@ -127,7 +127,6 @@
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -163,6 +162,7 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -331,14 +331,13 @@
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
}
}
}

View File

@@ -271,13 +271,16 @@
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -585,7 +585,7 @@
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
font-weight: 600;
}
}
}

View File

@@ -155,7 +155,6 @@ function QuerySearch({
// Reference to the editor view for programmatic autocompletion
const editorRef = useRef<EditorView | null>(null);
const lastKeyRef = useRef<string>('');
const lastFetchedKeyRef = useRef<string>('');
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
@@ -213,9 +212,6 @@ function QuerySearch({
setKeySuggestions([]);
return;
}
lastFetchedKeyRef.current = searchText || '';
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
@@ -383,7 +379,6 @@ function QuerySearch({
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
@@ -475,7 +470,6 @@ function QuerySearch({
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
toggleSuggestions,
],
@@ -821,7 +815,7 @@ function QuerySearch({
option.label.toLowerCase().includes(searchText),
);
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
if (options.length === 0 && lastKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}

View File

@@ -1,536 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { convertFiltersToExpression } from '../utils';
describe('convertFiltersToExpression', () => {
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: '!=',
value: 'error',
},
{
id: '3',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '4',
key: { key: 'count', type: 'number' },
op: '<=',
value: 50,
},
{
id: '5',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '6',
key: { key: 'enabled', type: 'boolean' },
op: '=',
value: false,
},
{
id: '7',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '7',
key: { key: 'regex', type: 'string' },
op: 'regex',
value: '.*',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
});
it('should handle string value formatting and escaping', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'message', type: 'string' },
op: '=',
value: "user's data",
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: '=',
value: '',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/api/v1/users'",
});
});
it('should handle IN operator with various value types and array formatting', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service'],
},
{
id: '2',
key: { key: 'status', type: 'string' },
op: 'IN',
value: 'success', // Single value should be converted to array
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'IN',
value: [], // Empty array
},
{
id: '4',
key: { key: 'name', type: 'string' },
op: 'IN',
value: ["John's", "Mary's", 'Bob'], // Values with quotes
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
});
it('should convert deprecated operators to their modern equivalents', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'nin',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'message', type: 'string' },
op: 'nlike',
value: 'error',
},
{
id: '3',
key: { key: 'path', type: 'string' },
op: 'nregex',
value: '/api/.*',
},
{
id: '4',
key: { key: 'service', type: 'string' },
op: 'NIN', // Test case insensitivity
value: ['api-gateway'],
},
{
id: '5',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '6',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '7',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '8',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
it('should handle non-value operators and function operators', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '', // Value should be ignored for EXISTS
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: 'some-value', // Value should be ignored for EXISTS
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'tags', type: 'string' },
op: 'hasAny',
value: ['production', 'staging'],
},
{
id: '5',
key: { key: 'tags', type: 'string' },
op: 'hasAll',
value: ['production', 'monitoring'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
});
it('should filter out invalid filters and handle edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: '=',
value: 'api-gateway',
},
{
id: '2',
key: undefined, // Invalid filter - should be skipped
op: '=',
value: 'error',
},
{
id: '3',
key: { key: '', type: 'string' }, // Invalid filter with empty key - should be skipped
op: '=',
value: 'test',
},
{
id: '4',
key: { key: 'status', type: 'string' },
op: ' = ', // Test whitespace handling
value: 'success',
},
{
id: '5',
key: { key: 'service', type: 'string' },
op: 'In', // Test mixed case handling
value: ['api-gateway'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
});
it('should handle complex mixed operator scenarios with proper joining', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'service', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service'],
},
{
id: '2',
key: { key: 'user_id', type: 'string' },
op: 'EXISTS',
value: '',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '4',
key: { key: 'duration', type: 'number' },
op: '>',
value: 100,
},
{
id: '5',
key: { key: 'status', type: 'string' },
op: 'nin',
value: ['error', 'timeout'],
},
{
id: '6',
key: { key: 'method', type: 'string' },
op: '=',
value: 'POST',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
});
it('should handle all numeric comparison operators and edge cases', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'count', type: 'number' },
op: '=',
value: 0,
},
{
id: '2',
key: { key: 'score', type: 'number' },
op: '>',
value: 100,
},
{
id: '3',
key: { key: 'limit', type: 'number' },
op: '>=',
value: 50,
},
{
id: '4',
key: { key: 'threshold', type: 'number' },
op: '<',
value: 1000,
},
{
id: '5',
key: { key: 'max_value', type: 'number' },
op: '<=',
value: 999,
},
{
id: '6',
key: { key: 'values', type: 'string' },
op: 'IN',
value: ['1', '2', '3', '4', '5'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
});
it('should handle boolean values and string comparisons with special characters', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'is_active', type: 'boolean' },
op: '=',
value: true,
},
{
id: '2',
key: { key: 'is_deleted', type: 'boolean' },
op: '=',
value: false,
},
{
id: '3',
key: { key: 'email', type: 'string' },
op: '=',
value: 'user@example.com',
},
{
id: '4',
key: { key: 'description', type: 'string' },
op: '=',
value: 'Contains "quotes" and \'apostrophes\'',
},
{
id: '5',
key: { key: 'path', type: 'string' },
op: '=',
value: '/api/v1/users/123?filter=true',
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"is_active = true AND is_deleted = false AND email = 'user@example.com' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/api/v1/users/123?filter=true'",
});
});
it('should handle all function operators and complex array scenarios', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'tags', type: 'string' },
op: 'has',
value: 'production',
},
{
id: '2',
key: { key: 'labels', type: 'string' },
op: 'hasAny',
value: ['env:prod', 'service:api'],
},
{
id: '3',
key: { key: 'metadata', type: 'string' },
op: 'hasAll',
value: ['version:1.0', 'team:backend'],
},
{
id: '4',
key: { key: 'services', type: 'string' },
op: 'IN',
value: ['api-gateway', 'user-service', 'auth-service', 'payment-service'],
},
{
id: '5',
key: { key: 'excluded_services', type: 'string' },
op: 'nin',
value: ['legacy-service', 'deprecated-service'],
},
{
id: '6',
key: { key: 'status_codes', type: 'string' },
op: 'IN',
value: ['200', '201', '400', '500'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
});
it('should handle specific deprecated operators: nhas, ncontains, nexists', () => {
const filters: TagFilter = {
items: [
{
id: '1',
key: { key: 'user_id', type: 'string' },
op: 'nexists',
value: '',
},
{
id: '2',
key: { key: 'description', type: 'string' },
op: 'ncontains',
value: 'error',
},
{
id: '3',
key: { key: 'tags', type: 'string' },
op: 'nhas',
value: 'production',
},
{
id: '4',
key: { key: 'labels', type: 'string' },
op: 'nhasany',
value: ['env:prod', 'service:api'],
},
],
op: 'AND',
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expression:
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
});
});

View File

@@ -1,10 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import {
DEPRECATED_OPERATORS_MAP,
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { cloneDeep } from 'lodash-es';
import { IQueryPair } from 'types/antlrQueryTypes';
@@ -25,7 +21,7 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { unquote } from 'utils/stringUtils';
import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
import { isFunctionOperator } from 'utils/tokenUtils';
import { v4 as uuid } from 'uuid';
/**
@@ -91,32 +87,12 @@ export const convertFiltersToExpression = (
return '';
}
let operator = op.trim().toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(operator)) {
operator =
DEPRECATED_OPERATORS_MAP[
operator as keyof typeof DEPRECATED_OPERATORS_MAP
];
if (isFunctionOperator(op)) {
return `${op}(${key.key}, ${value})`;
}
if (isNonValueOperator(operator)) {
return `${key.key} ${operator}`;
}
if (isFunctionOperator(operator)) {
// Get the proper function name from QUERY_BUILDER_FUNCTIONS
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
const properFunctionName =
functionOperators.find(
(func: string) => func.toLowerCase() === operator.toLowerCase(),
) || operator;
const formattedValue = formatValueForExpression(value, operator);
return `${properFunctionName}(${key.key}, ${formattedValue})`;
}
const formattedValue = formatValueForExpression(value, operator);
return `${key.key} ${operator} ${formattedValue}`;
const formattedValue = formatValueForExpression(value, op);
return `${key.key} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions
@@ -141,6 +117,7 @@ export const convertExpressionToFilters = (
if (!expression) return [];
const queryPairs = extractQueryPairs(expression);
const filters: TagFilterItem[] = [];
queryPairs.forEach((pair) => {
@@ -168,36 +145,19 @@ export const convertFiltersToExpressionWithExistingQuery = (
filters: TagFilter,
existingQuery: string | undefined,
): { filters: TagFilter; filter: { expression: string } } => {
// Check for deprecated operators and replace them with new operators
const updatedFilters = cloneDeep(filters);
// Replace deprecated operators in filter items
if (updatedFilters?.items) {
updatedFilters.items = updatedFilters.items.map((item) => {
const opLower = item.op?.toLowerCase();
if (Object.keys(DEPRECATED_OPERATORS_MAP).includes(opLower)) {
return {
...item,
op: DEPRECATED_OPERATORS_MAP[
opLower as keyof typeof DEPRECATED_OPERATORS_MAP
].toLowerCase(),
};
}
return item;
});
}
if (!existingQuery) {
// If no existing query, return filters with a newly generated expression
return {
filters: updatedFilters,
filter: convertFiltersToExpression(updatedFilters),
filters,
filter: convertFiltersToExpression(filters),
};
}
// Extract query pairs from the existing query
const queryPairs = extractQueryPairs(existingQuery.trim());
let queryPairsMap: Map<string, IQueryPair> = new Map();
const updatedFilters = cloneDeep(filters); // Clone filters to avoid direct mutation
const nonExistingFilters: TagFilterItem[] = [];
let modifiedQuery = existingQuery; // We'll modify this query as we proceed
const visitedPairs: Set<string> = new Set(); // Set to track visited query pairs

View File

@@ -5,11 +5,8 @@ import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useMemo } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
@@ -43,10 +40,6 @@ function OtherFilters({
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const isMeterDataSource = useMemo(
() => signal && signal === SignalType.METER_EXPLORER,
[signal],
);
const {
data: suggestionsData,
@@ -76,22 +69,7 @@ function OtherFilters({
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
},
);
const {
data: fieldKeysData,
isLoading: isLoadingFieldKeys,
} = useGetQueryKeySuggestions(
{
searchText: inputValue,
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
signalSource: 'meter',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isMeterDataSource,
enabled: !!signal && !isLogDataSource,
},
);
@@ -99,33 +77,13 @@ function OtherFilters({
let filterAttributes;
if (isLogDataSource) {
filterAttributes = suggestionsData?.payload?.attributes || [];
} else if (isMeterDataSource) {
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
fieldKeysData?.data?.data?.keys || {},
)?.flat();
filterAttributes = fieldKeys.map(
(attr) =>
({
key: attr.name,
dataType: attr.fieldDataType,
type: attr.fieldContext,
signal: attr.signal,
} as BaseAutocompleteData),
);
} else {
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
}
return filterAttributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
);
}, [
suggestionsData,
aggregateKeysData,
addedFilters,
isLogDataSource,
fieldKeysData,
isMeterDataSource,
]);
}, [suggestionsData, aggregateKeysData, addedFilters, isLogDataSource]);
const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [
@@ -141,8 +99,7 @@ function OtherFilters({
};
const renderFilters = (): React.ReactNode => {
const isLoading =
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
if (isLoading) return <OtherFiltersSkeleton />;
if (!otherFilters?.length)
return <div className="no-values-found">No values found</div>;

View File

@@ -54,7 +54,6 @@ const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
const FILTER_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@@ -78,11 +77,7 @@ const setupServer = (): void => {
putHandler(await req.json());
return res(ctx.status(200), ctx.json({}));
}),
rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
rest.get(fieldsValuesURL, (req, res, ctx) =>
rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
);

View File

@@ -23,7 +23,7 @@ export enum SignalType {
LOGS = 'logs',
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
METER_EXPLORER = 'meter_explorer',
}
export interface IQuickFiltersConfig {
@@ -54,5 +54,5 @@ export enum QuickFiltersSource {
TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
METER_EXPLORER = 'meter-explorer',
}

View File

@@ -1,63 +0,0 @@
import './styles.scss';
import { Select } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { UniversalYAxisUnitMappings, Y_AXIS_CATEGORIES } from './constants';
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
import { mapMetricUnitToUniversalUnit } from './utils';
function YAxisUnitSelector({
value,
onChange,
placeholder = 'Please select a unit',
loading = false,
}: YAxisUnitSelectorProps): JSX.Element {
const universalUnit = mapMetricUnitToUniversalUnit(value);
const handleSearch = (
searchTerm: string,
currentOption: DefaultOptionType | undefined,
): boolean => {
if (!currentOption?.value) return false;
const search = searchTerm.toLowerCase();
const unitId = currentOption.value.toString().toLowerCase();
const unitLabel = currentOption.children?.toString().toLowerCase() || '';
// Check label and id
if (unitId.includes(search) || unitLabel.includes(search)) return true;
// Check aliases (from the mapping) using array iteration
const aliases = Array.from(
UniversalYAxisUnitMappings[currentOption.value as UniversalYAxisUnit] ?? [],
);
return aliases.some((alias) => alias.toLowerCase().includes(search));
};
return (
<div className="y-axis-unit-selector-component">
<Select
showSearch
value={universalUnit}
onChange={onChange}
placeholder={placeholder}
filterOption={(input, option): boolean => handleSearch(input, option)}
loading={loading}
>
{Y_AXIS_CATEGORIES.map((category) => (
<Select.OptGroup key={category.name} label={category.name}>
{category.units.map((unit) => (
<Select.Option key={unit.id} value={unit.id}>
{unit.name}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</div>
);
}
export default YAxisUnitSelector;

View File

@@ -1,68 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import YAxisUnitSelector from '../YAxisUnitSelector';
describe('YAxisUnitSelector', () => {
const mockOnChange = jest.fn();
beforeEach(() => {
mockOnChange.mockClear();
});
it('renders with default placeholder', () => {
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
expect(screen.getByText('Please select a unit')).toBeInTheDocument();
});
it('renders with custom placeholder', () => {
render(
<YAxisUnitSelector
value=""
onChange={mockOnChange}
placeholder="Custom placeholder"
/>,
);
expect(screen.queryByText('Custom placeholder')).toBeInTheDocument();
});
it('calls onChange when a value is selected', () => {
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
const select = screen.getByRole('combobox');
fireEvent.mouseDown(select);
const option = screen.getByText('Bytes (B)');
fireEvent.click(option);
expect(mockOnChange).toHaveBeenCalledWith('By', {
children: 'Bytes (B)',
key: 'By',
value: 'By',
});
});
it('filters options based on search input', () => {
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
const select = screen.getByRole('combobox');
fireEvent.mouseDown(select);
const input = screen.getByRole('combobox');
fireEvent.change(input, { target: { value: 'byte' } });
expect(screen.getByText('Bytes/sec')).toBeInTheDocument();
});
it('shows all categories and their units', () => {
render(<YAxisUnitSelector value="" onChange={mockOnChange} />);
const select = screen.getByRole('combobox');
fireEvent.mouseDown(select);
// Check for category headers
expect(screen.getByText('Data')).toBeInTheDocument();
expect(screen.getByText('Time')).toBeInTheDocument();
// Check for some common units
expect(screen.getByText('Bytes (B)')).toBeInTheDocument();
expect(screen.getByText('Seconds (s)')).toBeInTheDocument();
});
});

View File

@@ -1,39 +0,0 @@
import {
getUniversalNameFromMetricUnit,
mapMetricUnitToUniversalUnit,
} from '../utils';
describe('YAxisUnitSelector utils', () => {
describe('mapMetricUnitToUniversalUnit', () => {
it('maps known units correctly', () => {
expect(mapMetricUnitToUniversalUnit('bytes')).toBe('By');
expect(mapMetricUnitToUniversalUnit('seconds')).toBe('s');
expect(mapMetricUnitToUniversalUnit('bytes_per_second')).toBe('By/s');
});
it('returns null or self for unknown units', () => {
expect(mapMetricUnitToUniversalUnit('unknown_unit')).toBe('unknown_unit');
expect(mapMetricUnitToUniversalUnit('')).toBe(null);
expect(mapMetricUnitToUniversalUnit(undefined)).toBe(null);
});
});
describe('getUniversalNameFromMetricUnit', () => {
it('returns human readable names for known units', () => {
expect(getUniversalNameFromMetricUnit('bytes')).toBe('Bytes (B)');
expect(getUniversalNameFromMetricUnit('seconds')).toBe('Seconds (s)');
expect(getUniversalNameFromMetricUnit('bytes_per_second')).toBe('Bytes/sec');
});
it('returns original unit for unknown units', () => {
expect(getUniversalNameFromMetricUnit('unknown_unit')).toBe('unknown_unit');
expect(getUniversalNameFromMetricUnit('')).toBe('-');
expect(getUniversalNameFromMetricUnit(undefined)).toBe('-');
});
it('handles case variations', () => {
expect(getUniversalNameFromMetricUnit('bytes')).toBe('Bytes (B)');
expect(getUniversalNameFromMetricUnit('s')).toBe('Seconds (s)');
});
});
});

View File

@@ -1,627 +0,0 @@
import { UniversalYAxisUnit, YAxisUnit } from './types';
// Mapping of universal y-axis units to their AWS, UCUM, and OpenMetrics equivalents
export const UniversalYAxisUnitMappings: Record<
UniversalYAxisUnit,
Set<YAxisUnit>
> = {
// Time
[UniversalYAxisUnit.NANOSECONDS]: new Set([
YAxisUnit.UCUM_NANOSECONDS,
YAxisUnit.OPEN_METRICS_NANOSECONDS,
]),
[UniversalYAxisUnit.MICROSECONDS]: new Set([
YAxisUnit.AWS_MICROSECONDS,
YAxisUnit.UCUM_MICROSECONDS,
YAxisUnit.OPEN_METRICS_MICROSECONDS,
]),
[UniversalYAxisUnit.MILLISECONDS]: new Set([
YAxisUnit.AWS_MILLISECONDS,
YAxisUnit.UCUM_MILLISECONDS,
YAxisUnit.OPEN_METRICS_MILLISECONDS,
]),
[UniversalYAxisUnit.SECONDS]: new Set([
YAxisUnit.AWS_SECONDS,
YAxisUnit.UCUM_SECONDS,
YAxisUnit.OPEN_METRICS_SECONDS,
]),
[UniversalYAxisUnit.MINUTES]: new Set([
YAxisUnit.UCUM_MINUTES,
YAxisUnit.OPEN_METRICS_MINUTES,
]),
[UniversalYAxisUnit.HOURS]: new Set([
YAxisUnit.UCUM_HOURS,
YAxisUnit.OPEN_METRICS_HOURS,
]),
[UniversalYAxisUnit.DAYS]: new Set([
YAxisUnit.UCUM_DAYS,
YAxisUnit.OPEN_METRICS_DAYS,
]),
[UniversalYAxisUnit.WEEKS]: new Set([YAxisUnit.UCUM_WEEKS]),
// Data
[UniversalYAxisUnit.BYTES]: new Set([
YAxisUnit.AWS_BYTES,
YAxisUnit.UCUM_BYTES,
YAxisUnit.OPEN_METRICS_BYTES,
]),
[UniversalYAxisUnit.KILOBYTES]: new Set([
YAxisUnit.AWS_KILOBYTES,
YAxisUnit.UCUM_KILOBYTES,
YAxisUnit.OPEN_METRICS_KILOBYTES,
]),
[UniversalYAxisUnit.MEGABYTES]: new Set([
YAxisUnit.AWS_MEGABYTES,
YAxisUnit.UCUM_MEGABYTES,
YAxisUnit.OPEN_METRICS_MEGABYTES,
]),
[UniversalYAxisUnit.GIGABYTES]: new Set([
YAxisUnit.AWS_GIGABYTES,
YAxisUnit.UCUM_GIGABYTES,
YAxisUnit.OPEN_METRICS_GIGABYTES,
]),
[UniversalYAxisUnit.TERABYTES]: new Set([
YAxisUnit.AWS_TERABYTES,
YAxisUnit.UCUM_TERABYTES,
YAxisUnit.OPEN_METRICS_TERABYTES,
]),
[UniversalYAxisUnit.PETABYTES]: new Set([
YAxisUnit.AWS_PETABYTES,
YAxisUnit.UCUM_PEBIBYTES,
YAxisUnit.OPEN_METRICS_PEBIBYTES,
]),
[UniversalYAxisUnit.EXABYTES]: new Set([
YAxisUnit.AWS_EXABYTES,
YAxisUnit.UCUM_EXABYTES,
YAxisUnit.OPEN_METRICS_EXABYTES,
]),
[UniversalYAxisUnit.ZETTABYTES]: new Set([
YAxisUnit.AWS_ZETTABYTES,
YAxisUnit.UCUM_ZETTABYTES,
YAxisUnit.OPEN_METRICS_ZETTABYTES,
]),
[UniversalYAxisUnit.YOTTABYTES]: new Set([
YAxisUnit.AWS_YOTTABYTES,
YAxisUnit.UCUM_YOTTABYTES,
YAxisUnit.OPEN_METRICS_YOTTABYTES,
]),
// Data Rate
[UniversalYAxisUnit.BYTES_SECOND]: new Set([
YAxisUnit.AWS_BYTES_SECOND,
YAxisUnit.UCUM_BYTES_SECOND,
YAxisUnit.OPEN_METRICS_BYTES_SECOND,
]),
[UniversalYAxisUnit.KILOBYTES_SECOND]: new Set([
YAxisUnit.AWS_KILOBYTES_SECOND,
YAxisUnit.UCUM_KILOBYTES_SECOND,
YAxisUnit.OPEN_METRICS_KILOBYTES_SECOND,
]),
[UniversalYAxisUnit.MEGABYTES_SECOND]: new Set([
YAxisUnit.AWS_MEGABYTES_SECOND,
YAxisUnit.UCUM_MEGABYTES_SECOND,
YAxisUnit.OPEN_METRICS_MEGABYTES_SECOND,
]),
[UniversalYAxisUnit.GIGABYTES_SECOND]: new Set([
YAxisUnit.AWS_GIGABYTES_SECOND,
YAxisUnit.UCUM_GIGABYTES_SECOND,
YAxisUnit.OPEN_METRICS_GIGABYTES_SECOND,
]),
[UniversalYAxisUnit.TERABYTES_SECOND]: new Set([
YAxisUnit.AWS_TERABYTES_SECOND,
YAxisUnit.UCUM_TERABYTES_SECOND,
YAxisUnit.OPEN_METRICS_TERABYTES_SECOND,
]),
[UniversalYAxisUnit.PETABYTES_SECOND]: new Set([
YAxisUnit.AWS_PETABYTES_SECOND,
YAxisUnit.UCUM_PETABYTES_SECOND,
YAxisUnit.OPEN_METRICS_PETABYTES_SECOND,
]),
[UniversalYAxisUnit.EXABYTES_SECOND]: new Set([
YAxisUnit.AWS_EXABYTES_SECOND,
YAxisUnit.UCUM_EXABYTES_SECOND,
YAxisUnit.OPEN_METRICS_EXABYTES_SECOND,
]),
[UniversalYAxisUnit.ZETTABYTES_SECOND]: new Set([
YAxisUnit.AWS_ZETTABYTES_SECOND,
YAxisUnit.UCUM_ZETTABYTES_SECOND,
YAxisUnit.OPEN_METRICS_ZETTABYTES_SECOND,
]),
[UniversalYAxisUnit.YOTTABYTES_SECOND]: new Set([
YAxisUnit.AWS_YOTTABYTES_SECOND,
YAxisUnit.UCUM_YOTTABYTES_SECOND,
YAxisUnit.OPEN_METRICS_YOTTABYTES_SECOND,
]),
// Bits
[UniversalYAxisUnit.BITS]: new Set([
YAxisUnit.AWS_BITS,
YAxisUnit.UCUM_BITS,
YAxisUnit.OPEN_METRICS_BITS,
]),
[UniversalYAxisUnit.KILOBITS]: new Set([
YAxisUnit.AWS_KILOBITS,
YAxisUnit.UCUM_KILOBITS,
YAxisUnit.OPEN_METRICS_KILOBITS,
]),
[UniversalYAxisUnit.MEGABITS]: new Set([
YAxisUnit.AWS_MEGABITS,
YAxisUnit.UCUM_MEGABITS,
YAxisUnit.OPEN_METRICS_MEGABITS,
]),
[UniversalYAxisUnit.GIGABITS]: new Set([
YAxisUnit.AWS_GIGABITS,
YAxisUnit.UCUM_GIGABITS,
YAxisUnit.OPEN_METRICS_GIGABITS,
]),
[UniversalYAxisUnit.TERABITS]: new Set([
YAxisUnit.AWS_TERABITS,
YAxisUnit.UCUM_TERABITS,
YAxisUnit.OPEN_METRICS_TERABITS,
]),
[UniversalYAxisUnit.PETABITS]: new Set([
YAxisUnit.AWS_PETABITS,
YAxisUnit.UCUM_PETABITS,
YAxisUnit.OPEN_METRICS_PETABITS,
]),
[UniversalYAxisUnit.EXABITS]: new Set([
YAxisUnit.AWS_EXABITS,
YAxisUnit.UCUM_EXABITS,
YAxisUnit.OPEN_METRICS_EXABITS,
]),
[UniversalYAxisUnit.ZETTABITS]: new Set([
YAxisUnit.AWS_ZETTABITS,
YAxisUnit.UCUM_ZETTABITS,
YAxisUnit.OPEN_METRICS_ZETTABITS,
]),
[UniversalYAxisUnit.YOTTABITS]: new Set([
YAxisUnit.AWS_YOTTABITS,
YAxisUnit.UCUM_YOTTABITS,
YAxisUnit.OPEN_METRICS_YOTTABITS,
]),
// Bit Rate
[UniversalYAxisUnit.BITS_SECOND]: new Set([
YAxisUnit.AWS_BITS_SECOND,
YAxisUnit.UCUM_BITS_SECOND,
YAxisUnit.OPEN_METRICS_BITS_SECOND,
]),
[UniversalYAxisUnit.KILOBITS_SECOND]: new Set([
YAxisUnit.AWS_KILOBITS_SECOND,
YAxisUnit.UCUM_KILOBITS_SECOND,
YAxisUnit.OPEN_METRICS_KILOBITS_SECOND,
]),
[UniversalYAxisUnit.MEGABITS_SECOND]: new Set([
YAxisUnit.AWS_MEGABITS_SECOND,
YAxisUnit.UCUM_MEGABITS_SECOND,
YAxisUnit.OPEN_METRICS_MEGABITS_SECOND,
]),
[UniversalYAxisUnit.GIGABITS_SECOND]: new Set([
YAxisUnit.AWS_GIGABITS_SECOND,
YAxisUnit.UCUM_GIGABITS_SECOND,
YAxisUnit.OPEN_METRICS_GIGABITS_SECOND,
]),
[UniversalYAxisUnit.TERABITS_SECOND]: new Set([
YAxisUnit.AWS_TERABITS_SECOND,
YAxisUnit.UCUM_TERABITS_SECOND,
YAxisUnit.OPEN_METRICS_TERABITS_SECOND,
]),
[UniversalYAxisUnit.PETABITS_SECOND]: new Set([
YAxisUnit.AWS_PETABITS_SECOND,
YAxisUnit.UCUM_PETABITS_SECOND,
YAxisUnit.OPEN_METRICS_PETABITS_SECOND,
]),
[UniversalYAxisUnit.EXABITS_SECOND]: new Set([
YAxisUnit.AWS_EXABITS_SECOND,
YAxisUnit.UCUM_EXABITS_SECOND,
YAxisUnit.OPEN_METRICS_EXABITS_SECOND,
]),
[UniversalYAxisUnit.ZETTABITS_SECOND]: new Set([
YAxisUnit.AWS_ZETTABITS_SECOND,
YAxisUnit.UCUM_ZETTABITS_SECOND,
YAxisUnit.OPEN_METRICS_ZETTABITS_SECOND,
]),
[UniversalYAxisUnit.YOTTABITS_SECOND]: new Set([
YAxisUnit.AWS_YOTTABITS_SECOND,
YAxisUnit.UCUM_YOTTABITS_SECOND,
YAxisUnit.OPEN_METRICS_YOTTABITS_SECOND,
]),
// Count
[UniversalYAxisUnit.COUNT]: new Set([
YAxisUnit.AWS_COUNT,
YAxisUnit.UCUM_COUNT,
YAxisUnit.OPEN_METRICS_COUNT,
]),
[UniversalYAxisUnit.COUNT_SECOND]: new Set([
YAxisUnit.AWS_COUNT_SECOND,
YAxisUnit.UCUM_COUNT_SECOND,
YAxisUnit.OPEN_METRICS_COUNT_SECOND,
]),
// Percent
[UniversalYAxisUnit.PERCENT]: new Set([
YAxisUnit.AWS_PERCENT,
YAxisUnit.UCUM_PERCENT,
YAxisUnit.OPEN_METRICS_PERCENT,
]),
[UniversalYAxisUnit.NONE]: new Set([
YAxisUnit.AWS_NONE,
YAxisUnit.UCUM_NONE,
YAxisUnit.OPEN_METRICS_NONE,
]),
[UniversalYAxisUnit.PERCENT_UNIT]: new Set([
YAxisUnit.OPEN_METRICS_PERCENT_UNIT,
]),
// Count Rate
[UniversalYAxisUnit.COUNT_MINUTE]: new Set([
YAxisUnit.UCUM_COUNTS_MINUTE,
YAxisUnit.OPEN_METRICS_COUNTS_MINUTE,
]),
[UniversalYAxisUnit.OPS_SECOND]: new Set([
YAxisUnit.UCUM_OPS_SECOND,
YAxisUnit.OPEN_METRICS_OPS_SECOND,
]),
[UniversalYAxisUnit.OPS_MINUTE]: new Set([
YAxisUnit.UCUM_OPS_MINUTE,
YAxisUnit.OPEN_METRICS_OPS_MINUTE,
]),
[UniversalYAxisUnit.REQUESTS_SECOND]: new Set([
YAxisUnit.UCUM_REQUESTS_SECOND,
YAxisUnit.OPEN_METRICS_REQUESTS_SECOND,
]),
[UniversalYAxisUnit.REQUESTS_MINUTE]: new Set([
YAxisUnit.UCUM_REQUESTS_MINUTE,
YAxisUnit.OPEN_METRICS_REQUESTS_MINUTE,
]),
[UniversalYAxisUnit.READS_SECOND]: new Set([
YAxisUnit.UCUM_READS_SECOND,
YAxisUnit.OPEN_METRICS_READS_SECOND,
]),
[UniversalYAxisUnit.WRITES_SECOND]: new Set([
YAxisUnit.UCUM_WRITES_SECOND,
YAxisUnit.OPEN_METRICS_WRITES_SECOND,
]),
[UniversalYAxisUnit.READS_MINUTE]: new Set([
YAxisUnit.UCUM_READS_MINUTE,
YAxisUnit.OPEN_METRICS_READS_MINUTE,
]),
[UniversalYAxisUnit.WRITES_MINUTE]: new Set([
YAxisUnit.UCUM_WRITES_MINUTE,
YAxisUnit.OPEN_METRICS_WRITES_MINUTE,
]),
[UniversalYAxisUnit.IOOPS_SECOND]: new Set([
YAxisUnit.UCUM_IOPS_SECOND,
YAxisUnit.OPEN_METRICS_IOPS_SECOND,
]),
};
// Mapping of universal y-axis units to their display labels
export const Y_AXIS_UNIT_NAMES: Record<UniversalYAxisUnit, string> = {
[UniversalYAxisUnit.SECONDS]: 'Seconds (s)',
[UniversalYAxisUnit.MILLISECONDS]: 'Milliseconds (ms)',
[UniversalYAxisUnit.MICROSECONDS]: 'Microseconds (µs)',
[UniversalYAxisUnit.BYTES]: 'Bytes (B)',
[UniversalYAxisUnit.KILOBYTES]: 'Kilobytes (KB)',
[UniversalYAxisUnit.MEGABYTES]: 'Megabytes (MB)',
[UniversalYAxisUnit.GIGABYTES]: 'Gigabytes (GB)',
[UniversalYAxisUnit.TERABYTES]: 'Terabytes (TB)',
[UniversalYAxisUnit.PETABYTES]: 'Petabytes (PB)',
[UniversalYAxisUnit.EXABYTES]: 'Exabytes (EB)',
[UniversalYAxisUnit.ZETTABYTES]: 'Zettabytes (ZB)',
[UniversalYAxisUnit.YOTTABYTES]: 'Yottabytes (YB)',
[UniversalYAxisUnit.BITS]: 'Bits (b)',
[UniversalYAxisUnit.KILOBITS]: 'Kilobits (Kb)',
[UniversalYAxisUnit.MEGABITS]: 'Megabits (Mb)',
[UniversalYAxisUnit.GIGABITS]: 'Gigabits (Gb)',
[UniversalYAxisUnit.TERABITS]: 'Terabits (Tb)',
[UniversalYAxisUnit.PETABITS]: 'Petabits (Pb)',
[UniversalYAxisUnit.EXABITS]: 'Exabits (Eb)',
[UniversalYAxisUnit.ZETTABITS]: 'Zettabits (Zb)',
[UniversalYAxisUnit.YOTTABITS]: 'Yottabits (Yb)',
[UniversalYAxisUnit.BYTES_SECOND]: 'Bytes/sec',
[UniversalYAxisUnit.KILOBYTES_SECOND]: 'Kilobytes/sec',
[UniversalYAxisUnit.MEGABYTES_SECOND]: 'Megabytes/sec',
[UniversalYAxisUnit.GIGABYTES_SECOND]: 'Gigabytes/sec',
[UniversalYAxisUnit.TERABYTES_SECOND]: 'Terabytes/sec',
[UniversalYAxisUnit.PETABYTES_SECOND]: 'Petabytes/sec',
[UniversalYAxisUnit.EXABYTES_SECOND]: 'Exabytes/sec',
[UniversalYAxisUnit.ZETTABYTES_SECOND]: 'Zettabytes/sec',
[UniversalYAxisUnit.YOTTABYTES_SECOND]: 'Yottabytes/sec',
[UniversalYAxisUnit.BITS_SECOND]: 'Bits/sec',
[UniversalYAxisUnit.KILOBITS_SECOND]: 'Kilobits/sec',
[UniversalYAxisUnit.MEGABITS_SECOND]: 'Megabits/sec',
[UniversalYAxisUnit.GIGABITS_SECOND]: 'Gigabits/sec',
[UniversalYAxisUnit.TERABITS_SECOND]: 'Terabits/sec',
[UniversalYAxisUnit.PETABITS_SECOND]: 'Petabits/sec',
[UniversalYAxisUnit.EXABITS_SECOND]: 'Exabits/sec',
[UniversalYAxisUnit.ZETTABITS_SECOND]: 'Zettabits/sec',
[UniversalYAxisUnit.YOTTABITS_SECOND]: 'Yottabits/sec',
[UniversalYAxisUnit.COUNT]: 'Count',
[UniversalYAxisUnit.COUNT_SECOND]: 'Count/sec',
[UniversalYAxisUnit.PERCENT]: 'Percent (0 - 100)',
[UniversalYAxisUnit.NONE]: 'None',
[UniversalYAxisUnit.WEEKS]: 'Weeks',
[UniversalYAxisUnit.DAYS]: 'Days',
[UniversalYAxisUnit.HOURS]: 'Hours',
[UniversalYAxisUnit.MINUTES]: 'Minutes',
[UniversalYAxisUnit.NANOSECONDS]: 'Nanoseconds',
[UniversalYAxisUnit.COUNT_MINUTE]: 'Count/min',
[UniversalYAxisUnit.OPS_SECOND]: 'Ops/sec',
[UniversalYAxisUnit.OPS_MINUTE]: 'Ops/min',
[UniversalYAxisUnit.REQUESTS_SECOND]: 'Requests/sec',
[UniversalYAxisUnit.REQUESTS_MINUTE]: 'Requests/min',
[UniversalYAxisUnit.READS_SECOND]: 'Reads/sec',
[UniversalYAxisUnit.WRITES_SECOND]: 'Writes/sec',
[UniversalYAxisUnit.READS_MINUTE]: 'Reads/min',
[UniversalYAxisUnit.WRITES_MINUTE]: 'Writes/min',
[UniversalYAxisUnit.IOOPS_SECOND]: 'IOPS/sec',
[UniversalYAxisUnit.PERCENT_UNIT]: 'Percent (0.0 - 1.0)',
};
// Splitting the universal y-axis units into categories
export const Y_AXIS_CATEGORIES = [
{
name: 'Time',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.SECONDS],
id: UniversalYAxisUnit.SECONDS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS],
id: UniversalYAxisUnit.MILLISECONDS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MICROSECONDS],
id: UniversalYAxisUnit.MICROSECONDS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NANOSECONDS],
id: UniversalYAxisUnit.NANOSECONDS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MINUTES],
id: UniversalYAxisUnit.MINUTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.HOURS],
id: UniversalYAxisUnit.HOURS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.DAYS],
id: UniversalYAxisUnit.DAYS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.WEEKS],
id: UniversalYAxisUnit.WEEKS,
},
],
},
{
name: 'Data',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES],
id: UniversalYAxisUnit.BYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.KILOBYTES],
id: UniversalYAxisUnit.KILOBYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MEGABYTES],
id: UniversalYAxisUnit.MEGABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.GIGABYTES],
id: UniversalYAxisUnit.GIGABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TERABYTES],
id: UniversalYAxisUnit.TERABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PETABYTES],
id: UniversalYAxisUnit.PETABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.EXABYTES],
id: UniversalYAxisUnit.EXABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ZETTABYTES],
id: UniversalYAxisUnit.ZETTABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.YOTTABYTES],
id: UniversalYAxisUnit.YOTTABYTES,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BITS],
id: UniversalYAxisUnit.BITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.KILOBITS],
id: UniversalYAxisUnit.KILOBITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MEGABITS],
id: UniversalYAxisUnit.MEGABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.GIGABITS],
id: UniversalYAxisUnit.GIGABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TERABITS],
id: UniversalYAxisUnit.TERABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PETABITS],
id: UniversalYAxisUnit.PETABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.EXABITS],
id: UniversalYAxisUnit.EXABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ZETTABITS],
id: UniversalYAxisUnit.ZETTABITS,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.YOTTABITS],
id: UniversalYAxisUnit.YOTTABITS,
},
],
},
{
name: 'Data Rate',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BYTES_SECOND],
id: UniversalYAxisUnit.BYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.KILOBYTES_SECOND],
id: UniversalYAxisUnit.KILOBYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MEGABYTES_SECOND],
id: UniversalYAxisUnit.MEGABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.GIGABYTES_SECOND],
id: UniversalYAxisUnit.GIGABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TERABYTES_SECOND],
id: UniversalYAxisUnit.TERABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PETABYTES_SECOND],
id: UniversalYAxisUnit.PETABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.EXABYTES_SECOND],
id: UniversalYAxisUnit.EXABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ZETTABYTES_SECOND],
id: UniversalYAxisUnit.ZETTABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.YOTTABYTES_SECOND],
id: UniversalYAxisUnit.YOTTABYTES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.BITS_SECOND],
id: UniversalYAxisUnit.BITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.KILOBITS_SECOND],
id: UniversalYAxisUnit.KILOBITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MEGABITS_SECOND],
id: UniversalYAxisUnit.MEGABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.GIGABITS_SECOND],
id: UniversalYAxisUnit.GIGABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.TERABITS_SECOND],
id: UniversalYAxisUnit.TERABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PETABITS_SECOND],
id: UniversalYAxisUnit.PETABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.EXABITS_SECOND],
id: UniversalYAxisUnit.EXABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.ZETTABITS_SECOND],
id: UniversalYAxisUnit.ZETTABITS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.YOTTABITS_SECOND],
id: UniversalYAxisUnit.YOTTABITS_SECOND,
},
],
},
{
name: 'Count',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.COUNT],
id: UniversalYAxisUnit.COUNT,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.COUNT_SECOND],
id: UniversalYAxisUnit.COUNT_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.COUNT_MINUTE],
id: UniversalYAxisUnit.COUNT_MINUTE,
},
],
},
{
name: 'Operations',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.OPS_SECOND],
id: UniversalYAxisUnit.OPS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.OPS_MINUTE],
id: UniversalYAxisUnit.OPS_MINUTE,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.REQUESTS_SECOND],
id: UniversalYAxisUnit.REQUESTS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.REQUESTS_MINUTE],
id: UniversalYAxisUnit.REQUESTS_MINUTE,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.READS_SECOND],
id: UniversalYAxisUnit.READS_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.WRITES_SECOND],
id: UniversalYAxisUnit.WRITES_SECOND,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.READS_MINUTE],
id: UniversalYAxisUnit.READS_MINUTE,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.WRITES_MINUTE],
id: UniversalYAxisUnit.WRITES_MINUTE,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.IOOPS_SECOND],
id: UniversalYAxisUnit.IOOPS_SECOND,
},
],
},
{
name: 'Percentage',
units: [
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
id: UniversalYAxisUnit.PERCENT,
},
{
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT_UNIT],
id: UniversalYAxisUnit.PERCENT_UNIT,
},
],
},
];

View File

@@ -1,3 +0,0 @@
import YAxisUnitSelector from './YAxisUnitSelector';
export default YAxisUnitSelector;

View File

@@ -1,5 +0,0 @@
.y-axis-unit-selector-component {
.ant-select {
width: 220px;
}
}

View File

@@ -1,365 +0,0 @@
export interface YAxisUnitSelectorProps {
value: string | undefined;
onChange: (value: UniversalYAxisUnit) => void;
placeholder?: string;
loading?: boolean;
disabled?: boolean;
}
export enum UniversalYAxisUnit {
// Time
WEEKS = 'wk',
DAYS = 'd',
HOURS = 'h',
MINUTES = 'min',
SECONDS = 's',
MICROSECONDS = 'us',
MILLISECONDS = 'ms',
NANOSECONDS = 'ns',
// Data
BYTES = 'By',
KILOBYTES = 'kBy',
MEGABYTES = 'MBy',
GIGABYTES = 'GBy',
TERABYTES = 'TBy',
PETABYTES = 'PBy',
EXABYTES = 'EBy',
ZETTABYTES = 'ZBy',
YOTTABYTES = 'YBy',
// Data Rate
BYTES_SECOND = 'By/s',
KILOBYTES_SECOND = 'kBy/s',
MEGABYTES_SECOND = 'MBy/s',
GIGABYTES_SECOND = 'GBy/s',
TERABYTES_SECOND = 'TBy/s',
PETABYTES_SECOND = 'PBy/s',
EXABYTES_SECOND = 'EBy/s',
ZETTABYTES_SECOND = 'ZBy/s',
YOTTABYTES_SECOND = 'YBy/s',
// Bits
BITS = 'bit',
KILOBITS = 'kbit',
MEGABITS = 'Mbit',
GIGABITS = 'Gbit',
TERABITS = 'Tbit',
PETABITS = 'Pbit',
EXABITS = 'Ebit',
ZETTABITS = 'Zbit',
YOTTABITS = 'Ybit',
// Bit Rate
BITS_SECOND = 'bit/s',
KILOBITS_SECOND = 'kbit/s',
MEGABITS_SECOND = 'Mbit/s',
GIGABITS_SECOND = 'Gbit/s',
TERABITS_SECOND = 'Tbit/s',
PETABITS_SECOND = 'Pbit/s',
EXABITS_SECOND = 'Ebit/s',
ZETTABITS_SECOND = 'Zbit/s',
YOTTABITS_SECOND = 'Ybit/s',
// Count
COUNT = '{count}',
COUNT_SECOND = '{count}/s',
COUNT_MINUTE = '{count}/min',
// Operations
OPS_SECOND = '{ops}/s',
OPS_MINUTE = '{ops}/min',
// Requests
REQUESTS_SECOND = '{req}/s',
REQUESTS_MINUTE = '{req}/min',
// Reads/Writes
READS_SECOND = '{read}/s',
WRITES_SECOND = '{write}/s',
READS_MINUTE = '{read}/min',
WRITES_MINUTE = '{write}/min',
// IO Operations
IOOPS_SECOND = '{iops}/s',
// Percent
PERCENT = '%',
PERCENT_UNIT = 'percentunit',
NONE = '1',
}
export enum YAxisUnit {
AWS_SECONDS = 'Seconds',
UCUM_SECONDS = 's',
OPEN_METRICS_SECONDS = 'seconds',
AWS_MICROSECONDS = 'Microseconds',
UCUM_MICROSECONDS = 'us',
OPEN_METRICS_MICROSECONDS = 'microseconds',
AWS_MILLISECONDS = 'Milliseconds',
UCUM_MILLISECONDS = 'ms',
OPEN_METRICS_MILLISECONDS = 'milliseconds',
AWS_BYTES = 'Bytes',
UCUM_BYTES = 'By',
OPEN_METRICS_BYTES = 'bytes',
AWS_KILOBYTES = 'Kilobytes',
UCUM_KILOBYTES = 'kBy',
OPEN_METRICS_KILOBYTES = 'kilobytes',
AWS_MEGABYTES = 'Megabytes',
UCUM_MEGABYTES = 'MBy',
OPEN_METRICS_MEGABYTES = 'megabytes',
AWS_GIGABYTES = 'Gigabytes',
UCUM_GIGABYTES = 'GBy',
OPEN_METRICS_GIGABYTES = 'gigabytes',
AWS_TERABYTES = 'Terabytes',
UCUM_TERABYTES = 'TBy',
OPEN_METRICS_TERABYTES = 'terabytes',
AWS_PETABYTES = 'Petabytes',
UCUM_PETABYTES = 'PBy',
OPEN_METRICS_PETABYTES = 'petabytes',
AWS_EXABYTES = 'Exabytes',
UCUM_EXABYTES = 'EBy',
OPEN_METRICS_EXABYTES = 'exabytes',
AWS_ZETTABYTES = 'Zettabytes',
UCUM_ZETTABYTES = 'ZBy',
OPEN_METRICS_ZETTABYTES = 'zettabytes',
AWS_YOTTABYTES = 'Yottabytes',
UCUM_YOTTABYTES = 'YBy',
OPEN_METRICS_YOTTABYTES = 'yottabytes',
AWS_BYTES_SECOND = 'Bytes/Second',
UCUM_BYTES_SECOND = 'By/s',
OPEN_METRICS_BYTES_SECOND = 'bytes_per_second',
AWS_KILOBYTES_SECOND = 'Kilobytes/Second',
UCUM_KILOBYTES_SECOND = 'kBy/s',
OPEN_METRICS_KILOBYTES_SECOND = 'kilobytes_per_second',
AWS_MEGABYTES_SECOND = 'Megabytes/Second',
UCUM_MEGABYTES_SECOND = 'MBy/s',
OPEN_METRICS_MEGABYTES_SECOND = 'megabytes_per_second',
AWS_GIGABYTES_SECOND = 'Gigabytes/Second',
UCUM_GIGABYTES_SECOND = 'GBy/s',
OPEN_METRICS_GIGABYTES_SECOND = 'gigabytes_per_second',
AWS_TERABYTES_SECOND = 'Terabytes/Second',
UCUM_TERABYTES_SECOND = 'TBy/s',
OPEN_METRICS_TERABYTES_SECOND = 'terabytes_per_second',
AWS_PETABYTES_SECOND = 'Petabytes/Second',
UCUM_PETABYTES_SECOND = 'PBy/s',
OPEN_METRICS_PETABYTES_SECOND = 'petabytes_per_second',
AWS_EXABYTES_SECOND = 'Exabytes/Second',
UCUM_EXABYTES_SECOND = 'EBy/s',
OPEN_METRICS_EXABYTES_SECOND = 'exabytes_per_second',
AWS_ZETTABYTES_SECOND = 'Zettabytes/Second',
UCUM_ZETTABYTES_SECOND = 'ZBy/s',
OPEN_METRICS_ZETTABYTES_SECOND = 'zettabytes_per_second',
AWS_YOTTABYTES_SECOND = 'Yottabytes/Second',
UCUM_YOTTABYTES_SECOND = 'YBy/s',
OPEN_METRICS_YOTTABYTES_SECOND = 'yottabytes_per_second',
AWS_BITS = 'Bits',
UCUM_BITS = 'bit',
OPEN_METRICS_BITS = 'bits',
AWS_KILOBITS = 'Kilobits',
UCUM_KILOBITS = 'kbit',
OPEN_METRICS_KILOBITS = 'kilobits',
AWS_MEGABITS = 'Megabits',
UCUM_MEGABITS = 'Mbit',
OPEN_METRICS_MEGABITS = 'megabits',
AWS_GIGABITS = 'Gigabits',
UCUM_GIGABITS = 'Gbit',
OPEN_METRICS_GIGABITS = 'gigabits',
AWS_TERABITS = 'Terabits',
UCUM_TERABITS = 'Tbit',
OPEN_METRICS_TERABITS = 'terabits',
AWS_PETABITS = 'Petabits',
UCUM_PETABITS = 'Pbit',
OPEN_METRICS_PETABITS = 'petabits',
AWS_EXABITS = 'Exabits',
UCUM_EXABITS = 'Ebit',
OPEN_METRICS_EXABITS = 'exabits',
AWS_ZETTABITS = 'Zettabits',
UCUM_ZETTABITS = 'Zbit',
OPEN_METRICS_ZETTABITS = 'zettabits',
AWS_YOTTABITS = 'Yottabits',
UCUM_YOTTABITS = 'Ybit',
OPEN_METRICS_YOTTABITS = 'yottabits',
AWS_BITS_SECOND = 'Bits/Second',
UCUM_BITS_SECOND = 'bit/s',
OPEN_METRICS_BITS_SECOND = 'bits_per_second',
AWS_KILOBITS_SECOND = 'Kilobits/Second',
UCUM_KILOBITS_SECOND = 'kbit/s',
OPEN_METRICS_KILOBITS_SECOND = 'kilobits_per_second',
AWS_MEGABITS_SECOND = 'Megabits/Second',
UCUM_MEGABITS_SECOND = 'Mbit/s',
OPEN_METRICS_MEGABITS_SECOND = 'megabits_per_second',
AWS_GIGABITS_SECOND = 'Gigabits/Second',
UCUM_GIGABITS_SECOND = 'Gbit/s',
OPEN_METRICS_GIGABITS_SECOND = 'gigabits_per_second',
AWS_TERABITS_SECOND = 'Terabits/Second',
UCUM_TERABITS_SECOND = 'Tbit/s',
OPEN_METRICS_TERABITS_SECOND = 'terabits_per_second',
AWS_PETABITS_SECOND = 'Petabits/Second',
UCUM_PETABITS_SECOND = 'Pbit/s',
OPEN_METRICS_PETABITS_SECOND = 'petabits_per_second',
AWS_EXABITS_SECOND = 'Exabits/Second',
UCUM_EXABITS_SECOND = 'Ebit/s',
OPEN_METRICS_EXABITS_SECOND = 'exabits_per_second',
AWS_ZETTABITS_SECOND = 'Zettabits/Second',
UCUM_ZETTABITS_SECOND = 'Zbit/s',
OPEN_METRICS_ZETTABITS_SECOND = 'zettabits_per_second',
AWS_YOTTABITS_SECOND = 'Yottabits/Second',
UCUM_YOTTABITS_SECOND = 'Ybit/s',
OPEN_METRICS_YOTTABITS_SECOND = 'yottabits_per_second',
AWS_COUNT = 'Count',
UCUM_COUNT = '{count}',
OPEN_METRICS_COUNT = 'count',
AWS_COUNT_SECOND = 'Count/Second',
UCUM_COUNT_SECOND = '{count}/s',
OPEN_METRICS_COUNT_SECOND = 'count_per_second',
AWS_PERCENT = 'Percent',
UCUM_PERCENT = '%',
OPEN_METRICS_PERCENT = 'ratio',
AWS_NONE = 'None',
UCUM_NONE = '1',
OPEN_METRICS_NONE = 'none',
UCUM_NANOSECONDS = 'ns',
OPEN_METRICS_NANOSECONDS = 'nanoseconds',
UCUM_MINUTES = 'min',
OPEN_METRICS_MINUTES = 'minutes',
UCUM_HOURS = 'h',
OPEN_METRICS_HOURS = 'hours',
UCUM_DAYS = 'd',
OPEN_METRICS_DAYS = 'days',
UCUM_WEEKS = 'wk',
OPEN_METRICS_WEEKS = 'weeks',
UCUM_KIBIBYTES = 'KiBy',
OPEN_METRICS_KIBIBYTES = 'kibibytes',
UCUM_MEBIBYTES = 'MiBy',
OPEN_METRICS_MEBIBYTES = 'mebibytes',
UCUM_GIBIBYTES = 'GiBy',
OPEN_METRICS_GIBIBYTES = 'gibibytes',
UCUM_TEBIBYTES = 'TiBy',
OPEN_METRICS_TEBIBYTES = 'tebibytes',
UCUM_PEBIBYTES = 'PiBy',
OPEN_METRICS_PEBIBYTES = 'pebibytes',
UCUM_KIBIBYTES_SECOND = 'KiBy/s',
OPEN_METRICS_KIBIBYTES_SECOND = 'kibibytes_per_second',
UCUM_KIBIBITS_SECOND = 'Kibit/s',
OPEN_METRICS_KIBIBITS_SECOND = 'kibibits_per_second',
UCUM_MEBIBYTES_SECOND = 'MiBy/s',
OPEN_METRICS_MEBIBYTES_SECOND = 'mebibytes_per_second',
UCUM_MEBIBITS_SECOND = 'Mibit/s',
OPEN_METRICS_MEBIBITS_SECOND = 'mebibits_per_second',
UCUM_GIBIBYTES_SECOND = 'GiBy/s',
OPEN_METRICS_GIBIBYTES_SECOND = 'gibibytes_per_second',
UCUM_GIBIBITS_SECOND = 'Gibit/s',
OPEN_METRICS_GIBIBITS_SECOND = 'gibibits_per_second',
UCUM_TEBIBYTES_SECOND = 'TiBy/s',
OPEN_METRICS_TEBIBYTES_SECOND = 'tebibytes_per_second',
UCUM_TEBIBITS_SECOND = 'Tibit/s',
OPEN_METRICS_TEBIBITS_SECOND = 'tebibits_per_second',
UCUM_PEBIBYTES_SECOND = 'PiBy/s',
OPEN_METRICS_PEBIBYTES_SECOND = 'pebibytes_per_second',
UCUM_PEBIBITS_SECOND = 'Pibit/s',
OPEN_METRICS_PEBIBITS_SECOND = 'pebibits_per_second',
UCUM_TRUE_FALSE = '{bool}',
OPEN_METRICS_TRUE_FALSE = 'boolean_true_false',
UCUM_YES_NO = '{bool}',
OPEN_METRICS_YES_NO = 'boolean_yes_no',
UCUM_COUNTS_SECOND = '{count}/s',
OPEN_METRICS_COUNTS_SECOND = 'counts_per_second',
UCUM_OPS_SECOND = '{ops}/s',
OPEN_METRICS_OPS_SECOND = 'ops_per_second',
UCUM_REQUESTS_SECOND = '{requests}/s',
OPEN_METRICS_REQUESTS_SECOND = 'requests_per_second',
UCUM_REQUESTS_MINUTE = '{requests}/min',
OPEN_METRICS_REQUESTS_MINUTE = 'requests_per_minute',
UCUM_READS_SECOND = '{reads}/s',
OPEN_METRICS_READS_SECOND = 'reads_per_second',
UCUM_WRITES_SECOND = '{writes}/s',
OPEN_METRICS_WRITES_SECOND = 'writes_per_second',
UCUM_IOPS_SECOND = '{iops}/s',
OPEN_METRICS_IOPS_SECOND = 'io_ops_per_second',
UCUM_COUNTS_MINUTE = '{count}/min',
OPEN_METRICS_COUNTS_MINUTE = 'counts_per_minute',
UCUM_OPS_MINUTE = '{ops}/min',
OPEN_METRICS_OPS_MINUTE = 'ops_per_minute',
UCUM_READS_MINUTE = '{reads}/min',
OPEN_METRICS_READS_MINUTE = 'reads_per_minute',
UCUM_WRITES_MINUTE = '{writes}/min',
OPEN_METRICS_WRITES_MINUTE = 'writes_per_minute',
OPEN_METRICS_PERCENT_UNIT = 'percentunit',
}

View File

@@ -1,33 +0,0 @@
import { UniversalYAxisUnitMappings, Y_AXIS_UNIT_NAMES } from './constants';
import { UniversalYAxisUnit, YAxisUnit } from './types';
export const mapMetricUnitToUniversalUnit = (
unit: string | undefined,
): UniversalYAxisUnit | null => {
if (!unit) {
return null;
}
const universalUnit = Object.values(UniversalYAxisUnit).find(
(u) => UniversalYAxisUnitMappings[u].has(unit as YAxisUnit) || unit === u,
);
return universalUnit || (unit as UniversalYAxisUnit) || null;
};
export const getUniversalNameFromMetricUnit = (
unit: string | undefined,
): string => {
if (!unit) {
return '-';
}
const universalUnit = mapMetricUnitToUniversalUnit(unit);
if (!universalUnit) {
return unit;
}
const universalName = Y_AXIS_UNIT_NAMES[universalUnit];
return universalName || unit || '-';
};

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/naming-convention */
export const OPERATORS = {
IN: 'IN',
LIKE: 'LIKE',
@@ -23,44 +21,6 @@ export const QUERY_BUILDER_FUNCTIONS = {
HASALL: 'hasAll',
};
export function negateOperator(operatorOrFunction: string): string {
// Special cases for equals/not equals
if (operatorOrFunction === OPERATORS['=']) {
return OPERATORS['!='];
}
if (operatorOrFunction === OPERATORS['!=']) {
return OPERATORS['='];
}
// For all other operators and functions, add NOT in front
return `${OPERATORS.NOT} ${operatorOrFunction}`;
}
export enum DEPRECATED_OPERATORS {
REGEX = 'regex',
NIN = 'nin',
NREGEX = 'nregex',
NLIKE = 'nlike',
NILIKE = 'nilike',
NEXTISTS = 'nexists',
NCONTAINS = 'ncontains',
NHAS = 'nhas',
NHASANY = 'nhasany',
NHASALL = 'nhasall',
}
export const DEPRECATED_OPERATORS_MAP = {
[DEPRECATED_OPERATORS.REGEX]: OPERATORS.REGEXP,
[DEPRECATED_OPERATORS.NIN]: negateOperator(OPERATORS.IN),
[DEPRECATED_OPERATORS.NREGEX]: negateOperator(OPERATORS.REGEXP),
[DEPRECATED_OPERATORS.NLIKE]: negateOperator(OPERATORS.LIKE),
[DEPRECATED_OPERATORS.NILIKE]: negateOperator(OPERATORS.ILIKE),
[DEPRECATED_OPERATORS.NEXTISTS]: negateOperator(OPERATORS.EXISTS),
[DEPRECATED_OPERATORS.NCONTAINS]: negateOperator(OPERATORS.CONTAINS),
[DEPRECATED_OPERATORS.NHAS]: negateOperator(QUERY_BUILDER_FUNCTIONS.HAS),
[DEPRECATED_OPERATORS.NHASANY]: negateOperator(QUERY_BUILDER_FUNCTIONS.HASANY),
[DEPRECATED_OPERATORS.NHASALL]: negateOperator(QUERY_BUILDER_FUNCTIONS.HASALL),
};
export const NON_VALUE_OPERATORS = [OPERATORS.EXISTS];
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -122,3 +82,15 @@ export const queryOperatorSuggestions = [
{ label: OPERATORS.NOT, type: 'operator', info: 'Not' },
...negationQueryOperatorSuggestions,
];
export function negateOperator(operatorOrFunction: string): string {
// Special cases for equals/not equals
if (operatorOrFunction === OPERATORS['=']) {
return OPERATORS['!='];
}
if (operatorOrFunction === OPERATORS['!=']) {
return OPERATORS['='];
}
// For all other operators and functions, add NOT in front
return `${OPERATORS.NOT} ${operatorOrFunction}`;
}

View File

@@ -49,5 +49,4 @@ export enum QueryParams {
tab = 'tab',
thresholds = 'thresholds',
selectedExplorerView = 'selectedExplorerView',
variableConfigs = 'variableConfigs',
}

View File

@@ -77,9 +77,9 @@ const ROUTES = {
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER: '/meter',
METER_EXPLORER: '/meter/explorer',
METER_EXPLORER_VIEWS: '/meter/explorer/views',
METER_EXPLORER_BASE: '/meter-explorer',
METER_EXPLORER: '/meter-explorer',
METER_EXPLORER_VIEWS: '/meter-explorer/views',
HOME_PAGE: '/',
} as const;

View File

@@ -6,7 +6,6 @@ export const GlobalShortcuts = {
NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
ToggleSidebar: 'b+shift',
};
export const GlobalShortcutsName = {
@@ -17,7 +16,6 @@ export const GlobalShortcutsName = {
NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
ToggleSidebar: 'shift+b',
};
export const GlobalShortcutsDescription = {
@@ -28,5 +26,4 @@ export const GlobalShortcutsDescription = {
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
ToggleSidebar: 'Toggle sidebar visibility',
};

View File

@@ -1,176 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
KeyboardHotkeysProvider,
useKeyboardHotkeys,
} from 'hooks/hotkeys/useKeyboardHotkeys';
import { QueryClient, QueryClientProvider } from 'react-query';
// Mock dependencies
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
const SHIFT_B_KEYBOARD_SHORTCUT = '{Shift>}b{/Shift}';
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
userPreferences: [
{
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
},
],
updateUserPreferenceInContext: mockUpdateUserPreferenceInContext,
})),
}));
function TestComponent({
mockHandleShortcut,
}: {
mockHandleShortcut: () => void;
}): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return <div data-testid="test">Test</div>;
}
describe('Sidebar Toggle Shortcut', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Global Shortcuts Constants', () => {
it('should have the correct shortcut key combination', () => {
expect(GlobalShortcuts.ToggleSidebar).toBe('b+shift');
});
});
describe('Keyboard Shortcut Registration', () => {
it('should register the sidebar toggle shortcut correctly', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Trigger the shortcut
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockHandleShortcut).toHaveBeenCalled();
});
it('should not trigger shortcut in input fields', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
function TestComponent(): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return (
<div>
<input data-testid="input-field" />
<div data-testid="test">Test</div>
</div>
);
}
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Focus on input field
const inputField = screen.getByTestId('input-field');
await user.click(inputField);
// Try to trigger shortcut while focused on input
await user.keyboard('{Shift>}b{/Shift}');
// Should not trigger the shortcut
expect(mockHandleShortcut).not.toHaveBeenCalled();
});
});
describe('Sidebar Toggle Functionality', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
it('should update user preference in context', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
};
mockUpdateUserPreferenceInContext(save);
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockUpdateUserPreferenceInContext).toHaveBeenCalledWith({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
});
});
});
});

View File

@@ -10,10 +10,8 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
import logEvent from 'api/common/logEvent';
import manageCreditCardApi from 'api/v1/portal/create';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import getUserVersion from 'api/v1/version/getVersion';
import { AxiosError } from 'axios';
import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
@@ -24,12 +22,10 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
@@ -72,10 +68,8 @@ import {
LicensePlatform,
LicenseState,
} from 'types/api/licensesV3/getActive';
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
getFormattedDate,
@@ -668,85 +662,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinnedPreference = userPreferences?.find(
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
// Add loading state to prevent layout shift during initial load
const [isSidebarLoaded, setIsSidebarLoaded] = useState(false);
// Get sidebar state from localStorage as fallback until preferences are loaded
const getSidebarStateFromLocalStorage = useCallback((): boolean => {
try {
const storedValue = getLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED);
return storedValue === 'true';
} catch {
return false;
}
}, []);
// Set sidebar as loaded after user preferences are fetched
useEffect(() => {
if (userPreferences !== null) {
setIsSidebarLoaded(true);
}
}, [userPreferences]);
// Use localStorage value as fallback until preferences are loaded
const isSideNavPinned = isSidebarLoaded
? sideNavPinnedPreference
: getSidebarStateFromLocalStorage();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const handleToggleSidebar = useCallback((): void => {
const newState = !isSideNavPinned;
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: isSideNavPinned,
newState,
});
// Save to localStorage immediately for instant feedback
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, newState.toString());
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
});
}, [
isSideNavPinned,
updateUserPreferenceInContext,
updateUserPreferenceMutation,
]);
// Register the sidebar toggle shortcut
useEffect(() => {
registerShortcut(GlobalShortcuts.ToggleSidebar, handleToggleSidebar);
return (): void => {
deregisterShortcut(GlobalShortcuts.ToggleSidebar);
};
}, [registerShortcut, deregisterShortcut, handleToggleSidebar]);
const SHOW_TRIAL_EXPIRY_BANNER =
showTrialExpiryBanner && !showPaymentFailedWarning;
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
@@ -820,14 +739,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
isSideNavPinned ? 'side-nav-pinned' : '',
sideNavPinned ? 'side-nav-pinned' : '',
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={isSideNavPinned} />
<SideNav isPinned={sideNavPinned} />
)}
<div
className={cx('app-content', {

View File

@@ -28,7 +28,6 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -314,7 +313,7 @@ function ChartPreview({
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const isWarning = !isEmpty(queryResponse.data?.warning);
const isWarning = !!queryResponse.data?.warning?.message;
return (
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>

View File

@@ -240,18 +240,8 @@ function FormAlertRules({
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
// Only update if functions actually changed to avoid resetting aggregateAttribute
const currentFunctions = queryData.functions || [];
const functionsChanged = !isEqual(currentFunctions, updatedFunctions);
if (functionsChanged) {
const updatedQueryData = {
...queryData,
functions: updatedFunctions,
};
handleSetQueryData(index, updatedQueryData);
}
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
}
};

View File

@@ -13,6 +13,7 @@ import { isEqual } from 'lodash-es';
import isEmpty from 'lodash-es/isEmpty';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
@@ -61,12 +62,14 @@ function GridCardGraph({
const {
toScrollWidgetId,
setToScrollWidgetId,
variablesToGetUpdated,
setDashboardQueryRangeCalled,
} = useDashboard();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryClient = useQueryClient();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -117,7 +120,11 @@ function GridCardGraph({
const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
const queryEnabledCondition =
isVisible &&
!isEmptyWidget &&
isQueryEnabled &&
isEmpty(variablesToGetUpdated);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
@@ -156,24 +163,22 @@ function GridCardGraph({
};
});
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
// useEffect(() => {
// if (variablesToGetUpdated.length > 0) {
// queryClient.cancelQueries([
// maxTime,
// minTime,
// globalSelectedInterval,
// variables,
// widget?.query,
// widget?.panelTypes,
// widget.timePreferance,
// widget.fillSpans,
// requestData,
// ]);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [variablesToGetUpdated]);
useEffect(() => {
if (variablesToGetUpdated.length > 0) {
queryClient.cancelQueries([
maxTime,
minTime,
globalSelectedInterval,
variables,
widget?.query,
widget?.panelTypes,
widget.timePreferance,
widget.fillSpans,
requestData,
]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variablesToGetUpdated]);
useEffect(() => {
if (!isEqual(updatedQuery, requestData.query)) {
@@ -219,15 +224,6 @@ function GridCardGraph({
widget.timePreferance,
widget.fillSpans,
requestData,
variables
? Object.entries(variables).reduce(
(acc, [id, variable]) => ({
...acc,
[id]: variable.selectedValue,
}),
{},
)
: {},
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
? [customTimeRange.startTime, customTimeRange.endTime]
: []),
@@ -305,7 +301,7 @@ function GridCardGraph({
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={!isEmpty(queryResponse.data?.warning)}
isWarning={!!queryResponse.data?.warning?.message}
version={version}
threshold={threshold}
headerMenuList={menuList}

View File

@@ -520,6 +520,12 @@ function ClusterDetails({
>
Cluster Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Cluster Name
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="entity-details-metadata-value">
@@ -527,6 +533,9 @@ function ClusterDetails({
{cluster.meta.k8s_cluster_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title="Cluster name">{cluster.meta.k8s_cluster_name}</Tooltip>
</Typography.Text>
</div>
</div>
</div>

View File

@@ -1,351 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen } from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import EntityEvents from '../EntityEvents';
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
const mockUseQuery = jest.fn();
jest.mock('react-query', () => ({
useQuery: (queryKey: any, queryFn: any, options: any): any =>
mockUseQuery(queryKey, queryFn, options),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}/`,
}),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
},
activeLicenseV3: {
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
license: {
license_key: 'test-license-key',
license_type: 'trial',
org_id: 'test-org-id',
plan_id: 'test-plan-id',
plan_name: 'test-plan-name',
plan_type: 'trial',
plan_version: 'test-plan-version',
},
},
} as any);
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
updateAllQueriesOperators: jest.fn(),
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
const timeRange = {
startTime: 1718236800,
endTime: 1718236800,
};
const mockHandleChangeEventFilters = jest.fn();
const mockFilters: IBuilderQuery['filters'] = {
items: [
{
id: 'pod-name',
key: {
id: 'pod-name',
dataType: DataTypes.String,
isColumn: true,
key: 'pod-name',
type: 'tag',
isJSON: false,
isIndexed: false,
},
op: '=',
value: 'pod-1',
},
],
op: 'and',
};
const isModalTimeSelection = false;
const mockHandleTimeChange = jest.fn();
const selectedInterval: Time = '1m';
const category = K8sCategory.PODS;
const queryKey = 'pod-events';
const mockEventsData = {
payload: {
data: {
newResult: {
data: {
result: [
{
list: [
{
timestamp: '2024-01-15T10:00:00Z',
data: {
id: 'event-1',
severity_text: 'INFO',
body: 'Test event 1',
resources_string: { 'pod.name': 'test-pod-1' },
attributes_string: { service: 'test-service' },
},
},
{
timestamp: '2024-01-15T10:01:00Z',
data: {
id: 'event-2',
severity_text: 'WARN',
body: 'Test event 2',
resources_string: { 'pod.name': 'test-pod-2' },
attributes_string: { service: 'test-service' },
},
},
],
},
],
},
},
},
},
};
const mockEmptyEventsData = {
payload: {
data: {
newResult: {
data: {
result: [
{
list: [],
},
],
},
},
},
},
};
const createMockEvent = (
id: string,
severity: string,
body: string,
podName: string,
): any => ({
timestamp: `2024-01-15T10:${id.padStart(2, '0')}:00Z`,
data: {
id: `event-${id}`,
severity_text: severity,
body,
resources_string: { 'pod.name': podName },
attributes_string: { service: 'test-service' },
},
});
const createMockMoreEventsData = (): any => ({
payload: {
data: {
newResult: {
data: {
result: [
{
list: Array.from({ length: 11 }, (_, i) =>
createMockEvent(
String(i + 1),
['INFO', 'WARN', 'ERROR', 'DEBUG'][i % 4],
`Test event ${i + 1}`,
`test-pod-${i + 1}`,
),
),
},
],
},
},
},
},
});
const renderEntityEvents = (overrides = {}): any => {
const defaultProps = {
timeRange,
handleChangeEventFilters: mockHandleChangeEventFilters,
filters: mockFilters,
isModalTimeSelection,
handleTimeChange: mockHandleTimeChange,
selectedInterval,
category,
queryKey,
...overrides,
};
return render(
<EntityEvents
timeRange={defaultProps.timeRange}
handleChangeEventFilters={defaultProps.handleChangeEventFilters}
filters={defaultProps.filters}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
category={defaultProps.category}
queryKey={defaultProps.queryKey}
/>,
);
};
describe('EntityEvents', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQuery.mockReturnValue({
data: mockEventsData,
isLoading: false,
isError: false,
isFetching: false,
});
});
it('should render events list with data', () => {
renderEntityEvents();
expect(screen.getByText('Prev')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(screen.getByText('Test event 1')).toBeInTheDocument();
expect(screen.getByText('Test event 2')).toBeInTheDocument();
expect(screen.getByText('INFO')).toBeInTheDocument();
expect(screen.getByText('WARN')).toBeInTheDocument();
});
it('renders empty state when no events are found', () => {
mockUseQuery.mockReturnValue({
data: mockEmptyEventsData,
isLoading: false,
isError: false,
isFetching: false,
});
renderEntityEvents();
expect(screen.getByText(/No events found for this pods/)).toBeInTheDocument();
});
it('renders loader when fetching events', () => {
mockUseQuery.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
isFetching: true,
});
renderEntityEvents();
expect(screen.getByTestId('loader')).toBeInTheDocument();
});
it('shows pagination controls when events are present', () => {
renderEntityEvents();
expect(screen.getByText('Prev')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
});
it('disables Prev button on first page', () => {
renderEntityEvents();
const prevButton = screen.getByText('Prev').closest('button');
expect(prevButton).toBeDisabled();
});
it('enables Next button when more events are available', () => {
mockUseQuery.mockReturnValue({
data: createMockMoreEventsData(),
isLoading: false,
isError: false,
isFetching: false,
});
renderEntityEvents();
const nextButton = screen.getByText('Next').closest('button');
expect(nextButton).not.toBeDisabled();
});
it('navigates to next page when Next button is clicked', () => {
mockUseQuery.mockReturnValue({
data: createMockMoreEventsData(),
isLoading: false,
isError: false,
isFetching: false,
});
renderEntityEvents();
const nextButton = screen.getByText('Next').closest('button');
expect(nextButton).not.toBeNull();
fireEvent.click(nextButton as Element);
const { calls } = mockUseQuery.mock;
const hasPage2Call = calls.some((call) => {
const { queryKey: callQueryKey } = call[0] || {};
return Array.isArray(callQueryKey) && callQueryKey.includes(2);
});
expect(hasPage2Call).toBe(true);
});
it('navigates to previous page when Prev button is clicked', () => {
mockUseQuery.mockReturnValue({
data: createMockMoreEventsData(),
isLoading: false,
isError: false,
isFetching: false,
});
renderEntityEvents();
const nextButton = screen.getByText('Next').closest('button');
expect(nextButton).not.toBeNull();
fireEvent.click(nextButton as Element);
const prevButton = screen.getByText('Prev').closest('button');
expect(prevButton).not.toBeNull();
fireEvent.click(prevButton as Element);
const { calls } = mockUseQuery.mock;
const hasPage1Call = calls.some((call) => {
const { queryKey: callQueryKey } = call[0] || {};
return Array.isArray(callQueryKey) && callQueryKey.includes(1);
});
expect(hasPage1Call).toBe(true);
});
});

View File

@@ -1,374 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable sonarjs/no-duplicate-string */
import { render, screen } from '@testing-library/react';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import EntityMetrics from '../EntityMetrics';
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
getUPlotChartOptions: jest.fn().mockReturnValue({}),
}));
jest.mock('lib/uPlotLib/utils/getUplotChartData', () => ({
getUPlotChartData: jest.fn().mockReturnValue([]),
}));
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
jest.mock('components/Uplot', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="uplot-chart">Uplot Chart</div>,
}));
jest.mock('container/InfraMonitoringK8s/commonUtils', () => ({
__esModule: true,
getMetricsTableData: jest.fn().mockReturnValue([
{
rows: [
{ data: { timestamp: '2024-01-15T10:00:00Z', value: '42.5' } },
{ data: { timestamp: '2024-01-15T10:01:00Z', value: '43.2' } },
],
columns: [
{ key: 'timestamp', label: 'Timestamp', isValueColumn: false },
{ key: 'value', label: 'Value', isValueColumn: true },
],
},
]),
MetricsTable: jest
.fn()
.mockImplementation(
(): JSX.Element => <div data-testid="metrics-table">Metrics Table</div>,
),
}));
const mockUseQueries = jest.fn();
jest.mock('react-query', () => ({
useQueries: (queryConfigs: any[]): any[] => mockUseQueries(queryConfigs),
}));
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('hooks/useDimensions', () => ({
useResizeObserver: (): { width: number; height: number } => ({
width: 800,
height: 600,
}),
}));
jest.mock('hooks/useMultiIntersectionObserver', () => ({
useMultiIntersectionObserver: (count: number): any => ({
visibilities: new Array(count).fill(true),
setElement: jest.fn(),
}),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
},
activeLicenseV3: {
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
license: {
license_key: 'test-license-key',
license_type: 'trial',
org_id: 'test-org-id',
plan_id: 'test-plan-id',
plan_name: 'test-plan-name',
plan_type: 'trial',
plan_version: 'test-plan-version',
},
},
featureFlags: [
{
name: 'DOT_METRICS_ENABLED',
active: false,
},
],
} as any);
const mockEntity = {
id: 'test-entity-1',
name: 'test-entity',
type: 'pod',
};
const mockEntityWidgetInfo = [
{
title: 'CPU Usage',
yAxisUnit: 'percentage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
},
];
const mockGetEntityQueryPayload = jest.fn().mockReturnValue([
{
query: 'cpu_usage',
start: 1705315200,
end: 1705318800,
},
{
query: 'memory_usage',
start: 1705315200,
end: 1705318800,
},
]);
const mockTimeRange = {
startTime: 1705315200,
endTime: 1705318800,
};
const mockHandleTimeChange = jest.fn();
const mockQueries = [
{
data: {
payload: {
data: {
result: [
{
table: {
rows: [
{ data: { timestamp: '2024-01-15T10:00:00Z', value: '42.5' } },
{ data: { timestamp: '2024-01-15T10:01:00Z', value: '43.2' } },
],
columns: [
{ key: 'timestamp', label: 'Timestamp', isValueColumn: false },
{ key: 'value', label: 'Value', isValueColumn: true },
],
},
},
],
},
},
params: {
compositeQuery: {
panelType: 'time_series',
},
},
},
isLoading: false,
isError: false,
error: null,
},
{
data: {
payload: {
data: {
result: [
{
table: {
rows: [
{ data: { timestamp: '2024-01-15T10:00:00Z', value: '1024' } },
{ data: { timestamp: '2024-01-15T10:01:00Z', value: '1028' } },
],
columns: [
{ key: 'timestamp', label: 'Timestamp', isValueColumn: false },
{ key: 'value', label: 'Value', isValueColumn: true },
],
},
},
],
},
},
params: {
compositeQuery: {
panelType: 'table',
},
},
},
isLoading: false,
isError: false,
error: null,
},
];
const mockLoadingQueries = [
{
data: undefined,
isLoading: true,
isError: false,
error: null,
},
{
data: undefined,
isLoading: true,
isError: false,
error: null,
},
];
const mockErrorQueries = [
{
data: undefined,
isLoading: false,
isError: true,
error: new Error('API Error'),
},
{
data: undefined,
isLoading: false,
isError: true,
error: new Error('Network Error'),
},
];
const mockEmptyQueries = [
{
data: {
payload: {
data: {
result: [],
},
},
params: {
compositeQuery: {
panelType: 'time_series',
},
},
},
isLoading: false,
isError: false,
error: null,
},
{
data: {
payload: {
data: {
result: [],
},
},
params: {
compositeQuery: {
panelType: 'table',
},
},
},
isLoading: false,
isError: false,
error: null,
},
];
const renderEntityMetrics = (overrides = {}): any => {
const defaultProps = {
timeRange: mockTimeRange,
isModalTimeSelection: false,
handleTimeChange: mockHandleTimeChange,
selectedInterval: '5m' as Time,
entity: mockEntity,
entityWidgetInfo: mockEntityWidgetInfo,
getEntityQueryPayload: mockGetEntityQueryPayload,
queryKey: 'test-query-key',
category: K8sCategory.PODS,
...overrides,
};
return render(
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>,
);
};
describe('EntityMetrics', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueries.mockReturnValue(mockQueries);
});
it('should render metrics with data', () => {
renderEntityMetrics();
expect(screen.getByText('CPU Usage')).toBeInTheDocument();
expect(screen.getByText('Memory Usage')).toBeInTheDocument();
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
expect(screen.getByTestId('uplot-chart')).toBeInTheDocument();
expect(screen.getByTestId('metrics-table')).toBeInTheDocument();
});
it('renders loading state when fetching metrics', () => {
mockUseQueries.mockReturnValue(mockLoadingQueries);
renderEntityMetrics();
expect(screen.getAllByText('CPU Usage')).toHaveLength(1);
expect(screen.getAllByText('Memory Usage')).toHaveLength(1);
});
it('renders error state when query fails', () => {
mockUseQueries.mockReturnValue(mockErrorQueries);
renderEntityMetrics();
expect(screen.getByText('API Error')).toBeInTheDocument();
expect(screen.getByText('Network Error')).toBeInTheDocument();
});
it('renders empty state when no metrics data', () => {
mockUseQueries.mockReturnValue(mockEmptyQueries);
renderEntityMetrics();
expect(screen.getByTestId('uplot-chart')).toBeInTheDocument();
expect(screen.getByTestId('metrics-table')).toBeInTheDocument();
});
it('calls handleTimeChange when datetime selection changes', () => {
renderEntityMetrics();
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
});
it('renders multiple metric widgets', () => {
renderEntityMetrics();
expect(screen.getByText('CPU Usage')).toBeInTheDocument();
expect(screen.getByText('Memory Usage')).toBeInTheDocument();
});
it('handles different panel types correctly', () => {
renderEntityMetrics();
expect(screen.getByTestId('uplot-chart')).toBeInTheDocument();
expect(screen.getByTestId('metrics-table')).toBeInTheDocument();
});
it('applies intersection observer for visibility', () => {
renderEntityMetrics();
expect(mockUseQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
enabled: true,
}),
]),
);
});
it('generates correct query payloads', () => {
renderEntityMetrics();
expect(mockGetEntityQueryPayload).toHaveBeenCalledWith(
mockEntity,
mockTimeRange.startTime,
mockTimeRange.endTime,
false,
);
});
});

View File

@@ -1,288 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable sonarjs/no-duplicate-string */
import { render, screen } from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import EntityTraces from '../EntityTraces';
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
const mockUseQuery = jest.fn();
jest.mock('react-query', () => ({
useQuery: (queryKey: any, queryFn: any, options: any): any =>
mockUseQuery(queryKey, queryFn, options),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: '/test-path',
}),
useNavigate: (): jest.Mock => jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: jest.fn(),
}),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
},
activeLicenseV3: {
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
license: {
license_key: 'test-license-key',
license_type: 'trial',
org_id: 'test-org-id',
plan_id: 'test-plan-id',
plan_name: 'test-plan-name',
plan_type: 'trial',
plan_version: 'test-plan-version',
},
},
} as any);
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
updateAllQueriesOperators: jest.fn(),
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
const timeRange = {
startTime: 1718236800,
endTime: 1718236800,
};
const mockHandleChangeTracesFilters = jest.fn();
const mockTracesFilters: IBuilderQuery['filters'] = {
items: [
{
id: 'service-name',
key: {
id: 'service-name',
dataType: DataTypes.String,
isColumn: true,
key: 'service.name',
type: 'tag',
isJSON: false,
isIndexed: false,
},
op: '=',
value: 'test-service',
},
],
op: 'and',
};
const isModalTimeSelection = false;
const mockHandleTimeChange = jest.fn();
const selectedInterval: Time = '5m';
const category = K8sCategory.PODS;
const queryKey = 'pod-traces';
const queryKeyFilters = ['service.name'];
const mockTracesData = {
payload: {
data: {
newResult: {
data: {
result: [
{
list: [
{
timestamp: '2024-01-15T10:00:00Z',
data: {
trace_id: 'trace-1',
span_id: 'span-1',
service_name: 'test-service-1',
operation_name: 'test-operation-1',
duration: 100,
status_code: 200,
},
},
{
timestamp: '2024-01-15T10:01:00Z',
data: {
trace_id: 'trace-2',
span_id: 'span-2',
service_name: 'test-service-2',
operation_name: 'test-operation-2',
duration: 150,
status_code: 500,
},
},
],
},
],
},
},
},
},
};
const mockEmptyTracesData = {
payload: {
data: {
newResult: {
data: {
result: [
{
list: [],
},
],
},
},
},
},
};
const renderEntityTraces = (overrides = {}): any => {
const defaultProps = {
timeRange,
isModalTimeSelection,
handleTimeChange: mockHandleTimeChange,
handleChangeTracesFilters: mockHandleChangeTracesFilters,
tracesFilters: mockTracesFilters,
selectedInterval,
queryKey,
category,
queryKeyFilters,
...overrides,
};
return render(
<EntityTraces
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
handleChangeTracesFilters={defaultProps.handleChangeTracesFilters}
tracesFilters={defaultProps.tracesFilters}
selectedInterval={defaultProps.selectedInterval}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
queryKeyFilters={defaultProps.queryKeyFilters}
/>,
);
};
describe('EntityTraces', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQuery.mockReturnValue({
data: mockTracesData,
isLoading: false,
isError: false,
isFetching: false,
});
});
it('should render traces list with data', () => {
renderEntityTraces();
expect(screen.getByText('Previous')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(
screen.getByText(/Search Filter : select options from suggested values/),
).toBeInTheDocument();
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
});
it('renders empty state when no traces are found', () => {
mockUseQuery.mockReturnValue({
data: mockEmptyTracesData,
isLoading: false,
isError: false,
isFetching: false,
});
renderEntityTraces();
expect(screen.getByText(/No traces yet./)).toBeInTheDocument();
});
it('renders loader when fetching traces', () => {
mockUseQuery.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
isFetching: true,
});
renderEntityTraces();
expect(screen.getByText('pending_data_placeholder')).toBeInTheDocument();
});
it('shows error state when query fails', () => {
mockUseQuery.mockReturnValue({
data: { error: 'API Error' },
isLoading: false,
isError: true,
isFetching: false,
});
renderEntityTraces();
expect(screen.getByText('API Error')).toBeInTheDocument();
});
it('calls handleChangeTracesFilters when query builder search changes', () => {
renderEntityTraces();
expect(
screen.getByText(/Search Filter : select options from suggested values/),
).toBeInTheDocument();
});
it('calls handleTimeChange when datetime selection changes', () => {
renderEntityTraces();
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
});
it('shows pagination controls when traces are present', () => {
renderEntityTraces();
expect(screen.getByText('Previous')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
});
it('disables pagination buttons when no more data', () => {
renderEntityTraces();
const prevButton = screen.getByText('Previous').closest('button');
const nextButton = screen.getByText('Next').closest('button');
expect(prevButton).toBeDisabled();
expect(nextButton).toBeDisabled();
});
});

View File

@@ -4,7 +4,7 @@ import { Skeleton } from 'antd';
function LoadingContainer(): JSX.Element {
return (
<div className="k8s-list-loading-state" data-testid="loader">
<div className="k8s-list-loading-state">
<Skeleton.Input
className="k8s-list-loading-state-item"
size="large"

View File

@@ -1,131 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import ClusterDetails from 'container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('ClusterDetails', () => {
const mockCluster = {
meta: {
k8s_cluster_name: 'test-cluster',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,141 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import DaemonSetDetails from 'container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('DaemonSetDetails', () => {
const mockDaemonSet = {
meta: {
k8s_daemonset_name: 'test-daemon-set',
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const daemonSetNameElements = screen.getAllByText('test-daemon-set');
expect(daemonSetNameElements.length).toBeGreaterThan(0);
expect(daemonSetNameElements[0]).toBeInTheDocument();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,141 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import DeploymentDetails from 'container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('DeploymentDetails', () => {
const mockDeployment = {
meta: {
k8s_deployment_name: 'test-deployment',
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const deploymentNameElements = screen.getAllByText('test-deployment');
expect(deploymentNameElements.length).toBeGreaterThan(0);
expect(deploymentNameElements[0]).toBeInTheDocument();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,116 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('JobDetails', () => {
const mockJob = {
meta: {
k8s_job_name: 'test-job',
k8s_namespace_name: 'test-namespace',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const jobNameElements = screen.getAllByText('test-job');
expect(jobNameElements.length).toBeGreaterThan(0);
expect(jobNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,136 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import NamespaceDetails from 'container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('NamespaceDetails', () => {
const mockNamespace = {
namespaceName: 'test-namespace',
meta: {
k8s_cluster_name: 'test-cluster',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,116 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import NodeDetails from 'container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('NodeDetails', () => {
const mockNode = {
meta: {
k8s_node_name: 'test-node',
k8s_cluster_name: 'test-cluster',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const nodeNameElements = screen.getAllByText('test-node');
expect(nodeNameElements.length).toBeGreaterThan(0);
expect(nodeNameElements[0]).toBeInTheDocument();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,122 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import PodDetails from 'container/InfraMonitoringK8s/Pods/PodDetails/PodDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('PodDetails', () => {
const mockPod = {
podName: 'test-pod',
meta: {
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
k8s_node_name: 'test-node',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
const nodeNameElements = screen.getAllByText('test-node');
expect(nodeNameElements.length).toBeGreaterThan(0);
expect(nodeNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,136 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import StatefulSetDetails from 'container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('StatefulSetDetails', () => {
const mockStatefulSet = {
meta: {
k8s_namespace_name: 'test-namespace',
k8s_statefulset_name: 'test-stateful-set',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const statefulSetNameElements = screen.getAllByText('test-stateful-set');
expect(statefulSetNameElements.length).toBeGreaterThan(0);
expect(statefulSetNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByText('Metrics');
expect(metricsTab).toBeInTheDocument();
const eventsTab = screen.getByText('Events');
expect(eventsTab).toBeInTheDocument();
const logsTab = screen.getByText('Logs');
expect(logsTab).toBeInTheDocument();
const tracesTab = screen.getByText('Traces');
expect(tracesTab).toBeInTheDocument();
});
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,73 +0,0 @@
/* eslint-disable import/first */
// eslint-disable-next-line import/order
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { fireEvent, render, screen } from '@testing-library/react';
import VolumeDetails from 'container/InfraMonitoringK8s/Volumes/VolumeDetails/VolumeDetails';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
const queryClient = new QueryClient();
describe('VolumeDetails', () => {
const mockVolume = {
persistentVolumeClaimName: 'test-volume',
meta: {
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<VolumeDetails
volume={mockVolume}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const volumeNameElements = screen.getAllByText('test-volume');
expect(volumeNameElements.length).toBeGreaterThan(0);
expect(volumeNameElements[0]).toBeInTheDocument();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<VolumeDetails
volume={mockVolume}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,121 +0,0 @@
import * as appContextHooks from 'providers/App/App';
import * as timezoneHooks from 'providers/Timezone';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
const setupCommonMocks = (): void => {
const createMockObserver = (): {
observe: jest.Mock;
unobserve: jest.Mock;
disconnect: jest.Mock;
} => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
});
global.IntersectionObserver = jest.fn().mockImplementation(createMockObserver);
global.ResizeObserver = jest.fn().mockImplementation(createMockObserver);
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(() => ({
globalTime: {
selectedTime: {
startTime: 1713734400000,
endTime: 1713738000000,
},
maxTime: 1713738000000,
minTime: 1713734400000,
},
})),
}));
jest.mock('uplot', () => ({
paths: {
spline: jest.fn(),
bars: jest.fn(),
},
default: jest.fn(() => ({
paths: {
spline: jest.fn(),
bars: jest.fn(),
},
})),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useSearchParams: jest.fn().mockReturnValue([
{
get: jest.fn(),
entries: jest.fn(() => []),
set: jest.fn(),
},
jest.fn(),
]),
useNavigationType: (): any => 'PUSH',
}));
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => ({
set: jest.fn(),
delete: jest.fn(),
get: jest.fn(),
has: jest.fn(),
entries: jest.fn(() => []),
append: jest.fn(),
toString: jest.fn(() => ''),
})),
}));
jest.mock('lib/getMinMax', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
minTime: 1713734400000,
maxTime: 1713738000000,
})),
isValidTimeFormat: jest.fn().mockReturnValue(true),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
},
activeLicenseV3: {
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
license: {
license_key: 'test-license-key',
license_type: 'trial',
org_id: 'test-org-id',
plan_id: 'test-plan-id',
plan_name: 'test-plan-name',
plan_type: 'trial',
plan_version: 'test-plan-version',
},
},
} as any);
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
timezone: {
offset: 0,
},
browserTimezone: {
offset: 0,
},
} as any);
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
};
export default setupCommonMocks;

View File

@@ -73,8 +73,6 @@ export default function TableRow({
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
if (!column.key) return null;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
@@ -99,7 +97,6 @@ export default function TableRow({
fontSize={fontSize}
columnKey={column.key as string}
onClick={handleShowLogDetails}
className={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>

View File

@@ -136,7 +136,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
key={column.key}
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
{...(isDragColumn && { className: 'dragHandler' })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}

View File

@@ -1,5 +1,4 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { isEmpty } from 'lodash-es';
import { IField } from 'types/api/logs/fields';
import {
IBuilderQuery,
@@ -9,13 +8,11 @@ import {
export const convertKeysToColumnFields = (
keys: TelemetryFieldKey[],
): IField[] =>
keys
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
keys.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
/**
* Determines if a query represents a trace-to-logs navigation
* by checking for the presence of a trace_id filter.

View File

@@ -256,6 +256,7 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
listChartQuery,
PANEL_TYPES.TIME_SERIES,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
enabled:
@@ -278,6 +279,7 @@ function LogsExplorerViewsContainer({
} = useGetExplorerQueryRange(
requestData,
panelType,
// ENTITY_VERSION_V4,
ENTITY_VERSION_V5,
{
keepPreviousData: true,

View File

@@ -1,92 +0,0 @@
.meter-explorer-breakdown {
display: flex;
flex-direction: column;
.meter-explorer-date-time {
display: flex;
min-height: 30px;
justify-content: end;
border-bottom: 1px solid var(--bg-slate-500);
padding: 10px 16px;
}
.meter-explorer-graphs {
display: flex;
flex-direction: column;
padding: 20px;
gap: 36px;
.meter-column-graph {
.row-card {
background-color: var(--bg-ink-400);
padding-left: 10px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
.section-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}
.graph-description {
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.meter-page-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: flex-start;
gap: 10px;
.meter-graph {
height: 400px;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
}
}
.total {
.meter-column-graph {
.meter-page-grid {
grid-template-columns: repeat(3, 1fr);
.meter-graph {
height: 200px;
}
}
}
}
}
}
.lightMode {
.meter-explorer-breakdown {
.meter-explorer-date-time {
border-bottom: none;
}
.meter-explorer-graphs {
.meter-column-graph {
.row-card {
background-color: var(--bg-vanilla-300);
.section-title {
color: var(--bg-ink-400);
}
}
}
}
}
}

View File

@@ -1,200 +0,0 @@
import './BreakDown.styles.scss';
import { Typography } from 'antd';
// import useFilterConfig from 'components/QuickFilters/hooks/useFilterConfig';
// import { SignalType } from 'components/QuickFilters/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card, CardContainer } from 'container/GridCardLayout/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
// import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
// import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
getLogCountWidgetData,
getLogSizeWidgetData,
getMetricCountWidgetData,
getSpanCountWidgetData,
getSpanSizeWidgetData,
getTotalLogSizeWidgetData,
getTotalMetricDatapointCountWidgetData,
getTotalTraceSizeWidgetData,
} from './graphs';
type MetricSection = {
id: string;
title: string;
graphs: Widgets[];
};
const sections: MetricSection[] = [
{
id: uuid(),
title: 'Total',
graphs: [
getTotalLogSizeWidgetData(),
getTotalTraceSizeWidgetData(),
getTotalMetricDatapointCountWidgetData(),
],
},
{
id: uuid(),
title: 'Logs',
graphs: [getLogCountWidgetData(), getLogSizeWidgetData()],
},
{
id: uuid(),
title: 'Traces',
graphs: [getSpanCountWidgetData(), getSpanSizeWidgetData()],
},
{
id: uuid(),
title: 'Metrics',
graphs: [getMetricCountWidgetData()],
},
];
function Section(section: MetricSection): JSX.Element {
const isDarkMode = useIsDarkMode();
const { title, graphs } = section;
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
return (
<div className="meter-column-graph">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<Typography.Text className="section-title">{title}</Typography.Text>
</CardContainer>
<div className="meter-page-grid">
{graphs.map((widget) => (
<Card
key={widget?.id}
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.BAR}
className="meter-graph"
>
<GridCard widget={widget} onDragSelect={onDragSelect} version="v5" />
</Card>
))}
</div>
</div>
);
}
// function FilterDropdown({ attrKey }: { attrKey: string }): JSX.Element {
// const {
// data: keyValueSuggestions,
// isLoading: isLoadingKeyValueSuggestions,
// } = useGetQueryKeyValueSuggestions({
// key: attrKey,
// signal: DataSource.METRICS,
// signalSource: 'meter',
// options: {
// keepPreviousData: true,
// },
// });
// const responseData = keyValueSuggestions?.data as any;
// const values = responseData?.data?.values || {};
// const stringValues = values.stringValues || [];
// const numberValues = values.numberValues || [];
// const stringOptions = stringValues.filter(
// (value: string | null | undefined): value is string =>
// value !== null && value !== undefined && value !== '',
// );
// const numberOptions = numberValues
// .filter(
// (value: number | null | undefined): value is number =>
// value !== null && value !== undefined,
// )
// .map((value: number) => value.toString());
// const vals = [...stringOptions, ...numberOptions];
// return (
// <div className="filter-dropdown">
// <Typography.Text>{attrKey}</Typography.Text>
// <Select
// loading={isLoadingKeyValueSuggestions}
// options={vals?.map((suggestion: any) => ({
// label: suggestion,
// value: suggestion,
// }))}
// placeholder={`Select ${attrKey}`}
// />
// </div>
// );
// }
function BreakDown(): JSX.Element {
// const { customFilters } = useFilterConfig({
// signal: SignalType.METER_EXPLORER,
// config: [],
// });
return (
<div className="meter-explorer-breakdown">
<section className="meter-explorer-date-time">
{/* {customFilters.map((filter) => (
<FilterDropdown key={filter.key} attrKey={filter.key} />
))} */}
<DateTimeSelectionV2 showAutoRefresh={false} />
</section>
<section className="meter-explorer-graphs">
<section className="total">
<Section
id={sections[0].id}
title={sections[0].title}
graphs={sections[0].graphs}
/>
</section>
{sections.map((section, idx) => {
if (idx === 0) {
return;
}
return (
<Section
key={section.id}
id={section.id}
title={section.title}
graphs={section.graphs}
/>
);
})}
</section>
</div>
);
}
export default BreakDown;

View File

@@ -1,390 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
interface GetWidgetQueryProps {
title: string;
description: string;
queryData: IBuilderQuery[];
queryFormulas?: IBuilderFormula[];
panelTypes?: PANEL_TYPES;
yAxisUnit?: string;
columnUnits?: Record<string, string>;
}
interface GetWidgetQueryPropsReturn extends GetWidgetQueryBuilderProps {
description?: string;
nullZeroValues: string;
columnUnits?: Record<string, string>;
}
export const getWidgetQueryBuilder = ({
query,
title = '',
panelTypes,
yAxisUnit = '',
fillSpans = false,
id,
nullZeroValues,
description,
}: GetWidgetQueryPropsReturn): Widgets => ({
description: description || '',
id: id || uuid(),
isStacked: false,
nullZeroValues: nullZeroValues || '',
opacity: '1',
panelTypes,
query,
timePreferance: 'GLOBAL_TIME',
title,
yAxisUnit,
softMax: null,
softMin: null,
selectedLogFields: [],
selectedTracesFields: [],
fillSpans,
});
export function getWidgetQuery(
props: GetWidgetQueryProps,
): GetWidgetQueryPropsReturn {
const { title, description, panelTypes, yAxisUnit, columnUnits } = props;
return {
title,
yAxisUnit: yAxisUnit || 'none',
panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES,
fillSpans: false,
description,
nullZeroValues: 'zero',
columnUnits,
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryData: props.queryData,
queryFormulas: (props.queryFormulas as IBuilderFormula[]) || [],
},
clickhouse_sql: [],
id: uuid(),
},
};
}
export const getTotalLogSizeWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.log.size',
id: 'signoz.meter.log.size--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Total size of log records ingested',
description: '',
panelTypes: PANEL_TYPES.VALUE,
yAxisUnit: 'bytes',
}),
);
export const getTotalTraceSizeWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.span.size',
id: 'signoz.meter.span.size--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Total size of spans ingested',
description: '',
panelTypes: PANEL_TYPES.VALUE,
yAxisUnit: 'bytes',
}),
);
export const getTotalMetricDatapointCountWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.metric.datapoint.count',
id: 'signoz.meter.metric.datapoint.count--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Total metric datapoints ingested',
description: '',
panelTypes: PANEL_TYPES.VALUE,
yAxisUnit: 'short',
}),
);
export const getLogCountWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.log.count',
id: 'signoz.meter.log.count--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Count of log records ingested',
description: '',
panelTypes: PANEL_TYPES.BAR,
yAxisUnit: 'short',
}),
);
export const getLogSizeWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.log.size',
id: 'signoz.meter.log.size--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'size',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Size of log records ingested',
description: '',
panelTypes: PANEL_TYPES.BAR,
yAxisUnit: 'bytes',
}),
);
export const getSpanCountWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.span.count',
id: 'signoz.meter.span.count--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Count of spans ingested',
description: '',
panelTypes: PANEL_TYPES.BAR,
yAxisUnit: 'short',
}),
);
export const getSpanSizeWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.span.size',
id: 'signoz.meter.span.size--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'size',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Size of spans ingested',
description: '',
panelTypes: PANEL_TYPES.BAR,
yAxisUnit: 'bytes',
}),
);
export const getMetricCountWidgetData = (): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'signoz.meter.metric.datapoint.count',
id: 'signoz.meter.metric.datapoint.count--float64--Sum--true',
isColumn: true,
isJSON: false,
type: 'Sum',
},
aggregateOperator: 'increase',
dataSource: DataSource.METRICS,
source: 'meter',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: 'count',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'increase',
},
],
title: 'Count of metric datapoints ingested',
description: '',
panelTypes: PANEL_TYPES.BAR,
yAxisUnit: 'short',
}),
);

View File

@@ -141,19 +141,6 @@
background: var(--bg-vanilla-500);
}
}
.meter-explorer-content-section {
.explore-content {
.time-series-view-panel {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
}
}
.meter-explorer-quick-filters-section {
border-right: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -43,7 +43,7 @@ function Explorer(): JSX.Element {
() =>
updateAllQueriesOperators(
initialQueryMeterWithType,
PANEL_TYPES.BAR,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
@@ -54,7 +54,7 @@ function Explorer(): JSX.Element {
() =>
updateAllQueriesOperators(
currentQuery || initialQueryMeterWithType,
PANEL_TYPES.BAR,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
@@ -75,7 +75,7 @@ function Explorer(): JSX.Element {
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.BAR,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
});

View File

@@ -69,7 +69,7 @@ function TimeSeries(): JSX.Element {
GetMetricQueryRange(
{
query: payload,
graphType: PANEL_TYPES.BAR,
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
@@ -131,7 +131,6 @@ function TimeSeries(): JSX.Element {
data={datapoint}
dataSource={DataSource.METRICS}
yAxisUnit={yAxisUnit}
panelType={PANEL_TYPES.BAR}
/>
</div>
))}

View File

@@ -0,0 +1,220 @@
import { render, screen } from '@testing-library/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as appContextHooks from 'providers/App/App';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import * as timezoneHooks from 'providers/Timezone';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom-v5-compat';
import store from 'store';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { DataSource } from 'types/common/queryBuilder';
import Explorer from '../Explorer';
const mockSetSearchParams = jest.fn();
const queryClient = new QueryClient();
const mockUpdateAllQueriesOperators = jest.fn();
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
updateAllQueriesOperators: mockUpdateAllQueriesOperators,
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
};
jest.mock('react-router-dom-v5-compat', () => {
const actual = jest.requireActual('react-router-dom-v5-compat');
return {
...actual,
useSearchParams: jest.fn(),
useNavigationType: (): any => 'PUSH',
};
});
jest.mock('hooks/useDimensions', () => ({
useResizeObserver: (): { width: number; height: number } => ({
width: 800,
height: 400,
}),
}));
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: jest.fn().mockReturnValue({
getQueriesData: jest.fn(),
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): any => ({
notifications: {
error: jest.fn(),
},
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: (): any => ({
globalTime: {
selectedTime: {
startTime: 1713734400000,
endTime: 1713738000000,
},
maxTime: 1713738000000,
minTime: 1713734400000,
},
}),
}));
jest.spyOn(useUpdateDashboardHooks, 'useUpdateDashboard').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
} as any);
jest.spyOn(useOptionsMenuHooks, 'useOptionsMenu').mockReturnValue({
options: {
selectColumns: [],
},
} as any);
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
timezone: {
offset: 0,
},
browserTimezone: {
offset: 0,
},
} as any);
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
},
activeLicenseV3: {
event_queue: {
created_at: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
status: '',
updated_at: '0',
},
license: {
license_key: 'test-license-key',
license_type: 'trial',
org_id: 'test-org-id',
plan_id: 'test-plan-id',
plan_name: 'test-plan-name',
plan_type: 'trial',
plan_version: 'test-plan-version',
},
},
} as any);
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
describe('Explorer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render Explorer query builder with metrics datasource selected', () => {
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
stagedQuery: initialQueriesMap[DataSource.TRACES],
} as any);
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({ isOneChartPerQueryEnabled: 'false' }),
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
expect(mockUpdateAllQueriesOperators).toHaveBeenCalledWith(
initialQueriesMap[DataSource.METRICS],
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
);
});
it('should enable one chart per query toggle when oneChartPerQuery=true in URL', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({ isOneChartPerQueryEnabled: 'true' }),
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
const toggle = screen.getByRole('switch');
expect(toggle).toBeChecked();
});
it('should disable one chart per query toggle when oneChartPerQuery=false in URL', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({ isOneChartPerQueryEnabled: 'false' }),
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
const toggle = screen.getByRole('switch');
expect(toggle).not.toBeChecked();
});
});

View File

@@ -13,7 +13,6 @@ import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
@@ -136,7 +135,7 @@ function Explorer(): JSX.Element {
/>
</div>
<div className="explore-header-right-actions">
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
{warning?.message && <WarningPopover warningData={warning} />}
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}

View File

@@ -1,6 +1,5 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
import { initialQueriesMap } from 'constants/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -111,15 +110,6 @@ export function getMetricDetailsQuery(
isJSON: false,
dataType: DataTypes.String,
},
aggregations: [
{
metricName,
timeAggregation: timeAggregation as TimeAggregation,
spaceAggregation: spaceAggregation as SpaceAggregation,
reduceTo: 'avg',
temporality: '',
},
],
aggregateOperator,
timeAggregation,
spaceAggregation,

View File

@@ -1,7 +1,6 @@
import './MySettings.styles.scss';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
@@ -110,9 +109,6 @@ function MySettings(): JSX.Element {
// Optimistically update the UI
setSideNavPinned(checked);
// Save to localStorage immediately for instant feedback
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, checked.toString());
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
@@ -134,8 +130,6 @@ function MySettings(): JSX.Element {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: !checked,
} as UserPreference);
// Also revert localStorage
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, (!checked).toString());
showErrorNotification(notifications, error as AxiosError);
},
},

View File

@@ -1,42 +0,0 @@
.dynamic-variable-container {
display: grid;
grid-template-columns: 1fr 32px 200px;
gap: 32px;
align-items: center;
width: 100%;
margin: 24px 0;
.ant-select {
.ant-select-selector {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
}
}
.ant-input {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
max-width: 300px;
}
.dynamic-variable-from-text {
font-family: 'Space Mono';
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
}
.lightMode {
.dynamic-variable-container {
.ant-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
}
}
.ant-input {
border: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -1,179 +0,0 @@
import './DynamicVariable.styles.scss';
import { Select, Typography } from 'antd';
import CustomSelect from 'components/NewSelect/CustomSelect';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import useDebounce from 'hooks/useDebounce';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { FieldKey } from 'types/api/dynamicVariables/getFieldKeys';
enum AttributeSource {
ALL_SOURCES = 'All Sources',
LOGS = 'Logs',
METRICS = 'Metrics',
TRACES = 'Traces',
}
function DynamicVariable({
setDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue,
}: {
setDynamicVariablesSelectedValue: Dispatch<
SetStateAction<
| {
name: string;
value: string;
}
| undefined
>
>;
dynamicVariablesSelectedValue:
| {
name: string;
value: string;
}
| undefined;
}): JSX.Element {
const sources = [
AttributeSource.ALL_SOURCES,
AttributeSource.LOGS,
AttributeSource.TRACES,
AttributeSource.METRICS,
];
const [attributeSource, setAttributeSource] = useState<AttributeSource>();
const [attributes, setAttributes] = useState<Record<string, FieldKey[]>>({});
const [selectedAttribute, setSelectedAttribute] = useState<string>();
const [apiSearchText, setApiSearchText] = useState<string>('');
const debouncedApiSearchText = useDebounce(apiSearchText, DEBOUNCE_DELAY);
const [filteredAttributes, setFilteredAttributes] = useState<
Record<string, FieldKey[]>
>({});
useEffect(() => {
if (dynamicVariablesSelectedValue?.name) {
setSelectedAttribute(dynamicVariablesSelectedValue.name);
}
if (dynamicVariablesSelectedValue?.value) {
setAttributeSource(dynamicVariablesSelectedValue.value as AttributeSource);
}
}, [
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
]);
const { data, error, isLoading, refetch } = useGetFieldKeys({
signal:
attributeSource === AttributeSource.ALL_SOURCES
? undefined
: (attributeSource?.toLowerCase() as 'traces' | 'logs' | 'metrics'),
name: debouncedApiSearchText,
});
const isComplete = useMemo(() => data?.payload?.complete === true, [data]);
useEffect(() => {
if (data) {
const newAttributes = data.payload?.keys ?? {};
setAttributes(newAttributes);
setFilteredAttributes(newAttributes);
}
}, [data]);
// refetch when attributeSource changes
useEffect(() => {
if (attributeSource) {
refetch();
}
}, [attributeSource, refetch, debouncedApiSearchText]);
// Handle search based on whether we have complete data or not
const handleSearch = useCallback(
(text: string) => {
if (isComplete) {
// If complete is true, do client-side filtering
if (!text) {
setFilteredAttributes(attributes);
return;
}
const filtered: Record<string, FieldKey[]> = {};
Object.keys(attributes).forEach((key) => {
if (key.toLowerCase().includes(text.toLowerCase())) {
filtered[key] = attributes[key];
}
});
setFilteredAttributes(filtered);
} else {
// If complete is false, debounce the API call
setApiSearchText(text);
}
},
[attributes, isComplete],
);
// update setDynamicVariablesSelectedValue with debounce when attribute and source is selected
useEffect(() => {
if (selectedAttribute || attributeSource) {
setDynamicVariablesSelectedValue({
name: selectedAttribute || dynamicVariablesSelectedValue?.name || '',
value:
attributeSource ||
dynamicVariablesSelectedValue?.value ||
AttributeSource.ALL_SOURCES,
});
}
}, [
selectedAttribute,
attributeSource,
setDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
]);
const errorMessage = (error as any)?.message;
return (
<div className="dynamic-variable-container">
<CustomSelect
placeholder="Select an Attribute"
options={Object.keys(filteredAttributes).map((key) => ({
label: key,
value: key,
}))}
loading={isLoading}
status={errorMessage ? 'error' : undefined}
onChange={(value): void => {
setSelectedAttribute(value);
}}
showSearch
errorMessage={errorMessage as any}
value={selectedAttribute || dynamicVariablesSelectedValue?.name}
onSearch={handleSearch}
onRetry={(): void => {
refetch();
}}
/>
<Typography className="dynamic-variable-from-text">from</Typography>
<Select
placeholder="Source"
defaultValue={AttributeSource.ALL_SOURCES}
options={sources.map((source) => ({ label: source, value: source }))}
onChange={(value): void => setAttributeSource(value as AttributeSource)}
value={attributeSource || dynamicVariablesSelectedValue?.value}
/>
</div>
);
}
export default DynamicVariable;

View File

@@ -1,376 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import DynamicVariable from '../DynamicVariable';
// Mock scrollIntoView since it's not available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Mock dependencies
jest.mock('hooks/dynamicVariables/useGetFieldKeys', () => ({
useGetFieldKeys: jest.fn(),
}));
jest.mock('hooks/useDebounce', () => ({
__esModule: true,
default: (value: any): any => value, // Return the same value without debouncing for testing
}));
describe('DynamicVariable Component', () => {
const mockSetDynamicVariablesSelectedValue = jest.fn();
const ATTRIBUTE_PLACEHOLDER = 'Select an Attribute';
const LOADING_TEXT = 'We are updating the values...';
const DEFAULT_PROPS = {
setDynamicVariablesSelectedValue: mockSetDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue: undefined,
};
const mockFieldKeysResponse = {
payload: {
keys: {
'service.name': [],
'http.status_code': [],
duration: [],
},
complete: true,
},
statusCode: 200,
};
beforeEach(() => {
jest.clearAllMocks();
// Default mock implementation
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: jest.fn(),
});
});
// Helper function to get the attribute select element
const getAttributeSelect = (): HTMLElement =>
screen.getAllByRole('combobox')[0];
// Helper function to get the source select element
const getSourceSelect = (): HTMLElement => screen.getAllByRole('combobox')[1];
it('renders with default state', () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Check for main components
expect(screen.getByText(ATTRIBUTE_PLACEHOLDER)).toBeInTheDocument();
expect(screen.getByText('All Sources')).toBeInTheDocument();
expect(screen.getByText('from')).toBeInTheDocument();
});
it('uses existing values from dynamicVariablesSelectedValue prop', () => {
const selectedValue = {
name: 'service.name',
value: 'Logs',
};
render(
<DynamicVariable
setDynamicVariablesSelectedValue={mockSetDynamicVariablesSelectedValue}
dynamicVariablesSelectedValue={selectedValue}
/>,
);
// Verify values are set
expect(screen.getByText('service.name')).toBeInTheDocument();
expect(screen.getByText('Logs')).toBeInTheDocument();
});
it('shows loading state when fetching data', () => {
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: null,
isLoading: true,
refetch: jest.fn(),
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Should show loading state
expect(screen.getByText(LOADING_TEXT)).toBeInTheDocument();
});
it('shows error message when API fails', () => {
const errorMessage = 'Failed to fetch field keys';
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: { message: errorMessage },
isLoading: false,
refetch: jest.fn(),
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Should show error message
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
it('updates filteredAttributes when data is loaded', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Wait for options to appear in the dropdown
await waitFor(() => {
// Looking for option-content elements inside the CustomSelect dropdown
const options = document.querySelectorAll('.option-content');
expect(options.length).toBeGreaterThan(0);
// Check if all expected options are present
let foundServiceName = false;
let foundHttpStatusCode = false;
let foundDuration = false;
options.forEach((option) => {
const text = option.textContent?.trim();
if (text === 'service.name') foundServiceName = true;
if (text === 'http.status_code') foundHttpStatusCode = true;
if (text === 'duration') foundDuration = true;
});
expect(foundServiceName).toBe(true);
expect(foundHttpStatusCode).toBe(true);
expect(foundDuration).toBe(true);
});
});
it('calls setDynamicVariablesSelectedValue when attribute is selected', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Wait for options to appear, then click on service.name
await waitFor(() => {
// Need to find the option-item containing service.name
const serviceNameOption = screen.getByText('service.name');
expect(serviceNameOption).not.toBeNull();
expect(serviceNameOption?.textContent).toBe('service.name');
// Click on the option-item that contains service.name
const optionElement = serviceNameOption?.closest('.option-item');
if (optionElement) {
fireEvent.click(optionElement);
}
});
// Check if the setter was called with the correct value
expect(mockSetDynamicVariablesSelectedValue).toHaveBeenCalledWith({
name: 'service.name',
value: 'All Sources',
});
});
it('calls setDynamicVariablesSelectedValue when source is selected', () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Get the Select component
const select = screen
.getByText('All Sources')
.closest('div[class*="ant-select"]');
expect(select).toBeInTheDocument();
// Directly call the onChange handler by simulating the Select's onChange
// Find the props.onChange of the Select component and call it directly
fireEvent.mouseDown(select as HTMLElement);
// Use a more specific selector to find the "Logs" option
const optionsContainer = document.querySelector(
'.rc-virtual-list-holder-inner',
);
expect(optionsContainer).not.toBeNull();
// Find the option with Logs text content
const logsOption = Array.from(
optionsContainer?.querySelectorAll('.ant-select-item-option-content') || [],
)
.find((element) => element.textContent === 'Logs')
?.closest('.ant-select-item-option');
expect(logsOption).not.toBeNull();
// Click on it
if (logsOption) {
fireEvent.click(logsOption);
}
// Check if the setter was called with the correct value
expect(mockSetDynamicVariablesSelectedValue).toHaveBeenCalledWith(
expect.objectContaining({
value: 'Logs',
}),
);
});
it('filters attributes locally when complete is true', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Mock the filter function behavior
const attributeKeys = Object.keys(mockFieldKeysResponse.payload.keys);
// Only "http.status_code" should match the filter
const expectedFilteredKeys = attributeKeys.filter((key) =>
key.includes('http'),
);
// Verify our expected filtering logic
expect(expectedFilteredKeys).toContain('http.status_code');
expect(expectedFilteredKeys).not.toContain('service.name');
expect(expectedFilteredKeys).not.toContain('duration');
// Now verify the component's filtering ability by inputting the search text
const inputElement = screen
.getAllByRole('combobox')[0]
.querySelector('input');
if (inputElement) {
fireEvent.change(inputElement, { target: { value: 'http' } });
}
});
it('triggers API call when complete is false and search text changes', async () => {
const mockRefetch = jest.fn();
// Set up the mock to indicate that data is not complete
// and needs to be fetched from the server
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: {
payload: {
keys: {
'http.status_code': [],
},
complete: false, // This indicates server-side filtering is needed
},
},
error: null,
isLoading: false,
refetch: mockRefetch,
});
// Render with Logs as the initial source
render(
<DynamicVariable
{...DEFAULT_PROPS}
dynamicVariablesSelectedValue={{
name: '',
value: 'Logs',
}}
/>,
);
// Clear any initial calls
mockRefetch.mockClear();
// Now test the search functionality
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Find the input element and simulate typing
const inputElement = document.querySelector(
'.ant-select-selection-search-input',
);
if (inputElement) {
// Simulate typing in the search input
fireEvent.change(inputElement, { target: { value: 'http' } });
// Verify that the input has the correct value
expect((inputElement as HTMLInputElement).value).toBe('http');
// Wait for the effect to run and verify refetch was called
await waitFor(
() => {
expect(mockRefetch).toHaveBeenCalled();
},
{ timeout: 3000 },
); // Increase timeout to give more time for the effect to run
}
});
it('triggers refetch when attributeSource changes', async () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Clear any initial calls
mockRefetch.mockClear();
// Find and click on the source select to open dropdown
const sourceSelectElement = getSourceSelect();
fireEvent.mouseDown(sourceSelectElement);
// Find and click on the "Metrics" option
const metricsOption = screen.getByText('Metrics');
fireEvent.click(metricsOption);
// Wait for the effect to run
await waitFor(() => {
// Verify that refetch was called after source selection
expect(mockRefetch).toHaveBeenCalled();
});
});
it('shows retry button when error occurs', () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: { message: 'Failed to fetch field keys' },
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Find and click reload icon (retry button)
const reloadIcon = screen.getByLabelText('reload');
fireEvent.click(reloadIcon);
// Should trigger refetch
expect(mockRefetch).toHaveBeenCalled();
});
});

View File

@@ -100,6 +100,7 @@
.variable-type-btn-group {
display: flex;
width: 342px;
height: 32px;
flex-shrink: 0;
border-radius: 2px;
@@ -198,21 +199,6 @@
}
}
.default-value-section {
display: grid;
grid-template-columns: max-content 1fr;
.default-value-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
.variable-textbox-section {
justify-content: space-between;
margin-bottom: 0;

View File

@@ -6,9 +6,7 @@ import { Button, Collapse, Input, Select, Switch, Tag, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import cx from 'classnames';
import Editor from 'components/Editor';
import { CustomSelect } from 'components/NewSelect';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { map } from 'lodash-es';
@@ -18,20 +16,16 @@ import {
ClipboardType,
DatabaseZap,
LayoutList,
Pyramid,
X,
} from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import {
IDashboardVariable,
TSortVariableValuesType,
TVariableQueryType,
VariableSortTypeArr,
} from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import {
@@ -40,7 +34,6 @@ import {
} from '../../../DashboardVariablesSelection/util';
import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableMode } from '../types';
import DynamicVariable from './DynamicVariable/DynamicVariable';
import { LabelContainer, VariableItemRow } from './styles';
const { Option } = Select;
@@ -68,7 +61,7 @@ function VariableItem({
variableData.description || '',
);
const [queryType, setQueryType] = useState<TVariableQueryType>(
variableData.type || 'DYNAMIC',
variableData.type || 'QUERY',
);
const [variableQueryValue, setVariableQueryValue] = useState<string>(
variableData.queryValue || '',
@@ -92,53 +85,11 @@ function VariableItem({
variableData.showALLOption || false,
);
const [previewValues, setPreviewValues] = useState<string[]>([]);
const [variableDefaultValue, setVariableDefaultValue] = useState<string>(
(variableData.defaultValue as string) || '',
);
const [
dynamicVariablesSelectedValue,
setDynamicVariablesSelectedValue,
] = useState<{ name: string; value: string }>();
useEffect(() => {
if (
variableData.dynamicVariablesAttribute &&
variableData.dynamicVariablesSource
) {
setDynamicVariablesSelectedValue({
name: variableData.dynamicVariablesAttribute,
value: variableData.dynamicVariablesSource,
});
}
}, [
variableData.dynamicVariablesAttribute,
variableData.dynamicVariablesSource,
]);
// Error messages
const [errorName, setErrorName] = useState<boolean>(false);
const [errorPreview, setErrorPreview] = useState<string | null>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data: fieldValues } = useGetFieldValues({
signal:
dynamicVariablesSelectedValue?.value === 'All Sources'
? undefined
: (dynamicVariablesSelectedValue?.value?.toLowerCase() as
| 'traces'
| 'logs'
| 'metrics'),
name: dynamicVariablesSelectedValue?.name || '',
enabled:
!!dynamicVariablesSelectedValue?.name &&
!!dynamicVariablesSelectedValue?.value,
startUnixMilli: minTime,
endUnixMilli: maxTime,
});
useEffect(() => {
if (queryType === 'CUSTOM') {
setPreviewValues(
@@ -159,29 +110,6 @@ function VariableItem({
variableSortType,
]);
useEffect(() => {
if (
queryType === 'DYNAMIC' &&
fieldValues &&
dynamicVariablesSelectedValue?.name &&
dynamicVariablesSelectedValue?.value
) {
setPreviewValues(
sortValues(
fieldValues.payload?.normalizedValues || [],
variableSortType,
) as never,
);
}
}, [
fieldValues,
variableSortType,
queryType,
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
dynamicVariablesSelectedValue,
]);
const handleSave = (): void => {
// Check for cyclic dependencies
const newVariable = {
@@ -198,16 +126,9 @@ function VariableItem({
selectedValue: (variableData.selectedValue ||
variableTextboxValue) as never,
}),
...(queryType !== 'TEXTBOX' && {
defaultValue: variableDefaultValue as never,
}),
modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,
...(queryType === 'DYNAMIC' && {
dynamicVariablesAttribute: dynamicVariablesSelectedValue?.name,
dynamicVariablesSource: dynamicVariablesSelectedValue?.value,
}),
};
const allVariables = [...Object.values(existingVariables), newVariable];
@@ -337,18 +258,18 @@ function VariableItem({
<div className="variable-type-btn-group">
<Button
type="text"
icon={<Pyramid size={14} />}
icon={<DatabaseZap size={14} />}
className={cx(
// eslint-disable-next-line sonarjs/no-duplicate-string
'variable-type-btn',
queryType === 'DYNAMIC' ? 'selected' : '',
queryType === 'QUERY' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('DYNAMIC');
setQueryType('QUERY');
setPreviewValues([]);
}}
>
Dynamic
Query
</Button>
<Button
type="text"
@@ -378,31 +299,8 @@ function VariableItem({
>
Custom
</Button>
<Button
type="text"
icon={<DatabaseZap size={14} />}
className={cx(
// eslint-disable-next-line sonarjs/no-duplicate-string
'variable-type-btn',
queryType === 'QUERY' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('QUERY');
setPreviewValues([]);
}}
>
Query
</Button>
</div>
</VariableItemRow>
{queryType === 'DYNAMIC' && (
<div className="variable-dynamic-section">
<DynamicVariable
setDynamicVariablesSelectedValue={setDynamicVariablesSelectedValue}
dynamicVariablesSelectedValue={dynamicVariablesSelectedValue}
/>
</div>
)}
{queryType === 'QUERY' && (
<div className="query-container">
<LabelContainer>
@@ -490,9 +388,7 @@ function VariableItem({
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' ||
queryType === 'CUSTOM' ||
queryType === 'DYNAMIC') && (
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
<>
<VariableItemRow className="variables-preview-section">
<LabelContainer style={{ width: '100%' }}>
@@ -561,25 +457,6 @@ function VariableItem({
/>
</VariableItemRow>
)}
<VariableItemRow className="default-value-section">
<LabelContainer>
<Typography className="typography-variables">Default Value</Typography>
<Typography className="default-value-description">
{queryType === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography>
</LabelContainer>
<CustomSelect
placeholder="Select a default value"
value={variableDefaultValue}
onChange={(value): void => setVariableDefaultValue(value)}
options={previewValues.map((value) => ({
label: value,
value,
}))}
/>
</VariableItemRow>
</>
)}
</div>

View File

@@ -1,15 +1,14 @@
import { Row } from 'antd';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import './DashboardVariableSelection.styles.scss';
import { Alert, Row } from 'antd';
import { isEmpty } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import DynamicVariableSelection from './DynamicVariableSelection';
import {
buildDependencies,
buildDependencyGraph,
@@ -28,8 +27,6 @@ function DashboardVariableSelection(): JSX.Element | null {
setVariablesToGetUpdated,
} = useDashboard();
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { data } = selectedDashboard || {};
const { variables } = data || {};
@@ -63,11 +60,8 @@ function DashboardVariableSelection(): JSX.Element | null {
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
// Initialize variables with default values if not in URL
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
}, [getUrlVariables, updateUrlVariable, variables]);
}, [variables]);
useEffect(() => {
if (variablesTableData.length > 0) {
@@ -110,14 +104,12 @@ function DashboardVariableSelection(): JSX.Element | null {
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
// isMountedCall?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
if (id) {
updateLocalStorageDashboardVariables(name, value, allSelected);
updateUrlVariable(id, value, allSelected);
if (selectedDashboard) {
setSelectedDashboard((prev) => {
if (prev) {
@@ -129,7 +121,6 @@ function DashboardVariableSelection(): JSX.Element | null {
...oldVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (oldVariables?.[name]) {
@@ -137,7 +128,6 @@ function DashboardVariableSelection(): JSX.Element | null {
...oldVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
return {
@@ -180,22 +170,22 @@ function DashboardVariableSelection(): JSX.Element | null {
);
return (
<Row style={{ display: 'flex', gap: '12px' }}>
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) =>
variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
/>
) : (
<>
{dependencyData?.hasCycle && (
<Alert
message={`Circular dependency detected: ${dependencyData?.cycleNodes?.join(
' → ',
)}`}
type="error"
showIcon
className="cycle-error-alert"
/>
)}
<Row style={{ display: 'flex', gap: '12px' }}>
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) => (
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
@@ -208,9 +198,9 @@ function DashboardVariableSelection(): JSX.Element | null {
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
),
)}
</Row>
))}
</Row>
</>
);
}

View File

@@ -1,385 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable no-nested-ternary */
import './DashboardVariableSelection.styles.scss';
import { Tooltip, Typography } from 'antd';
import { getFieldValues } from 'api/dynamicVariables/getFieldValues';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useDebounce from 'hooks/useDebounce';
import { isEmpty, isUndefined } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import { ALL_SELECT_VALUE } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual } from './util';
import { getSelectValue } from './VariableItem';
interface DynamicVariableSelectionProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
name: string,
id: string,
arg1: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
) => void;
}
function DynamicVariableSelection({
variableData,
onValueUpdate,
existingVariables,
}: DynamicVariableSelectionProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const [isComplete, setIsComplete] = useState<boolean>(false);
const [filteredOptionsData, setFilteredOptionsData] = useState<
(string | number | boolean)[]
>([]);
const [tempSelection, setTempSelection] = useState<
string | string[] | undefined
>(undefined);
// Create a dependency key from all dynamic variables
const dynamicVariablesKey = useMemo(() => {
if (!existingVariables) return 'no_variables';
const dynamicVars = Object.values(existingVariables)
.filter((v) => v.type === 'DYNAMIC')
.map(
(v) => `${v.name || 'unnamed'}:${JSON.stringify(v.selectedValue || null)}`,
)
.join('|');
return dynamicVars || 'no_dynamic_variables';
}, [existingVariables]);
const [apiSearchText, setApiSearchText] = useState<string>('');
const debouncedApiSearchText = useDebounce(apiSearchText, DEBOUNCE_DELAY);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { isLoading, refetch } = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableData.name || `variable_${variableData.id}`,
dynamicVariablesKey,
minTime,
maxTime,
],
{
enabled: variableData.type === 'DYNAMIC',
queryFn: () =>
getFieldValues(
variableData.dynamicVariablesSource?.toLowerCase() === 'all sources'
? undefined
: (variableData.dynamicVariablesSource?.toLowerCase() as
| 'traces'
| 'logs'
| 'metrics'),
variableData.dynamicVariablesAttribute,
debouncedApiSearchText,
minTime,
maxTime,
),
onSuccess: (data) => {
setOptionsData(data.payload?.normalizedValues || []);
setIsComplete(data.payload?.complete || false);
setFilteredOptionsData(data.payload?.normalizedValues || []);
},
onError: (error: any) => {
if (error) {
let message = SOMETHING_WENT_WRONG;
if (error?.message) {
message = error?.message;
} else {
message =
'Please make sure configuration is valid and you have required setup and permissions';
}
setErrorMessage(message);
}
},
},
);
const handleChange = useCallback(
(inputValue: string | string[]): void => {
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(
variableData.name,
variableData.id,
value,
optionsData.every((v) => value.includes(v.toString())),
Array.isArray(value) &&
!value.every((v) => optionsData.includes(v.toString())),
);
}
}
},
[variableData, onValueUpdate, optionsData],
);
useEffect(() => {
if (
variableData.dynamicVariablesSource &&
variableData.dynamicVariablesAttribute
) {
refetch();
}
}, [
refetch,
variableData.dynamicVariablesSource,
variableData.dynamicVariablesAttribute,
debouncedApiSearchText,
]);
const handleSearch = useCallback(
(text: string) => {
if (isComplete) {
if (!text) {
setFilteredOptionsData(optionsData);
return;
}
const localFilteredOptionsData: (string | number | boolean)[] = [];
optionsData.forEach((option) => {
if (option.toString().toLowerCase().includes(text.toLowerCase())) {
localFilteredOptionsData.push(option);
}
});
setFilteredOptionsData(localFilteredOptionsData);
} else {
setApiSearchText(text);
}
},
[isComplete, optionsData],
);
const { selectedValue } = variableData;
const selectedValueStringified = useMemo(
() => getSelectValue(selectedValue, variableData),
[selectedValue, variableData],
);
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
const selectValue =
variableData.allSelected && enableSelectAll
? ALL_SELECT_VALUE
: selectedValueStringified;
// Add a handler for tracking temporary selection changes
const handleTempChange = (inputValue: string | string[]): void => {
// Store the selection in temporary state while dropdown is open
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
setTempSelection(value);
};
// Handle dropdown visibility changes
const handleDropdownVisibleChange = (visible: boolean): void => {
// Initialize temp selection when opening dropdown
if (visible) {
if (isUndefined(tempSelection) && selectValue === ALL_SELECT_VALUE) {
// set all options from the optionsData and the selectedValue, make sure to remove duplicates
const allOptions = [
...new Set([
...optionsData.map((option) => option.toString()),
...(variableData.selectedValue
? Array.isArray(variableData.selectedValue)
? variableData.selectedValue.map((v) => v.toString())
: [variableData.selectedValue.toString()]
: []),
]),
];
setTempSelection(allOptions);
} else {
setTempSelection(getSelectValue(variableData.selectedValue, variableData));
}
}
// Apply changes when closing dropdown
else if (!visible && tempSelection !== undefined) {
// Call handleChange with the temporarily stored selection
handleChange(tempSelection);
setTempSelection(undefined);
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const finalSelectedValues = useMemo(() => {
if (variableData.multiSelect) {
let value = tempSelection || selectedValue;
if (isEmpty(value)) {
if (variableData.showALLOption) {
if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData;
}
} else if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData?.[0];
}
}
return value;
}
if (isEmpty(selectedValue)) {
if (variableData.defaultValue) {
return variableData.defaultValue;
}
return optionsData[0]?.toString();
}
return selectedValue;
}, [
variableData.multiSelect,
variableData.showALLOption,
variableData.defaultValue,
selectedValue,
tempSelection,
optionsData,
]);
useEffect(() => {
if (
(variableData.multiSelect && !(tempSelection || selectValue)) ||
isEmpty(selectValue)
) {
handleChange(finalSelectedValues as string[] | string);
}
}, [
finalSelectedValues,
handleChange,
selectValue,
tempSelection,
variableData.multiSelect,
]);
return (
<div className="variable-item">
<Typography.Text className="variable-name" ellipsis>
${variableData.name}
</Typography.Text>
<div className="variable-value">
{variableData.multiSelect ? (
<CustomMultiSelect
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
options={filteredOptionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
defaultValue={variableData.defaultValue}
onChange={handleTempChange}
bordered={false}
placeholder="Select value"
placement="bottomLeft"
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
maxTagCount={2}
getPopupContainer={popupContainer}
value={
(tempSelection || selectValue) === ALL_SELECT_VALUE
? 'ALL'
: tempSelection || selectValue
}
onDropdownVisibleChange={handleDropdownVisibleChange}
errorMessage={errorMessage}
// eslint-disable-next-line react/no-unstable-nested-components
maxTagPlaceholder={(omittedValues): JSX.Element => (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
<span>+ {omittedValues.length} </span>
</Tooltip>
)}
onClear={(): void => {
handleChange([]);
}}
enableAllSelection={enableSelectAll}
maxTagTextLength={30}
onSearch={handleSearch}
onRetry={(): void => {
refetch();
}}
showIncompleteDataMessage={!isComplete && filteredOptionsData.length > 0}
/>
) : (
<CustomSelect
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
onChange={handleChange}
bordered={false}
placeholder="Select value"
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
getPopupContainer={popupContainer}
options={filteredOptionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
value={selectValue}
defaultValue={variableData.defaultValue}
errorMessage={errorMessage}
onSearch={handleSearch}
onRetry={(): void => {
refetch();
}}
showIncompleteDataMessage={!isComplete && filteredOptionsData.length > 0}
/>
)}
</div>
</div>
);
}
export default DynamicVariableSelection;

View File

@@ -8,14 +8,23 @@ import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors';
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Tooltip, Typography } from 'antd';
import {
Checkbox,
Input,
Popover,
Select,
Tag,
Tooltip,
Typography,
} from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { debounce, isArray, isString } from 'lodash-es';
import map from 'lodash-es/map';
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -24,10 +33,17 @@ import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
const ALL_SELECT_VALUE = '__ALL__';
enum ToggleTagValue {
Only = 'Only',
All = 'All',
}
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
@@ -42,7 +58,7 @@ interface VariableItemProps {
dependencyData: IDependencyData | null;
}
export const getSelectValue = (
const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable,
): string | string[] | undefined => {
@@ -67,9 +83,6 @@ function VariableItem({
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [tempSelection, setTempSelection] = useState<
string | string[] | undefined
>(undefined);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -133,10 +146,18 @@ function VariableItem({
variableData.name &&
(validVariableUpdate() || valueNotInList || variableData.allSelected)
) {
const value = variableData.selectedValue;
let value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
// The default value for multi-select is ALL and first value for
// single select
if (valueNotInList) {
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
}
} else if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
@@ -221,57 +242,26 @@ function VariableItem({
},
);
const handleChange = useCallback(
(inputValue: string | string[]): void => {
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(variableData.name, variableData.id, value, false);
}
}
},
[
variableData.multiSelect,
variableData.selectedValue,
variableData.name,
variableData.id,
onValueUpdate,
optionsData,
],
);
// Add a handler for tracking temporary selection changes
const handleTempChange = (inputValue: string | string[]): void => {
// Store the selection in temporary state while dropdown is open
const handleChange = (inputValue: string | string[]): void => {
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
setTempSelection(value);
};
// Handle dropdown visibility changes
const handleDropdownVisibleChange = (visible: boolean): void => {
// Initialize temp selection when opening dropdown
if (visible) {
setTempSelection(getSelectValue(variableData.selectedValue, variableData));
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
// Apply changes when closing dropdown
else if (!visible && tempSelection !== undefined) {
// Call handleChange with the temporarily stored selection
handleChange(tempSelection);
setTempSelection(undefined);
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(variableData.name, variableData.id, value, false);
}
}
};
@@ -291,58 +281,10 @@ function VariableItem({
? 'ALL'
: selectedValueStringified;
// Apply default value on first render if no selection exists
// eslint-disable-next-line sonarjs/cognitive-complexity
const finalSelectedValues = useMemo(() => {
if (variableData.multiSelect) {
let value = tempSelection || selectedValue;
if (isEmpty(value)) {
if (variableData.showALLOption) {
if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData;
}
} else if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData?.[0];
}
}
return value;
}
if (isEmpty(selectedValue)) {
if (variableData.defaultValue) {
return variableData.defaultValue;
}
return optionsData[0]?.toString();
}
return selectedValue;
}, [
variableData.multiSelect,
variableData.showALLOption,
variableData.defaultValue,
selectedValue,
tempSelection,
optionsData,
]);
useEffect(() => {
if (
(variableData.multiSelect && !(tempSelection || selectValue)) ||
isEmpty(selectValue)
) {
handleChange(finalSelectedValues as string[] | string);
}
}, [
finalSelectedValues,
handleChange,
selectValue,
tempSelection,
variableData.multiSelect,
]);
const mode: 'multiple' | undefined =
variableData.multiSelect && !variableData.allSelected
? 'multiple'
: undefined;
useEffect(() => {
// Fetch options for CUSTOM Type
@@ -352,6 +294,113 @@ function VariableItem({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData.type, variableData.customValue]);
const checkAll = (e: MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
const isChecked =
variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE);
if (isChecked) {
handleChange([]);
} else {
handleChange(ALL_SELECT_VALUE);
}
};
const handleOptionSelect = (
e: CheckboxChangeEvent,
option: string | number | boolean,
): void => {
const newSelectedValue = Array.isArray(selectedValue)
? ((selectedValue.filter(
(val) => val.toString() !== option.toString(),
) as unknown) as string[])
: [];
if (
!e.target.checked &&
Array.isArray(selectedValueStringified) &&
selectedValueStringified.includes(option.toString())
) {
if (newSelectedValue.length === 1) {
handleChange(newSelectedValue[0].toString());
return;
}
handleChange(newSelectedValue);
} else if (!e.target.checked && selectedValue === option.toString()) {
handleChange(ALL_SELECT_VALUE);
} else if (newSelectedValue.length === optionsData.length - 1) {
handleChange(ALL_SELECT_VALUE);
}
};
const [optionState, setOptionState] = useState({
tag: '',
visible: false,
});
function currentToggleTagValue({
option,
}: {
option: string;
}): ToggleTagValue {
if (
option.toString() === selectValue ||
(Array.isArray(selectValue) &&
selectValue?.includes(option.toString()) &&
selectValue.length === 1)
) {
return ToggleTagValue.All;
}
return ToggleTagValue.Only;
}
function handleToggle(e: ChangeEvent, option: string): void {
e.stopPropagation();
const mode = currentToggleTagValue({ option: option as string });
const isChecked =
variableData.allSelected ||
option.toString() === selectValue ||
(Array.isArray(selectValue) && selectValue?.includes(option.toString()));
if (isChecked) {
if (mode === ToggleTagValue.Only && variableData.multiSelect) {
handleChange([option.toString()]);
} else if (!variableData.multiSelect) {
handleChange(option.toString());
} else {
handleChange(ALL_SELECT_VALUE);
}
} else {
handleChange(option.toString());
}
}
function retProps(
option: string,
): {
onMouseOver: () => void;
onMouseOut: () => void;
} {
return {
onMouseOver: (): void =>
setOptionState({
tag: option.toString(),
visible: true,
}),
onMouseOut: (): void =>
setOptionState({
tag: option.toString(),
visible: false,
}),
};
}
const ensureValidOption = (option: string): boolean =>
!(
currentToggleTagValue({ option }) === ToggleTagValue.All && !enableSelectAll
);
return (
<div className="variable-item">
<Typography.Text className="variable-name" ellipsis>
@@ -379,73 +428,105 @@ function VariableItem({
}}
/>
) : (
optionsData &&
(variableData.multiSelect ? (
<CustomMultiSelect
!errorMessage &&
optionsData && (
<Select
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
options={optionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
defaultValue={variableData.defaultValue || selectValue}
onChange={handleTempChange}
defaultValue={selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
placement="bottomLeft"
mode={mode}
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
maxTagCount={2}
maxTagCount={4}
getPopupContainer={popupContainer}
value={tempSelection || selectValue}
onDropdownVisibleChange={handleDropdownVisibleChange}
errorMessage={errorMessage}
// eslint-disable-next-line react/no-unstable-nested-components
tagRender={(props): JSX.Element => (
<Tag closable onClose={props.onClose}>
{props.value}
</Tag>
)}
// eslint-disable-next-line react/no-unstable-nested-components
maxTagPlaceholder={(omittedValues): JSX.Element => (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
<span>+ {omittedValues.length} </span>
</Tooltip>
)}
onClear={(): void => {
handleChange([]);
}}
enableAllSelection={enableSelectAll}
maxTagTextLength={30}
allowClear={selectValue !== ALL_SELECT_VALUE && selectValue !== 'ALL'}
/>
) : (
<CustomSelect
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
defaultValue={variableData.defaultValue || selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
getPopupContainer={popupContainer}
options={optionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
value={selectValue}
errorMessage={errorMessage}
/>
))
>
{enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
<div className="all-label" onClick={(e): void => checkAll(e as any)}>
<Checkbox checked={variableData.allSelected} />
ALL
</div>
</Select.Option>
)}
{map(optionsData, (option) => (
<Select.Option
data-testid={`option-${option}`}
key={option.toString()}
value={option}
>
<div
className={variableData.multiSelect ? 'dropdown-checkbox-label' : ''}
>
{variableData.multiSelect && (
<Checkbox
onChange={(e): void => {
e.stopPropagation();
e.preventDefault();
handleOptionSelect(e, option);
}}
checked={
variableData.allSelected ||
option.toString() === selectValue ||
(Array.isArray(selectValue) &&
selectValue?.includes(option.toString()))
}
/>
)}
<div
className="dropdown-value"
{...retProps(option as string)}
onClick={(e): void => handleToggle(e as any, option as string)}
>
<Typography.Text
ellipsis={{
tooltip: {
placement: variableData.multiSelect ? 'top' : 'right',
autoAdjustOverflow: true,
},
}}
className="option-text"
>
{option.toString()}
</Typography.Text>
{variableData.multiSelect &&
optionState.tag === option.toString() &&
optionState.visible &&
ensureValidOption(option as string) && (
<Typography.Text className="toggle-tag-label">
{currentToggleTagValue({ option: option as string })}
</Typography.Text>
)}
</div>
</div>
</Select.Option>
))}
</Select>
)
)}
{variableData.type !== 'TEXTBOX' && errorMessage && (
<span style={{ margin: '0 0.5rem' }}>

View File

@@ -1,274 +0,0 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen } from '@testing-library/react';
import * as ReactQuery from 'react-query';
import * as ReactRedux from 'react-redux';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DynamicVariableSelection from '../DynamicVariableSelection';
// Don't mock the components - use real ones
// Mock for useQuery
const mockQueryResult = {
data: undefined,
error: null,
isError: false,
isIdle: false,
isLoading: false,
isPreviousData: false,
isSuccess: true,
status: 'success',
isFetched: true,
isFetchingNextPage: false,
isFetchingPreviousPage: false,
isPlaceholderData: false,
isPaused: false,
isRefetchError: false,
isRefetching: false,
isStale: false,
isLoadingError: false,
isFetching: false,
isFetchedAfterMount: true,
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
refetch: jest.fn(),
remove: jest.fn(),
fetchNextPage: jest.fn(),
fetchPreviousPage: jest.fn(),
hasNextPage: false,
hasPreviousPage: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
// Sample data for testing
const mockApiResponse = {
payload: {
normalizedValues: ['frontend', 'backend', 'database'],
complete: true,
},
statusCode: 200,
};
// Mock scrollIntoView since it's not available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
describe('DynamicVariableSelection Component', () => {
const mockOnValueUpdate = jest.fn();
const mockDynamicVariableData: IDashboardVariable = {
id: 'var1',
name: 'service',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'Traces',
selectedValue: 'frontend',
multiSelect: false,
showALLOption: false,
allSelected: false,
description: '',
sort: 'DISABLED',
};
const mockMultiSelectDynamicVariableData: IDashboardVariable = {
...mockDynamicVariableData,
id: 'var2',
name: 'services',
multiSelect: true,
selectedValue: ['frontend', 'backend'],
showALLOption: true,
};
const mockExistingVariables: Record<string, IDashboardVariable> = {
var1: mockDynamicVariableData,
var2: mockMultiSelectDynamicVariableData,
};
beforeEach(() => {
jest.clearAllMocks();
mockOnValueUpdate.mockClear();
// Mock useSelector
const useSelectorSpy = jest.spyOn(ReactRedux, 'useSelector');
useSelectorSpy.mockReturnValue({
minTime: '2023-01-01T00:00:00Z',
maxTime: '2023-01-02T00:00:00Z',
});
// Mock useQuery with success state
const useQuerySpy = jest.spyOn(ReactQuery, 'useQuery');
useQuerySpy.mockReturnValue({
...mockQueryResult,
data: mockApiResponse,
isLoading: false,
error: null,
});
});
it('renders with single select variable correctly', () => {
render(
<DynamicVariableSelection
variableData={mockDynamicVariableData}
existingVariables={mockExistingVariables}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify component renders correctly
expect(
screen.getByText(`$${mockDynamicVariableData.name}`),
).toBeInTheDocument();
// Verify the selected value is displayed
const selectedItem = screen.getByRole('combobox');
expect(selectedItem).toBeInTheDocument();
// CustomSelect doesn't use the 'mode' attribute for single select
expect(selectedItem).not.toHaveAttribute('mode');
});
it('renders with multi select variable correctly', () => {
// First set up allSelected to true to properly test the ALL display
const multiSelectWithAllSelected = {
...mockMultiSelectDynamicVariableData,
allSelected: true,
};
render(
<DynamicVariableSelection
variableData={multiSelectWithAllSelected}
existingVariables={mockExistingVariables}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify variable name is rendered
expect(
screen.getByText(`$${multiSelectWithAllSelected.name}`),
).toBeInTheDocument();
// In ALL selected mode, there should be an "ALL" text element
expect(screen.getByText('ALL')).toBeInTheDocument();
});
it('shows loading state correctly', () => {
// Mock loading state
jest.spyOn(ReactQuery, 'useQuery').mockReturnValue({
...mockQueryResult,
data: null,
isLoading: true,
isFetching: true,
isSuccess: false,
status: 'loading',
});
render(
<DynamicVariableSelection
variableData={mockDynamicVariableData}
existingVariables={mockExistingVariables}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify component renders in loading state
expect(
screen.getByText(`$${mockDynamicVariableData.name}`),
).toBeInTheDocument();
// Open dropdown to see loading text
const selectElement = screen.getByRole('combobox');
fireEvent.mouseDown(selectElement);
// The loading text should appear in the dropdown
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
});
it('handles error state correctly', () => {
const errorMessage = 'Failed to fetch data';
// Mock error state
jest.spyOn(ReactQuery, 'useQuery').mockReturnValue({
...mockQueryResult,
data: null,
isLoading: false,
isSuccess: false,
isError: true,
status: 'error',
error: { message: errorMessage },
});
render(
<DynamicVariableSelection
variableData={mockDynamicVariableData}
existingVariables={mockExistingVariables}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify the component renders
expect(
screen.getByText(`$${mockDynamicVariableData.name}`),
).toBeInTheDocument();
// For error states, we should check that error handling is in place
// Without opening the dropdown as the error message might be handled differently
expect(ReactQuery.useQuery).toHaveBeenCalled();
// We don't need to check refetch as it might be called during component initialization
});
it('makes API call to fetch variable values', () => {
render(
<DynamicVariableSelection
variableData={mockDynamicVariableData}
existingVariables={mockExistingVariables}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify the useQuery hook was called with expected parameters
expect(ReactQuery.useQuery).toHaveBeenCalledWith(
[
'DASHBOARD_BY_ID',
mockDynamicVariableData.name,
'service:"frontend"|services:["frontend","backend"]', // The actual dynamicVariablesKey
'2023-01-01T00:00:00Z', // minTime from useSelector mock
'2023-01-02T00:00:00Z', // maxTime from useSelector mock
],
expect.objectContaining({
enabled: true, // Type is 'DYNAMIC'
queryFn: expect.any(Function),
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
});
it('has the correct selected value', () => {
// Use a different variable configuration to test different behavior
const customVariable = {
...mockDynamicVariableData,
id: 'custom1',
name: 'customService',
selectedValue: 'backend',
};
render(
<DynamicVariableSelection
variableData={customVariable}
existingVariables={{ ...mockExistingVariables, custom1: customVariable }}
onValueUpdate={mockOnValueUpdate}
/>,
);
// Verify the component correctly displays the selected value
expect(screen.getByText(`$${customVariable.name}`)).toBeInTheDocument();
// Find the selection item in the component using data-testid
const selectElement = screen.getByTestId('variable-select');
expect(selectElement).toBeInTheDocument();
// Check that the selected value is displayed in the select element
expect(selectElement).toHaveTextContent('backend');
});
});

View File

@@ -14,5 +14,3 @@ export function variablePropsToPayloadVariables(
return payloadVariables;
}
export const ALL_SELECT_VALUE = '__ALL__';

View File

@@ -6,7 +6,6 @@ import { Card } from 'container/GridCardLayout/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { memo } from 'react';
import { Warning } from 'types/api';
@@ -39,7 +38,7 @@ function WidgetGraph({
<div className="header">
<div className="header-left">
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{!isEmpty(queryResponse.data?.warning) && (
{queryResponse.data?.warning?.message && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>

View File

@@ -6,7 +6,6 @@ import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKey
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -453,9 +452,7 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetchingV5,
value:
preferences?.columns.filter((item) => has(item, 'name')) ||
defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')),
value: preferences?.columns || defaultOptionsQuery.selectColumns,
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,

View File

@@ -4,7 +4,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
query: IBuilderQuery;
onChange: (value: BaseAutocompleteData, isEditMode?: boolean) => void;
onChange: (value: BaseAutocompleteData) => void;
defaultValue?: string;
onSelect?: (value: BaseAutocompleteData) => void;
index?: number;

View File

@@ -137,7 +137,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
if (metricData) {
// Update the aggregateAttribute with the fetched type information
onChange(metricData, true);
onChange(metricData);
}
}
}, [
@@ -223,16 +223,11 @@ export const AggregatorFilter = memo(function AggregatorFilter({
);
const handleBlur = useCallback(async () => {
if (searchText && searchText !== queryAggregation.metricName) {
if (searchText) {
const aggregateAttributes = await getResponseAttributes();
handleChangeCustomValue(searchText, aggregateAttributes);
}
}, [
getResponseAttributes,
handleChangeCustomValue,
searchText,
queryAggregation?.metricName,
]);
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
const handleChange = useCallback(
(

View File

@@ -266,8 +266,8 @@ export const defaultMoreMenuItems: SidebarItem[] = [
itemKey: 'external-apis',
},
{
key: ROUTES.METER,
label: 'Cost Meter',
key: ROUTES.METER_EXPLORER,
label: 'Meter Explorer',
icon: <ChartArea size={16} />,
isNew: false,
isEnabled: false,

View File

@@ -1,160 +0,0 @@
import { Button, Popover, Spin, Tooltip } from 'antd';
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { useTraceActions } from 'hooks/trace/useTraceActions';
import { ArrowDownToDot, ArrowUpFromDot, Copy, Ellipsis } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
interface AttributeRecord {
field: string;
value: string;
}
interface AttributeActionsProps {
record: AttributeRecord;
}
export default function AttributeActions({
record,
}: AttributeActionsProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isFilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isFilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const {
onAddToQuery,
onGroupByAttribute,
onCopyFieldName,
onCopyFieldValue,
} = useTraceActions();
const textToCopy = useMemo(() => {
const str = record.value == null ? '' : String(record.value);
// Remove surrounding double-quotes only (e.g., JSON-encoded string values)
return str.replace(/^"|"$/g, '');
}, [record.value]);
const handleFilterIn = useCallback(async (): Promise<void> => {
if (!onAddToQuery || isFilterInLoading) return;
setIsFilterInLoading(true);
try {
await Promise.resolve(
onAddToQuery(record.field, record.value, OPERATORS['=']),
);
} finally {
setIsFilterInLoading(false);
}
}, [onAddToQuery, record.field, record.value, isFilterInLoading]);
const handleFilterOut = useCallback(async (): Promise<void> => {
if (!onAddToQuery || isFilterOutLoading) return;
setIsFilterOutLoading(true);
try {
await Promise.resolve(
onAddToQuery(record.field, record.value, OPERATORS['!=']),
);
} finally {
setIsFilterOutLoading(false);
}
}, [onAddToQuery, record.field, record.value, isFilterOutLoading]);
const handleGroupBy = useCallback((): void => {
if (onGroupByAttribute) {
onGroupByAttribute(record.field);
}
setIsOpen(false);
}, [onGroupByAttribute, record.field]);
const handleCopyFieldName = useCallback((): void => {
if (onCopyFieldName) {
onCopyFieldName(record.field);
}
setIsOpen(false);
}, [onCopyFieldName, record.field]);
const handleCopyFieldValue = useCallback((): void => {
if (onCopyFieldValue) {
onCopyFieldValue(textToCopy);
}
setIsOpen(false);
}, [onCopyFieldValue, textToCopy]);
const moreActionsContent = (
<div className="attribute-actions-menu">
<Button
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={handleGroupBy}
block
>
Group By Attribute
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={handleCopyFieldName}
block
>
Copy Field Name
</Button>
<Button
type="text"
icon={<Copy size={14} />}
onClick={handleCopyFieldValue}
block
>
Copy Field Value
</Button>
</div>
);
return (
<div className="action-btn">
<Tooltip title="Filter for value">
<Button
className="filter-btn periscope-btn"
aria-label="Filter for value"
disabled={isFilterInLoading}
icon={
isFilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={handleFilterIn}
/>
</Tooltip>
<Tooltip title="Filter out value">
<Button
className="filter-btn periscope-btn"
aria-label="Filter out value"
disabled={isFilterOutLoading}
icon={
isFilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={handleFilterOut}
/>
</Tooltip>
<Popover
open={isOpen}
onOpenChange={setIsOpen}
arrow={false}
content={moreActionsContent}
rootClassName="attribute-actions-content"
trigger="hover"
placement="bottomLeft"
>
<Button
icon={<Ellipsis size={14} />}
className="filter-btn periscope-btn"
/>
</Popover>
</div>
);
}

View File

@@ -24,13 +24,6 @@
flex-direction: column;
gap: 8px;
justify-content: flex-start;
position: relative;
&:hover {
.action-btn {
display: flex;
}
}
.item-key {
color: var(--bg-vanilla-100);
@@ -47,12 +40,11 @@
padding: 2px 8px;
align-items: center;
width: fit-content;
max-width: calc(100% - 120px); /* Reserve space for action buttons */
max-width: 100%;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
.item-value {
color: var(--bg-vanilla-400);
font-family: Inter;
@@ -63,35 +55,6 @@
letter-spacing: 0.56px;
}
}
.action-btn {
display: none;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
gap: 4px;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
padding: 2px;
.filter-btn {
display: flex;
align-items: center;
border: none;
box-shadow: none;
border-radius: 2px;
background: var(--bg-slate-400);
padding: 4px;
gap: 3px;
height: 24px;
width: 24px;
&:hover {
background: var(--bg-slate-300);
}
}
}
}
}
@@ -100,36 +63,6 @@
}
}
.attribute-actions-menu {
display: flex;
flex-direction: column;
gap: 4px;
.ant-btn {
text-align: left;
height: auto;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 8px;
&:hover {
background-color: var(--bg-slate-400);
}
}
.group-by-clause {
color: var(--text-primary);
}
}
.attribute-actions-content {
.ant-popover-inner {
padding: 8px;
min-width: 160px;
}
}
.lightMode {
.attributes-corner {
.attributes-container {
@@ -146,18 +79,6 @@
color: var(--bg-ink-400);
}
}
.action-btn {
background: rgba(255, 255, 255, 0.9);
.filter-btn {
background: var(--bg-vanilla-200);
&:hover {
background: var(--bg-vanilla-100);
}
}
}
}
}
@@ -165,12 +86,4 @@
border-top: 1px solid var(--bg-vanilla-300);
}
}
.attribute-actions-menu {
.ant-btn {
&:hover {
background-color: var(--bg-vanilla-200);
}
}
}
}

View File

@@ -2,13 +2,11 @@ import './Attributes.styles.scss';
import { Input, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { flattenObject } from 'container/LogDetailedView/utils';
import { useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
import AttributeActions from './AttributeActions';
interface IAttributesProps {
span: Span;
@@ -55,13 +53,10 @@ function Attributes(props: IAttributesProps): JSX.Element {
</Typography.Text>
<div className="value-wrapper">
<Tooltip title={item.value}>
<CopyClipboardHOC entityKey={item.value} textToCopy={item.value}>
<Typography.Text className="item-value" ellipsis>
{item.value}
</Typography.Text>
</CopyClipboardHOC>
<Typography.Text className="item-value" ellipsis>
{item.value}
</Typography.Text>
</Tooltip>
<AttributeActions record={item} />
</div>
</div>
))}

View File

@@ -1,7 +1,7 @@
.span-details-drawer {
display: flex;
flex-direction: column;
width: 450px;
width: 330px;
border-left: 1px solid var(--bg-slate-400);
overflow-y: auto;
@@ -176,34 +176,6 @@
justify-content: center;
width: 100%;
padding: 4px 8px;
margin-right: 8px;
gap: 4px;
.tab-label {
display: flex;
align-items: center;
}
.count-badge {
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 10px;
background: rgba(171, 189, 255, 0.1);
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
text-transform: uppercase;
}
}
.attributes-tab-btn:hover,

View File

@@ -54,10 +54,7 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
icon={<Bookmark size="14" />}
className="attributes-tab-btn"
>
<span className="tab-label">Attributes</span>
<span className="count-badge">
{Object.keys(span.tagMap || {}).length}
</span>
Attributes
</Button>
),
key: 'attributes',
@@ -66,8 +63,7 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
{
label: (
<Button type="text" icon={<Anvil size="14" />} className="events-tab-btn">
<span className="tab-label">Events</span>
<span className="count-badge">{span.event?.length || 0}</span>
Events
</Button>
),
key: 'events',
@@ -86,14 +82,7 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
icon={<Link2 size="14" />}
className="linked-spans-tab-btn"
>
<span className="tab-label">Links</span>
<span className="count-badge">
{
(
span.references?.filter((ref: any) => ref.refType !== 'CHILD_OF') || []
).length
}
</span>
Links
</Button>
),
key: 'linked-spans',

View File

@@ -4,7 +4,6 @@ import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import Uplot from 'components/Uplot';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
@@ -55,7 +54,6 @@ function TimeSeriesView({
isFilterApplied,
dataSource,
setWarning,
panelType = PANEL_TYPES.TIME_SERIES,
}: TimeSeriesViewProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
@@ -193,7 +191,6 @@ function TimeSeriesView({
maxTimeScale,
softMax: null,
softMin: null,
panelType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
@@ -262,7 +259,6 @@ interface TimeSeriesViewProps {
isFilterApplied: boolean;
dataSource: DataSource;
setWarning?: Dispatch<SetStateAction<Warning | undefined>>;
panelType?: PANEL_TYPES;
}
TimeSeriesView.defaultProps = {
@@ -270,7 +266,6 @@ TimeSeriesView.defaultProps = {
yAxisUnit: 'short',
error: undefined,
setWarning: undefined,
panelType: PANEL_TYPES.TIME_SERIES,
};
export default TimeSeriesView;

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