From 932918e3a490bf6d302662caf44d24d96d23f05f Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 8 Aug 2025 12:03:26 +0530 Subject: [PATCH] feat: meter explorer (#8741) * feat: meter explorer * feat: meter explorer * fix: remove meter as data source * fix: change meter-explorer to meter - quick filter * chore: delete test file * fix: failing test cases --- frontend/public/locales/en-GB/titles.json | 5 +- frontend/public/locales/en/titles.json | 5 +- frontend/src/AppRoutes/routes.ts | 23 +++ .../api/queryBuilder/getAggregateAttribute.ts | 3 +- .../api/querySuggestions/getKeySuggestions.ts | 4 +- .../querySuggestions/getValueSuggestion.ts | 5 +- frontend/src/api/saveView/getAllViews.ts | 2 +- .../queryRange/prepareQueryRangePayloadV5.ts | 1 + .../PanelDataLoading.styles.scss | 19 ++ .../PanelDataLoading/PanelDataLoading.tsx | 19 ++ .../QueryBuilderV2/QueryBuilderV2.tsx | 1 + .../MetricsAggregateSection.tsx | 4 + .../QueryV2/MetricsSelect/MetricsSelect.tsx | 3 + .../QueryV2/QuerySearch/QuerySearch.tsx | 15 +- .../QueryBuilderV2/QueryV2/QueryV2.tsx | 4 + .../FilterRenderers/Checkbox/Checkbox.tsx | 60 +++++- .../QuickFilters/QuickFilters.styles.scss | 33 +++ .../components/QuickFilters/QuickFilters.tsx | 9 +- .../QuickFiltersSettings/constants.ts | 1 + .../QuickFilters/tests/QuickFilters.test.tsx | 7 +- frontend/src/components/QuickFilters/types.ts | 2 + frontend/src/constants/queryBuilder.ts | 50 +++++ .../src/constants/queryBuilderOperators.ts | 120 +++++++++++ frontend/src/constants/routes.ts | 3 + .../ExplorerOptions/ExplorerOptionWrapper.tsx | 2 + .../ExplorerOptions.styles.scss | 17 +- .../ExplorerOptions/ExplorerOptions.tsx | 48 +++-- .../container/ExplorerOptions/constants.ts | 1 + .../src/container/ExplorerOptions/types.ts | 5 +- .../src/container/ExplorerOptions/utils.ts | 2 +- .../Explorer/Explorer.styles.scss | 195 ++++++++++++++++++ .../MeterExplorer/Explorer/Explorer.tsx | 182 ++++++++++++++++ .../MeterExplorer/Explorer/NoData.tsx | 13 ++ .../MeterExplorer/Explorer/QuerySection.tsx | 43 ++++ .../MeterExplorer/Explorer/TimeSeries.tsx | 142 +++++++++++++ .../container/MeterExplorer/Explorer/index.ts | 3 + .../container/MeterExplorer/Explorer/types.ts | 37 ++++ .../MeterExplorer/Explorer/utils.tsx | 37 ++++ .../src/container/MeterExplorer/events.ts | 51 +++++ .../MetricsExplorer/Explorer/Explorer.tsx | 2 +- .../QueryBuilder/QueryBuilder.interfaces.ts | 3 +- .../components/Query/Query.interfaces.ts | 1 + .../AggregatorFilter.intefaces.ts | 1 + .../AggregatorFilter/AggregatorFilter.tsx | 19 +- .../BuilderUnitsFilter/BuilderUnits.tsx | 6 +- .../GroupByFilter/GroupByFilter.interfaces.ts | 1 + .../filters/GroupByFilter/GroupByFilter.tsx | 20 +- frontend/src/container/SideNav/menuItems.tsx | 14 +- .../TimeSeriesView/TimeSeriesView.tsx | 1 + .../src/container/TimeSeriesView/index.tsx | 4 +- .../TopNav/DateTimeSelectionV2/config.ts | 3 + .../useGetQueryKeyValueSuggestions.ts | 18 +- .../src/hooks/saveViews/useGetAllViews.ts | 4 +- .../__tests__/mapQueryDataFromApiInputs.ts | 1 + .../MeterExplorer/MeterExplorer.styles.scss | 16 ++ .../pages/MeterExplorer/MeterExplorerPage.tsx | 22 ++ .../src/pages/MeterExplorer/constants.tsx | 32 +++ frontend/src/pages/MeterExplorer/index.tsx | 3 + frontend/src/pages/SaveView/constants.ts | 2 + frontend/src/pages/SaveView/index.tsx | 8 + frontend/src/providers/QueryBuilder.tsx | 19 +- .../queryBuilder/getAggregatorAttribute.ts | 1 + .../api/queryBuilder/queryBuilderData.ts | 1 + .../src/types/api/querySuggestions/types.ts | 4 + frontend/src/types/api/v5/queryRange.ts | 9 +- frontend/src/types/common/queryBuilder.ts | 37 ++++ frontend/src/utils/permission/index.ts | 3 + 67 files changed, 1366 insertions(+), 65 deletions(-) create mode 100644 frontend/src/components/PanelDataLoading/PanelDataLoading.styles.scss create mode 100644 frontend/src/components/PanelDataLoading/PanelDataLoading.tsx create mode 100644 frontend/src/container/MeterExplorer/Explorer/Explorer.styles.scss create mode 100644 frontend/src/container/MeterExplorer/Explorer/Explorer.tsx create mode 100644 frontend/src/container/MeterExplorer/Explorer/NoData.tsx create mode 100644 frontend/src/container/MeterExplorer/Explorer/QuerySection.tsx create mode 100644 frontend/src/container/MeterExplorer/Explorer/TimeSeries.tsx create mode 100644 frontend/src/container/MeterExplorer/Explorer/index.ts create mode 100644 frontend/src/container/MeterExplorer/Explorer/types.ts create mode 100644 frontend/src/container/MeterExplorer/Explorer/utils.tsx create mode 100644 frontend/src/container/MeterExplorer/events.ts create mode 100644 frontend/src/pages/MeterExplorer/MeterExplorer.styles.scss create mode 100644 frontend/src/pages/MeterExplorer/MeterExplorerPage.tsx create mode 100644 frontend/src/pages/MeterExplorer/constants.tsx create mode 100644 frontend/src/pages/MeterExplorer/index.tsx 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..d8852c55f276 100644 --- a/frontend/src/api/querySuggestions/getValueSuggestion.ts +++ b/frontend/src/api/querySuggestions/getValueSuggestion.ts @@ -8,13 +8,14 @@ import { export const getValueSuggestions = ( props: QueryKeyValueRequestProps, ): Promise> => { - const { signal, key, searchText } = props; + const { signal, key, searchText, signalSource } = props; const encodedSignal = encodeURIComponent(signal); const encodedKey = encodeURIComponent(key); 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}&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..0d13d0ec5b5e 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,7 @@ function QuerySearch({ key, searchText: sanitizedSearchText, signal: dataSource, + signalSource: signalSource as 'meter' | '', }); // Skip updates if component unmounted or key changed @@ -465,8 +470,13 @@ function QuerySearch({ setIsFetchingCompleteValuesList(false); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [activeKey, dataSource, isFocused], + [ + activeKey, + dataSource, + isLoadingSuggestions, + signalSource, + toggleSuggestions, + ], ); const debouncedFetchValueSuggestions = useMemo( @@ -1440,6 +1450,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/container/ExplorerOptions/ExplorerOptionWrapper.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptionWrapper.tsx index beef5c66861d..a2ecfce579d1 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptionWrapper.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptionWrapper.tsx @@ -16,6 +16,7 @@ function ExplorerOptionWrapper({ sourcepage, isOneChartPerQuery, splitedQueries, + signalSource, }: ExplorerOptionsWrapperProps): JSX.Element { const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false); @@ -32,6 +33,7 @@ function ExplorerOptionWrapper({ isLoading={isLoading} onExport={onExport} sourcepage={sourcepage} + signalSource={signalSource} isExplorerOptionHidden={isExplorerOptionHidden} setIsExplorerOptionHidden={setIsExplorerOptionHidden} isOneChartPerQuery={isOneChartPerQuery} diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.styles.scss b/frontend/src/container/ExplorerOptions/ExplorerOptions.styles.scss index 9ea720a27dfe..93dad5eb029d 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.styles.scss +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.styles.scss @@ -1,12 +1,12 @@ .explorer-options-container { position: fixed; - bottom: 24px; + bottom: 8px; left: calc(50% + 240px); transform: translate(calc(-50% - 120px), 0); transition: left 0.2s linear; display: flex; - gap: 16px; + gap: 8px; background-color: transparent; .multi-alert-button, @@ -33,11 +33,12 @@ display: inline-flex; align-items: center; gap: 12px; - padding: 10px 10px; - border-radius: 50px; - border: 1px solid var(--bg-slate-400); + padding: 10px 12px; background: rgba(22, 24, 29, 0.6); + border: 1px solid var(--bg-slate-500); + border-radius: 4px; backdrop-filter: blur(20px); + box-sizing: border-box; .action-icon { display: flex; @@ -64,9 +65,9 @@ .explorer-options { padding: 10px 12px; - border: 1px solid var(--bg-slate-400); - border-radius: 50px; - background: rgba(22, 24, 29, 0.6); + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); backdrop-filter: blur(20px); cursor: default; diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index d73a7957c8b6..d425b29d4d05 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -93,6 +93,7 @@ function ExplorerOptions({ onExport, query, sourcepage, + signalSource, isExplorerOptionHidden = false, setIsExplorerOptionHidden, isOneChartPerQuery = false, @@ -110,6 +111,7 @@ function ExplorerOptions({ const isLogsExplorer = sourcepage === DataSource.LOGS; const isMetricsExplorer = sourcepage === DataSource.METRICS; + const isMeterExplorer = signalSource === 'meter'; const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS; @@ -120,8 +122,11 @@ function ExplorerOptions({ if (isMetricsExplorer) { return PreservedViewsTypes.METRICS; } + if (isMeterExplorer) { + return PreservedViewsTypes.METER; + } return PreservedViewsTypes.TRACES; - }, [isLogsExplorer, isMetricsExplorer]); + }, [isLogsExplorer, isMetricsExplorer, isMeterExplorer]); const onModalToggle = useCallback((value: boolean) => { 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 = ( + + } + /> + + ); +} + +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/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 +