mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-07 12:00:42 +01:00
Compare commits
1 Commits
refactor/p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
391f685e57 |
@@ -2,8 +2,10 @@ import { cloneDeep, isEmpty } from 'lodash-es';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BuilderQuery,
|
||||
DistributionData,
|
||||
MetricRangePayloadV5,
|
||||
QueryEnvelope,
|
||||
QueryRangeRequestV5,
|
||||
RawData,
|
||||
ScalarData,
|
||||
@@ -11,6 +13,11 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
const isBuilderQueryEnvelope = (
|
||||
envelope: QueryEnvelope,
|
||||
): envelope is QueryEnvelope & { spec: BuilderQuery } =>
|
||||
envelope.type === 'builder_query' || envelope.type === 'builder_ai_query';
|
||||
|
||||
function getColName(
|
||||
col: ScalarData['columns'][number],
|
||||
legendMap: Record<string, string>,
|
||||
@@ -409,21 +416,19 @@ export function convertV5ResponseToLegacy(
|
||||
const v5Data = payload?.data;
|
||||
|
||||
const aggregationPerQuery =
|
||||
params?.compositeQuery?.queries
|
||||
?.filter((query) => query.type === 'builder_query')
|
||||
.reduce(
|
||||
(acc, query) => {
|
||||
if (
|
||||
query.type === 'builder_query' &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
params?.compositeQuery?.queries?.filter(isBuilderQueryEnvelope).reduce(
|
||||
(acc, query) => {
|
||||
if (
|
||||
isBuilderQueryEnvelope(query) &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
|
||||
// clickhouse_sql queries have no aggregation metadata; their value columns
|
||||
// are named/keyed by the real SQL alias the response carries (see getColId).
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
QueryBuilderFormula as V5QueryBuilderFormula,
|
||||
QueryEnvelope,
|
||||
QueryRangePayloadV5,
|
||||
RequestType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -935,3 +936,41 @@ describe('convertBuilderQueriesToV5 having normalization', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertBuilderQueriesToV5 builder query type', () => {
|
||||
const buildEnvelope = (
|
||||
builderQueryType: IBuilderQuery['builderQueryType'],
|
||||
requestType: RequestType,
|
||||
): QueryEnvelope => {
|
||||
const [envelope] = convertBuilderQueriesToV5(
|
||||
{
|
||||
A: {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
builderQueryType,
|
||||
} as unknown as IBuilderQuery,
|
||||
},
|
||||
requestType,
|
||||
);
|
||||
return envelope;
|
||||
};
|
||||
|
||||
it.each<[RequestType]>([
|
||||
['trace'],
|
||||
['raw'],
|
||||
['time_series'],
|
||||
['scalar'],
|
||||
['distribution'],
|
||||
])('sends builder_ai_query for the %s request type', (requestType) => {
|
||||
expect(buildEnvelope('builder_ai_query', requestType).type).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it.each<[string, IBuilderQuery['builderQueryType']]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('sends builder_query for %s', (_label, builderQueryType) => {
|
||||
expect(buildEnvelope(builderQueryType, 'trace').type).toBe('builder_query');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -365,7 +365,7 @@ export function convertBuilderQueriesToV5(
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'builder_query' as QueryType,
|
||||
type: queryData.builderQueryType ?? 'builder_query',
|
||||
spec,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -16,8 +16,6 @@ import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||
import { Button, Card, Collapse, Popover, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
negationQueryOperatorSuggestions,
|
||||
@@ -54,6 +52,12 @@ import {
|
||||
SUGGESTION_FETCH_DEBOUNCE_MS,
|
||||
SUGGESTIONS_SECTION,
|
||||
} from './constants';
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
SuggestedFieldKey,
|
||||
SuggestedFieldKeysByName,
|
||||
} from './fieldSuggestions';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
@@ -261,10 +265,8 @@ function QuerySearch({
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
const generateOptions = (keys: SuggestedFieldKeysByName): any[] =>
|
||||
Object.values(keys).flatMap((items: SuggestedFieldKey[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
@@ -317,8 +319,9 @@ function QuerySearch({
|
||||
|
||||
lastFetchedKeyRef.current = searchText || '';
|
||||
|
||||
const response = await getKeySuggestions({
|
||||
signal: dataSource,
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
@@ -360,6 +363,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -493,10 +497,11 @@ function QuerySearch({
|
||||
try {
|
||||
const values = valueSuggestionsOverride
|
||||
? await valueSuggestionsOverride(key, sanitizedSearchText)
|
||||
: await getValueSuggestions({
|
||||
: await fetchFieldValuesForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText: sanitizedSearchText,
|
||||
signal: dataSource,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
}).then((response) => {
|
||||
@@ -601,6 +606,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
} from '../fieldSuggestions';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsKeys: jest.fn(),
|
||||
getAIObservabilityFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIKeys = getAIObservabilityFieldsKeys as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsKeys
|
||||
>;
|
||||
const mockedGenericKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
>;
|
||||
const mockedAIValues = getAIObservabilityFieldsValues as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsValues
|
||||
>;
|
||||
const mockedGenericValues = getValueSuggestions as jest.MockedFunction<
|
||||
typeof getValueSuggestions
|
||||
>;
|
||||
|
||||
const aiValuesResponse = (
|
||||
values: { stringValues?: string[]; numberValues?: number[] } | null,
|
||||
complete = true,
|
||||
): Awaited<ReturnType<typeof getAIObservabilityFieldsValues>> =>
|
||||
({
|
||||
status: 'success',
|
||||
data: { complete, values },
|
||||
}) as Awaited<ReturnType<typeof getAIObservabilityFieldsValues>>;
|
||||
|
||||
describe('fetchFieldKeysForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
},
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const keys = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'llm',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).toHaveBeenCalledWith({ searchText: 'llm' });
|
||||
expect(mockedGenericKeys).not.toHaveBeenCalled();
|
||||
expect(keys.data.data).toStrictEqual({
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
mockedGenericKeys.mockResolvedValue({
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as Awaited<ReturnType<typeof getKeySuggestions>>);
|
||||
|
||||
await fetchFieldKeysForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'svc',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).not.toHaveBeenCalled();
|
||||
expect(mockedGenericKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.TRACES, searchText: 'svc' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a null ai_observability keys payload to an empty map', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: false, keys: null },
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(response.data.data).toStrictEqual({ complete: false, keys: {} });
|
||||
});
|
||||
|
||||
it('passes the generic response through untouched', async () => {
|
||||
const genericResponse = {
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as unknown as Awaited<ReturnType<typeof getKeySuggestions>>;
|
||||
mockedGenericKeys.mockResolvedValue(genericResponse);
|
||||
|
||||
await expect(
|
||||
fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFieldValuesForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIValues.mockResolvedValue(
|
||||
aiValuesResponse({ stringValues: ['gpt-4o'], numberValues: [] }),
|
||||
);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'gen_ai.request.model',
|
||||
searchText: 'gpt',
|
||||
});
|
||||
|
||||
expect(mockedGenericValues).not.toHaveBeenCalled();
|
||||
expect(response).toStrictEqual({
|
||||
data: {
|
||||
data: {
|
||||
complete: true,
|
||||
values: { stringValues: ['gpt-4o'], numberValues: [] },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the key as the name the endpoint expects', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse({}));
|
||||
|
||||
await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).toHaveBeenCalledWith({
|
||||
name: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps the ai_observability payload in the envelope the call site unwraps', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse(null, false));
|
||||
|
||||
await expect(
|
||||
fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'llm_call_count',
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toStrictEqual({
|
||||
data: { data: { complete: false, values: null } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const genericResponse = {
|
||||
data: {
|
||||
data: { complete: false, values: { stringValues: ['frontend'] } },
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof getValueSuggestions>>;
|
||||
mockedGenericValues.mockResolvedValue(genericResponse);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).not.toHaveBeenCalled();
|
||||
expect(mockedGenericValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signal: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
}),
|
||||
);
|
||||
expect(response).toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface SuggestedFieldKey {
|
||||
name: string;
|
||||
fieldContext?: string;
|
||||
fieldDataType?: string;
|
||||
}
|
||||
|
||||
export type SuggestedFieldKeysByName = Record<string, SuggestedFieldKey[]>;
|
||||
|
||||
export interface SuggestedFieldKeysPayload {
|
||||
complete: boolean;
|
||||
keys: SuggestedFieldKeysByName;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldKeysResponse {
|
||||
data: { data?: SuggestedFieldKeysPayload };
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesPayload {
|
||||
complete?: boolean;
|
||||
values?: {
|
||||
stringValues?: string[] | null;
|
||||
numberValues?: number[] | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesResponse {
|
||||
data: { data?: SuggestedFieldValuesPayload };
|
||||
}
|
||||
|
||||
interface FetchFieldKeysParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
metricNamespace?: string;
|
||||
}
|
||||
|
||||
interface FetchFieldValuesParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
key: string;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
}
|
||||
|
||||
export const fetchFieldKeysForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
}: FetchFieldKeysParams): Promise<SuggestedFieldKeysResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsKeys({ searchText });
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: response.data
|
||||
? { complete: response.data.complete, keys: response.data.keys ?? {} }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchFieldValuesForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
}: FetchFieldValuesParams): Promise<SuggestedFieldValuesResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsValues({
|
||||
name: key,
|
||||
searchText,
|
||||
});
|
||||
|
||||
return { data: { data: response.data } };
|
||||
}
|
||||
|
||||
// getValueSuggestions' declared response type does not match what the endpoint returns.
|
||||
return getValueSuggestions({
|
||||
signal: dataSource,
|
||||
key,
|
||||
searchText,
|
||||
signalSource,
|
||||
metricName,
|
||||
}) as unknown as Promise<SuggestedFieldValuesResponse>;
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const showFunctions = query?.functions?.length > 0;
|
||||
const { dataSource } = query;
|
||||
const { dataSource, builderQueryType } = query;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
@@ -94,8 +94,9 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
);
|
||||
|
||||
const showSpanScopeSelector = useMemo(
|
||||
() => dataSource === DataSource.TRACES,
|
||||
[dataSource],
|
||||
() =>
|
||||
dataSource === DataSource.TRACES && builderQueryType !== 'builder_ai_query',
|
||||
[dataSource, builderQueryType],
|
||||
);
|
||||
|
||||
const showInlineQuerySearch = useMemo(() => {
|
||||
|
||||
@@ -348,6 +348,19 @@ export const initialQueryMeterWithType: Query = {
|
||||
},
|
||||
};
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
builder: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||
string: Object.values(StringOperators),
|
||||
number: Object.values(NumberOperators),
|
||||
|
||||
@@ -11,17 +11,13 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import {
|
||||
getExportQueryData,
|
||||
getQueryByPanelType,
|
||||
} from 'container/TracesExplorer/explorerUtils';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
@@ -52,6 +48,7 @@ import {
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TOOLBAR_VIEWS } from './constants';
|
||||
import { getExportQueryData, getQueryByPanelType } from './explorerUtils';
|
||||
import ListView from './ListView/ListView';
|
||||
import { defaultSelectedColumns } from './ListView/configs';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
@@ -118,7 +115,7 @@ function Explorer(): JSX.Element {
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
@@ -185,7 +182,7 @@ function Explorer(): JSX.Element {
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
getQueryByPanelType(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
stagedQuery || initialQueryAIWithType,
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
),
|
||||
[stagedQuery, panelType],
|
||||
|
||||
@@ -17,12 +17,11 @@ import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import {
|
||||
getTraceLink,
|
||||
transformSpanRows,
|
||||
@@ -43,6 +42,7 @@ import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import {
|
||||
defaultSelectedColumns,
|
||||
PER_PAGE_OPTIONS,
|
||||
@@ -94,7 +94,7 @@ function ListView({
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -10,33 +8,16 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
function QuerySection(): JSX.Element {
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isList = panelTypes === PANEL_TYPES.LIST;
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
// Only reaches the builder for timeseries/table; list/trace panels use QueryBuilderV2's listViewTracesFilterConfigs.
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
|
||||
() => ({
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
limit: { isHidden: isList, isDisabled: true },
|
||||
having: { isHidden: isList, isDisabled: true },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps) => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
),
|
||||
limit: { isHidden: false, isDisabled: true },
|
||||
having: { isHidden: false, isDisabled: true },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
|
||||
const shouldRenderCustomOrderBy =
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
|
||||
|
||||
return {
|
||||
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
@@ -45,14 +26,10 @@ function QuerySection(): JSX.Element {
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
showOnlyWhereClause={isListViewPanel}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,10 +14,9 @@ import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
import { getTraceLink } from 'container/TracesExplorer/ListView/utils';
|
||||
import { TracesTableRow } from 'container/TracesExplorer/TracesTable/getFieldColumn';
|
||||
import TracesTable from 'container/TracesExplorer/TracesTable/TracesTable';
|
||||
@@ -31,6 +30,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import styles from './TracesView.module.scss';
|
||||
|
||||
@@ -60,7 +60,7 @@ function TracesView({
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
orderBy?: string,
|
||||
): Query => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryData.orderBy = orderByPayload;
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const getQueryByPanelType = (
|
||||
stagedQuery: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||
return getListViewQuery(stagedQuery);
|
||||
}
|
||||
return stagedQuery;
|
||||
};
|
||||
|
||||
export const getExportQueryData = (
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
options: OptionsQuery,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
const updatedQuery = cloneDeep(query);
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panel
|
||||
import { resolveSignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
import type { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../utils/legendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
import ConfigActions from './ConfigActions/ConfigActions';
|
||||
import SectionSlot from './SectionSlot/SectionSlot';
|
||||
|
||||
@@ -5,18 +5,8 @@ import PanelTypeSwitcher from '../PanelTypeSwitcher';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
// Stub the registry so the test doesn't pull in the real renderers and chart libs.
|
||||
jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
|
||||
getPanelDefinition: jest.fn(),
|
||||
PANEL_OPTIONS: [
|
||||
{ kind: 'signoz/TimeSeriesPanel', displayName: 'Time Series' },
|
||||
{ kind: 'signoz/NumberPanel', displayName: 'Number' },
|
||||
{ kind: 'signoz/TablePanel', displayName: 'Table' },
|
||||
{ kind: 'signoz/BarChartPanel', displayName: 'Bar Chart' },
|
||||
{ kind: 'signoz/PieChartPanel', displayName: 'Pie Chart' },
|
||||
{ kind: 'signoz/HistogramPanel', displayName: 'Histogram' },
|
||||
{ kind: 'signoz/ListPanel', displayName: 'List' },
|
||||
].map((option) => ({ ...option, icon: (): null => null })),
|
||||
}));
|
||||
|
||||
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { PANEL_OPTIONS } from '../../../Panels/registry';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { PANEL_TYPES } from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/constants';
|
||||
import type { ConfigSelectItem } from '../controls/ConfigSelect/ConfigSelect';
|
||||
|
||||
import { getPanelTypeDisabledReason } from './utils';
|
||||
@@ -27,17 +27,17 @@ export function usePanelTypeSelectItems({
|
||||
}: UsePanelTypeSelectItemsArgs): ConfigSelectItem<PanelKind>[] {
|
||||
return useMemo(
|
||||
() =>
|
||||
PANEL_OPTIONS.map(({ kind, displayName, icon: Icon }) => {
|
||||
PANEL_TYPES.map(({ panelKind, label, Icon }) => {
|
||||
// One reason drives both the disabled flag and the tooltip, so they can't disagree.
|
||||
const disabledReason = getPanelTypeDisabledReason({
|
||||
kind,
|
||||
kind: panelKind,
|
||||
queryType,
|
||||
signal,
|
||||
label: displayName,
|
||||
label,
|
||||
});
|
||||
return {
|
||||
value: kind,
|
||||
label: displayName,
|
||||
value: panelKind,
|
||||
label,
|
||||
icon: <Icon size={14} />,
|
||||
disabled: !!disabledReason,
|
||||
tooltip: disabledReason,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Input } from 'antd';
|
||||
import type { DashboardtypesLegendDTOCustomColors } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../utils/legendSeries';
|
||||
import LegendColorRow from './LegendColorRow';
|
||||
import {
|
||||
clearSeriesColor,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../../utils/legendSeries';
|
||||
import LegendColors from '../LegendColors';
|
||||
|
||||
const SERIES: LegendSeries[] = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../../utils/legendSeries';
|
||||
import {
|
||||
clearSeriesColor,
|
||||
filterLegendSeries,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DashboardtypesLegendDTOCustomColors } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../utils/legendSeries';
|
||||
|
||||
/** Case-insensitive substring filter over series labels. Empty query → all series. */
|
||||
export function filterLegendSeries(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PanelKind } from '../../Panels/types/panelKind';
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../utils/legendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
|
||||
supportedSignals: ['metrics', 'logs', 'traces'],
|
||||
supportedQueryTypes: ['builder', 'clickhouse_sql', 'promql'],
|
||||
})),
|
||||
PANEL_OPTIONS: [
|
||||
{ kind: 'signoz/TimeSeriesPanel', displayName: 'Time Series' },
|
||||
{ kind: 'signoz/TablePanel', displayName: 'Table' },
|
||||
].map((option) => ({ ...option, icon: (): null => null })),
|
||||
}));
|
||||
|
||||
// Open the antd Select by clicking its selector, then pick the option by label.
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { SectionKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
|
||||
import type { PanelQueryData } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
|
||||
|
||||
import {
|
||||
type LegendSeries,
|
||||
resolvePieLegendSeries,
|
||||
resolveTimeSeriesLegendSeries,
|
||||
} from '../utils/legendSeries';
|
||||
|
||||
/**
|
||||
* Resolves the panel's rendered series into `{ label, defaultColor }` pairs so the
|
||||
* legend-colors control can key overrides by the exact labels the chart draws, using
|
||||
* the resolver the kind declares as its `colors` control.
|
||||
* legend-colors control can key overrides by the exact labels the chart draws. Only the
|
||||
* kinds that expose a colors control resolve series (Pie from its scalar slices, Time
|
||||
* Series from its flat series); every other kind returns none.
|
||||
*/
|
||||
export function useLegendSeries(
|
||||
panel: DashboardtypesPanelDTO,
|
||||
data: PanelQueryData,
|
||||
): LegendSeries[] {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const kind = panel.spec.plugin.kind;
|
||||
|
||||
return useMemo(() => {
|
||||
const resolve = getSectionControls(kind, SectionKind.Legend)?.colors;
|
||||
return resolve
|
||||
? resolve({ queries: panel.spec.queries, data, isDarkMode })
|
||||
: [];
|
||||
}, [kind, panel.spec.queries, data, isDarkMode]);
|
||||
switch (panel.spec.plugin.kind) {
|
||||
case 'signoz/PieChartPanel':
|
||||
return resolvePieLegendSeries(data, isDarkMode);
|
||||
case 'signoz/TimeSeriesPanel':
|
||||
case 'signoz/BarChartPanel':
|
||||
case 'signoz/HistogramPanel':
|
||||
return resolveTimeSeriesLegendSeries(panel.spec.queries, data, isDarkMode);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [panel.spec.plugin.kind, panel.spec.queries, data, isDarkMode]);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schem
|
||||
import { themeColors } from 'constants/theme';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { preparePieData } from '../kinds/PieChartPanel/prepareData';
|
||||
import { getBuilderQueries } from './getBuilderQueries';
|
||||
import { resolveSeriesLabelV5 } from './resolveSeriesLabel';
|
||||
import { preparePieData } from 'pages/DashboardPage/DashboardContainer/Panels/kinds/PieChartPanel/prepareData';
|
||||
import { getBuilderQueries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
|
||||
import { prepareScalarTables } from 'pages/DashboardPage/DashboardContainer/queryV5/prepareScalarTables';
|
||||
import type { PanelQueryData } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
|
||||
import {
|
||||
@@ -22,15 +22,6 @@ export interface LegendSeries {
|
||||
|
||||
type PanelQueries = DashboardtypesPanelDTO['spec']['queries'];
|
||||
|
||||
export interface LegendSeriesArgs {
|
||||
queries: PanelQueries;
|
||||
data: PanelQueryData;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
/** Resolves a kind's output into the legend entries the colors control keys overrides by. */
|
||||
export type LegendSeriesResolver = (args: LegendSeriesArgs) => LegendSeries[];
|
||||
|
||||
/**
|
||||
* Dedupes `labels` (first-seen order, empties dropped) into `{ label, defaultColor }`
|
||||
* pairs, resolving each unique label's color lazily via `colorFor` — so a repeated
|
||||
@@ -57,10 +48,10 @@ function buildLegendSeries(
|
||||
* draws (without overrides, so their colors are the defaults) so the color control keys
|
||||
* overrides by the same labels the chart does.
|
||||
*/
|
||||
export function resolvePieLegendSeries({
|
||||
data,
|
||||
isDarkMode,
|
||||
}: LegendSeriesArgs): LegendSeries[] {
|
||||
export function resolvePieLegendSeries(
|
||||
data: PanelQueryData,
|
||||
isDarkMode: boolean,
|
||||
): LegendSeries[] {
|
||||
const slices = preparePieData({
|
||||
tables: prepareScalarTables({
|
||||
results: getScalarResults(data.response),
|
||||
@@ -79,11 +70,11 @@ export function resolvePieLegendSeries({
|
||||
* Time-series kinds: resolve each flattened series' label the way the renderer does
|
||||
* (`getLabelName` → `resolveSeriesLabelV5`) and color it with `generateColor`.
|
||||
*/
|
||||
export function resolveTimeSeriesLegendSeries({
|
||||
queries,
|
||||
data,
|
||||
isDarkMode,
|
||||
}: LegendSeriesArgs): LegendSeries[] {
|
||||
export function resolveTimeSeriesLegendSeries(
|
||||
queries: PanelQueries,
|
||||
data: PanelQueryData,
|
||||
isDarkMode: boolean,
|
||||
): LegendSeries[] {
|
||||
const palette = isDarkMode
|
||||
? themeColors.chartcolors
|
||||
: themeColors.lightModeColor;
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
kind: 'signoz/BarChartPanel',
|
||||
displayName: 'Bar Chart',
|
||||
icon: BarChart,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
@@ -14,10 +13,7 @@ export const sections: SectionConfig[] = [
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.LABEL },
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
kind: 'signoz/HistogramPanel',
|
||||
displayName: 'Histogram',
|
||||
icon: BarChart,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
@@ -10,7 +9,7 @@ export const sections: SectionConfig[] = [
|
||||
},
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
|
||||
controls: { position: true, colors: true },
|
||||
// Merging all queries collapses to one distribution with no legend.
|
||||
isHidden: (spec): boolean =>
|
||||
Boolean(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { List } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -13,7 +11,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/ListPanel'> = {
|
||||
kind: 'signoz/ListPanel',
|
||||
displayName: 'List',
|
||||
icon: List,
|
||||
Renderer,
|
||||
// Raw records come from logs and traces; metrics don't produce row data.
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Hash } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
|
||||
kind: 'signoz/NumberPanel',
|
||||
displayName: 'Number',
|
||||
icon: Hash,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ChartPie } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
|
||||
kind: 'signoz/PieChartPanel',
|
||||
displayName: 'Pie Chart',
|
||||
icon: ChartPie,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolvePieLegendSeries } from '../../utils/legendSeries';
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
|
||||
// Pie has no axes, thresholds, or stacking — just value formatting and a legend
|
||||
@@ -9,9 +8,6 @@ export const sections: SectionConfig[] = [
|
||||
controls: { switchPanelKind: true, timePreference: true },
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolvePieLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Table } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/TablePanel'> = {
|
||||
kind: 'signoz/TablePanel',
|
||||
displayName: 'Table',
|
||||
icon: Table,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ChartLine } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
displayName: 'Time Series',
|
||||
icon: ChartLine,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
@@ -12,10 +11,7 @@ export const sections: SectionConfig[] = [
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{
|
||||
kind: SectionKind.ChartAppearance,
|
||||
controls: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
|
||||
import {
|
||||
NO_PANEL_ACTIONS,
|
||||
@@ -19,8 +18,6 @@ import Renderer from './Renderer';
|
||||
export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
|
||||
kind: '<unsupported>' as RenderablePanelDefinition['kind'],
|
||||
displayName: 'Unsupported panel',
|
||||
// Never offered in the UI — the kind lists come from the registry, which omits this.
|
||||
icon: TriangleAlert,
|
||||
Renderer,
|
||||
sections: [],
|
||||
supportedSignals: [],
|
||||
|
||||
@@ -7,33 +7,22 @@ import { definition as Table } from './kinds/TablePanel/definition';
|
||||
import { definition as List } from './kinds/ListPanel/definition';
|
||||
import { UNSUPPORTED_PANEL } from './kinds/UnsupportedPanel/definition';
|
||||
import type {
|
||||
PanelDefinition,
|
||||
PanelRegistry,
|
||||
RenderablePanelDefinition,
|
||||
} from './types/panelDefinition';
|
||||
import { PanelKind } from './types/panelKind';
|
||||
|
||||
// Each kind owns its PanelDefinition; registering a new panel is one entry here.
|
||||
// Declaration order is the order kinds are offered in the UI.
|
||||
export const PANELS: PanelRegistry = {
|
||||
[TimeSeries.kind]: TimeSeries,
|
||||
[NumberValue.kind]: NumberValue,
|
||||
[Table.kind]: Table,
|
||||
[BarChart.kind]: BarChart,
|
||||
[PieChart.kind]: PieChart,
|
||||
[Histogram.kind]: Histogram,
|
||||
[NumberValue.kind]: NumberValue,
|
||||
[PieChart.kind]: PieChart,
|
||||
[Table.kind]: Table,
|
||||
[List.kind]: List,
|
||||
};
|
||||
|
||||
export type PanelOption = Pick<
|
||||
PanelDefinition,
|
||||
'kind' | 'displayName' | 'icon'
|
||||
>;
|
||||
|
||||
// Backs both the new-panel picker and the editor's kind switcher; derived from PANELS
|
||||
// so a registered kind can't end up unreachable from the UI.
|
||||
export const PANEL_OPTIONS: PanelOption[] = Object.values(PANELS);
|
||||
|
||||
/**
|
||||
* Whether this build can render the kind. `PanelKind` spans every kind the API declares,
|
||||
* but a dashboard spec written by a newer SigNoz can name one this client has never heard
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ChartLine } from '@signozhq/icons';
|
||||
import type { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import type { SectionConfig } from './sections';
|
||||
@@ -61,14 +60,9 @@ export const NO_PANEL_ACTIONS: PanelActionCapabilities = {
|
||||
drilldown: false,
|
||||
};
|
||||
|
||||
// Derived from an icon component so the props stay exact (size is a constrained
|
||||
// IconSize union) and ForwardRef-compatible.
|
||||
export type PanelIcon = typeof ChartLine;
|
||||
|
||||
export interface PanelDefinition<K extends PanelKind = PanelKind> {
|
||||
kind: K;
|
||||
displayName: string;
|
||||
icon: PanelIcon;
|
||||
Renderer: ComponentType<PanelRendererProps<K>>;
|
||||
sections: SectionConfig[];
|
||||
/** Signals this kind can visualize. */
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
DashboardtypesTimeSeriesChartAppearanceDTO,
|
||||
TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { LegendSeriesResolver } from '../utils/legendSeries';
|
||||
import {
|
||||
Antenna,
|
||||
BarChart,
|
||||
@@ -106,12 +105,7 @@ export interface SectionControls {
|
||||
columnUnits?: boolean;
|
||||
};
|
||||
[SectionKind.Axes]: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
|
||||
[SectionKind.Legend]: {
|
||||
position?: boolean;
|
||||
// colors → customColors; the resolver supplies the labels overrides are keyed by,
|
||||
// so a kind can't offer color overrides with nothing to color
|
||||
colors?: LegendSeriesResolver;
|
||||
};
|
||||
[SectionKind.Legend]: { position?: boolean; colors?: boolean }; // colors → customColors
|
||||
[SectionKind.ChartAppearance]: {
|
||||
lineStyle?: boolean;
|
||||
lineInterpolation?: boolean;
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('buildPluginSpec', () => {
|
||||
|
||||
it('omits the key entirely when a seed produces an empty slice (never key: undefined)', () => {
|
||||
const result = buildPluginSpec([
|
||||
{ kind: SectionKind.Legend, controls: { colors: (): [] => [] } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
]);
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
@@ -129,7 +129,7 @@ describe('buildPluginSpec', () => {
|
||||
it('seeds neither when their defaulting controls are absent', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Visualization, controls: { switchPanelKind: true } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: (): [] => [] } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
@@ -180,10 +180,7 @@ describe('buildPluginSpec', () => {
|
||||
|
||||
it('carries old legend position but never customColors', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: (): [] => [] },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
];
|
||||
const oldSpec = oldSpecWith({
|
||||
legend: {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { SectionKind, ThresholdVariant } from '../../types/sections';
|
||||
import { getSectionControls } from '../getSectionControls';
|
||||
|
||||
describe('getSectionControls', () => {
|
||||
it('returns the controls a kind declares for a section', () => {
|
||||
expect(
|
||||
getSectionControls('signoz/TimeSeriesPanel', SectionKind.Formatting),
|
||||
).toStrictEqual({ unit: true, decimals: true });
|
||||
});
|
||||
|
||||
it('distinguishes kinds that key units per column from kinds with a panel unit', () => {
|
||||
expect(
|
||||
getSectionControls('signoz/TablePanel', SectionKind.Formatting)?.unit,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getSectionControls('signoz/TablePanel', SectionKind.Formatting)?.columnUnits,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('reports the threshold variant each kind edits', () => {
|
||||
expect(
|
||||
getSectionControls('signoz/NumberPanel', SectionKind.Thresholds)?.variant,
|
||||
).toBe(ThresholdVariant.COMPARISON);
|
||||
expect(
|
||||
getSectionControls('signoz/BarChartPanel', SectionKind.Thresholds)?.variant,
|
||||
).toBe(ThresholdVariant.LABEL);
|
||||
});
|
||||
|
||||
it('returns undefined when the kind does not expose the section', () => {
|
||||
expect(
|
||||
getSectionControls('signoz/ListPanel', SectionKind.Formatting),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getSectionControls('signoz/HistogramPanel', SectionKind.Thresholds),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for an unregistered kind', () => {
|
||||
expect(
|
||||
getSectionControls(
|
||||
'signoz/FuturePanel' as Parameters<typeof getSectionControls>[0],
|
||||
SectionKind.Formatting,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { getPanelDefinition } from '../registry';
|
||||
import type { PanelKind } from '../types/panelKind';
|
||||
import type { ControlledSectionKind, SectionControls } from '../types/sections';
|
||||
|
||||
/**
|
||||
* The controls a kind declares for one section, or `undefined` when it doesn't expose
|
||||
* that section — so callers read `kinds/<Kind>/sections.ts` instead of switching on kind.
|
||||
*/
|
||||
export function getSectionControls<K extends ControlledSectionKind>(
|
||||
kind: PanelKind,
|
||||
sectionKind: K,
|
||||
): SectionControls[K] | undefined {
|
||||
const section = getPanelDefinition(kind).sections.find(
|
||||
(candidate) => candidate.kind === sectionKind,
|
||||
);
|
||||
if (!section || !('controls' in section)) {
|
||||
return undefined;
|
||||
}
|
||||
// `find` can't correlate the matched member's `controls` with `sectionKind`; the
|
||||
// SectionConfig union guarantees it.
|
||||
return section.controls as SectionControls[K];
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { useDashboardSections } from '../../../hooks/useDashboardSections';
|
||||
import { PANEL_OPTIONS } from '../../../Panels/registry';
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
import { PANEL_TYPES } from './constants';
|
||||
import PanelTypeSelectionModalFooter from './PanelTypeSelectionModalFooter';
|
||||
import { buildSectionOptions, resolveDefaultSectionValue } from './utils';
|
||||
import styles from './PanelTypeSelectionModal.module.scss';
|
||||
@@ -91,19 +91,19 @@ function PanelTypeSelectionModal({
|
||||
<span className={styles.pickerLabel}>Select panel type</span>
|
||||
)}
|
||||
<div className={styles.grid}>
|
||||
{PANEL_OPTIONS.map(({ kind, displayName, icon: Icon }) => (
|
||||
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
|
||||
<button
|
||||
key={kind}
|
||||
key={panelKind}
|
||||
type="button"
|
||||
className={cx(styles.panelTypeCard, {
|
||||
[styles.panelTypeCardSelected]: kind === selectedPanelKind,
|
||||
[styles.panelTypeCardSelected]: panelKind === selectedPanelKind,
|
||||
})}
|
||||
data-testid={`panel-type-${kind}`}
|
||||
aria-pressed={kind === selectedPanelKind}
|
||||
onClick={(): void => handleTileClick(kind)}
|
||||
data-testid={`panel-type-${panelKind}`}
|
||||
aria-pressed={panelKind === selectedPanelKind}
|
||||
onClick={(): void => handleTileClick(panelKind)}
|
||||
>
|
||||
<Icon size={24} color={Color.BG_ROBIN_400} />
|
||||
{displayName}
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
BarChart,
|
||||
ChartLine,
|
||||
ChartPie,
|
||||
Hash,
|
||||
List,
|
||||
Table,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import type { PanelType } from './types';
|
||||
|
||||
export const PANEL_TYPES: PanelType[] = [
|
||||
{
|
||||
panelKind: 'signoz/TimeSeriesPanel',
|
||||
label: 'Time Series',
|
||||
Icon: ChartLine,
|
||||
},
|
||||
{ panelKind: 'signoz/NumberPanel', label: 'Number', Icon: Hash },
|
||||
{ panelKind: 'signoz/TablePanel', label: 'Table', Icon: Table },
|
||||
{ panelKind: 'signoz/BarChartPanel', label: 'Bar Chart', Icon: BarChart },
|
||||
{ panelKind: 'signoz/PieChartPanel', label: 'Pie Chart', Icon: ChartPie },
|
||||
{ panelKind: 'signoz/HistogramPanel', label: 'Histogram', Icon: BarChart },
|
||||
{ panelKind: 'signoz/ListPanel', label: 'List', Icon: List },
|
||||
];
|
||||
@@ -1,11 +1,20 @@
|
||||
import type { IconSize } from '@signozhq/icons';
|
||||
import type { ComponentType, SVGProps } from 'react';
|
||||
|
||||
import type { PanelKind } from '../../../Panels/types/panelKind';
|
||||
|
||||
type IconProps = Omit<SVGProps<SVGSVGElement>, 'ref'> & {
|
||||
size?: number | IconSize;
|
||||
strokeWidth?: number;
|
||||
};
|
||||
|
||||
export interface PanelType {
|
||||
panelKind: PanelKind;
|
||||
label: string;
|
||||
/** Icon component — the consumer renders it and controls size/color/etc. */
|
||||
Icon: ComponentType<IconProps>;
|
||||
}
|
||||
|
||||
export interface SectionOption {
|
||||
/** The section's `layoutIndex`, stringified for the Select value. */
|
||||
value: string;
|
||||
|
||||
@@ -8,24 +8,24 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
SectionKind,
|
||||
type PanelFormattingSlice,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
|
||||
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { deriveAlertPrefill, PanelAlertPrefill } from './deriveAlertPrefill';
|
||||
|
||||
/** The panel's configured y-axis unit, for the kinds that declare one. */
|
||||
/** The panel's configured y-axis unit, for the kinds that carry one. */
|
||||
export function readPanelUnit(
|
||||
plugin: DashboardtypesPanelPluginDTO,
|
||||
): string | undefined {
|
||||
if (!getSectionControls(plugin.kind, SectionKind.Formatting)?.unit) {
|
||||
return undefined;
|
||||
switch (plugin.kind) {
|
||||
case 'signoz/TimeSeriesPanel':
|
||||
case 'signoz/BarChartPanel':
|
||||
case 'signoz/NumberPanel':
|
||||
case 'signoz/PieChartPanel':
|
||||
return plugin.spec.formatting?.unit;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
return (plugin.spec as { formatting?: PanelFormattingSlice }).formatting?.unit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,15 +11,7 @@ import {
|
||||
AlertThresholdOperator,
|
||||
Threshold,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
|
||||
import {
|
||||
THRESHOLD_COLOR_DANGER_ORDER,
|
||||
type ComparisonThresholdShape,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/threshold';
|
||||
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { THRESHOLD_COLOR_DANGER_ORDER } from 'pages/DashboardPage/DashboardContainer/Panels/types/threshold';
|
||||
import type { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -72,35 +64,27 @@ export function uniformReduceTo(query: Query): ReduceOperators | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel's thresholds, normalized for alert prefill, read through the variant the
|
||||
* kind declares. A `table` variant contributes nothing: per-column thresholds have no
|
||||
* meaning for a panel-wide alert condition.
|
||||
*/
|
||||
function readPanelThresholds(
|
||||
plugin: DashboardtypesPanelPluginDTO,
|
||||
): NormalizedPanelThreshold[] {
|
||||
const variant = getSectionControls(
|
||||
plugin.kind,
|
||||
SectionKind.Thresholds,
|
||||
)?.variant;
|
||||
if (
|
||||
variant !== ThresholdVariant.LABEL &&
|
||||
variant !== ThresholdVariant.COMPARISON
|
||||
) {
|
||||
return [];
|
||||
switch (plugin.kind) {
|
||||
case 'signoz/TimeSeriesPanel':
|
||||
case 'signoz/BarChartPanel':
|
||||
return (plugin.spec.thresholds ?? []).map((t) => ({
|
||||
color: t.color,
|
||||
value: t.value,
|
||||
unit: t.unit,
|
||||
}));
|
||||
case 'signoz/NumberPanel':
|
||||
return (plugin.spec.thresholds ?? []).map((t) => ({
|
||||
color: t.color,
|
||||
value: t.value,
|
||||
unit: t.unit,
|
||||
operator: t.operator,
|
||||
}));
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
const thresholds =
|
||||
(plugin.spec as { thresholds?: ComparisonThresholdShape[] }).thresholds ?? [];
|
||||
return thresholds.map((threshold) => ({
|
||||
color: threshold.color,
|
||||
value: threshold.value,
|
||||
unit: threshold.unit,
|
||||
// Only comparison thresholds carry an operator.
|
||||
...(variant === ThresholdVariant.COMPARISON && {
|
||||
operator: threshold.operator,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
// Match case-insensitively (picker emits lowercase hex); unknown colors sort last.
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
buildPluginSpec,
|
||||
type SeededPluginSpec,
|
||||
} from '../DashboardContainer/Panels/utils/buildPluginSpec';
|
||||
import { getSectionControls } from '../DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { toPerses } from '../DashboardContainer/queryV5/persesQueryAdapters';
|
||||
|
||||
interface NewPanelSeed {
|
||||
@@ -22,6 +21,15 @@ interface NewPanelSeed {
|
||||
pluginSpec: SeededPluginSpec;
|
||||
}
|
||||
|
||||
function kindSupportsUnit(kind: PanelKind): boolean {
|
||||
return getPanelDefinition(kind).sections.some(
|
||||
(section) =>
|
||||
section.kind === SectionKind.Formatting &&
|
||||
'controls' in section &&
|
||||
section.controls.unit === true,
|
||||
);
|
||||
}
|
||||
|
||||
/** Kind to fall back to for a query language a builder-only kind (List) can't hold. */
|
||||
const FALLBACK_KIND_BY_QUERY_TYPE: Partial<Record<EQueryType, PanelKind>> = {
|
||||
[EQueryType.PROM]: 'signoz/TimeSeriesPanel',
|
||||
@@ -66,10 +74,7 @@ export function buildNewPanelSeed(
|
||||
const queries = converted.length > 0 ? converted : buildDefaultQueries(kind);
|
||||
|
||||
// Explorers put the single `unit` on the query itself, not the panel spec.
|
||||
if (
|
||||
compositeQuery.unit &&
|
||||
getSectionControls(kind, SectionKind.Formatting)?.unit
|
||||
) {
|
||||
if (compositeQuery.unit && kindSupportsUnit(kind)) {
|
||||
return {
|
||||
kind,
|
||||
queries,
|
||||
|
||||
@@ -475,6 +475,7 @@ export function QueryBuilderProvider({
|
||||
const newQuery: IBuilderQuery = {
|
||||
...initialBuilderQuery,
|
||||
source: queries?.[0]?.source || '',
|
||||
builderQueryType: queries?.[0]?.builderQueryType,
|
||||
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
|
||||
expression: createNewBuilderItemName({
|
||||
existNames,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
const renderQueryBuilder = (
|
||||
initialQuery: Query,
|
||||
): ReturnType<
|
||||
typeof renderHook<ReturnType<typeof useQueryBuilder>, unknown>
|
||||
> => {
|
||||
const hook = renderHook(() => useQueryBuilder(), {
|
||||
wrapper: AllTheProviders,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
hook.result.current.initQueryBuilderData(initialQuery);
|
||||
});
|
||||
|
||||
return hook;
|
||||
};
|
||||
|
||||
describe('createNewBuilderQuery builderQueryType propagation', () => {
|
||||
it('carries builderQueryType from the first query onto an added query', () => {
|
||||
const { result } = renderQueryBuilder(initialQueryAIWithType);
|
||||
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[0].builderQueryType,
|
||||
).toBe('builder_ai_query');
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
|
||||
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[1].builderQueryType,
|
||||
).toBe('builder_ai_query');
|
||||
});
|
||||
|
||||
it('leaves builderQueryType unset when the first query has none', () => {
|
||||
const { result } = renderQueryBuilder(initialQueriesMap.traces);
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
|
||||
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[1].builderQueryType,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
BuilderQueryType,
|
||||
Filter,
|
||||
Having as HavingV5,
|
||||
LogAggregation,
|
||||
@@ -90,6 +91,7 @@ export type IBuilderQuery = {
|
||||
offset?: number;
|
||||
selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[];
|
||||
source?: 'meter' | '';
|
||||
builderQueryType?: BuilderQueryType;
|
||||
};
|
||||
|
||||
export interface IClickHouseQuery {
|
||||
|
||||
@@ -16,6 +16,7 @@ export type RequestType =
|
||||
|
||||
export type QueryType =
|
||||
| 'builder_query'
|
||||
| 'builder_ai_query'
|
||||
| 'builder_trace_operator'
|
||||
| 'builder_formula'
|
||||
| 'builder_sub_query'
|
||||
@@ -23,6 +24,11 @@ export type QueryType =
|
||||
| 'clickhouse_sql'
|
||||
| 'promql';
|
||||
|
||||
export type BuilderQueryType = Extract<
|
||||
QueryType,
|
||||
'builder_query' | 'builder_ai_query'
|
||||
>;
|
||||
|
||||
export type OrderDirection = 'asc' | 'desc';
|
||||
|
||||
export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross';
|
||||
|
||||
Reference in New Issue
Block a user