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
This commit is contained in:
Yunus M 2025-08-08 12:03:26 +05:30 committed by GitHub
parent aa3bc16dcb
commit 932918e3a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 1366 additions and 65 deletions

View File

@ -46,5 +46,8 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", "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"
} }

View File

@ -69,5 +69,8 @@
"METRICS_EXPLORER": "SigNoz | Metrics Explorer", "METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer", "METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "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"
} }

View File

@ -1,5 +1,6 @@
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues'; import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom'; import { RouteProps } from 'react-router-dom';
import { import {
@ -434,6 +435,28 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS', key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true, 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, path: ROUTES.API_MONITORING,
exact: true, exact: true,

View File

@ -17,6 +17,7 @@ export const getAggregateAttribute = async ({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource,
source,
}: IGetAggregateAttributePayload): Promise< }: IGetAggregateAttributePayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => { > => {
@ -27,7 +28,7 @@ export const getAggregateAttribute = async ({
`/autocomplete/aggregate_attributes?${createQueryParams({ `/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator, aggregateOperator,
searchText, searchText,
dataSource, dataSource: source === 'meter' ? 'meter' : dataSource,
})}`, })}`,
); );

View File

@ -14,6 +14,7 @@ export const getKeySuggestions = (
metricName = '', metricName = '',
fieldContext = '', fieldContext = '',
fieldDataType = '', fieldDataType = '',
signalSource = '',
} = props; } = props;
const encodedSignal = encodeURIComponent(signal); const encodedSignal = encodeURIComponent(signal);
@ -21,8 +22,9 @@ export const getKeySuggestions = (
const encodedMetricName = encodeURIComponent(metricName); const encodedMetricName = encodeURIComponent(metricName);
const encodedFieldContext = encodeURIComponent(fieldContext); const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType); const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
return axios.get( 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}`,
); );
}; };

View File

@ -8,13 +8,14 @@ import {
export const getValueSuggestions = ( export const getValueSuggestions = (
props: QueryKeyValueRequestProps, props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => { ): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText } = props; const { signal, key, searchText, signalSource } = props;
const encodedSignal = encodeURIComponent(signal); const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key); const encodedKey = encodeURIComponent(key);
const encodedSearchText = encodeURIComponent(searchText); const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
return axios.get( return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`, `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&source=${encodedSource}`,
); );
}; };

View File

@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = ( export const getAllViews = (
sourcepage: DataSource, sourcepage: DataSource | 'meter',
): Promise<AxiosResponse<AllViewsProps>> => ): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`); axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@ -260,6 +260,7 @@ export function convertBuilderQueriesToV5(
spec = { spec = {
name: queryName, name: queryName,
signal: 'metrics' as const, signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec, ...baseSpec,
aggregations: aggregations as MetricAggregation[], aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo, // reduceTo: queryData.reduceTo,

View File

@ -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;
}
}
}

View File

@ -0,0 +1,19 @@
import './PanelDataLoading.styles.scss';
import { Typography } from 'antd';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

@ -131,6 +131,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'} queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause} showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel} isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/> />
))} ))}

View File

@ -18,11 +18,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
index, index,
version, version,
panelType, panelType,
signalSource = '',
}: { }: {
query: IBuilderQuery; query: IBuilderQuery;
index: number; index: number;
version: string; version: string;
panelType: PANEL_TYPES | null; panelType: PANEL_TYPES | null;
signalSource: string;
}): JSX.Element { }): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context(); const { setAggregationOptions } = useQueryBuilderV2Context();
const { const {
@ -208,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName} disabled={!queryAggregation.metricName}
query={query} query={query}
onChange={handleChangeGroupByKeys} onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/> />
</div> </div>
</div> </div>
@ -244,6 +247,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName} disabled={!queryAggregation.metricName}
query={query} query={query}
onChange={handleChangeGroupByKeys} onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/> />
</div> </div>
</div> </div>

View File

@ -9,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({
query, query,
index, index,
version, version,
signalSource,
}: { }: {
query: IBuilderQuery; query: IBuilderQuery;
index: number; index: number;
version: string; version: string;
signalSource: 'meter' | '';
}): JSX.Element { }): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({ const { handleChangeAggregatorAttribute } = useQueryOperations({
index, index,
@ -26,6 +28,7 @@ export const MetricsSelect = memo(function MetricsSelect({
onChange={handleChangeAggregatorAttribute} onChange={handleChangeAggregatorAttribute}
query={query} query={query}
index={index} index={index}
signalSource={signalSource || ''}
/> />
</div> </div>
); );

View File

@ -81,10 +81,12 @@ function QuerySearch({
queryData, queryData,
dataSource, dataSource,
onRun, onRun,
signalSource,
}: { }: {
onChange: (value: string) => void; onChange: (value: string) => void;
queryData: IBuilderQuery; queryData: IBuilderQuery;
dataSource: DataSource; dataSource: DataSource;
signalSource?: string;
onRun?: (query: string) => void; onRun?: (query: string) => void;
}): JSX.Element { }): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -218,6 +220,7 @@ function QuerySearch({
signal: dataSource, signal: dataSource,
searchText: searchText || '', searchText: searchText || '',
metricName: debouncedMetricName ?? undefined, metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
}); });
if (response.data.data) { if (response.data.data) {
@ -245,6 +248,7 @@ function QuerySearch({
keySuggestions, keySuggestions,
toggleSuggestions, toggleSuggestions,
queryData.aggregateAttribute?.key, queryData.aggregateAttribute?.key,
signalSource,
], ],
); );
@ -378,6 +382,7 @@ function QuerySearch({
key, key,
searchText: sanitizedSearchText, searchText: sanitizedSearchText,
signal: dataSource, signal: dataSource,
signalSource: signalSource as 'meter' | '',
}); });
// Skip updates if component unmounted or key changed // Skip updates if component unmounted or key changed
@ -465,8 +470,13 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false); setIsFetchingCompleteValuesList(false);
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [
[activeKey, dataSource, isFocused], activeKey,
dataSource,
isLoadingSuggestions,
signalSource,
toggleSuggestions,
],
); );
const debouncedFetchValueSuggestions = useMemo( const debouncedFetchValueSuggestions = useMemo(
@ -1440,6 +1450,7 @@ function QuerySearch({
QuerySearch.defaultProps = { QuerySearch.defaultProps = {
onRun: undefined, onRun: undefined,
signalSource: '',
}; };
export default QuerySearch; export default QuerySearch;

View File

@ -28,6 +28,7 @@ export const QueryV2 = memo(function QueryV2({
isListViewPanel = false, isListViewPanel = false,
version, version,
showOnlyWhereClause = false, showOnlyWhereClause = false,
signalSource = '',
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element { }: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder(); const { cloneQuery, panelType } = useQueryBuilder();
@ -175,6 +176,7 @@ export const QueryV2 = memo(function QueryV2({
query={query} query={query}
index={index} index={index}
version={ENTITY_VERSION_V5} version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
/> />
</div> </div>
)} )}
@ -186,6 +188,7 @@ export const QueryV2 = memo(function QueryV2({
onChange={handleSearchChange} onChange={handleSearchChange}
queryData={query} queryData={query}
dataSource={dataSource} dataSource={dataSource}
signalSource={signalSource}
/> />
</div> </div>
@ -218,6 +221,7 @@ export const QueryV2 = memo(function QueryV2({
index={index} index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`} key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4" version="v4"
signalSource={signalSource as 'meter' | ''}
/> />
)} )}

View File

@ -17,6 +17,7 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es'; import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight } from 'lucide-react';
@ -73,18 +74,59 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
searchText: searchText ?? '', searchText: searchText ?? '',
}, },
{ {
enabled: isOpen, enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true, 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 attributeValues: string[] = useMemo(() => {
const dataType = filter.attributeKey.dataType || DataTypes.String; 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]; const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return (data?.payload?.[key] || []).filter( return (data?.payload?.[key] || []).filter(
(val) => val !== undefined && val !== null, (val) => val !== undefined && val !== null,
); );
}, [data?.payload, filter.attributeKey.dataType]); }, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount); const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
@ -478,12 +520,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)} )}
</section> </section>
</section> </section>
{isOpen && isLoading && !attributeValues.length && ( {isOpen &&
<section className="loading"> (isLoading || isLoadingKeyValueSuggestions) &&
<Skeleton paragraph={{ rows: 4 }} /> !attributeValues.length && (
</section> <section className="loading">
)} <Skeleton paragraph={{ rows: 4 }} />
{isOpen && !isLoading && ( </section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
<> <>
{!isEmptyStateWithDocsEnabled && ( {!isEmptyStateWithDocsEnabled && (
<section className="search"> <section className="search">

View File

@ -1,6 +1,8 @@
.quick-filters-container { .quick-filters-container {
display: flex; display: flex;
height: 100%; height: 100%;
position: relative;
.quick-filters-settings-container { .quick-filters-settings-container {
position: relative; position: relative;
} }
@ -102,6 +104,37 @@
margin: 8px 12px; 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 { .lightMode {

View File

@ -15,7 +15,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams'; import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction, isNull } from 'lodash-es'; 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 { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -236,6 +236,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
); );
} }
})} })}
{filterConfig.length === 0 && (
<div className="no-filters-container">
<Frown size={16} />
<Typography.Text>No filters found</Typography.Text>
</div>
)}
</section> </section>
</> </>
</OverlayScrollbar> </OverlayScrollbar>

View File

@ -6,4 +6,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.TRACES]: DataSource.TRACES, [SignalType.TRACES]: DataSource.TRACES,
[SignalType.EXCEPTIONS]: DataSource.TRACES, [SignalType.EXCEPTIONS]: DataSource.TRACES,
[SignalType.API_MONITORING]: DataSource.TRACES, [SignalType.API_MONITORING]: DataSource.TRACES,
[SignalType.METER_EXPLORER]: DataSource.METRICS,
}; };

View File

@ -54,6 +54,7 @@ const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`; const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`; const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`; 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_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name'; const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@ -77,7 +78,11 @@ const setupServer = (): void => {
putHandler(await req.json()); putHandler(await req.json());
return res(ctx.status(200), ctx.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)), res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
), ),
); );

View File

@ -23,6 +23,7 @@ export enum SignalType {
LOGS = 'logs', LOGS = 'logs',
API_MONITORING = 'api_monitoring', API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions', EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
} }
export interface IQuickFiltersConfig { export interface IQuickFiltersConfig {
@ -53,4 +54,5 @@ export enum QuickFiltersSource {
TRACES_EXPLORER = 'traces-explorer', TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring', API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions', EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
} }

View File

@ -23,6 +23,7 @@ import {
BoolOperators, BoolOperators,
DataSource, DataSource,
LogsAggregatorOperator, LogsAggregatorOperator,
MeterAggregateOperator,
MetricAggregateOperator, MetricAggregateOperator,
NumberOperators, NumberOperators,
QueryAdditionalFilter, QueryAdditionalFilter,
@ -36,6 +37,7 @@ import { v4 as uuid } from 'uuid';
import { import {
logsAggregateOperatorOptions, logsAggregateOperatorOptions,
meterAggregateOperatorOptions,
metricAggregateOperatorOptions, metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions, metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions, metricsGaugeSpaceAggregateOperatorOptions,
@ -79,6 +81,7 @@ export const mapOfOperators = {
metrics: metricAggregateOperatorOptions, metrics: metricAggregateOperatorOptions,
logs: logsAggregateOperatorOptions, logs: logsAggregateOperatorOptions,
traces: tracesAggregateOperatorOptions, traces: tracesAggregateOperatorOptions,
meter: meterAggregateOperatorOptions,
}; };
export const metricsOperatorsByType = { export const metricsOperatorsByType = {
@ -193,6 +196,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
groupBy: [], groupBy: [],
legend: '', legend: '',
reduceTo: 'avg', reduceTo: 'avg',
source: '',
}; };
const initialQueryBuilderFormLogsValues: IBuilderQuery = { const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@ -209,6 +213,39 @@ const initialQueryBuilderFormTracesValues: IBuilderQuery = {
dataSource: DataSource.TRACES, 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< export const initialQueryBuilderFormValuesMap: Record<
DataSource, DataSource,
IBuilderQuery IBuilderQuery
@ -285,6 +322,19 @@ export const initialQueriesMap: Record<DataSource, Query> = {
traces: initialQueryTracesWithType, traces: initialQueryTracesWithType,
}; };
export const initialQueryMeterWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.metrics,
source: 'meter',
},
],
},
};
export const operatorsByTypes: Record<LocalDataType, string[]> = { export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators), string: Object.values(StringOperators),
number: Object.values(NumberOperators), number: Object.values(NumberOperators),

View File

@ -125,6 +125,126 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
}, },
]; ];
export const meterAggregateOperatorOptions: SelectOption<string, string>[] = [
{
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<string, string>[] = [ export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
{ {
value: TracesAggregatorOperator.COUNT, value: TracesAggregatorOperator.COUNT,

View File

@ -77,6 +77,9 @@ const ROUTES = {
API_MONITORING: '/api-monitoring/explorer', API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer', METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted', WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER_EXPLORER_BASE: '/meter-explorer',
METER_EXPLORER: '/meter-explorer',
METER_EXPLORER_VIEWS: '/meter-explorer/views',
HOME_PAGE: '/', HOME_PAGE: '/',
} as const; } as const;

View File

@ -16,6 +16,7 @@ function ExplorerOptionWrapper({
sourcepage, sourcepage,
isOneChartPerQuery, isOneChartPerQuery,
splitedQueries, splitedQueries,
signalSource,
}: ExplorerOptionsWrapperProps): JSX.Element { }: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false); const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
@ -32,6 +33,7 @@ function ExplorerOptionWrapper({
isLoading={isLoading} isLoading={isLoading}
onExport={onExport} onExport={onExport}
sourcepage={sourcepage} sourcepage={sourcepage}
signalSource={signalSource}
isExplorerOptionHidden={isExplorerOptionHidden} isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden} setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery} isOneChartPerQuery={isOneChartPerQuery}

View File

@ -1,12 +1,12 @@
.explorer-options-container { .explorer-options-container {
position: fixed; position: fixed;
bottom: 24px; bottom: 8px;
left: calc(50% + 240px); left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0); transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear; transition: left 0.2s linear;
display: flex; display: flex;
gap: 16px; gap: 8px;
background-color: transparent; background-color: transparent;
.multi-alert-button, .multi-alert-button,
@ -33,11 +33,12 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 10px; padding: 10px 12px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6); background: rgba(22, 24, 29, 0.6);
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
box-sizing: border-box;
.action-icon { .action-icon {
display: flex; display: flex;
@ -64,9 +65,9 @@
.explorer-options { .explorer-options {
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--bg-slate-400); border-radius: 4px;
border-radius: 50px; border: 1px solid var(--bg-slate-500);
background: rgba(22, 24, 29, 0.6); background: var(--bg-ink-400);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
cursor: default; cursor: default;

View File

@ -93,6 +93,7 @@ function ExplorerOptions({
onExport, onExport,
query, query,
sourcepage, sourcepage,
signalSource,
isExplorerOptionHidden = false, isExplorerOptionHidden = false,
setIsExplorerOptionHidden, setIsExplorerOptionHidden,
isOneChartPerQuery = false, isOneChartPerQuery = false,
@ -110,6 +111,7 @@ function ExplorerOptions({
const isLogsExplorer = sourcepage === DataSource.LOGS; const isLogsExplorer = sourcepage === DataSource.LOGS;
const isMetricsExplorer = sourcepage === DataSource.METRICS; const isMetricsExplorer = sourcepage === DataSource.METRICS;
const isMeterExplorer = signalSource === 'meter';
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS; const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
@ -120,8 +122,11 @@ function ExplorerOptions({
if (isMetricsExplorer) { if (isMetricsExplorer) {
return PreservedViewsTypes.METRICS; return PreservedViewsTypes.METRICS;
} }
if (isMeterExplorer) {
return PreservedViewsTypes.METER;
}
return PreservedViewsTypes.TRACES; return PreservedViewsTypes.TRACES;
}, [isLogsExplorer, isMetricsExplorer]); }, [isLogsExplorer, isMetricsExplorer, isMeterExplorer]);
const onModalToggle = useCallback((value: boolean) => { const onModalToggle = useCallback((value: boolean) => {
setIsExport(value); setIsExport(value);
@ -150,6 +155,10 @@ function ExplorerOptions({
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery, [MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
panelType, panelType,
}); });
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Save view clicked', {
panelType,
});
} }
setIsSaveModalOpen(!isSaveModalOpen); setIsSaveModalOpen(!isSaveModalOpen);
}; };
@ -243,7 +252,7 @@ function ExplorerOptions({
error, error,
isRefetching, isRefetching,
refetch: refetchAllView, refetch: refetchAllView,
} = useGetAllViews(sourcepage); } = useGetAllViews(isMeterExplorer ? 'meter' : sourcepage);
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType); const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
@ -316,7 +325,7 @@ function ExplorerOptions({
compositeQuery, compositeQuery,
viewKey, viewKey,
extraData: updatedExtraData, extraData: updatedExtraData,
sourcePage: sourcepage, sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName, viewName,
}); });
@ -332,7 +341,7 @@ function ExplorerOptions({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType), compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey, viewKey,
extraData: updatedExtraData, extraData: updatedExtraData,
sourcePage: sourcepage, sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName, viewName,
}, },
{ {
@ -459,6 +468,11 @@ function ExplorerOptions({
panelType, panelType,
viewName: option?.value, viewName: option?.value,
}); });
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Select view', {
panelType,
viewName: option?.value,
});
} }
updatePreservedViewInLocalStorage(option); updatePreservedViewInLocalStorage(option);
@ -505,6 +519,11 @@ function ExplorerOptions({
: defaultLogsSelectedColumns, : defaultLogsSelectedColumns,
}); });
if (signalSource === 'meter') {
history.replace(ROUTES.METER_EXPLORER);
return;
}
history.replace(DATASOURCE_VS_ROUTES[sourcepage]); history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
}; };
@ -549,7 +568,7 @@ function ExplorerOptions({
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
refetchAllView, refetchAllView,
saveViewAsync, saveViewAsync,
sourcePage: sourcepage, sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName: newViewName, viewName: newViewName,
setNewViewName, setNewViewName,
}); });
@ -668,7 +687,7 @@ function ExplorerOptions({
return `Query ${query.builder.queryData[0].queryName}`; return `Query ${query.builder.queryData[0].queryName}`;
}; };
const alertButton = useMemo(() => { const CreateAlertButton = useMemo(() => {
if (isOneChartPerQuery) { if (isOneChartPerQuery) {
const selectLabel = ( const selectLabel = (
<Button <Button
@ -721,7 +740,7 @@ function ExplorerOptions({
splitedQueries, splitedQueries,
]); ]);
const dashboardButton = useMemo(() => { const AddToDashboardButton = useMemo(() => {
if (isOneChartPerQuery) { if (isOneChartPerQuery) {
const selectLabel = ( const selectLabel = (
<Button <Button
@ -829,7 +848,7 @@ function ExplorerOptions({
style={{ style={{
background: extraData background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)` ? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent', : 'initial',
}} }}
> >
<div className="view-options"> <div className="view-options">
@ -884,10 +903,13 @@ function ExplorerOptions({
<hr className={isEditDeleteSupported ? '' : 'hidden'} /> <hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}> {signalSource !== 'meter' && (
{alertButton} <div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{dashboardButton} {CreateAlertButton}
</div> {AddToDashboardButton}
</div>
)}
<div className="actions"> <div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */} {/* Hide the info icon for metrics explorer until we get the docs link */}
{!isMetricsExplorer && ( {!isMetricsExplorer && (
@ -993,6 +1015,7 @@ export interface ExplorerOptionsProps {
query: Query | null; query: Query | null;
disabled: boolean; disabled: boolean;
sourcepage: DataSource; sourcepage: DataSource;
signalSource?: string;
isExplorerOptionHidden?: boolean; isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>; setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
isOneChartPerQuery?: boolean; isOneChartPerQuery?: boolean;
@ -1005,6 +1028,7 @@ ExplorerOptions.defaultProps = {
setIsExplorerOptionHidden: undefined, setIsExplorerOptionHidden: undefined,
isOneChartPerQuery: false, isOneChartPerQuery: false,
splitedQueries: [], splitedQueries: [],
signalSource: '',
}; };
export default ExplorerOptions; export default ExplorerOptions;

View File

@ -2,4 +2,5 @@ export enum PreservedViewsTypes {
LOGS = 'logs', LOGS = 'logs',
TRACES = 'traces', TRACES = 'traces',
METRICS = 'metrics', METRICS = 'metrics',
METER = 'meter',
} }

View File

@ -13,7 +13,7 @@ import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps { export interface SaveNewViewHandlerProps {
viewName: string; viewName: string;
compositeQuery: ICompositeMetricQuery; compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource; sourcePage: DataSource | 'meter';
extraData: SaveViewProps['extraData']; extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null; panelType: PANEL_TYPES | null;
notifications: NotificationInstance; notifications: NotificationInstance;
@ -32,7 +32,8 @@ export interface SaveNewViewHandlerProps {
export type PreservedViewType = export type PreservedViewType =
| PreservedViewsTypes.LOGS | PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES | PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS; | PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
export type PreservedViewsInLocalStorage = Partial< export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }> Record<PreservedViewType, { key: string; value: string }>

View File

@ -37,7 +37,7 @@ export const saveNewViewHandler = ({
{ {
viewName, viewName,
compositeQuery, compositeQuery,
sourcePage, sourcePage: sourcePage as DataSource,
extraData, extraData,
}, },
{ {

View File

@ -0,0 +1,195 @@
.meter-explorer-container {
display: flex;
flex-direction: row;
.meter-explorer-quick-filters-section {
width: 280px;
border-right: 1px solid var(--bg-slate-500);
&.hidden {
display: none;
}
}
.meter-explorer-content-section {
width: 100%;
.explore-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0;
padding: 0 8px;
.explore-header-left-actions {
display: flex;
align-items: flex-start;
gap: 10px;
}
.explore-header-right-actions {
display: flex;
align-items: flex-end;
gap: 10px;
}
}
.query-section {
max-height: 450px;
overflow-y: auto;
.rc-virtual-list-holder {
height: 150px;
}
}
.explore-tabs {
margin: 15px 0;
.tab {
background-color: var(--bg-slate-500);
border-color: var(--bg-ink-200);
width: 180px;
padding: 16px 0;
display: flex;
justify-content: center;
align-items: center;
}
.tab:first-of-type {
border-top-left-radius: 2px;
}
.tab:last-of-type {
border-top-right-radius: 2px;
}
.selected-view {
background: var(--bg-ink-500);
}
}
.explore-content {
.ant-space {
margin-top: 10px;
margin-bottom: 20px;
}
.empty-meter-search {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.time-series-view-panel {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
padding: 8px !important;
margin: 8px;
}
.time-series-container {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, calc(50% - 8px)), 1fr)
);
gap: 16px;
width: 100%;
height: fit-content;
}
}
}
&.quick-filters-open {
.meter-explorer-content-section {
width: calc(100% - 280px);
}
}
padding-bottom: 80px;
}
.meter-time-series-container {
display: flex;
flex-direction: column;
gap: 10px;
.builder-units-filter {
padding: 0 8px;
margin-bottom: 0px !important;
.builder-units-filter-label {
margin-bottom: 0px !important;
font-size: 13px;
}
}
}
.lightMode {
.meter-explorer-container {
.explore-tabs {
.tab {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
.selected-view {
background: var(--bg-vanilla-500);
}
}
}
}
.dashboards-and-alerts-popover-container {
display: flex;
gap: 16px;
.dashboards-and-alerts-popover {
border-radius: 20px;
padding: 4px 8px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
&:hover {
opacity: 0.8;
}
}
.dashboards-popover {
border: 1px solid var(--bg-sienna-500);
.ant-typography {
color: var(--bg-sienna-500);
}
}
.alerts-popover {
border: 1px solid var(--bg-sakura-500);
.ant-typography {
color: var(--bg-sakura-500);
}
}
}
.no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
gap: 16px;
.no-data-text {
color: var(--text-vanilla-500);
font-size: 14px;
line-height: 20px;
text-align: center;
}
}

View File

@ -0,0 +1,182 @@
import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Filter } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 as uuid } from 'uuid';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
import TimeSeries from './TimeSeries';
import { splitQueryIntoOneChartPerQuery } from './utils';
function Explorer(): JSX.Element {
const {
handleRunQuery,
stagedQuery,
updateAllQueriesOperators,
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const [showQuickFilters, setShowQuickFilters] = useState(true);
const defaultQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueryMeterWithType,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueryMeterWithType,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[currentQuery, updateAllQueriesOperators],
);
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleExport = useCallback(
(
dashboard: Dashboard | null,
_isNewDashboard?: boolean,
queryToExport?: Query,
): void => {
if (!dashboard) return;
const widgetId = uuid();
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
});
safeNavigate(dashboardEditView);
},
[exportDefaultQuery, safeNavigate],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(stagedQuery || initialQueryMeterWithType),
[stagedQuery],
);
useEffect(() => {
logEvent(MeterExplorerEvents.TabChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
className={cx('meter-explorer-container', {
'quick-filters-open': showQuickFilters,
})}
>
<div
className={cx('meter-explorer-quick-filters-section', {
hidden: !showQuickFilters,
})}
>
<QuickFilters
className="qf-meter-explorer"
source={QuickFiltersSource.METER_EXPLORER}
signal={SignalType.METER_EXPLORER}
showFilterCollapse
showQueryName={false}
handleFilterVisibilityChange={(): void => {
setShowQuickFilters(!showQuickFilters);
}}
/>
</div>
<div className="meter-explorer-content-section">
<div className="meter-explorer-explore-content">
<div className="explore-header">
<div className="explore-header-left-actions">
{!showQuickFilters && (
<Tooltip title="Show Quick Filters" placement="right" arrow={false}>
<Button
className="periscope-btn outline"
icon={<Filter size={16} />}
onClick={(): void => setShowQuickFilters(!showQuickFilters)}
/>
</Tooltip>
)}
</div>
<div className="explore-header-right-actions">
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
/>
</div>
</div>
<QueryBuilderV2
config={{
initialDataSource: DataSource.METRICS,
queryVariant: 'static',
signalSource: 'meter',
}}
panelType={PANEL_TYPES.TIME_SERIES}
queryComponents={queryComponents}
showFunctions={false}
version="v3"
/>
<div className="explore-content">
<TimeSeries />
</div>
</div>
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
signalSource="meter"
onExport={handleExport}
isOneChartPerQuery={false}
splitedQueries={splitedQueries}
/>
</div>
</div>
</Sentry.ErrorBoundary>
);
}
export default Explorer;

View File

@ -0,0 +1,13 @@
import { Typography } from 'antd';
import { ChartLine } from 'lucide-react';
export default function NoData(): JSX.Element {
return (
<div className="no-data-container">
<ChartLine size={48} />
<Typography.Text className="no-data-text">
No data found for the selected query
</Typography.Text>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
return (
<div className="query-section">
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
version="v4"
actions={
<ButtonWrapper>
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>
}
/>
</div>
);
}
export default QuerySection;

View File

@ -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<string>('');
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<SuccessResponse<MetricRangePayloadProps>> =>
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 (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{responseData.map((datapoint, index) => (
<div
className="time-series-view-panel"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading}
data={datapoint}
dataSource={DataSource.METRICS}
yAxisUnit={yAxisUnit}
/>
</div>
))}
</div>
</div>
);
}
export default TimeSeries;

View File

@ -0,0 +1,3 @@
import Explorer from './Explorer';
export default Explorer;

View File

@ -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<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@ -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;
};

View File

@ -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',
}

View File

@ -189,7 +189,7 @@ function Explorer(): JSX.Element {
query={exportDefaultQuery} query={exportDefaultQuery}
sourcepage={DataSource.METRICS} sourcepage={DataSource.METRICS}
onExport={handleExport} onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery} isOneChartPerQuery={false}
splitedQueries={splitedQueries} splitedQueries={splitedQueries}
/> />
</Sentry.ErrorBoundary> </Sentry.ErrorBoundary>

View File

@ -17,8 +17,9 @@ export type QueryBuilderConfig =
| { | {
queryVariant: 'static'; queryVariant: 'static';
initialDataSource: DataSource; initialDataSource: DataSource;
signalSource?: string;
} }
| { queryVariant: 'dropdown' }; | { queryVariant: 'dropdown'; signalSource?: string };
export type QueryBuilderProps = { export type QueryBuilderProps = {
config?: QueryBuilderConfig; config?: QueryBuilderConfig;

View File

@ -11,4 +11,5 @@ export type QueryProps = {
version: string; version: string;
showSpanScopeSelector?: boolean; showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean; showOnlyWhereClause?: boolean;
signalSource?: string;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>; } & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@ -8,4 +8,5 @@ export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
defaultValue?: string; defaultValue?: string;
onSelect?: (value: BaseAutocompleteData) => void; onSelect?: (value: BaseAutocompleteData) => void;
index?: number; index?: number;
signalSource?: 'meter' | '';
}; };

View File

@ -38,6 +38,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
defaultValue, defaultValue,
onSelect, onSelect,
index, index,
signalSource,
}: AgregatorFilterProps): JSX.Element { }: AgregatorFilterProps): JSX.Element {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]); const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
@ -73,6 +74,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
searchText: debouncedValue, searchText: debouncedValue,
aggregateOperator: queryAggregation.timeAggregation, aggregateOperator: queryAggregation.timeAggregation,
dataSource: query.dataSource, dataSource: query.dataSource,
source: signalSource || '',
}), }),
{ {
enabled: enabled:
@ -152,10 +154,17 @@ export const AggregatorFilter = memo(function AggregatorFilter({
setSearchText(text); setSearchText(text);
}, []); }, []);
const placeholder: string = const getPlaceholder = useCallback(() => {
query.dataSource === DataSource.METRICS if (signalSource === 'meter') {
? `Search metric name` return 'Meter name';
: 'Aggregate attribute'; }
if (query.dataSource === DataSource.METRICS) {
return 'Metric name';
}
return 'Aggregate attribute';
}, [signalSource, query.dataSource]);
const getAttributesData = useCallback( const getAttributesData = useCallback(
(): BaseAutocompleteData[] => (): BaseAutocompleteData[] =>
@ -289,7 +298,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
return ( return (
<AutoComplete <AutoComplete
getPopupContainer={popupContainer} getPopupContainer={popupContainer}
placeholder={placeholder} placeholder={getPlaceholder()}
style={selectStyle} style={selectStyle}
filterOption={false} filterOption={false}
onSearch={handleSearchText} onSearch={handleSearchText}

View File

@ -30,8 +30,10 @@ function BuilderUnitsFilter({
}; };
return ( return (
<Space> <Space className="builder-units-filter">
<DefaultLabel>Y-axis unit</DefaultLabel> <DefaultLabel className="builder-units-filter-label">
Y-axis unit
</DefaultLabel>
<Select <Select
getPopupContainer={popupContainer} getPopupContainer={popupContainer}
style={selectStyles} style={selectStyles}

View File

@ -5,4 +5,5 @@ export type GroupByFilterProps = {
query: IBuilderQuery; query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void; onChange: (values: BaseAutocompleteData[]) => void;
disabled: boolean; disabled: boolean;
signalSource?: string;
}; };

View File

@ -10,9 +10,17 @@ import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAut
// ** Helpers // ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { isEqual, uniqWith } from 'lodash-es'; import { isEqual, uniqWith } from 'lodash-es';
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'; import {
memo,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select'; import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
@ -25,6 +33,7 @@ export const GroupByFilter = memo(function GroupByFilter({
query, query,
onChange, onChange,
disabled, disabled,
signalSource,
}: GroupByFilterProps): JSX.Element { }: GroupByFilterProps): JSX.Element {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
@ -38,10 +47,17 @@ export const GroupByFilter = memo(function GroupByFilter({
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY); const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
const dataSource = useMemo(() => {
if (signalSource === 'meter') {
return 'meter' as DataSource;
}
return query.dataSource;
}, [signalSource, query.dataSource]);
const { isFetching } = useGetAggregateKeys( const { isFetching } = useGetAggregateKeys(
{ {
aggregateAttribute: query.aggregateAttribute?.key || '', aggregateAttribute: query.aggregateAttribute?.key || '',
dataSource: query.dataSource, dataSource,
aggregateOperator: query.aggregateOperator || '', aggregateOperator: query.aggregateOperator || '',
searchText: debouncedValue, searchText: debouncedValue,
}, },

View File

@ -8,6 +8,7 @@ import {
Book, Book,
Boxes, Boxes,
BugIcon, BugIcon,
ChartArea,
Cloudy, Cloudy,
DraftingCompass, DraftingCompass,
FileKey2, FileKey2,
@ -113,7 +114,7 @@ const menuItems: SidebarItem[] = [
key: ROUTES.METRICS_EXPLORER, key: ROUTES.METRICS_EXPLORER,
label: 'Metrics', label: 'Metrics',
icon: <BarChart2 size={16} />, icon: <BarChart2 size={16} />,
isNew: true, isNew: false,
itemKey: 'metrics', itemKey: 'metrics',
}, },
{ {
@ -230,7 +231,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
key: ROUTES.METRICS_EXPLORER, key: ROUTES.METRICS_EXPLORER,
label: 'Metrics', label: 'Metrics',
icon: <BarChart2 size={16} />, icon: <BarChart2 size={16} />,
isNew: true, isNew: false,
isEnabled: true, isEnabled: true,
itemKey: 'metrics', itemKey: 'metrics',
}, },
@ -264,6 +265,15 @@ export const defaultMoreMenuItems: SidebarItem[] = [
isEnabled: true, isEnabled: true,
itemKey: 'external-apis', itemKey: 'external-apis',
}, },
{
key: ROUTES.METER_EXPLORER,
label: 'Meter Explorer',
icon: <ChartArea size={16} />,
isNew: false,
isEnabled: false,
isBeta: true,
itemKey: 'meter-explorer',
},
{ {
key: ROUTES.MESSAGING_QUEUES_OVERVIEW, key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues', label: 'Messaging Queues',

View File

@ -205,6 +205,7 @@ function TimeSeriesView({
return ( return (
<div className="time-series-view"> <div className="time-series-view">
{isError && error && <ErrorInPlace error={error as APIError} />} {isError && error && <ErrorInPlace error={error as APIError} />}
<div <div
className="graph-container" className="graph-container"
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}

View File

@ -47,7 +47,7 @@ function TimeSeriesViewContainer({
return isValid.every(Boolean); return isValid.every(Boolean);
}, [currentQuery]); }, [currentQuery]);
const { data, isLoading, isError, error } = useGetQueryRange( const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
{ {
query: stagedQuery || initialQueriesMap[dataSource], query: stagedQuery || initialQueriesMap[dataSource],
graphType: panelType || PANEL_TYPES.TIME_SERIES, graphType: panelType || PANEL_TYPES.TIME_SERIES,
@ -88,7 +88,7 @@ function TimeSeriesViewContainer({
isFilterApplied={isFilterApplied} isFilterApplied={isFilterApplied}
isError={isError} isError={isError}
error={error as APIError} error={error as APIError}
isLoading={isLoading} isLoading={isLoading || isFetching}
data={responseData} data={responseData}
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'} yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
dataSource={dataSource} dataSource={dataSource}

View File

@ -233,6 +233,9 @@ export const routesToSkip = [
ROUTES.ALL_ERROR, ROUTES.ALL_ERROR,
ROUTES.UN_AUTHORIZED, ROUTES.UN_AUTHORIZED,
ROUTES.NOT_FOUND, ROUTES.NOT_FOUND,
ROUTES.METER_EXPLORER,
ROUTES.METER_EXPLORER_BASE,
ROUTES.METER_EXPLORER_VIEWS,
ROUTES.SOMETHING_WENT_WRONG, ROUTES.SOMETHING_WENT_WRONG,
]; ];

View File

@ -1,22 +1,34 @@
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion'; import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query'; import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse } from 'react-router-dom-v5-compat';
import { SuccessResponse } from 'types/api';
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types'; import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
export const useGetQueryKeyValueSuggestions = ({ export const useGetQueryKeyValueSuggestions = ({
key, key,
signal, signal,
searchText, searchText,
signalSource,
}: { }: {
key: string; key: string;
signal: 'traces' | 'logs' | 'metrics'; signal: 'traces' | 'logs' | 'metrics';
searchText?: string; searchText?: string;
signalSource?: 'meter' | '';
options?: UseQueryOptions<
SuccessResponse<QueryKeyValueSuggestionsResponseProps> | ErrorResponse
>;
}): UseQueryResult< }): UseQueryResult<
AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
AxiosError AxiosError
> => > =>
useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({ useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({
queryKey: ['queryKeyValueSuggestions', key, signal, searchText], queryKey: ['queryKeyValueSuggestions', key, signal, searchText, signalSource],
queryFn: () => queryFn: () =>
getValueSuggestions({ signal, key, searchText: searchText || '' }), getValueSuggestions({
signal,
key,
searchText: searchText || '',
signalSource: signalSource as 'meter' | '',
}),
}); });

View File

@ -5,9 +5,9 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
export const useGetAllViews = ( export const useGetAllViews = (
sourcepage: DataSource, sourcepage: DataSource | 'meter',
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> => ): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({ useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
queryKey: [{ sourcepage }], queryKey: [{ sourcepage }],
queryFn: () => getAllViews(sourcepage), queryFn: () => getAllViews(sourcepage as DataSource),
}); });

View File

@ -490,6 +490,7 @@ export const defaultOutput = {
pageSize: 0, pageSize: 0,
queryName: 'A', queryName: 'A',
reduceTo: 'avg', reduceTo: 'avg',
source: '',
spaceAggregation: 'sum', spaceAggregation: 'sum',
stepInterval: 240, stepInterval: 240,
timeAggregation: 'rate', timeAggregation: 'rate',

View File

@ -0,0 +1,16 @@
.meter-explorer-page {
.ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-tab {
padding: 8px 16px;
margin: 0 8px 0 0 !important;
.tab-item {
display: flex;
gap: 8px;
align-items: center;
}
}
}

View File

@ -0,0 +1,22 @@
import './MeterExplorer.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Explorer, Views } from './constants';
function MeterExplorerPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Explorer, Views];
return (
<div className="meter-explorer-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}
export default MeterExplorerPage;

View File

@ -0,0 +1,32 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import ExplorerPage from 'container/MeterExplorer/Explorer';
import { Compass, TowerControl } from 'lucide-react';
import SaveView from 'pages/SaveView';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
export const Explorer: TabRoutes = {
Component: (): JSX.Element => (
<PreferenceContextProvider>
<ExplorerPage />
</PreferenceContextProvider>
),
name: (
<div className="tab-item">
<Compass size={16} /> Explorer
</div>
),
route: ROUTES.METER_EXPLORER,
key: ROUTES.METER_EXPLORER,
};
export const Views: TabRoutes = {
Component: SaveView,
name: (
<div className="tab-item">
<TowerControl size={16} /> Views
</div>
),
route: ROUTES.METER_EXPLORER_VIEWS,
key: ROUTES.METER_EXPLORER_VIEWS,
};

View File

@ -0,0 +1,3 @@
import MeterExplorerPage from './MeterExplorerPage';
export default MeterExplorerPage;

View File

@ -6,6 +6,7 @@ export const SOURCEPAGE_VS_ROUTES: {
logs: ROUTES.LOGS_EXPLORER, logs: ROUTES.LOGS_EXPLORER,
traces: ROUTES.TRACES_EXPLORER, traces: ROUTES.TRACES_EXPLORER,
metrics: ROUTES.METRICS_EXPLORER_EXPLORER, metrics: ROUTES.METRICS_EXPLORER_EXPLORER,
meter: ROUTES.METER_EXPLORER,
} as const; } as const;
export const ROUTES_VS_SOURCEPAGE: { export const ROUTES_VS_SOURCEPAGE: {
@ -14,4 +15,5 @@ export const ROUTES_VS_SOURCEPAGE: {
[ROUTES.LOGS_SAVE_VIEWS]: 'logs', [ROUTES.LOGS_SAVE_VIEWS]: 'logs',
[ROUTES.TRACES_SAVE_VIEWS]: 'traces', [ROUTES.TRACES_SAVE_VIEWS]: 'traces',
[ROUTES.METRICS_EXPLORER_VIEWS]: 'metrics', [ROUTES.METRICS_EXPLORER_VIEWS]: 'metrics',
[ROUTES.METER_EXPLORER_VIEWS]: 'meter',
} as const; } as const;

View File

@ -17,6 +17,10 @@ import {
} from 'components/ExplorerCard/utils'; } from 'components/ExplorerCard/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getRandomColor } from 'container/ExplorerOptions/utils'; import { getRandomColor } from 'container/ExplorerOptions/utils';
import {
MeterExplorerEventKeys,
MeterExplorerEvents,
} from 'container/MeterExplorer/events';
import { import {
MetricsExplorerEventKeys, MetricsExplorerEventKeys,
MetricsExplorerEvents, MetricsExplorerEvents,
@ -163,6 +167,10 @@ function SaveView(): JSX.Element {
logEvent(MetricsExplorerEvents.TabChanged, { logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'views', [MetricsExplorerEventKeys.Tab]: 'views',
}); });
} else if (sourcepage === 'meter') {
logEvent(MeterExplorerEvents.TabChanged, {
[MeterExplorerEventKeys.Tab]: 'views',
});
} }
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }

View File

@ -241,12 +241,26 @@ export function QueryBuilderProvider({
); );
const updateAllQueriesOperators = useCallback( const updateAllQueriesOperators = useCallback(
(query: Query, panelType: PANEL_TYPES, dataSource: DataSource): Query => { (
query: Query,
panelType: PANEL_TYPES,
dataSource: DataSource,
signalSource?: 'meter' | '',
): Query => {
const queryData = query.builder.queryData?.map((item) => const queryData = query.builder.queryData?.map((item) =>
getElementWithActualOperator(item, dataSource, panelType), getElementWithActualOperator(item, dataSource, panelType),
); );
return { ...query, builder: { ...query.builder, queryData } }; return {
...query,
builder: {
...query.builder,
queryData: queryData.map((item) => ({
...item,
source: signalSource,
})),
},
};
}, },
[getElementWithActualOperator], [getElementWithActualOperator],
@ -854,6 +868,7 @@ export function QueryBuilderProvider({
const handleRunQuery = useCallback( const handleRunQuery = useCallback(
(shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => { (shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => {
let currentQueryData = currentQuery; let currentQueryData = currentQuery;
if (newQBQuery) { if (newQBQuery) {
currentQueryData = { currentQueryData = {
...currentQueryData, ...currentQueryData,

View File

@ -4,4 +4,5 @@ export interface IGetAggregateAttributePayload {
aggregateOperator: string; aggregateOperator: string;
dataSource: DataSource; dataSource: DataSource;
searchText: string; searchText: string;
source?: 'meter' | '';
} }

View File

@ -87,6 +87,7 @@ export type IBuilderQuery = {
pageSize?: number; pageSize?: number;
offset?: number; offset?: number;
selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[]; selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[];
source?: 'meter' | '';
}; };
export interface IClickHouseQuery { export interface IClickHouseQuery {

View File

@ -28,6 +28,7 @@ export interface QueryKeyRequestProps {
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span'; fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
fieldDataType?: QUERY_BUILDER_KEY_TYPES; fieldDataType?: QUERY_BUILDER_KEY_TYPES;
metricName?: string; metricName?: string;
signalSource?: 'meter' | '';
} }
export interface QueryKeyValueSuggestionsProps { export interface QueryKeyValueSuggestionsProps {
@ -44,4 +45,7 @@ export interface QueryKeyValueRequestProps {
signal: 'traces' | 'logs' | 'metrics'; signal: 'traces' | 'logs' | 'metrics';
key: string; key: string;
searchText: string; searchText: string;
signalSource?: 'meter' | '';
} }
export type SignalType = 'traces' | 'logs' | 'metrics';

View File

@ -239,10 +239,17 @@ export interface MetricBuilderQuery extends BaseBuilderQuery {
aggregations?: MetricAggregation[]; aggregations?: MetricAggregation[];
} }
export interface MeterBuilderQuery extends BaseBuilderQuery {
signal: 'metrics';
source: 'meter';
aggregations?: MetricAggregation[];
}
export type BuilderQuery = export type BuilderQuery =
| TraceBuilderQuery | TraceBuilderQuery
| LogBuilderQuery | LogBuilderQuery
| MetricBuilderQuery; | MetricBuilderQuery
| MeterBuilderQuery;
export interface QueryBuilderFormula { export interface QueryBuilderFormula {
name: string; name: string;

View File

@ -105,6 +105,42 @@ export enum MetricAggregateOperator {
LATEST = 'latest', LATEST = 'latest',
} }
export enum MeterAggregateOperator {
EMPTY = '', // used as time aggregator for histograms
NOOP = 'noop',
COUNT = 'count',
COUNT_DISTINCT = 'count_distinct',
SUM = 'sum',
AVG = 'avg',
MAX = 'max',
MIN = 'min',
P05 = 'p05',
P10 = 'p10',
P20 = 'p20',
P25 = 'p25',
P50 = 'p50',
P75 = 'p75',
P90 = 'p90',
P95 = 'p95',
P99 = 'p99',
RATE = 'rate',
SUM_RATE = 'sum_rate',
AVG_RATE = 'avg_rate',
MAX_RATE = 'max_rate',
MIN_RATE = 'min_rate',
RATE_SUM = 'rate_sum',
RATE_AVG = 'rate_avg',
RATE_MIN = 'rate_min',
RATE_MAX = 'rate_max',
HIST_QUANTILE_50 = 'hist_quantile_50',
HIST_QUANTILE_75 = 'hist_quantile_75',
HIST_QUANTILE_90 = 'hist_quantile_90',
HIST_QUANTILE_95 = 'hist_quantile_95',
HIST_QUANTILE_99 = 'hist_quantile_99',
INCREASE = 'increase',
LATEST = 'latest',
}
export enum TracesAggregatorOperator { export enum TracesAggregatorOperator {
NOOP = 'noop', NOOP = 'noop',
COUNT = 'count', COUNT = 'count',
@ -237,6 +273,7 @@ export type QueryBuilderContextType = {
queryData: Query, queryData: Query,
panelType: PANEL_TYPES, panelType: PANEL_TYPES,
dataSource: DataSource, dataSource: DataSource,
signalSource?: 'meter' | '',
) => Query; ) => Query;
updateQueriesData: <T extends keyof QueryBuilderData>( updateQueriesData: <T extends keyof QueryBuilderData>(
query: Query, query: Query,

View File

@ -123,4 +123,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
INFRASTRUCTURE_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'], INFRASTRUCTURE_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
API_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'], API_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_BASE: ['ADMIN', 'EDITOR', 'VIEWER'], MESSAGING_QUEUES_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };