feat: add advanced filters toggle functionality across various components

This commit is contained in:
Simon Larsen
2025-10-16 16:35:57 +01:00
parent 003e44d331
commit 94290c77db
9 changed files with 236 additions and 57 deletions

View File

@@ -30,6 +30,9 @@ export interface ComponentProps<T extends GenericObject> {
isModalLoading?: boolean;
onFilterRefreshClick?: undefined | (() => void);
filterData?: FilterData<T> | undefined;
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
}
type FilterComponentFunction = <T extends GenericObject>(
@@ -355,7 +358,7 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
<div>
{showViewer && (
<div>
<div className="mt-5 mb-5 bg-gray-50 rounded rounded-xl p-5 border border-2 border-gray-100">
<div className="mt-5 mb-5 bg-gray-50 rounded-xl p-5 border-2 border-gray-100">
<div className="flex mt-1 mb-2">
<div className="flex-auto py-0.5 text-sm leading-5">
<span className="font-semibold">
@@ -442,6 +445,7 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
onFilterChanged={(filterData: FilterData<T>) => {
setTempFilterDataForModal(filterData);
}}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
/>
</Modal>
)}

View File

@@ -43,6 +43,9 @@ export interface ComponentProps<T extends GenericObject> {
onFilterRefreshClick?: undefined | (() => void);
onFilterModalClose?: (() => void) | undefined;
onFilterModalOpen?: (() => void) | undefined;
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
}
type ListFunction = <T extends GenericObject>(
@@ -118,6 +121,7 @@ const List: ListFunction = <T extends GenericObject>(
}}
singularLabel={props.singularLabel}
pluralLabel={props.pluralLabel}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
/>
</div>
<div className="">

View File

@@ -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: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
onFilterModalOpen={() => {
setShowFilterModal(true);
}}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
onSortChanged={(
sortBy: keyof TBaseModel | null,
sortOrder: SortOrder,
@@ -1662,6 +1667,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
onFilterModalOpen={() => {
setShowFilterModal(true);
}}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
singularLabel={props.singularName || model.singularName || "Item"}
pluralLabel={props.pluralName || model.pluralName || "Items"}
error={error}

View File

@@ -54,6 +54,9 @@ export interface ComponentProps<T extends GenericObject> {
onFilterModalClose?: (() => void) | undefined;
onFilterModalOpen?: (() => void) | undefined;
filterData?: undefined | FilterData<T>;
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
enableDragAndDrop?: boolean | undefined;
dragDropIndexField?: keyof T | undefined;
@@ -242,6 +245,7 @@ const Table: TableFunction = <T extends GenericObject>(
singularLabel={props.singularLabel}
pluralLabel={props.pluralLabel}
filterData={props.filterData}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
/>
{props.bulkActions?.buttons && (
<BulkUpdateForm

View File

@@ -1,6 +1,11 @@
import FiltersForm from "Common/UI/Components/Filters/FiltersForm";
import FieldType from "Common/UI/Components/Types/FieldType";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useState,
} from "react";
import DropdownUtil from "Common/UI/Utils/Dropdown";
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
import Query from "Common/Types/BaseDatabase/Query";
@@ -13,11 +18,20 @@ export interface ComponentProps {
onDataChanged: (filterData: MetricQueryData) => void;
metricTypes: Array<MetricType>;
telemetryAttributes: string[];
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
isAttributesLoading?: boolean | undefined;
attributesError?: string | undefined;
onAttributesRetry?: (() => void) | undefined;
}
const MetricFilter: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [showAdvancedFilters, setShowAdvancedFilters] =
useState<boolean>(false);
return (
<Fragment>
<div>
@@ -31,6 +45,17 @@ const MetricFilter: FunctionComponent<ComponentProps> = (
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<ComponentProps> = (
type: FieldType.JSON,
title: "Filter by Attributes",
jsonKeys: props.telemetryAttributes,
isAdvancedFilter: true,
},
{
key: "aggegationType",

View File

@@ -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<ComponentProps> = (
@@ -56,6 +62,10 @@ const MetricGraphConfig: FunctionComponent<ComponentProps> = (
}}
metricTypes={props.metricTypes}
telemetryAttributes={props.telemetryAttributes}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
isAttributesLoading={props.attributesLoading}
attributesError={props.attributesError}
onAttributesRetry={props.onAttributesRetry}
/>
)}
{props.onRemove && (

View File

@@ -92,12 +92,18 @@ const MetricView: FunctionComponent<ComponentProps> = (
const [telemetryAttributes, setTelemetryAttributes] = useState<Array<string>>(
[],
);
const [telemetryAttributesLoaded, setTelemetryAttributesLoaded] =
useState<boolean>(false);
const [telemetryAttributesLoading, setTelemetryAttributesLoading] =
useState<boolean>(false);
const [telemetryAttributesError, setTelemetryAttributesError] =
useState<string>("");
const metricViewDataRef: React.MutableRefObject<MetricViewData> =
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<ComponentProps> = (
useState<boolean>(false);
const [metricResultsError, setMetricResultsError] = useState<string>("");
const loadAllMetricsTypes: PromiseVoidFunction = async (): Promise<void> => {
const loadMetricTypes: PromiseVoidFunction = async (): Promise<void> => {
try {
setIsPageLoading(true);
const {
metricTypes,
telemetryAttributes,
}: {
metricTypes: Array<MetricType>;
telemetryAttributes: Array<string>;
} = 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<ComponentProps> = (
}
};
const loadTelemetryAttributes: PromiseVoidFunction =
async (): Promise<void> => {
if (telemetryAttributesLoading || telemetryAttributesLoaded) {
return;
}
try {
setTelemetryAttributesLoading(true);
setTelemetryAttributesError("");
const attributes: Array<string> =
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<void> => {
setIsMetricResultsLoading(true);
@@ -276,6 +319,13 @@ const MetricView: FunctionComponent<ComponentProps> = (
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);

View File

@@ -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<MetricType>;
telemetryAttributes: Array<string>;
telemetryAttributesError?: string;
}> {
const includeAttributes: boolean = options?.includeAttributes ?? true;
const metrics: ListResult<MetricType> = await ModelAPI.getList({
modelType: MetricType,
select: {
@@ -94,6 +99,27 @@ export default class MetricUtil {
const metricTypes: Array<MetricType> = metrics.data;
let telemetryAttributes: Array<string> = [];
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<Array<string>> {
const metricAttributesResponse:
| HTTPResponse<JSONObject>
| HTTPErrorResponse = await API.post({
@@ -106,17 +132,10 @@ export default class MetricUtil {
},
});
let attributes: Array<string> = [];
if (metricAttributesResponse instanceof HTTPErrorResponse) {
throw metricAttributesResponse;
} else {
attributes = metricAttributesResponse.data["attributes"] as Array<string>;
}
return {
metricTypes: metricTypes,
telemetryAttributes: attributes,
};
return (metricAttributesResponse.data["attributes"] || []) as Array<string>;
}
}

View File

@@ -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<ComponentProps> = (
const modelId: ObjectID | undefined = props.modelId;
const [attributes, setAttributes] = React.useState<Array<string>>([]);
const [attributesLoaded, setAttributesLoaded] =
React.useState<boolean>(false);
const [attributesLoading, setAttributesLoading] =
React.useState<boolean>(false);
const [attributesError, setAttributesError] = React.useState<string>("");
const [isPageLoading, setIsPageLoading] = React.useState<boolean>(true);
const [pageError, setPageError] = React.useState<string>("");
@@ -58,17 +64,56 @@ const TraceTable: FunctionComponent<ComponentProps> = (
Array<TelemetryService>
>([]);
const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] =
useState<boolean>(false);
useEffect(() => {
if (props.spanQuery) {
setSpanQuery(props.spanQuery);
}
}, [props.spanQuery]);
const loadItems: PromiseVoidFunction = async (): Promise<void> => {
try {
setIsPageLoading(true);
const loadTelemetryServices: PromiseVoidFunction =
async (): Promise<void> => {
try {
setIsPageLoading(true);
setPageError("");
const attributeRepsonse: HTTPResponse<JSONObject> | HTTPErrorResponse =
const telemetryServicesResponse: ListResult<TelemetryService> =
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<void> => {
if (attributesLoading || attributesLoaded) {
return;
}
try {
setAttributesLoading(true);
setAttributesError("");
const attributeResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.post({
url: URL.fromString(APP_API_URL.toString()).addRoute(
"/telemetry/traces/get-attributes",
@@ -79,49 +124,40 @@ const TraceTable: FunctionComponent<ComponentProps> = (
},
});
if (attributeRepsonse instanceof HTTPErrorResponse) {
throw attributeRepsonse;
} else {
const attributes: Array<string> = attributeRepsonse.data[
"attributes"
] as Array<string>;
setAttributes(attributes);
if (attributeResponse instanceof HTTPErrorResponse) {
throw attributeResponse;
}
// Load telemetry services
const telemetryServices: ListResult<TelemetryService> =
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<string> = (attributeResponse.data[
"attributes"
] || []) as Array<string>;
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<DropdownOption> =
SpanUtil.getSpanKindDropdownOptions();
@@ -142,12 +178,31 @@ const TraceTable: FunctionComponent<ComponentProps> = (
return <PageLoader isVisible={true} />;
}
if (pageError) {
return <ErrorMessage message={pageError} />;
}
return (
<Fragment>
{pageError && (
<div className="mb-4">
<ErrorMessage
message={`We couldn't load telemetry services. ${pageError}`}
onRefreshClick={() => {
void loadTelemetryServices();
}}
/>
</div>
)}
{areAdvancedFiltersVisible && attributesError && (
<div className="mb-4">
<ErrorMessage
message={`We couldn't load trace attributes. ${attributesError}`}
onRefreshClick={() => {
setAttributesLoaded(false);
void loadAttributes();
}}
/>
</div>
)}
<div className="rounded">
<AnalyticsModelTable<Span>
userPreferencesKey="trace-table"
@@ -251,6 +306,7 @@ const TraceTable: FunctionComponent<ComponentProps> = (
jsonKeys: attributes,
},
]}
onAdvancedFiltersToggle={handleAdvancedFiltersToggle}
selectMoreFields={{
statusCode: true,
}}