mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add advanced filters toggle functionality across various components
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user