From 94290c77db064161d00faa0a66778e46df42696c Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 16 Oct 2025 16:35:57 +0100 Subject: [PATCH] feat: add advanced filters toggle functionality across various components --- Common/UI/Components/Filters/FilterViewer.tsx | 6 +- Common/UI/Components/List/List.tsx | 4 + .../Components/ModelTable/BaseModelTable.tsx | 6 + Common/UI/Components/Table/Table.tsx | 4 + .../src/Components/Metrics/MetricQuery.tsx | 28 +++- .../Components/Metrics/MetricQueryConfig.tsx | 10 ++ .../src/Components/Metrics/MetricView.tsx | 62 +++++++- .../src/Components/Metrics/Utils/Metrics.ts | 37 +++-- .../src/Components/Traces/TraceTable.tsx | 136 ++++++++++++------ 9 files changed, 236 insertions(+), 57 deletions(-) diff --git a/Common/UI/Components/Filters/FilterViewer.tsx b/Common/UI/Components/Filters/FilterViewer.tsx index a02b516e90..64acbe6221 100644 --- a/Common/UI/Components/Filters/FilterViewer.tsx +++ b/Common/UI/Components/Filters/FilterViewer.tsx @@ -30,6 +30,9 @@ export interface ComponentProps { isModalLoading?: boolean; onFilterRefreshClick?: undefined | (() => void); filterData?: FilterData | undefined; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); } type FilterComponentFunction = ( @@ -355,7 +358,7 @@ const FilterComponent: FilterComponentFunction = (
{showViewer && (
-
+
@@ -442,6 +445,7 @@ const FilterComponent: FilterComponentFunction = ( onFilterChanged={(filterData: FilterData) => { setTempFilterDataForModal(filterData); }} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} /> )} diff --git a/Common/UI/Components/List/List.tsx b/Common/UI/Components/List/List.tsx index 9001a4bf84..b2ead415d8 100644 --- a/Common/UI/Components/List/List.tsx +++ b/Common/UI/Components/List/List.tsx @@ -43,6 +43,9 @@ export interface ComponentProps { onFilterRefreshClick?: undefined | (() => void); onFilterModalClose?: (() => void) | undefined; onFilterModalOpen?: (() => void) | undefined; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); } type ListFunction = ( @@ -118,6 +121,7 @@ const List: ListFunction = ( }} singularLabel={props.singularLabel} pluralLabel={props.pluralLabel} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} />
diff --git a/Common/UI/Components/ModelTable/BaseModelTable.tsx b/Common/UI/Components/ModelTable/BaseModelTable.tsx index e9d8a46982..52ff377414 100644 --- a/Common/UI/Components/ModelTable/BaseModelTable.tsx +++ b/Common/UI/Components/ModelTable/BaseModelTable.tsx @@ -224,6 +224,10 @@ export interface BaseTableProps< formSummary?: FormSummaryConfig | undefined; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); + /* * this key is used to save table user preferences in local storage. * If you provide this key, the table will save the user preferences in local storage. @@ -1516,6 +1520,7 @@ const BaseModelTable: ( onFilterModalOpen={() => { setShowFilterModal(true); }} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} onSortChanged={( sortBy: keyof TBaseModel | null, sortOrder: SortOrder, @@ -1662,6 +1667,7 @@ const BaseModelTable: ( onFilterModalOpen={() => { setShowFilterModal(true); }} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} singularLabel={props.singularName || model.singularName || "Item"} pluralLabel={props.pluralName || model.pluralName || "Items"} error={error} diff --git a/Common/UI/Components/Table/Table.tsx b/Common/UI/Components/Table/Table.tsx index d50b1de2db..7d8f619ac7 100644 --- a/Common/UI/Components/Table/Table.tsx +++ b/Common/UI/Components/Table/Table.tsx @@ -54,6 +54,9 @@ export interface ComponentProps { onFilterModalClose?: (() => void) | undefined; onFilterModalOpen?: (() => void) | undefined; filterData?: undefined | FilterData; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); enableDragAndDrop?: boolean | undefined; dragDropIndexField?: keyof T | undefined; @@ -242,6 +245,7 @@ const Table: TableFunction = ( singularLabel={props.singularLabel} pluralLabel={props.pluralLabel} filterData={props.filterData} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} /> {props.bulkActions?.buttons && ( void; metricTypes: Array; telemetryAttributes: string[]; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); + isAttributesLoading?: boolean | undefined; + attributesError?: string | undefined; + onAttributesRetry?: (() => void) | undefined; } const MetricFilter: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + const [showAdvancedFilters, setShowAdvancedFilters] = + useState(false); + return (
@@ -31,6 +45,17 @@ const MetricFilter: FunctionComponent = ( filterData, }); }} + onAdvancedFiltersToggle={(show: boolean) => { + setShowAdvancedFilters(show); + props.onAdvancedFiltersToggle?.(show); + }} + isFilterLoading={ + showAdvancedFilters ? props.isAttributesLoading : false + } + filterError={showAdvancedFilters ? props.attributesError : undefined} + onFilterRefreshClick={ + showAdvancedFilters ? props.onAttributesRetry : undefined + } filters={[ { key: "metricName", @@ -47,6 +72,7 @@ const MetricFilter: FunctionComponent = ( type: FieldType.JSON, title: "Filter by Attributes", jsonKeys: props.telemetryAttributes, + isAdvancedFilter: true, }, { key: "aggegationType", diff --git a/Dashboard/src/Components/Metrics/MetricQueryConfig.tsx b/Dashboard/src/Components/Metrics/MetricQueryConfig.tsx index eb8c737b4d..b8036c7c0f 100644 --- a/Dashboard/src/Components/Metrics/MetricQueryConfig.tsx +++ b/Dashboard/src/Components/Metrics/MetricQueryConfig.tsx @@ -23,6 +23,12 @@ export interface ComponentProps { onBlur?: (() => void) | undefined; tabIndex?: number | undefined; hideCard?: boolean | undefined; + onAdvancedFiltersToggle?: + | undefined + | ((showAdvancedFilters: boolean) => void); + attributesLoading?: boolean | undefined; + attributesError?: string | undefined; + onAttributesRetry?: (() => void) | undefined; } const MetricGraphConfig: FunctionComponent = ( @@ -56,6 +62,10 @@ const MetricGraphConfig: FunctionComponent = ( }} metricTypes={props.metricTypes} telemetryAttributes={props.telemetryAttributes} + onAdvancedFiltersToggle={props.onAdvancedFiltersToggle} + isAttributesLoading={props.attributesLoading} + attributesError={props.attributesError} + onAttributesRetry={props.onAttributesRetry} /> )} {props.onRemove && ( diff --git a/Dashboard/src/Components/Metrics/MetricView.tsx b/Dashboard/src/Components/Metrics/MetricView.tsx index 5385ea505c..09fcfde0f3 100644 --- a/Dashboard/src/Components/Metrics/MetricView.tsx +++ b/Dashboard/src/Components/Metrics/MetricView.tsx @@ -92,12 +92,18 @@ const MetricView: FunctionComponent = ( const [telemetryAttributes, setTelemetryAttributes] = useState>( [], ); + const [telemetryAttributesLoaded, setTelemetryAttributesLoaded] = + useState(false); + const [telemetryAttributesLoading, setTelemetryAttributesLoading] = + useState(false); + const [telemetryAttributesError, setTelemetryAttributesError] = + useState(""); const metricViewDataRef: React.MutableRefObject = React.useRef(props.data); useEffect(() => { - loadAllMetricsTypes().catch((err: Error) => { + loadMetricTypes().catch((err: Error) => { setPageError(API.getFriendlyErrorMessage(err as Error)); }); }, []); @@ -134,20 +140,23 @@ const MetricView: FunctionComponent = ( useState(false); const [metricResultsError, setMetricResultsError] = useState(""); - const loadAllMetricsTypes: PromiseVoidFunction = async (): Promise => { + const loadMetricTypes: PromiseVoidFunction = async (): Promise => { try { setIsPageLoading(true); const { metricTypes, - telemetryAttributes, }: { metricTypes: Array; - telemetryAttributes: Array; - } = await MetricUtil.loadAllMetricsTypes(); + } = await MetricUtil.loadAllMetricsTypes({ + includeAttributes: false, + }); setMetricTypes(metricTypes); - setTelemetryAttributes(telemetryAttributes); + setTelemetryAttributes([]); + setTelemetryAttributesLoaded(false); + setTelemetryAttributesLoading(false); + setTelemetryAttributesError(""); setIsPageLoading(false); setPageError(""); @@ -195,6 +204,40 @@ const MetricView: FunctionComponent = ( } }; + const loadTelemetryAttributes: PromiseVoidFunction = + async (): Promise => { + if (telemetryAttributesLoading || telemetryAttributesLoaded) { + return; + } + + try { + setTelemetryAttributesLoading(true); + setTelemetryAttributesError(""); + + const attributes: Array = + await MetricUtil.getTelemetryAttributes(); + + setTelemetryAttributes(attributes); + setTelemetryAttributesLoaded(true); + } catch (err) { + setTelemetryAttributes([]); + setTelemetryAttributesLoaded(false); + setTelemetryAttributesError( + `We couldn't load metric attributes. ${API.getFriendlyErrorMessage(err as Error)}`, + ); + } finally { + setTelemetryAttributesLoading(false); + } + }; + + const handleAdvancedFiltersToggle: (show: boolean) => void = ( + show: boolean, + ): void => { + if (show && !telemetryAttributesLoaded && !telemetryAttributesLoading) { + void loadTelemetryAttributes(); + } + }; + const fetchAggregatedResults: PromiseVoidFunction = async (): Promise => { setIsMetricResultsLoading(true); @@ -276,6 +319,13 @@ const MetricView: FunctionComponent = ( hideCard={props.hideCardInQueryElements} telemetryAttributes={telemetryAttributes} metricTypes={metricTypes} + onAdvancedFiltersToggle={handleAdvancedFiltersToggle} + attributesLoading={telemetryAttributesLoading} + attributesError={telemetryAttributesError} + onAttributesRetry={() => { + setTelemetryAttributesLoaded(false); + void loadTelemetryAttributes(); + }} onRemove={() => { if (props.data.queryConfigs.length === 1) { setShowCannotRemoveOneRemainingQueryError(true); diff --git a/Dashboard/src/Components/Metrics/Utils/Metrics.ts b/Dashboard/src/Components/Metrics/Utils/Metrics.ts index 4a8a3ad37a..81f1296e78 100644 --- a/Dashboard/src/Components/Metrics/Utils/Metrics.ts +++ b/Dashboard/src/Components/Metrics/Utils/Metrics.ts @@ -72,10 +72,15 @@ export default class MetricUtil { return results; } - public static async loadAllMetricsTypes(): Promise<{ + public static async loadAllMetricsTypes(options?: { + includeAttributes?: boolean; + }): Promise<{ metricTypes: Array; telemetryAttributes: Array; + telemetryAttributesError?: string; }> { + const includeAttributes: boolean = options?.includeAttributes ?? true; + const metrics: ListResult = await ModelAPI.getList({ modelType: MetricType, select: { @@ -94,6 +99,27 @@ export default class MetricUtil { const metricTypes: Array = metrics.data; + let telemetryAttributes: Array = []; + let telemetryAttributesError: string | undefined; + + if (includeAttributes) { + try { + telemetryAttributes = await MetricUtil.getTelemetryAttributes(); + } catch (err) { + telemetryAttributesError = API.getFriendlyErrorMessage(err as Error); + } + } + + return { + metricTypes: metricTypes, + telemetryAttributes, + ...(telemetryAttributesError !== undefined + ? { telemetryAttributesError } + : {}), + }; + } + + public static async getTelemetryAttributes(): Promise> { const metricAttributesResponse: | HTTPResponse | HTTPErrorResponse = await API.post({ @@ -106,17 +132,10 @@ export default class MetricUtil { }, }); - let attributes: Array = []; - if (metricAttributesResponse instanceof HTTPErrorResponse) { throw metricAttributesResponse; - } else { - attributes = metricAttributesResponse.data["attributes"] as Array; } - return { - metricTypes: metricTypes, - telemetryAttributes: attributes, - }; + return (metricAttributesResponse.data["attributes"] || []) as Array; } } diff --git a/Dashboard/src/Components/Traces/TraceTable.tsx b/Dashboard/src/Components/Traces/TraceTable.tsx index dee88a6fb2..969e50ed7c 100644 --- a/Dashboard/src/Components/Traces/TraceTable.tsx +++ b/Dashboard/src/Components/Traces/TraceTable.tsx @@ -11,6 +11,7 @@ import React, { FunctionComponent, ReactElement, useEffect, + useState, } from "react"; import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; import PageMap from "../../Utils/PageMap"; @@ -46,6 +47,11 @@ const TraceTable: FunctionComponent = ( const modelId: ObjectID | undefined = props.modelId; const [attributes, setAttributes] = React.useState>([]); + const [attributesLoaded, setAttributesLoaded] = + React.useState(false); + const [attributesLoading, setAttributesLoading] = + React.useState(false); + const [attributesError, setAttributesError] = React.useState(""); const [isPageLoading, setIsPageLoading] = React.useState(true); const [pageError, setPageError] = React.useState(""); @@ -58,17 +64,56 @@ const TraceTable: FunctionComponent = ( Array >([]); + const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] = + useState(false); + useEffect(() => { if (props.spanQuery) { setSpanQuery(props.spanQuery); } }, [props.spanQuery]); - const loadItems: PromiseVoidFunction = async (): Promise => { - try { - setIsPageLoading(true); + const loadTelemetryServices: PromiseVoidFunction = + async (): Promise => { + try { + setIsPageLoading(true); + setPageError(""); - const attributeRepsonse: HTTPResponse | HTTPErrorResponse = + const telemetryServicesResponse: ListResult = + await ModelAPI.getList({ + modelType: TelemetryService, + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + }, + select: { + serviceColor: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: { + name: SortOrder.Ascending, + }, + }); + + setTelemetryServices(telemetryServicesResponse.data || []); + } catch (err) { + setPageError(API.getFriendlyErrorMessage(err as Error)); + } finally { + setIsPageLoading(false); + } + }; + + const loadAttributes: PromiseVoidFunction = async (): Promise => { + if (attributesLoading || attributesLoaded) { + return; + } + + try { + setAttributesLoading(true); + setAttributesError(""); + + const attributeResponse: HTTPResponse | HTTPErrorResponse = await API.post({ url: URL.fromString(APP_API_URL.toString()).addRoute( "/telemetry/traces/get-attributes", @@ -79,49 +124,40 @@ const TraceTable: FunctionComponent = ( }, }); - if (attributeRepsonse instanceof HTTPErrorResponse) { - throw attributeRepsonse; - } else { - const attributes: Array = attributeRepsonse.data[ - "attributes" - ] as Array; - setAttributes(attributes); + if (attributeResponse instanceof HTTPErrorResponse) { + throw attributeResponse; } - // Load telemetry services - const telemetryServices: ListResult = - await ModelAPI.getList({ - modelType: TelemetryService, - query: { - projectId: ProjectUtil.getCurrentProjectId()!, - }, - select: { - serviceColor: true, - name: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: { - name: SortOrder.Ascending, - }, - }); - - setTelemetryServices(telemetryServices.data || []); - - setIsPageLoading(false); - setPageError(""); + const fetchedAttributes: Array = (attributeResponse.data[ + "attributes" + ] || []) as Array; + setAttributes(fetchedAttributes); + setAttributesLoaded(true); } catch (err) { - setIsPageLoading(false); - setPageError(API.getFriendlyErrorMessage(err as Error)); + setAttributes([]); + setAttributesLoaded(false); + setAttributesError(API.getFriendlyErrorMessage(err as Error)); + } finally { + setAttributesLoading(false); } }; useEffect(() => { - loadItems().catch((err: Error) => { + loadTelemetryServices().catch((err: Error) => { setPageError(API.getFriendlyErrorMessage(err as Error)); }); }, []); + const handleAdvancedFiltersToggle: (show: boolean) => void = ( + show: boolean, + ): void => { + setAreAdvancedFiltersVisible(show); + + if (show && !attributesLoaded && !attributesLoading) { + void loadAttributes(); + } + }; + const spanKindDropdownOptions: Array = SpanUtil.getSpanKindDropdownOptions(); @@ -142,12 +178,31 @@ const TraceTable: FunctionComponent = ( return ; } - if (pageError) { - return ; - } - return ( + {pageError && ( +
+ { + void loadTelemetryServices(); + }} + /> +
+ )} + + {areAdvancedFiltersVisible && attributesError && ( +
+ { + setAttributesLoaded(false); + void loadAttributes(); + }} + /> +
+ )} +
userPreferencesKey="trace-table" @@ -251,6 +306,7 @@ const TraceTable: FunctionComponent = ( jsonKeys: attributes, }, ]} + onAdvancedFiltersToggle={handleAdvancedFiltersToggle} selectMoreFields={{ statusCode: true, }}