diff --git a/.devenv/docker/clickhouse/compose.yaml b/.devenv/docker/clickhouse/compose.yaml index e8c72d679ef7..7592202c1183 100644 --- a/.devenv/docker/clickhouse/compose.yaml +++ b/.devenv/docker/clickhouse/compose.yaml @@ -40,7 +40,7 @@ services: timeout: 5s retries: 3 schema-migrator-sync: - image: signoz/signoz-schema-migrator:v0.128.2 + image: signoz/signoz-schema-migrator:v0.129.0 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.128.2 + image: signoz/signoz-schema-migrator:v0.129.0 container_name: schema-migrator-async command: - async diff --git a/.devenv/docker/signoz-otel-collector/compose.yaml b/.devenv/docker/signoz-otel-collector/compose.yaml new file mode 100644 index 000000000000..62a931b38de5 --- /dev/null +++ b/.devenv/docker/signoz-otel-collector/compose.yaml @@ -0,0 +1,29 @@ +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" \ No newline at end of file diff --git a/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml new file mode 100644 index 000000000000..43a888fffb79 --- /dev/null +++ b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml @@ -0,0 +1,96 @@ +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] \ No newline at end of file diff --git a/Makefile b/Makefile index a8743a561ae9..415dc82385d1 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,17 @@ 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 ############################################################## diff --git a/conf/example.yaml b/conf/example.yaml index 1dfbec121aef..7fd1fb9e976c 100644 --- a/conf/example.yaml +++ b/conf/example.yaml @@ -121,6 +121,8 @@ 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: diff --git a/deploy/docker-swarm/docker-compose.ha.yaml b/deploy/docker-swarm/docker-compose.ha.yaml index cdde64259bf4..139ab973d299 100644 --- a/deploy/docker-swarm/docker-compose.ha.yaml +++ b/deploy/docker-swarm/docker-compose.ha.yaml @@ -174,7 +174,7 @@ services: # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml signoz: !!merge <<: *db-depend - image: signoz/signoz:v0.91.0 + image: signoz/signoz:v0.92.1 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.128.2 + image: signoz/signoz-otel-collector:v0.129.0 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.128.2 + image: signoz/signoz-schema-migrator:v0.129.0 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker-swarm/docker-compose.yaml b/deploy/docker-swarm/docker-compose.yaml index 90e94be7a066..5b76651ba38d 100644 --- a/deploy/docker-swarm/docker-compose.yaml +++ b/deploy/docker-swarm/docker-compose.yaml @@ -115,7 +115,7 @@ services: # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml signoz: !!merge <<: *db-depend - image: signoz/signoz:v0.91.0 + image: signoz/signoz:v0.92.1 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.128.2 + image: signoz/signoz-otel-collector:v0.129.0 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.128.2 + image: signoz/signoz-schema-migrator:v0.129.0 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/docker-compose.ha.yaml b/deploy/docker/docker-compose.ha.yaml index c6c9f6796f33..4a57794246d3 100644 --- a/deploy/docker/docker-compose.ha.yaml +++ b/deploy/docker/docker-compose.ha.yaml @@ -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.91.0} + image: signoz/signoz:${VERSION:-v0.92.1} 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.128.2} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0} 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.128.2} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0} 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.128.2} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0} container_name: schema-migrator-async command: - async diff --git a/deploy/docker/docker-compose.yaml b/deploy/docker/docker-compose.yaml index 35db637ef0a8..cf0176c4f440 100644 --- a/deploy/docker/docker-compose.yaml +++ b/deploy/docker/docker-compose.yaml @@ -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.91.0} + image: signoz/signoz:${VERSION:-v0.92.1} 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.128.2} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0} 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.128.2} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0} 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.128.2} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0} container_name: schema-migrator-async command: - async diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 27cbc0355722..41f51567b1bf 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -44,20 +44,35 @@ 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. Starting the Backend +### 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 1. Run the backend server: ```bash @@ -73,7 +88,7 @@ This command: > 💡 **Tip**: The API server runs at `http://localhost:8080/` by default -### 3. Setting up the Frontend +### 4. Setting up the Frontend 1. Navigate to the frontend directory: ```bash @@ -98,3 +113,25 @@ 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"}]}]}]}' +``` diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index 95a58e615f98..1e16e3ecc56d 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -46,5 +46,8 @@ "ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", - "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring" + "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring", + "METER_EXPLORER": "SigNoz | Meter Explorer", + "METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer", + "METER_EXPLORER_BASE": "SigNoz | Meter Explorer" } diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 9e4039a86b74..ec43f8c026d3 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -69,5 +69,8 @@ "METRICS_EXPLORER": "SigNoz | Metrics Explorer", "METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer", "METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer", - "API_MONITORING": "SigNoz | External APIs" + "API_MONITORING": "SigNoz | External APIs", + "METER_EXPLORER": "SigNoz | Meter Explorer", + "METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer", + "METER_EXPLORER_BASE": "SigNoz | Meter Explorer" } diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index d403a3b19dd4..c2528be48bf6 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -1,5 +1,6 @@ import ROUTES from 'constants/routes'; import MessagingQueues from 'pages/MessagingQueues'; +import MeterExplorer from 'pages/MeterExplorer'; import { RouteProps } from 'react-router-dom'; import { @@ -434,6 +435,28 @@ const routes: AppRoutes[] = [ key: 'METRICS_EXPLORER_VIEWS', isPrivate: true, }, + + { + path: ROUTES.METER_EXPLORER_BASE, + exact: true, + component: MeterExplorer, + key: 'METER_EXPLORER_BASE', + isPrivate: true, + }, + { + path: ROUTES.METER_EXPLORER, + exact: true, + component: MeterExplorer, + key: 'METER_EXPLORER', + isPrivate: true, + }, + { + path: ROUTES.METER_EXPLORER_VIEWS, + exact: true, + component: MeterExplorer, + key: 'METER_EXPLORER_VIEWS', + isPrivate: true, + }, { path: ROUTES.API_MONITORING, exact: true, diff --git a/frontend/src/api/queryBuilder/getAggregateAttribute.ts b/frontend/src/api/queryBuilder/getAggregateAttribute.ts index f13c3da4a891..8080d9cc6f90 100644 --- a/frontend/src/api/queryBuilder/getAggregateAttribute.ts +++ b/frontend/src/api/queryBuilder/getAggregateAttribute.ts @@ -17,6 +17,7 @@ export const getAggregateAttribute = async ({ aggregateOperator, searchText, dataSource, + source, }: IGetAggregateAttributePayload): Promise< SuccessResponse | ErrorResponse > => { @@ -27,7 +28,7 @@ export const getAggregateAttribute = async ({ `/autocomplete/aggregate_attributes?${createQueryParams({ aggregateOperator, searchText, - dataSource, + dataSource: source === 'meter' ? 'meter' : dataSource, })}`, ); diff --git a/frontend/src/api/querySuggestions/getKeySuggestions.ts b/frontend/src/api/querySuggestions/getKeySuggestions.ts index 01f737fb4011..50626dee8b88 100644 --- a/frontend/src/api/querySuggestions/getKeySuggestions.ts +++ b/frontend/src/api/querySuggestions/getKeySuggestions.ts @@ -14,6 +14,7 @@ export const getKeySuggestions = ( metricName = '', fieldContext = '', fieldDataType = '', + signalSource = '', } = props; const encodedSignal = encodeURIComponent(signal); @@ -21,8 +22,9 @@ export const getKeySuggestions = ( const encodedMetricName = encodeURIComponent(metricName); const encodedFieldContext = encodeURIComponent(fieldContext); const encodedFieldDataType = encodeURIComponent(fieldDataType); + const encodedSource = encodeURIComponent(signalSource); return axios.get( - `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`, + `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`, ); }; diff --git a/frontend/src/api/querySuggestions/getValueSuggestion.ts b/frontend/src/api/querySuggestions/getValueSuggestion.ts index cbd2ea46245a..f91a40139b9a 100644 --- a/frontend/src/api/querySuggestions/getValueSuggestion.ts +++ b/frontend/src/api/querySuggestions/getValueSuggestion.ts @@ -8,13 +8,15 @@ import { export const getValueSuggestions = ( props: QueryKeyValueRequestProps, ): Promise> => { - const { signal, key, searchText } = props; + const { signal, key, searchText, signalSource, metricName } = 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}`, + `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`, ); }; diff --git a/frontend/src/api/saveView/getAllViews.ts b/frontend/src/api/saveView/getAllViews.ts index 4a54d6af0df4..a26fd13441f5 100644 --- a/frontend/src/api/saveView/getAllViews.ts +++ b/frontend/src/api/saveView/getAllViews.ts @@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types'; import { DataSource } from 'types/common/queryBuilder'; export const getAllViews = ( - sourcepage: DataSource, + sourcepage: DataSource | 'meter', ): Promise> => axios.get(`/explorer/views?sourcePage=${sourcepage}`); diff --git a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts index 20fed08cae73..e77786492e40 100644 --- a/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts +++ b/frontend/src/api/v5/queryRange/prepareQueryRangePayloadV5.ts @@ -260,6 +260,7 @@ export function convertBuilderQueriesToV5( spec = { name: queryName, signal: 'metrics' as const, + source: queryData.source || '', ...baseSpec, aggregations: aggregations as MetricAggregation[], // reduceTo: queryData.reduceTo, diff --git a/frontend/src/components/PanelDataLoading/PanelDataLoading.styles.scss b/frontend/src/components/PanelDataLoading/PanelDataLoading.styles.scss new file mode 100644 index 000000000000..69f260eee45d --- /dev/null +++ b/frontend/src/components/PanelDataLoading/PanelDataLoading.styles.scss @@ -0,0 +1,19 @@ +.loading-panel-data { + padding: 24px 0; + height: 240px; + + display: flex; + justify-content: center; + align-items: flex-start; + + .loading-panel-data-content { + display: flex; + align-items: flex-start; + flex-direction: column; + + .loading-gif { + height: 72px; + margin-left: -24px; + } + } +} diff --git a/frontend/src/components/PanelDataLoading/PanelDataLoading.tsx b/frontend/src/components/PanelDataLoading/PanelDataLoading.tsx new file mode 100644 index 000000000000..b40a1d2ae78b --- /dev/null +++ b/frontend/src/components/PanelDataLoading/PanelDataLoading.tsx @@ -0,0 +1,19 @@ +import './PanelDataLoading.styles.scss'; + +import { Typography } from 'antd'; + +export function PanelDataLoading(): JSX.Element { + return ( +
+
+ wait-icon + + Fetching data... +
+
+ ); +} diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx index 37708eb8d269..d0621dad7a02 100644 --- a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx @@ -131,6 +131,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({ queryVariant={config?.queryVariant || 'dropdown'} showOnlyWhereClause={showOnlyWhereClause} isListViewPanel={isListViewPanel} + signalSource={config?.signalSource || ''} /> ))} diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/MerticsAggregateSection/MetricsAggregateSection.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/MerticsAggregateSection/MetricsAggregateSection.tsx index db7b140da3c9..6f9820ab1655 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/MerticsAggregateSection/MetricsAggregateSection.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/MerticsAggregateSection/MetricsAggregateSection.tsx @@ -18,11 +18,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({ index, version, panelType, + signalSource = '', }: { query: IBuilderQuery; index: number; version: string; panelType: PANEL_TYPES | null; + signalSource: string; }): JSX.Element { const { setAggregationOptions } = useQueryBuilderV2Context(); const { @@ -208,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({ disabled={!queryAggregation.metricName} query={query} onChange={handleChangeGroupByKeys} + signalSource={signalSource} /> @@ -244,6 +247,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({ disabled={!queryAggregation.metricName} query={query} onChange={handleChangeGroupByKeys} + signalSource={signalSource} /> diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/MetricsSelect/MetricsSelect.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/MetricsSelect/MetricsSelect.tsx index ce3e748509ab..b04e1d303ffc 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/MetricsSelect/MetricsSelect.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/MetricsSelect/MetricsSelect.tsx @@ -9,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({ query, index, version, + signalSource, }: { query: IBuilderQuery; index: number; version: string; + signalSource: 'meter' | ''; }): JSX.Element { const { handleChangeAggregatorAttribute } = useQueryOperations({ index, @@ -26,6 +28,7 @@ export const MetricsSelect = memo(function MetricsSelect({ onChange={handleChangeAggregatorAttribute} query={query} index={index} + signalSource={signalSource || ''} /> ); diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx index 4e45d231bf2c..6df5e647b794 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx @@ -81,10 +81,12 @@ function QuerySearch({ queryData, dataSource, onRun, + signalSource, }: { onChange: (value: string) => void; queryData: IBuilderQuery; dataSource: DataSource; + signalSource?: string; onRun?: (query: string) => void; }): JSX.Element { const isDarkMode = useIsDarkMode(); @@ -218,6 +220,7 @@ function QuerySearch({ signal: dataSource, searchText: searchText || '', metricName: debouncedMetricName ?? undefined, + signalSource: signalSource as 'meter' | '', }); if (response.data.data) { @@ -245,6 +248,7 @@ function QuerySearch({ keySuggestions, toggleSuggestions, queryData.aggregateAttribute?.key, + signalSource, ], ); @@ -378,6 +382,8 @@ function QuerySearch({ key, searchText: sanitizedSearchText, signal: dataSource, + signalSource: signalSource as 'meter' | '', + metricName: debouncedMetricName ?? undefined, }); // Skip updates if component unmounted or key changed @@ -465,8 +471,14 @@ function QuerySearch({ setIsFetchingCompleteValuesList(false); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [activeKey, dataSource, isFocused], + [ + activeKey, + dataSource, + isLoadingSuggestions, + debouncedMetricName, + signalSource, + toggleSuggestions, + ], ); const debouncedFetchValueSuggestions = useMemo( @@ -1440,6 +1452,7 @@ function QuerySearch({ QuerySearch.defaultProps = { onRun: undefined, + signalSource: '', }; export default QuerySearch; diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx index f13228ec8c84..2108161a307f 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/QueryV2.tsx @@ -28,6 +28,7 @@ export const QueryV2 = memo(function QueryV2({ isListViewPanel = false, version, showOnlyWhereClause = false, + signalSource = '', }: QueryProps & { ref: React.RefObject }): JSX.Element { const { cloneQuery, panelType } = useQueryBuilder(); @@ -175,6 +176,7 @@ export const QueryV2 = memo(function QueryV2({ query={query} index={index} version={ENTITY_VERSION_V5} + signalSource={signalSource as 'meter' | ''} /> )} @@ -186,6 +188,7 @@ export const QueryV2 = memo(function QueryV2({ onChange={handleSearchChange} queryData={query} dataSource={dataSource} + signalSource={signalSource} /> @@ -218,6 +221,7 @@ export const QueryV2 = memo(function QueryV2({ index={index} key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`} version="v4" + signalSource={signalSource as 'meter' | ''} /> )} diff --git a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx index eec211ff8402..35c26b0eb5e5 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx @@ -17,6 +17,7 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es'; import { ChevronDown, ChevronRight } from 'lucide-react'; @@ -73,18 +74,59 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { searchText: searchText ?? '', }, { - enabled: isOpen, + enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER, keepPreviousData: true, }, ); + const { + data: keyValueSuggestions, + isLoading: isLoadingKeyValueSuggestions, + } = useGetQueryKeyValueSuggestions({ + key: filter.attributeKey.key, + signal: filter.dataSource || DataSource.LOGS, + signalSource: 'meter', + options: { + enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER, + keepPreviousData: true, + }, + }); + const attributeValues: string[] = useMemo(() => { const dataType = filter.attributeKey.dataType || DataTypes.String; + + if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) { + // Process the response data + const responseData = keyValueSuggestions?.data as any; + const values = responseData.data?.values || {}; + const stringValues = values.stringValues || []; + const numberValues = values.numberValues || []; + + // Generate options from string values - explicitly handle empty strings + const stringOptions = stringValues + // Strict filtering for empty string - we'll handle it as a special case if needed + .filter( + (value: string | null | undefined): value is string => + value !== null && value !== undefined && value !== '', + ); + + // Generate options from number values + const numberOptions = numberValues + .filter( + (value: number | null | undefined): value is number => + value !== null && value !== undefined, + ) + .map((value: number) => value.toString()); + + // Combine all options and make sure we don't have duplicate labels + return [...stringOptions, ...numberOptions]; + } + const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType]; return (data?.payload?.[key] || []).filter( (val) => val !== undefined && val !== null, ); - }, [data?.payload, filter.attributeKey.dataType]); + }, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]); const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount); @@ -478,12 +520,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { )} - {isOpen && isLoading && !attributeValues.length && ( -
- -
- )} - {isOpen && !isLoading && ( + {isOpen && + (isLoading || isLoadingKeyValueSuggestions) && + !attributeValues.length && ( +
+ +
+ )} + {isOpen && !isLoading && !isLoadingKeyValueSuggestions && ( <> {!isEmptyStateWithDocsEnabled && (
diff --git a/frontend/src/components/QuickFilters/QuickFilters.styles.scss b/frontend/src/components/QuickFilters/QuickFilters.styles.scss index 3c80c2bac992..ebf5c97701c5 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.styles.scss +++ b/frontend/src/components/QuickFilters/QuickFilters.styles.scss @@ -1,6 +1,8 @@ .quick-filters-container { display: flex; height: 100%; + position: relative; + .quick-filters-settings-container { position: relative; } @@ -102,6 +104,37 @@ margin: 8px 12px; } } + + .no-filters-container { + display: flex; + height: 100%; + gap: 8px; + align-items: center; + padding: 8px; + } +} + +.perilin-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: radial-gradient(circle, #fff 10%, transparent 0); + background-size: 12px 12px; + opacity: 1; + + mask-image: radial-gradient( + circle at 50% 0, + rgba(11, 12, 14, 0.1) 0, + rgba(11, 12, 14, 0) 100% + ); + -webkit-mask-image: radial-gradient( + circle at 50% 0, + rgba(11, 12, 14, 0.1) 0, + rgba(11, 12, 14, 0) 100% + ); } .lightMode { diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index c36efdfa883e..c70ca59c2df6 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -15,7 +15,7 @@ import { LOCALSTORAGE } from 'constants/localStorage'; import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { cloneDeep, isFunction, isNull } from 'lodash-es'; -import { Settings2 as SettingsIcon } from 'lucide-react'; +import { Frown, Settings2 as SettingsIcon } from 'lucide-react'; import { useAppContext } from 'providers/App/App'; import { useMemo, useState } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -236,6 +236,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { ); } })} + + {filterConfig.length === 0 && ( +
+ + No filters found +
+ )}
diff --git a/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts b/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts index 24ba1454d7a9..4f357fd2e69c 100644 --- a/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts +++ b/frontend/src/components/QuickFilters/QuickFiltersSettings/constants.ts @@ -6,4 +6,5 @@ export const SIGNAL_DATA_SOURCE_MAP = { [SignalType.TRACES]: DataSource.TRACES, [SignalType.EXCEPTIONS]: DataSource.TRACES, [SignalType.API_MONITORING]: DataSource.TRACES, + [SignalType.METER_EXPLORER]: DataSource.METRICS, }; diff --git a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx index f998e587eeb0..252a4d23084a 100644 --- a/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx +++ b/frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx @@ -54,6 +54,7 @@ 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'; @@ -77,7 +78,11 @@ const setupServer = (): void => { putHandler(await req.json()); return res(ctx.status(200), ctx.json({})); }), - rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) => + + rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) => + res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), + ), + rest.get(fieldsValuesURL, (req, res, ctx) => res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)), ), ); diff --git a/frontend/src/components/QuickFilters/types.ts b/frontend/src/components/QuickFilters/types.ts index 45c671e77e39..69bcadeda80f 100644 --- a/frontend/src/components/QuickFilters/types.ts +++ b/frontend/src/components/QuickFilters/types.ts @@ -23,6 +23,7 @@ export enum SignalType { LOGS = 'logs', API_MONITORING = 'api_monitoring', EXCEPTIONS = 'exceptions', + METER_EXPLORER = 'meter', } export interface IQuickFiltersConfig { @@ -53,4 +54,5 @@ export enum QuickFiltersSource { TRACES_EXPLORER = 'traces-explorer', API_MONITORING = 'api-monitoring', EXCEPTIONS = 'exceptions', + METER_EXPLORER = 'meter', } diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 15bb3a8297c3..0a94b4f074ca 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -23,6 +23,7 @@ import { BoolOperators, DataSource, LogsAggregatorOperator, + MeterAggregateOperator, MetricAggregateOperator, NumberOperators, QueryAdditionalFilter, @@ -36,6 +37,7 @@ import { v4 as uuid } from 'uuid'; import { logsAggregateOperatorOptions, + meterAggregateOperatorOptions, metricAggregateOperatorOptions, metricsGaugeAggregateOperatorOptions, metricsGaugeSpaceAggregateOperatorOptions, @@ -79,6 +81,7 @@ export const mapOfOperators = { metrics: metricAggregateOperatorOptions, logs: logsAggregateOperatorOptions, traces: tracesAggregateOperatorOptions, + meter: meterAggregateOperatorOptions, }; export const metricsOperatorsByType = { @@ -193,6 +196,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = { groupBy: [], legend: '', reduceTo: 'avg', + source: '', }; const initialQueryBuilderFormLogsValues: IBuilderQuery = { @@ -209,6 +213,39 @@ const initialQueryBuilderFormTracesValues: IBuilderQuery = { dataSource: DataSource.TRACES, }; +export const initialQueryBuilderFormMeterValues: IBuilderQuery = { + dataSource: DataSource.METRICS, + queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), + aggregateOperator: MeterAggregateOperator.COUNT, + aggregateAttribute: initialAutocompleteData, + timeAggregation: MeterAggregateOperator.RATE, + spaceAggregation: MeterAggregateOperator.SUM, + filter: { expression: '' }, + aggregations: [ + { + metricName: '', + temporality: '', + timeAggregation: MeterAggregateOperator.COUNT, + spaceAggregation: MeterAggregateOperator.SUM, + reduceTo: 'avg', + }, + ], + functions: [], + filters: { items: [], op: 'AND' }, + expression: createNewBuilderItemName({ + existNames: [], + sourceNames: alphabet, + }), + disabled: false, + stepInterval: undefined, + having: [], + limit: null, + orderBy: [], + groupBy: [], + legend: '', + reduceTo: 'avg', +}; + export const initialQueryBuilderFormValuesMap: Record< DataSource, IBuilderQuery @@ -285,6 +322,19 @@ export const initialQueriesMap: Record = { traces: initialQueryTracesWithType, }; +export const initialQueryMeterWithType: Query = { + ...initialQueryWithType, + builder: { + ...initialQueryWithType.builder, + queryData: [ + { + ...initialQueryBuilderFormValuesMap.metrics, + source: 'meter', + }, + ], + }, +}; + export const operatorsByTypes: Record = { string: Object.values(StringOperators), number: Object.values(NumberOperators), diff --git a/frontend/src/constants/queryBuilderOperators.ts b/frontend/src/constants/queryBuilderOperators.ts index 3ba9498ea7e1..06486462b606 100644 --- a/frontend/src/constants/queryBuilderOperators.ts +++ b/frontend/src/constants/queryBuilderOperators.ts @@ -125,6 +125,126 @@ export const metricAggregateOperatorOptions: SelectOption[] = [ }, ]; +export const meterAggregateOperatorOptions: SelectOption[] = [ + { + value: MetricAggregateOperator.COUNT, + label: 'Count', + }, + { + value: MetricAggregateOperator.COUNT_DISTINCT, + // eslint-disable-next-line sonarjs/no-duplicate-string + label: 'Count Distinct', + }, + { + value: MetricAggregateOperator.SUM, + label: 'Sum', + }, + { + value: MetricAggregateOperator.AVG, + label: 'Avg', + }, + { + value: MetricAggregateOperator.MAX, + label: 'Max', + }, + { + value: MetricAggregateOperator.MIN, + label: 'Min', + }, + { + value: MetricAggregateOperator.P05, + label: 'P05', + }, + { + value: MetricAggregateOperator.P10, + label: 'P10', + }, + { + value: MetricAggregateOperator.P20, + label: 'P20', + }, + { + value: MetricAggregateOperator.P25, + label: 'P25', + }, + { + value: MetricAggregateOperator.P50, + label: 'P50', + }, + { + value: MetricAggregateOperator.P75, + label: 'P75', + }, + { + value: MetricAggregateOperator.P90, + label: 'P90', + }, + { + value: MetricAggregateOperator.P95, + label: 'P95', + }, + { + value: MetricAggregateOperator.P99, + label: 'P99', + }, + { + value: MetricAggregateOperator.RATE, + label: 'Rate', + }, + { + value: MetricAggregateOperator.SUM_RATE, + label: 'Sum_rate', + }, + { + value: MetricAggregateOperator.AVG_RATE, + label: 'Avg_rate', + }, + { + value: MetricAggregateOperator.MAX_RATE, + label: 'Max_rate', + }, + { + value: MetricAggregateOperator.MIN_RATE, + label: 'Min_rate', + }, + { + value: MetricAggregateOperator.RATE_SUM, + label: 'Rate_sum', + }, + { + value: MetricAggregateOperator.RATE_AVG, + label: 'Rate_avg', + }, + { + value: MetricAggregateOperator.RATE_MIN, + label: 'Rate_min', + }, + { + value: MetricAggregateOperator.RATE_MAX, + label: 'Rate_max', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_50, + label: 'Hist_quantile_50', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_75, + label: 'Hist_quantile_75', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_90, + label: 'Hist_quantile_90', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_95, + label: 'Hist_quantile_95', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_99, + label: 'Hist_quantile_99', + }, +]; + export const tracesAggregateOperatorOptions: SelectOption[] = [ { value: TracesAggregatorOperator.COUNT, diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 7dc5b326fc2d..b5ec8a4d11d5 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -77,6 +77,9 @@ const ROUTES = { API_MONITORING: '/api-monitoring/explorer', METRICS_EXPLORER_BASE: '/metrics-explorer', WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted', + METER_EXPLORER_BASE: '/meter-explorer', + METER_EXPLORER: '/meter-explorer', + METER_EXPLORER_VIEWS: '/meter-explorer/views', HOME_PAGE: '/', } as const; diff --git a/frontend/src/constants/shortcuts/globalShortcuts.ts b/frontend/src/constants/shortcuts/globalShortcuts.ts index 8b68b7195e2d..4cc969504d68 100644 --- a/frontend/src/constants/shortcuts/globalShortcuts.ts +++ b/frontend/src/constants/shortcuts/globalShortcuts.ts @@ -6,6 +6,7 @@ export const GlobalShortcuts = { NavigateToAlerts: 'a+shift', NavigateToExceptions: 'e+shift', NavigateToMessagingQueues: 'm+shift', + ToggleSidebar: 'b+shift', }; export const GlobalShortcutsName = { @@ -16,6 +17,7 @@ export const GlobalShortcutsName = { NavigateToAlerts: 'shift+a', NavigateToExceptions: 'shift+e', NavigateToMessagingQueues: 'shift+m', + ToggleSidebar: 'shift+b', }; export const GlobalShortcutsDescription = { @@ -26,4 +28,5 @@ export const GlobalShortcutsDescription = { NavigateToAlerts: 'Navigate to alerts page', NavigateToExceptions: 'Navigate to Exceptions page', NavigateToMessagingQueues: 'Navigate to Messaging Queues page', + ToggleSidebar: 'Toggle sidebar visibility', }; diff --git a/frontend/src/container/AppLayout/__tests__/sidebar-toggle-shortcut.test.tsx b/frontend/src/container/AppLayout/__tests__/sidebar-toggle-shortcut.test.tsx new file mode 100644 index 000000000000..b0b2828194b1 --- /dev/null +++ b/frontend/src/container/AppLayout/__tests__/sidebar-toggle-shortcut.test.tsx @@ -0,0 +1,176 @@ +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
Test
; +} + +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( + + + + + , + ); + + // 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 ( +
+ +
Test
+
+ ); + } + + render( + + + + + , + ); + + // 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( + + + + + , + ); + + 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( + + + + + , + ); + + await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT); + + expect(mockUpdateUserPreferenceInContext).toHaveBeenCalledWith({ + name: USER_PREFERENCES.SIDENAV_PINNED, + value: true, + }); + }); + }); +}); diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 43d6b9884115..06f5ada65b93 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -10,8 +10,10 @@ 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'; @@ -22,10 +24,12 @@ 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'; @@ -68,8 +72,10 @@ 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, @@ -662,10 +668,85 @@ function AppLayout(props: AppLayoutProps): JSX.Element { ); - const sideNavPinned = userPreferences?.find( + const sideNavPinnedPreference = 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; @@ -739,14 +820,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element { className={cx( 'app-layout', isDarkMode ? 'darkMode dark' : 'lightMode', - sideNavPinned ? 'side-nav-pinned' : '', + isSideNavPinned ? 'side-nav-pinned' : '', SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '', SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '', SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '', )} > {isToDisplayLayout && !renderFullScreen && ( - + )}
{ setIsExport(value); @@ -150,6 +155,10 @@ function ExplorerOptions({ [MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery, panelType, }); + } else if (isMeterExplorer) { + logEvent('Meter Explorer: Save view clicked', { + panelType, + }); } setIsSaveModalOpen(!isSaveModalOpen); }; @@ -243,7 +252,7 @@ function ExplorerOptions({ error, isRefetching, refetch: refetchAllView, - } = useGetAllViews(sourcepage); + } = useGetAllViews(isMeterExplorer ? 'meter' : sourcepage); const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType); @@ -316,7 +325,7 @@ function ExplorerOptions({ compositeQuery, viewKey, extraData: updatedExtraData, - sourcePage: sourcepage, + sourcePage: isMeterExplorer ? 'meter' : sourcepage, viewName, }); @@ -332,7 +341,7 @@ function ExplorerOptions({ compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType), viewKey, extraData: updatedExtraData, - sourcePage: sourcepage, + sourcePage: isMeterExplorer ? 'meter' : sourcepage, viewName, }, { @@ -459,6 +468,11 @@ function ExplorerOptions({ panelType, viewName: option?.value, }); + } else if (isMeterExplorer) { + logEvent('Meter Explorer: Select view', { + panelType, + viewName: option?.value, + }); } updatePreservedViewInLocalStorage(option); @@ -505,6 +519,11 @@ function ExplorerOptions({ : defaultLogsSelectedColumns, }); + if (signalSource === 'meter') { + history.replace(ROUTES.METER_EXPLORER); + return; + } + history.replace(DATASOURCE_VS_ROUTES[sourcepage]); }; @@ -549,7 +568,7 @@ function ExplorerOptions({ redirectWithQueryBuilderData, refetchAllView, saveViewAsync, - sourcePage: sourcepage, + sourcePage: isMeterExplorer ? 'meter' : sourcepage, viewName: newViewName, setNewViewName, }); @@ -668,7 +687,7 @@ function ExplorerOptions({ return `Query ${query.builder.queryData[0].queryName}`; }; - const alertButton = useMemo(() => { + const CreateAlertButton = useMemo(() => { if (isOneChartPerQuery) { const selectLabel = (
+ +
+ + handleRunQuery(true, true)} + /> +
+ + + +
+ +
+ + + + + + ); +} + +export default Explorer; diff --git a/frontend/src/container/MeterExplorer/Explorer/NoData.tsx b/frontend/src/container/MeterExplorer/Explorer/NoData.tsx new file mode 100644 index 000000000000..8cf427684df8 --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/NoData.tsx @@ -0,0 +1,13 @@ +import { Typography } from 'antd'; +import { ChartLine } from 'lucide-react'; + +export default function NoData(): JSX.Element { + return ( +
+ + + No data found for the selected query + +
+ ); +} diff --git a/frontend/src/container/MeterExplorer/Explorer/QuerySection.tsx b/frontend/src/container/MeterExplorer/Explorer/QuerySection.tsx new file mode 100644 index 000000000000..4964e8abcaaa --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/QuerySection.tsx @@ -0,0 +1,43 @@ +import { Button } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { DataSource } from 'types/common/queryBuilder'; + +import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events'; + +function QuerySection(): JSX.Element { + const { handleRunQuery } = useQueryBuilder(); + + const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES); + + return ( +
+ + + + } + /> +
+ ); +} + +export default QuerySection; diff --git a/frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx b/frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx new file mode 100644 index 000000000000..fc60e1dcd329 --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx @@ -0,0 +1,142 @@ +import { isAxiosError } from 'axios'; +import { ENTITY_VERSION_V5 } from 'constants/app'; +import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter'; +import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; +import { convertDataValueToMs } from 'container/TimeSeriesView/utils'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { useErrorModal } from 'providers/ErrorModalProvider'; +import { useMemo, useState } from 'react'; +import { useQueries } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; +import APIError from 'types/api/error'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { DataSource } from 'types/common/queryBuilder'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +function TimeSeries(): JSX.Element { + const { stagedQuery, currentQuery } = useQueryBuilder(); + + const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const [yAxisUnit, setYAxisUnit] = useState(''); + + const isValidToConvertToMs = useMemo(() => { + const isValid: boolean[] = []; + + currentQuery.builder.queryData.forEach( + ({ aggregateAttribute, aggregateOperator }) => { + const isExistDurationNanoAttribute = + aggregateAttribute?.key === 'durationNano' || + aggregateAttribute?.key === 'duration_nano'; + + const isCountOperator = + aggregateOperator === 'count' || aggregateOperator === 'count_distinct'; + + isValid.push(!isCountOperator && isExistDurationNanoAttribute); + }, + ); + + return isValid.every(Boolean); + }, [currentQuery]); + + const queryPayloads = useMemo( + () => [stagedQuery || initialQueryMeterWithType], + [stagedQuery], + ); + + const { showErrorModal } = useErrorModal(); + + const queries = useQueries( + queryPayloads.map((payload, index) => ({ + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + payload, + ENTITY_VERSION_V5, + globalSelectedTime, + maxTime, + minTime, + index, + ], + queryFn: (): Promise> => + GetMetricQueryRange( + { + query: payload, + graphType: PANEL_TYPES.TIME_SERIES, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: globalSelectedTime, + params: { + dataSource: DataSource.METRICS, + }, + }, + ENTITY_VERSION_V5, + ), + enabled: !!payload, + retry: (failureCount: number, error: Error): boolean => { + let status: number | undefined; + + if (error instanceof APIError) { + status = error.getHttpStatusCode(); + } else if (isAxiosError(error)) { + status = error.response?.status; + } + + if (status && status >= 400 && status < 500) { + return false; + } + + return failureCount < 3; + }, + onError: (error: APIError): void => { + showErrorModal(error); + }, + })), + ); + + const data = useMemo(() => queries.map(({ data }) => data) ?? [], [queries]); + + const responseData = useMemo( + () => + data.map((datapoint) => + isValidToConvertToMs ? convertDataValueToMs(datapoint) : datapoint, + ), + [data, isValidToConvertToMs], + ); + + const onUnitChangeHandler = (value: string): void => { + setYAxisUnit(value); + }; + + return ( +
+ +
+ {responseData.map((datapoint, index) => ( +
+ +
+ ))} +
+
+ ); +} + +export default TimeSeries; diff --git a/frontend/src/container/MeterExplorer/Explorer/index.ts b/frontend/src/container/MeterExplorer/Explorer/index.ts new file mode 100644 index 000000000000..8473e81d4601 --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/index.ts @@ -0,0 +1,3 @@ +import Explorer from './Explorer'; + +export default Explorer; diff --git a/frontend/src/container/MeterExplorer/Explorer/types.ts b/frontend/src/container/MeterExplorer/Explorer/types.ts new file mode 100644 index 000000000000..fdcfd91422a1 --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/types.ts @@ -0,0 +1,37 @@ +import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics'; +import { UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +export enum ExplorerTabs { + TIME_SERIES = 'time-series', + RELATED_METRICS = 'related-metrics', +} + +export interface TimeSeriesProps { + showOneChartPerQuery: boolean; +} + +export interface RelatedMetricsProps { + metricNames: string[]; +} + +export interface RelatedMetricsCardProps { + metric: RelatedMetricWithQueryResult; +} + +export interface UseGetRelatedMetricsGraphsProps { + selectedMetricName: string | null; + startMs: number; + endMs: number; +} + +export interface UseGetRelatedMetricsGraphsReturn { + relatedMetrics: RelatedMetricWithQueryResult[]; + isRelatedMetricsLoading: boolean; + isRelatedMetricsError: boolean; +} + +export interface RelatedMetricWithQueryResult extends RelatedMetric { + queryResult: UseQueryResult, unknown>; +} diff --git a/frontend/src/container/MeterExplorer/Explorer/utils.tsx b/frontend/src/container/MeterExplorer/Explorer/utils.tsx new file mode 100644 index 000000000000..78af29668485 --- /dev/null +++ b/frontend/src/container/MeterExplorer/Explorer/utils.tsx @@ -0,0 +1,37 @@ +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; + +export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => { + const queries: Query[] = []; + + query.builder.queryData.forEach((currentQuery) => { + const newQuery = { + ...query, + id: uuid(), + builder: { + ...query.builder, + queryData: [currentQuery], + queryFormulas: [], + }, + }; + queries.push(newQuery); + }); + + query.builder.queryFormulas.forEach((currentFormula) => { + const newQuery = { + ...query, + id: uuid(), + builder: { + ...query.builder, + queryFormulas: [currentFormula], + queryData: query.builder.queryData.map((currentQuery) => ({ + ...currentQuery, + disabled: true, + })), + }, + }; + queries.push(newQuery); + }); + + return queries; +}; diff --git a/frontend/src/container/MeterExplorer/events.ts b/frontend/src/container/MeterExplorer/events.ts new file mode 100644 index 000000000000..926b9e62dff6 --- /dev/null +++ b/frontend/src/container/MeterExplorer/events.ts @@ -0,0 +1,51 @@ +/** + * This file contains all analytics events for the Meter Explorer. + */ +export enum MeterExplorerEvents { + TabChanged = 'Meter Explorer: Tab visited', + ModalOpened = 'Meter Explorer: Modal opened', + MeterClicked = 'Meter Explorer: Meter clicked', + FilterApplied = 'Meter Explorer: Filter applied', + TreemapViewChanged = 'Meter Explorer: Treemap view changed', + PageNumberChanged = 'Meter Explorer: Page number changed', + PageSizeChanged = 'Meter Explorer: Page size changed', + OrderByApplied = 'Meter Explorer: Order by applied', + MetricMetadataUpdated = 'Meter Explorer: Metric metadata updated', + OpenInExplorerClicked = 'Meter Explorer: Open in explorer clicked', + InspectViewChanged = 'Meter Explorer: Inspect view changed', + InspectQueryChanged = 'Meter Explorer: Inspect query changed', + InspectPointClicked = 'Meter Explorer: Inspect point clicked', + QueryBuilderQueryChanged = 'Meter Explorer: QueryBuilder query changed', + YAxisUnitApplied = 'Meter Explorer: Y axis unit applied', + AddToAlertClicked = 'Meter Explorer: Add to alert clicked', + AddToDashboardClicked = 'Meter Explorer: Add to dashboard clicked', + SaveViewClicked = 'Meter Explorer: Save view clicked', + SearchApplied = 'Meter Explorer: Search applied', + ViewEdited = 'Meter Explorer: View edited', + ViewDeleted = 'Meter Explorer: View deleted', +} + +export enum MeterExplorerEventKeys { + Tab = 'tab', + Modal = 'modal', + View = 'view', + Interval = 'interval', + ViewType = 'viewType', + PageNumber = 'pageNumber', + PageSize = 'pageSize', + ColumnName = 'columnName', + Order = 'order', + AttributeKey = 'attributeKey', + AttributeValue = 'attributeValue', + MetricName = 'metricName', + InspectView = 'inspectView', + TimeAggregationOption = 'timeAggregationOption', + TimeAggregationInterval = 'timeAggregationInterval', + SpaceAggregationOption = 'spaceAggregationOption', + SpaceAggregationLabels = 'spaceAggregationLabels', + OneChartPerQueryEnabled = 'oneChartPerQueryEnabled', + YAxisUnit = 'yAxisUnit', + ViewName = 'viewName', + Filters = 'filters', + TimeRange = 'timeRange', +} diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx index 33cbf3941131..f1d3addf768b 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx @@ -189,7 +189,7 @@ function Explorer(): JSX.Element { query={exportDefaultQuery} sourcepage={DataSource.METRICS} onExport={handleExport} - isOneChartPerQuery={showOneChartPerQuery} + isOneChartPerQuery={false} splitedQueries={splitedQueries} /> diff --git a/frontend/src/container/MySettings/index.tsx b/frontend/src/container/MySettings/index.tsx index b2650da7adbd..525cfc21c29e 100644 --- a/frontend/src/container/MySettings/index.tsx +++ b/frontend/src/container/MySettings/index.tsx @@ -1,6 +1,7 @@ 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'; @@ -109,6 +110,9 @@ 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, @@ -130,6 +134,8 @@ 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); }, }, diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 9fd41231f0ed..5432ebab2b04 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -6,6 +6,7 @@ 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'; @@ -452,7 +453,9 @@ const useOptionsMenu = ({ () => ({ addColumn: { isFetching: isSearchedAttributesFetchingV5, - value: preferences?.columns || defaultOptionsQuery.selectColumns, + value: + preferences?.columns.filter((item) => has(item, 'name')) || + defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')), options: optionsFromAttributeKeys || [], onFocus: handleFocus, onBlur: handleBlur, diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 7b891e91f92d..a80639e5ef02 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -17,8 +17,9 @@ export type QueryBuilderConfig = | { queryVariant: 'static'; initialDataSource: DataSource; + signalSource?: string; } - | { queryVariant: 'dropdown' }; + | { queryVariant: 'dropdown'; signalSource?: string }; export type QueryBuilderProps = { config?: QueryBuilderConfig; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts index 7f2f6ccde7a5..b5dd84a5732c 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts @@ -11,4 +11,5 @@ export type QueryProps = { version: string; showSpanScopeSelector?: boolean; showOnlyWhereClause?: boolean; + signalSource?: string; } & Pick; diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts index a94b78946ef8..fe98b9f3da73 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.intefaces.ts @@ -8,4 +8,5 @@ export type AgregatorFilterProps = Pick & { defaultValue?: string; onSelect?: (value: BaseAutocompleteData) => void; index?: number; + signalSource?: 'meter' | ''; }; diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index 3c30536468bf..c5b1a6a2a25f 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -38,6 +38,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({ defaultValue, onSelect, index, + signalSource, }: AgregatorFilterProps): JSX.Element { const queryClient = useQueryClient(); const [optionsData, setOptionsData] = useState([]); @@ -73,6 +74,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({ searchText: debouncedValue, aggregateOperator: queryAggregation.timeAggregation, dataSource: query.dataSource, + source: signalSource || '', }), { enabled: @@ -152,10 +154,17 @@ export const AggregatorFilter = memo(function AggregatorFilter({ setSearchText(text); }, []); - const placeholder: string = - query.dataSource === DataSource.METRICS - ? `Search metric name` - : 'Aggregate attribute'; + const getPlaceholder = useCallback(() => { + if (signalSource === 'meter') { + return 'Meter name'; + } + + if (query.dataSource === DataSource.METRICS) { + return 'Metric name'; + } + + return 'Aggregate attribute'; + }, [signalSource, query.dataSource]); const getAttributesData = useCallback( (): BaseAutocompleteData[] => @@ -289,7 +298,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({ return ( - Y-axis unit + + + Y-axis unit +