mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-24 23:10:39 +01:00
Compare commits
12 Commits
fix/tansta
...
issue_5721
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22833ea8fe | ||
|
|
8eb0a24492 | ||
|
|
7e0b19800c | ||
|
|
3bc476c2ed | ||
|
|
6a834ee944 | ||
|
|
ba6a78aea7 | ||
|
|
ca98231b18 | ||
|
|
1644ecd6fc | ||
|
|
f1e2e9f4f7 | ||
|
|
a59e372f07 | ||
|
|
ca368a5b38 | ||
|
|
dab5ceee0d |
@@ -1496,6 +1496,7 @@ components:
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- memorystore_redis
|
||||
- computeengine
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -2748,6 +2749,7 @@ components:
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesLink'
|
||||
nullable: true
|
||||
type: array
|
||||
panels:
|
||||
additionalProperties:
|
||||
@@ -2764,7 +2766,6 @@ components:
|
||||
- variables
|
||||
- panels
|
||||
- layouts
|
||||
- links
|
||||
type: object
|
||||
DashboardtypesDashboardView:
|
||||
properties:
|
||||
@@ -2843,22 +2844,14 @@ components:
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
DashboardtypesDynamicVariableSignal:
|
||||
enum:
|
||||
- traces
|
||||
- logs
|
||||
- metrics
|
||||
- all
|
||||
type: string
|
||||
DashboardtypesDynamicVariableSpec:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
signal:
|
||||
$ref: '#/components/schemas/DashboardtypesDynamicVariableSignal'
|
||||
$ref: '#/components/schemas/TelemetrytypesSignal'
|
||||
required:
|
||||
- name
|
||||
- signal
|
||||
type: object
|
||||
DashboardtypesFillMode:
|
||||
enum:
|
||||
@@ -3401,6 +3394,7 @@ components:
|
||||
links:
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardtypesLink'
|
||||
nullable: true
|
||||
type: array
|
||||
plugin:
|
||||
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
|
||||
@@ -3412,7 +3406,6 @@ components:
|
||||
- display
|
||||
- plugin
|
||||
- queries
|
||||
- links
|
||||
type: object
|
||||
DashboardtypesPatchOp:
|
||||
enum:
|
||||
|
||||
@@ -2815,6 +2815,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
memorystore_redis = 'memorystore_redis',
|
||||
computeengine = 'computeengine',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -4626,9 +4627,9 @@ export interface DashboardtypesQueryDTO {
|
||||
export interface DashboardtypesPanelSpecDTO {
|
||||
display: DashboardtypesDisplayDTO;
|
||||
/**
|
||||
* @type array
|
||||
* @type array,null
|
||||
*/
|
||||
links: DashboardtypesLinkDTO[];
|
||||
links?: DashboardtypesLinkDTO[] | null;
|
||||
plugin: DashboardtypesPanelPluginDTO;
|
||||
/**
|
||||
* @type array
|
||||
@@ -4668,18 +4669,12 @@ export type DashboardtypesVariableDefaultValueDTO = string | string[];
|
||||
export enum DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpecDTOKind {
|
||||
'signoz/DynamicVariable' = 'signoz/DynamicVariable',
|
||||
}
|
||||
export enum DashboardtypesDynamicVariableSignalDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
all = 'all',
|
||||
}
|
||||
export interface DashboardtypesDynamicVariableSpecDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
signal: DashboardtypesDynamicVariableSignalDTO;
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
}
|
||||
|
||||
export interface DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpecDTO {
|
||||
@@ -4821,9 +4816,9 @@ export interface DashboardtypesDashboardSpecDTO {
|
||||
*/
|
||||
layouts: DashboardtypesLayoutDTO[];
|
||||
/**
|
||||
* @type array
|
||||
* @type array,null
|
||||
*/
|
||||
links: DashboardtypesLinkDTO[];
|
||||
links?: DashboardtypesLinkDTO[] | null;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
|
||||
@@ -52,8 +52,6 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
@@ -224,12 +222,6 @@ function QuerySearch({
|
||||
QueryKeyDataSuggestionsProps[] | null
|
||||
>(null);
|
||||
|
||||
// dedupe keySuggestions by label/name
|
||||
const dedupedKeySuggestions = useMemo(
|
||||
() => dedupeOptionsByLabel(keySuggestions || []),
|
||||
[keySuggestions],
|
||||
);
|
||||
|
||||
const [showExamples] = useState(false);
|
||||
|
||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||
@@ -254,11 +246,9 @@ function QuerySearch({
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
items.map(({ name, fieldDataType }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
fieldContext,
|
||||
fieldDataType,
|
||||
info: '',
|
||||
details: '',
|
||||
})),
|
||||
@@ -317,17 +307,13 @@ function QuerySearch({
|
||||
if (response.data.data) {
|
||||
const { keys } = response.data.data;
|
||||
const options = generateOptions(keys);
|
||||
// Deduplicate by full variant identity (name + context + data type), NOT by
|
||||
// label. deduping by label removes varient which is not expected. If we need
|
||||
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
|
||||
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
|
||||
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
|
||||
// Use a Map to deduplicate by label and preserve order: new options take precedence
|
||||
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
|
||||
options.forEach((opt) => merged.set(variantId(opt), opt));
|
||||
options.forEach((opt) => merged.set(opt.label, opt));
|
||||
if (searchText && lastKeyRef.current !== searchText) {
|
||||
(keySuggestions || []).forEach((opt) => {
|
||||
if (!merged.has(variantId(opt))) {
|
||||
merged.set(variantId(opt), opt);
|
||||
if (!merged.has(opt.label)) {
|
||||
merged.set(opt.label, opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -933,55 +919,8 @@ function QuerySearch({
|
||||
|
||||
if (queryContext.isInKey) {
|
||||
const searchText = word?.text.toLowerCase().trim() ?? '';
|
||||
const fieldContextMatch = getFieldContextPrefix(searchText);
|
||||
|
||||
if (fieldContextMatch) {
|
||||
const { context: fieldContext, remainder } = fieldContextMatch;
|
||||
|
||||
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
|
||||
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
//suggestions that actually do start with <fieldContext>.
|
||||
const nameMatches = (keySuggestions || [])
|
||||
.filter((option) => option.label.toLowerCase().includes(searchText))
|
||||
.map((option) => ({ ...option, boost: 100 }));
|
||||
|
||||
//suggestions which do not start with the prefix but qualifies for suggestion
|
||||
const contextQualified = (keySuggestions || [])
|
||||
.filter(
|
||||
(option) =>
|
||||
option.fieldContext === fieldContext &&
|
||||
option.label.toLowerCase().includes(remainder),
|
||||
)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
label: `${fieldContext}.${option.label}`,
|
||||
boost: 0,
|
||||
}));
|
||||
|
||||
const contextOptions = dedupeOptionsByLabel([
|
||||
...nameMatches,
|
||||
...contextQualified,
|
||||
]);
|
||||
|
||||
// If contextOptions is empty fetch again.
|
||||
if (
|
||||
contextOptions.length === 0 &&
|
||||
lastFetchedKeyRef.current !== searchText
|
||||
) {
|
||||
debouncedFetchKeySuggestions(searchText);
|
||||
}
|
||||
|
||||
return {
|
||||
from: word?.from ?? 0,
|
||||
to: word?.to ?? cursorPos.ch,
|
||||
options: addSpaceToOptions(contextOptions),
|
||||
};
|
||||
}
|
||||
|
||||
options = dedupedKeySuggestions.filter((option) =>
|
||||
options = (keySuggestions || []).filter((option) =>
|
||||
option.label.toLowerCase().includes(searchText),
|
||||
);
|
||||
|
||||
@@ -1030,26 +969,9 @@ function QuerySearch({
|
||||
|
||||
// If we have a key context, add that info to the operator suggestions
|
||||
if (keyName) {
|
||||
const keyContextMatch = getFieldContextPrefix(keyName);
|
||||
// key-suggestion can contain multiple variants of a single key
|
||||
// In variants we capture ones that match the label to typed keyName exactly or,
|
||||
// if it has a prefix fieldContext remove it and then match.
|
||||
const variants = (keySuggestions || []).filter(
|
||||
(k) =>
|
||||
k.label === keyName ||
|
||||
(keyContextMatch !== null &&
|
||||
k.fieldContext === keyContextMatch.context &&
|
||||
k.label === keyContextMatch.remainder),
|
||||
);
|
||||
const variantTypes = new Set(
|
||||
variants
|
||||
.map((k) =>
|
||||
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
//if there are multi-variant, show all suggestions else just the one
|
||||
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
|
||||
// Find the key details from suggestions
|
||||
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
|
||||
const keyType = keyDetails?.type || '';
|
||||
|
||||
// Filter operators based on key type
|
||||
if (keyType) {
|
||||
@@ -1292,7 +1214,7 @@ function QuerySearch({
|
||||
if (curChar === '(') {
|
||||
// In expression context, suggest keys, functions, or nested parentheses
|
||||
options = [
|
||||
...dedupedKeySuggestions,
|
||||
...(keySuggestions || []),
|
||||
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
|
||||
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
|
||||
...options.filter((opt) => opt.type === 'function'),
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldContextPrefix', () => {
|
||||
it('matches a complete context prefix with a remainder', () => {
|
||||
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'status',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches a bare context prefix with empty remainder', () => {
|
||||
expect(getFieldContextPrefix('resource.')).toStrictEqual({
|
||||
context: 'resource',
|
||||
remainder: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches every backend field context', () => {
|
||||
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
|
||||
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
|
||||
context: ctx,
|
||||
remainder: 'x',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not match a partial context name', () => {
|
||||
expect(getFieldContextPrefix('attr')).toBeNull();
|
||||
expect(getFieldContextPrefix('attribute')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not match a non-context key with dots', () => {
|
||||
expect(getFieldContextPrefix('status.code')).toBeNull();
|
||||
});
|
||||
|
||||
it('matches context case-insensitively but keeps remainder casing', () => {
|
||||
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
|
||||
context: 'attribute',
|
||||
remainder: 'Status',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dedupeOptionsByLabel', () => {
|
||||
it('keeps the first occurrence per label, preserving order', () => {
|
||||
expect(
|
||||
dedupeOptionsByLabel([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'status.code', type: 'number' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{ label: 'status.code', type: 'keyword' },
|
||||
{ label: 'duration', type: 'number' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array for empty input', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
describe('combineInitialAndUserExpression', () => {
|
||||
it('returns user when initial is empty', () => {
|
||||
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
|
||||
'body contains error',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns initial when user is empty', () => {
|
||||
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
|
||||
'k8s.pod.name = "x"',
|
||||
);
|
||||
});
|
||||
|
||||
it('wraps user in parentheses with AND', () => {
|
||||
expect(
|
||||
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
|
||||
).toBe('k8s.pod.name = "x" AND (body = "a")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserExpressionFromCombined', () => {
|
||||
it('returns empty when combined equals initial', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('extracts user from wrapped form', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND (body = "a")',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('extracts user from legacy AND without parens', () => {
|
||||
expect(
|
||||
getUserExpressionFromCombined(
|
||||
'k8s.pod.name = "x"',
|
||||
'k8s.pod.name = "x" AND body = "a"',
|
||||
),
|
||||
).toBe('body = "a"');
|
||||
});
|
||||
|
||||
it('returns full combined when initial is empty', () => {
|
||||
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
|
||||
'service.name = "a"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,6 @@
|
||||
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
|
||||
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
|
||||
|
||||
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
|
||||
export const FIELD_CONTEXTS = [
|
||||
'attribute',
|
||||
'resource',
|
||||
'span',
|
||||
'body',
|
||||
'log',
|
||||
'metric',
|
||||
] as const;
|
||||
|
||||
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
|
||||
// recents from regular autocomplete completions in renderers and event handlers.
|
||||
export const RECENT_COMPLETION_TYPE = 'recent';
|
||||
|
||||
@@ -9,45 +9,11 @@ import type { SignalType } from 'types/api/v5/queryRange';
|
||||
import 'utils/timeUtils';
|
||||
|
||||
import {
|
||||
FIELD_CONTEXTS,
|
||||
RECENT_COMPLETION_TYPE,
|
||||
RECENTS_DISPLAY_CAP,
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
}
|
||||
|
||||
// This function checks if the text(query key) starts with a fieldContext
|
||||
// This util strictly checks that and returns context and remainder back.
|
||||
// This helps differentiate if the typed key was prefixed with a context or
|
||||
// was it an actual queryKey like (attribute.abc)
|
||||
export function getFieldContextPrefix(
|
||||
text: string,
|
||||
): FieldContextPrefixMatch | null {
|
||||
const lower = text.toLowerCase();
|
||||
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
|
||||
return context ? { context, remainder: text.slice(context.length + 1) } : null;
|
||||
}
|
||||
|
||||
// Keeps the first occurrence per label, preserving order. Key suggestions hold
|
||||
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
|
||||
// could show multiple labels and this avoids that.
|
||||
export function dedupeOptionsByLabel<T extends { label: string }>(
|
||||
options: T[],
|
||||
): T[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter((option) => {
|
||||
if (seen.has(option.label)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.label);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function combineInitialAndUserExpression(
|
||||
initial: string,
|
||||
user: string,
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
|
||||
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
__esModule: true,
|
||||
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
|
||||
handleRunQuery,
|
||||
};
|
||||
});
|
||||
|
||||
// Keys fixture: status.code exists as 3 variants (attribute string/number +
|
||||
// resource string); attribute.a.b.c is a key literally named with the prefix.
|
||||
const KEYS_FIXTURE = {
|
||||
'status.code': [
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'status.code',
|
||||
fieldContext: 'resource',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'attribute.a.b.c': [
|
||||
{
|
||||
name: 'attribute.a.b.c',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
'duration.nano': [
|
||||
{
|
||||
name: 'duration.nano',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'number',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fetchedSearchTexts: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
fetchedSearchTexts.length = 0;
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
|
||||
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { complete: true, keys: KEYS_FIXTURE },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: [], numberValues: [] } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
async function renderAndType(text: string): Promise<HTMLElement> {
|
||||
render(
|
||||
<QuerySearch
|
||||
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
|
||||
queryData={initialQueriesMap.logs.builder.queryData[0]}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
|
||||
await userEvent.click(editor);
|
||||
await userEvent.type(editor, text);
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Types a key, waits for its suggestion to render (proving the debounced key
|
||||
// fetch resolved into state), then types a space to enter operator context.
|
||||
async function typeKeyThenEnterOperatorContext(
|
||||
key: string,
|
||||
expectedKeyLabel: string,
|
||||
): Promise<void> {
|
||||
const editor = await renderAndType(key);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
|
||||
});
|
||||
|
||||
// skipClick: a click would reset the caret to position 0 under the mocked
|
||||
// DOM rects; we need the space appended at the end of the typed key.
|
||||
await userEvent.type(editor, ' ', { skipClick: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('=');
|
||||
});
|
||||
}
|
||||
|
||||
function visibleOptionLabels(): string[] {
|
||||
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
|
||||
(el) => el.textContent ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
describe('QuerySearch context-prefixed key suggestions', () => {
|
||||
it('shows one deduped suggestion per key name in normal mode', async () => {
|
||||
await renderAndType('statu');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('status.code');
|
||||
});
|
||||
|
||||
const statusOptions = visibleOptionLabels().filter(
|
||||
(label) => label === 'status.code',
|
||||
);
|
||||
expect(statusOptions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows context-scoped suggestions for a complete context prefix', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(visibleOptionLabels()).toContain('attribute.status.code');
|
||||
});
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
// Literal key name match ranks first
|
||||
expect(labels[0]).toBe('attribute.a.b.c');
|
||||
// Context-qualified form of the literal key is also present
|
||||
expect(labels).toContain('attribute.attribute.a.b.c');
|
||||
// span-only key is not suggested under attribute.
|
||||
expect(labels).not.toContain('attribute.duration.nano');
|
||||
});
|
||||
|
||||
it('fetches the context page when the prefix is typed exactly', async () => {
|
||||
await renderAndType('attribute.');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchedSearchTexts).toContain('attribute.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('QuerySearch operator suggestions by key type', () => {
|
||||
it('shows all operators for a key with ambiguous types', async () => {
|
||||
// status.code is string + number across variants → no type filtering
|
||||
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).toContain('LIKE');
|
||||
});
|
||||
|
||||
it('shows numeric operators for a single-type number key', async () => {
|
||||
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('>');
|
||||
expect(labels).not.toContain('LIKE');
|
||||
});
|
||||
|
||||
it('narrows operators when a context prefix disambiguates the type', async () => {
|
||||
// status.code is ambiguous globally, but resource.status.code is string-only
|
||||
await typeKeyThenEnterOperatorContext(
|
||||
'resource.status.code',
|
||||
'resource.status.code',
|
||||
);
|
||||
|
||||
const labels = visibleOptionLabels();
|
||||
expect(labels).toContain('LIKE');
|
||||
expect(labels).not.toContain('>');
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,78 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QuerySearch from '../QuerySearch/QuerySearch';
|
||||
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
|
||||
|
||||
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
|
||||
|
||||
// Mock DOM APIs that CodeMirror needs
|
||||
beforeAll(() => {
|
||||
mockCodeMirrorDomApis();
|
||||
// Mock getClientRects and getBoundingClientRect for Range objects
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
});
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
|
||||
// (Range client rects + element bounding rects). Call from a beforeAll in
|
||||
// specs that render the real CodeMirror editor.
|
||||
export function mockCodeMirrorDomApis(): void {
|
||||
const mockRect: DOMRect = {
|
||||
width: 100,
|
||||
height: 20,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
bottom: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: (): DOMRect => mockRect,
|
||||
} as DOMRect;
|
||||
|
||||
// Create a minimal Range mock with only what CodeMirror actually uses
|
||||
const createMockRange = (): Range => {
|
||||
let startContainer: Node = document.createTextNode('');
|
||||
let endContainer: Node = document.createTextNode('');
|
||||
let startOffset = 0;
|
||||
let endOffset = 0;
|
||||
|
||||
const mockRange = {
|
||||
// CodeMirror uses these for text measurement
|
||||
getClientRects: (): DOMRectList =>
|
||||
({
|
||||
length: 1,
|
||||
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
|
||||
0: mockRect,
|
||||
*[Symbol.iterator](): Generator<DOMRect> {
|
||||
yield mockRect;
|
||||
},
|
||||
}) as unknown as DOMRectList,
|
||||
getBoundingClientRect: (): DOMRect => mockRect,
|
||||
// CodeMirror calls these to set up text ranges
|
||||
setStart: (node: Node, offset: number): void => {
|
||||
startContainer = node;
|
||||
startOffset = offset;
|
||||
},
|
||||
setEnd: (node: Node, offset: number): void => {
|
||||
endContainer = node;
|
||||
endOffset = offset;
|
||||
},
|
||||
// Minimal Range properties (TypeScript requires these)
|
||||
get startContainer(): Node {
|
||||
return startContainer;
|
||||
},
|
||||
get endContainer(): Node {
|
||||
return endContainer;
|
||||
},
|
||||
get startOffset(): number {
|
||||
return startOffset;
|
||||
},
|
||||
get endOffset(): number {
|
||||
return endOffset;
|
||||
},
|
||||
get collapsed(): boolean {
|
||||
return startContainer === endContainer && startOffset === endOffset;
|
||||
},
|
||||
commonAncestorContainer: document.body,
|
||||
};
|
||||
return mockRange as unknown as Range;
|
||||
};
|
||||
|
||||
// Mock document.createRange to return a new Range instance each time
|
||||
document.createRange = (): Range => createMockRange();
|
||||
|
||||
// Mock getBoundingClientRect for elements
|
||||
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
convertAggregationToExpression,
|
||||
convertExpressionToFilters,
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
formatValueForExpression,
|
||||
@@ -1599,36 +1598,4 @@ describe('formatValueForExpression', () => {
|
||||
expect(formatValueForExpression([123] as any)).toBe('[123]');
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: an escaped single quote must not gain a backslash on each
|
||||
// expression <-> filters round-trip (broke Create Alert from a panel).
|
||||
describe('escaped quote round-trip', () => {
|
||||
const EXPR = "name = 'it\\'s a ribbon'"; // name = 'it\'s a ribbon' (single backslash)
|
||||
|
||||
it('stores the unescaped value in the filter item', () => {
|
||||
const items = convertExpressionToFilters(EXPR);
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].value).toBe("it's a ribbon");
|
||||
});
|
||||
|
||||
it('is lossless: expression -> filters -> expression', () => {
|
||||
const items = convertExpressionToFilters(EXPR);
|
||||
expect(convertFiltersToExpression({ items, op: 'AND' }).expression).toBe(
|
||||
EXPR,
|
||||
);
|
||||
});
|
||||
|
||||
it('is idempotent across repeated existing-query conversions', () => {
|
||||
const pass1 = convertFiltersToExpressionWithExistingQuery(
|
||||
{ items: [], op: 'AND' },
|
||||
EXPR,
|
||||
);
|
||||
expect(pass1.filter.expression).toBe(EXPR);
|
||||
const pass2 = convertFiltersToExpressionWithExistingQuery(
|
||||
pass1.filters,
|
||||
pass1.filter.expression,
|
||||
);
|
||||
expect(pass2.filter.expression).toBe(EXPR);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,9 +176,7 @@ function formatSingleValueForFilter(
|
||||
}
|
||||
|
||||
if (isQuoted(value)) {
|
||||
// Unescape `\'` → `'` (inverse of formatSingleValue) so the round-trip doesn't
|
||||
// double the backslash each pass.
|
||||
return unquote(value).replace(/\\'/g, "'");
|
||||
return unquote(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ function TanStackRowCellsInner<TData, TItemKey = string>({
|
||||
// Stable references via destructuring, keep them as is
|
||||
const onRowClick = context?.onRowClick;
|
||||
const onRowClickNewTab = context?.onRowClickNewTab;
|
||||
const onRowDeactivate = context?.onRowDeactivate;
|
||||
const isRowActive = context?.isRowActive;
|
||||
const getRowKeyData = context?.getRowKeyData;
|
||||
const rowIndex = row.index;
|
||||
@@ -50,12 +51,22 @@ function TanStackRowCellsInner<TData, TItemKey = string>({
|
||||
return;
|
||||
}
|
||||
|
||||
// The table does not decide open vs. close — it reports the click and
|
||||
// the row's active state, and the consumer owns the routing.
|
||||
const isActive = isRowActive?.(rowData) ?? false;
|
||||
onRowClick?.(rowData, itemKey, { isActive });
|
||||
if (isActive && onRowDeactivate) {
|
||||
onRowDeactivate();
|
||||
} else {
|
||||
onRowClick?.(rowData, itemKey);
|
||||
}
|
||||
},
|
||||
[isRowActive, onRowClick, onRowClickNewTab, rowData, getRowKeyData, rowIndex],
|
||||
[
|
||||
isRowActive,
|
||||
onRowDeactivate,
|
||||
onRowClick,
|
||||
onRowClickNewTab,
|
||||
rowData,
|
||||
getRowKeyData,
|
||||
rowIndex,
|
||||
],
|
||||
);
|
||||
|
||||
if (itemKind === 'expansion') {
|
||||
@@ -109,6 +120,7 @@ function areRowCellsPropsEqual<TData>(
|
||||
prev.columnVisibilityKey === next.columnVisibilityKey &&
|
||||
prev.context?.onRowClick === next.context?.onRowClick &&
|
||||
prev.context?.onRowClickNewTab === next.context?.onRowClickNewTab &&
|
||||
prev.context?.onRowDeactivate === next.context?.onRowDeactivate &&
|
||||
prev.context?.isRowActive === next.context?.isRowActive &&
|
||||
prev.context?.getRowKeyData === next.context?.getRowKeyData &&
|
||||
prev.context?.renderRowActions === next.context?.renderRowActions &&
|
||||
|
||||
@@ -96,6 +96,7 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
renderRowActions,
|
||||
onRowClick,
|
||||
onRowClickNewTab,
|
||||
onRowDeactivate,
|
||||
onSort,
|
||||
activeRowIndex,
|
||||
renderExpandedRow,
|
||||
@@ -381,6 +382,7 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
renderRowActions,
|
||||
onRowClick,
|
||||
onRowClickNewTab,
|
||||
onRowDeactivate,
|
||||
renderExpandedRow,
|
||||
getRowKeyData,
|
||||
colCount: visibleColumnsCount,
|
||||
@@ -398,6 +400,7 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
renderRowActions,
|
||||
onRowClick,
|
||||
onRowClickNewTab,
|
||||
onRowDeactivate,
|
||||
renderExpandedRow,
|
||||
getRowKeyData,
|
||||
visibleColumnsCount,
|
||||
|
||||
@@ -85,9 +85,7 @@ describe('TanStackRowCells', () => {
|
||||
</table>,
|
||||
);
|
||||
await user.click(screen.getAllByRole('cell')[0]);
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
|
||||
isActive: false,
|
||||
});
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
|
||||
});
|
||||
|
||||
it('fires onRowClick with empty itemKey when getRowKeyData is not provided', async () => {
|
||||
@@ -119,19 +117,17 @@ describe('TanStackRowCells', () => {
|
||||
</table>,
|
||||
);
|
||||
await user.click(screen.getAllByRole('cell')[0]);
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '', {
|
||||
isActive: false,
|
||||
});
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '');
|
||||
});
|
||||
|
||||
it('calls onRowClick with isActive: true when the row is active', async () => {
|
||||
// The table no longer owns open/close — it reports the active state and the
|
||||
// consumer routes the click. An active row must still fire onRowClick.
|
||||
it('calls onRowDeactivate instead of onRowClick when row is active', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRowClick = jest.fn();
|
||||
const onRowDeactivate = jest.fn();
|
||||
const ctx: TableRowContext<Row> = {
|
||||
colCount: 1,
|
||||
onRowClick,
|
||||
onRowDeactivate,
|
||||
isRowActive: () => true,
|
||||
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
|
||||
hasSingleColumn: false,
|
||||
@@ -156,44 +152,8 @@ describe('TanStackRowCells', () => {
|
||||
</table>,
|
||||
);
|
||||
await user.click(screen.getAllByRole('cell')[0]);
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onRowClick with isActive: false when the row is not active', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRowClick = jest.fn();
|
||||
const ctx: TableRowContext<Row> = {
|
||||
colCount: 1,
|
||||
onRowClick,
|
||||
isRowActive: () => false,
|
||||
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
|
||||
hasSingleColumn: false,
|
||||
columnOrderKey: '',
|
||||
columnVisibilityKey: '',
|
||||
};
|
||||
const row = buildMockRow([{ id: 'body' }]);
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<TanStackRowCells<Row>
|
||||
row={row as never}
|
||||
context={ctx}
|
||||
itemKind="row"
|
||||
hasSingleColumn={false}
|
||||
columnOrderKey=""
|
||||
columnVisibilityKey=""
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
await user.click(screen.getAllByRole('cell')[0]);
|
||||
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
|
||||
isActive: false,
|
||||
});
|
||||
expect(onRowDeactivate).toHaveBeenCalled();
|
||||
expect(onRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render renderRowActions before hover', () => {
|
||||
|
||||
@@ -611,7 +611,6 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(onRowClick).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: '1', name: 'Item 1' }),
|
||||
'1',
|
||||
{ isActive: false },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -640,7 +639,6 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(onRowClick).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: '1', name: 'Item 1' }),
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ isActive: false },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -661,15 +659,15 @@ describe('TanStackTableView Integration', () => {
|
||||
expect(row).toHaveClass('tableRowActive');
|
||||
});
|
||||
|
||||
it('calls onRowClick with isActive: true when clicking the active row', async () => {
|
||||
// The consumer owns open/close routing — the table just reports the
|
||||
// active state via the click context.
|
||||
it('calls onRowDeactivate when clicking active row', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRowClick = jest.fn();
|
||||
const onRowDeactivate = jest.fn();
|
||||
|
||||
renderTanStackTable({
|
||||
props: {
|
||||
onRowClick,
|
||||
onRowDeactivate,
|
||||
isRowActive: (row) => row.id === '1',
|
||||
},
|
||||
});
|
||||
@@ -680,11 +678,8 @@ describe('TanStackTableView Integration', () => {
|
||||
|
||||
await user.click(screen.getByText('Item 1'));
|
||||
|
||||
expect(onRowClick).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: '1' }),
|
||||
expect.anything(),
|
||||
{ isActive: true },
|
||||
);
|
||||
expect(onRowDeactivate).toHaveBeenCalled();
|
||||
expect(onRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens in new tab on ctrl+click', async () => {
|
||||
|
||||
@@ -116,11 +116,9 @@ export * from './useTableParams';
|
||||
* getItemKey={(row) => row.id}
|
||||
* isRowActive={(row) => row.id === selectedId}
|
||||
* activeRowIndex={selectedIndex}
|
||||
* // The table reports the click + the row's active state; the consumer owns open/close.
|
||||
* onRowClick={(row, itemKey, { isActive }) =>
|
||||
* setSelectedId(isActive ? undefined : itemKey)
|
||||
* }
|
||||
* onRowClick={(row, itemKey) => setSelectedId(itemKey)}
|
||||
* onRowClickNewTab={(row, itemKey) => openInNewTab(itemKey)}
|
||||
* onRowDeactivate={() => setSelectedId(undefined)}
|
||||
* getRowClassName={(row) => (row.severity === 'error' ? 'row-error' : '')}
|
||||
* getRowStyle={(row) => (row.dimmed ? { opacity: 0.5 } : {})}
|
||||
* renderRowActions={(row) => <Button size="small">Open</Button>}
|
||||
|
||||
@@ -81,23 +81,15 @@ export type FlatItem<TData> =
|
||||
| { kind: 'row'; row: TanStackRowType<TData> }
|
||||
| { kind: 'expansion'; row: TanStackRowType<TData> };
|
||||
|
||||
/**
|
||||
* State of a row at click time, passed as the third argument to `onRowClick`.
|
||||
* The consumer owns the open/close decision — e.g. `isActive ? close() : open()`.
|
||||
*/
|
||||
export type RowClickContext = {
|
||||
/** Whether the clicked row is currently active (per `isRowActive`). */
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type TableRowContext<TData, TItemKey = string> = {
|
||||
getRowStyle?: (row: TData) => CSSProperties;
|
||||
getRowClassName?: (row: TData) => string;
|
||||
isRowActive?: (row: TData) => boolean;
|
||||
renderRowActions?: (row: TData) => ReactNode;
|
||||
onRowClick?: (row: TData, itemKey: TItemKey, context: RowClickContext) => void;
|
||||
onRowClick?: (row: TData, itemKey: TItemKey) => void;
|
||||
/** Called when ctrl+click or cmd+click on a row */
|
||||
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
|
||||
onRowDeactivate?: () => void;
|
||||
renderExpandedRow?: (
|
||||
row: TData,
|
||||
rowKey: string,
|
||||
@@ -188,9 +180,10 @@ export type TanStackTableProps<TData, TItemKey = string> = {
|
||||
getRowClassName?: (row: TData) => string;
|
||||
isRowActive?: (row: TData) => boolean;
|
||||
renderRowActions?: (row: TData) => ReactNode;
|
||||
onRowClick?: (row: TData, itemKey: TItemKey, context: RowClickContext) => void;
|
||||
onRowClick?: (row: TData, itemKey: TItemKey) => void;
|
||||
/** Called when ctrl+click or cmd+click on a row */
|
||||
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
|
||||
onRowDeactivate?: () => void;
|
||||
activeRowIndex?: number;
|
||||
renderExpandedRow?: (
|
||||
row: TData,
|
||||
|
||||
@@ -65,5 +65,4 @@
|
||||
min-width: 0;
|
||||
padding-left: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@@ -25,52 +25,20 @@ describe('calculateChartDimensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('RIGHT: reserves a side column capped at 320px / 40% of the width and keeps full height', () => {
|
||||
it('RIGHT: reserves a side column capped at 30% of the width and keeps full height', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 400,
|
||||
legendConfig: { position: LegendPosition.RIGHT },
|
||||
seriesLabels: labels(10, 40),
|
||||
});
|
||||
expect(dims.legendWidth).toBe(320);
|
||||
expect(dims.width).toBe(680);
|
||||
// 40-char labels approximate to 336px, capped at min(240, 30% of 1000).
|
||||
expect(dims.legendWidth).toBe(240);
|
||||
expect(dims.width).toBe(760);
|
||||
expect(dims.height).toBe(400);
|
||||
expect(dims.legendHeight).toBe(400);
|
||||
});
|
||||
|
||||
it('RIGHT: sizes the column to the longest label when it fits under the cap', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 400,
|
||||
legendConfig: { position: LegendPosition.RIGHT },
|
||||
seriesLabels: labels(5, 20),
|
||||
});
|
||||
expect(dims.legendWidth).toBe(216);
|
||||
expect(dims.width).toBe(784);
|
||||
});
|
||||
|
||||
it('RIGHT: never shrinks the column below the 150px floor', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
containerHeight: 400,
|
||||
legendConfig: { position: LegendPosition.RIGHT },
|
||||
seriesLabels: labels(3, 3),
|
||||
});
|
||||
expect(dims.legendWidth).toBe(150);
|
||||
expect(dims.width).toBe(850);
|
||||
});
|
||||
|
||||
it('RIGHT: on a narrow container the legend never takes more than 40% of the width', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 300,
|
||||
containerHeight: 400,
|
||||
legendConfig: { position: LegendPosition.RIGHT },
|
||||
seriesLabels: labels(10, 40),
|
||||
});
|
||||
expect(dims.legendWidth).toBe(120);
|
||||
expect(dims.width).toBe(180);
|
||||
});
|
||||
|
||||
it('BOTTOM: a single row of items reserves one legend row', () => {
|
||||
const dims = calculateChartDimensions({
|
||||
containerWidth: 1000,
|
||||
|
||||
@@ -15,12 +15,6 @@ const BASE_LEGEND_WIDTH = 16;
|
||||
const LEGEND_PADDING = 12;
|
||||
const LEGEND_LINE_HEIGHT = 28;
|
||||
|
||||
// RIGHT legend is a vertical column with its own width budget (cap protects the donut).
|
||||
const MAX_RIGHT_LEGEND_WIDTH = 320;
|
||||
const RIGHT_LEGEND_WIDTH_RATIO = 0.4;
|
||||
// Column padding + copy button, not covered by the text-length estimate.
|
||||
const RIGHT_LEGEND_RESERVED_WIDTH = 40;
|
||||
|
||||
/**
|
||||
* Calculates the average width of the legend items based on the labels of the series.
|
||||
* @param legends - The labels of the series.
|
||||
@@ -48,7 +42,7 @@ export function calculateAverageLegendWidth(legends: string[]): number {
|
||||
* Implementation details (high level):
|
||||
* - Approximates legend item width from label text length, using a fixed average char width.
|
||||
* - RIGHT legend:
|
||||
* - `legendWidth` fits the longest label, clamped to [150px, min(MAX_RIGHT_LEGEND_WIDTH, 40% width)].
|
||||
* - `legendWidth` is clamped between 150px and min(MAX_LEGEND_WIDTH, 30% of container width).
|
||||
* - Chart width is `containerWidth - legendWidth`.
|
||||
* - BOTTOM legend:
|
||||
* - Computes how many items fit per row, then uses at most 2 rows.
|
||||
@@ -86,22 +80,9 @@ export function calculateChartDimensions({
|
||||
const legendItemCount = seriesLabels.length;
|
||||
|
||||
if (legendConfig.position === LegendPosition.RIGHT) {
|
||||
// Size the column to the longest name (up to the cap) so it doesn't ellipsize.
|
||||
const longestLabelLength = seriesLabels.reduce(
|
||||
(max, label) => Math.max(max, label.length),
|
||||
0,
|
||||
);
|
||||
const desiredLegendWidth =
|
||||
BASE_LEGEND_WIDTH +
|
||||
longestLabelLength * AVG_CHAR_WIDTH +
|
||||
RIGHT_LEGEND_RESERVED_WIDTH;
|
||||
|
||||
const maxRightLegendWidth = Math.min(
|
||||
MAX_RIGHT_LEGEND_WIDTH,
|
||||
containerWidth * RIGHT_LEGEND_WIDTH_RATIO,
|
||||
);
|
||||
const maxRightLegendWidth = Math.min(MAX_LEGEND_WIDTH, containerWidth * 0.3);
|
||||
const rightLegendWidth = Math.min(
|
||||
Math.max(150, desiredLegendWidth),
|
||||
Math.max(150, approxLegendItemWidth),
|
||||
maxRightLegendWidth,
|
||||
);
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
|
||||
&--legend-right {
|
||||
flex-direction: row;
|
||||
|
||||
.chart-layout__legend-wrapper {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__legend-wrapper {
|
||||
|
||||
@@ -216,13 +216,10 @@ function LiveLogsList({
|
||||
),
|
||||
}) as CSSProperties
|
||||
}
|
||||
onRowClick={(log, _itemKey, { isActive }): void => {
|
||||
if (isActive) {
|
||||
handleCloseLogDetail();
|
||||
} else {
|
||||
handleSetActiveLog(log);
|
||||
}
|
||||
onRowClick={(log): void => {
|
||||
handleSetActiveLog(log);
|
||||
}}
|
||||
onRowDeactivate={handleCloseLogDetail}
|
||||
activeRowIndex={activeLogIndex}
|
||||
renderRowActions={(log): ReactNode => (
|
||||
<LogLinesActionButtons
|
||||
|
||||
@@ -211,7 +211,9 @@ function LogsExplorerList({
|
||||
data={logs}
|
||||
isLoading={isLoading || isFetching}
|
||||
onEndReached={onEndReached}
|
||||
isRowActive={(log): boolean => log.id === activeLog?.id}
|
||||
isRowActive={(log): boolean =>
|
||||
log.id === activeLog?.id || log.id === activeLogId
|
||||
}
|
||||
getRowStyle={(log): CSSProperties =>
|
||||
({
|
||||
'--row-active-bg': getRowBackgroundColor(
|
||||
@@ -224,13 +226,10 @@ function LogsExplorerList({
|
||||
),
|
||||
}) as CSSProperties
|
||||
}
|
||||
onRowClick={(log, _itemKey, { isActive }): void => {
|
||||
if (isActive) {
|
||||
handleCloseLogDetail();
|
||||
} else {
|
||||
handleSetActiveLog(log);
|
||||
}
|
||||
onRowClick={(log): void => {
|
||||
handleSetActiveLog(log);
|
||||
}}
|
||||
onRowDeactivate={handleCloseLogDetail}
|
||||
activeRowIndex={activeLogIndex}
|
||||
renderRowActions={(log): ReactNode => (
|
||||
<LogLinesActionButtons
|
||||
|
||||
@@ -55,7 +55,6 @@ export function useCreateExportDashboard({
|
||||
layouts: [],
|
||||
panels: {},
|
||||
variables: [],
|
||||
links: [],
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { DashboardtypesDynamicVariableSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
// eslint-disable-next-line signoz/no-antd-components -- fixed-option signal picker
|
||||
import { Select } from 'antd';
|
||||
@@ -15,16 +14,17 @@ import { isRetryableError } from 'utils/errorUtils';
|
||||
import {
|
||||
DYNAMIC_SIGNAL_LABEL,
|
||||
DYNAMIC_SIGNALS,
|
||||
type DynamicSignalOption,
|
||||
signalForApi,
|
||||
} from '../variableFormModel';
|
||||
import styles from './VariableForm.module.scss';
|
||||
|
||||
interface DynamicVariableFieldsProps {
|
||||
attribute: string;
|
||||
signal: DashboardtypesDynamicVariableSignalDTO;
|
||||
signal: DynamicSignalOption;
|
||||
onChange: (patch: {
|
||||
dynamicAttribute?: string;
|
||||
dynamicSignal?: DashboardtypesDynamicVariableSignalDTO;
|
||||
dynamicSignal?: DynamicSignalOption;
|
||||
}) => void;
|
||||
onPreview: (values: (string | number)[]) => void;
|
||||
/** Inline error shown under the attribute field (e.g. duplicate attribute). */
|
||||
@@ -117,9 +117,7 @@ function DynamicVariableFields({
|
||||
value: s,
|
||||
}))}
|
||||
onChange={(value): void =>
|
||||
onChange({
|
||||
dynamicSignal: value as DashboardtypesDynamicVariableSignalDTO,
|
||||
})
|
||||
onChange({ dynamicSignal: value as DynamicSignalOption })
|
||||
}
|
||||
data-testid="variable-signal-select"
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,11 @@ import type {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
DYNAMIC_SIGNAL_ALL,
|
||||
DYNAMIC_SIGNALS,
|
||||
type DynamicSignalOption,
|
||||
emptyVariableFormModel,
|
||||
signalForApi,
|
||||
VARIABLE_SORT_DISABLED,
|
||||
type VariableFormModel,
|
||||
} from './variableFormModel';
|
||||
@@ -65,8 +69,12 @@ export function dtoToFormModel(
|
||||
...listCommon,
|
||||
type: 'DYNAMIC',
|
||||
dynamicAttribute: plugin.spec.name ?? '',
|
||||
// signal is a required wire field (`all` = every telemetry signal), used as-is.
|
||||
dynamicSignal: plugin.spec.signal,
|
||||
// Unrecognized/empty signal → "all telemetry", so the source always shows.
|
||||
dynamicSignal: DYNAMIC_SIGNALS.includes(
|
||||
plugin.spec.signal as DynamicSignalOption,
|
||||
)
|
||||
? (plugin.spec.signal as DynamicSignalOption)
|
||||
: DYNAMIC_SIGNAL_ALL,
|
||||
};
|
||||
}
|
||||
// Default to Query (also covers a query plugin or a missing/unknown plugin).
|
||||
@@ -94,7 +102,7 @@ function buildPlugin(
|
||||
kind: DynamicPluginKind['signoz/DynamicVariable'],
|
||||
spec: {
|
||||
name: model.dynamicAttribute,
|
||||
signal: model.dynamicSignal,
|
||||
signal: signalForApi(model.dynamicSignal),
|
||||
},
|
||||
};
|
||||
case 'QUERY':
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
DashboardtypesListVariableSpecSortDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { DashboardtypesVariableDefaultValueDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { sortBy } from 'lodash-es';
|
||||
@@ -23,6 +23,23 @@ export const VARIABLE_TYPE_EVENT_LABEL: Record<VariableType, string> = {
|
||||
DYNAMIC: 'dynamic',
|
||||
};
|
||||
|
||||
/** Telemetry signal — the generated enum (traces / logs / metrics). */
|
||||
// A query/variable signal is only logs/traces/metrics. TelemetrytypesSignalDTO
|
||||
// also carries the empty "any" value used on field keys, which is not a valid
|
||||
// query/variable signal, so exclude it here.
|
||||
export type TelemetrySignal =
|
||||
| TelemetrytypesSignalDTO.logs
|
||||
| TelemetrytypesSignalDTO.traces
|
||||
| TelemetrytypesSignalDTO.metrics;
|
||||
|
||||
/**
|
||||
* Signal selected in the dynamic-variable editor. `'all'` is UI-only (the
|
||||
* generated `TelemetrytypesSignalDTO` has no "all") — it searches across every
|
||||
* signal and maps to an omitted `signal` on the wire (see {@link signalForApi}).
|
||||
*/
|
||||
export const DYNAMIC_SIGNAL_ALL = 'all' as const;
|
||||
export type DynamicSignalOption = TelemetrySignal | typeof DYNAMIC_SIGNAL_ALL;
|
||||
|
||||
/**
|
||||
* Sort order for list-variable values, keyed by the generated wire enum so the
|
||||
* form model and the DTO `sort` field share one source of truth. The friendly
|
||||
@@ -64,38 +81,25 @@ export const VARIABLE_SORT_LABEL: Record<VariableSort, string> = {
|
||||
[VARIABLE_SORT.CI_DESC]: 'Alphabetical, case-insensitive (descending)',
|
||||
};
|
||||
|
||||
export const DYNAMIC_SIGNALS: DashboardtypesDynamicVariableSignalDTO[] = [
|
||||
DashboardtypesDynamicVariableSignalDTO.all,
|
||||
DashboardtypesDynamicVariableSignalDTO.traces,
|
||||
DashboardtypesDynamicVariableSignalDTO.logs,
|
||||
DashboardtypesDynamicVariableSignalDTO.metrics,
|
||||
export const DYNAMIC_SIGNALS: DynamicSignalOption[] = [
|
||||
DYNAMIC_SIGNAL_ALL,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.metrics,
|
||||
];
|
||||
|
||||
export const DYNAMIC_SIGNAL_LABEL: Record<
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
string
|
||||
> = {
|
||||
[DashboardtypesDynamicVariableSignalDTO.all]: 'All telemetry',
|
||||
[DashboardtypesDynamicVariableSignalDTO.traces]: 'Traces',
|
||||
[DashboardtypesDynamicVariableSignalDTO.logs]: 'Logs',
|
||||
[DashboardtypesDynamicVariableSignalDTO.metrics]: 'Metrics',
|
||||
export const DYNAMIC_SIGNAL_LABEL: Record<DynamicSignalOption, string> = {
|
||||
[DYNAMIC_SIGNAL_ALL]: 'All telemetry',
|
||||
[TelemetrytypesSignalDTO.traces]: 'Traces',
|
||||
[TelemetrytypesSignalDTO.logs]: 'Logs',
|
||||
[TelemetrytypesSignalDTO.metrics]: 'Metrics',
|
||||
};
|
||||
|
||||
/**
|
||||
* Field-keys/values API param. The `all` signal is omitted (that endpoint only
|
||||
* accepts a concrete signal), everything else passes through.
|
||||
*/
|
||||
/** Maps the editor's signal selection to the wire value (`'all'` → omitted). */
|
||||
export function signalForApi(
|
||||
signal: DashboardtypesDynamicVariableSignalDTO,
|
||||
):
|
||||
| Exclude<
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
DashboardtypesDynamicVariableSignalDTO.all
|
||||
>
|
||||
| undefined {
|
||||
return signal === DashboardtypesDynamicVariableSignalDTO.all
|
||||
? undefined
|
||||
: signal;
|
||||
signal: DynamicSignalOption,
|
||||
): TelemetrySignal | undefined {
|
||||
return signal === DYNAMIC_SIGNAL_ALL ? undefined : signal;
|
||||
}
|
||||
|
||||
type SortableValues = (string | number | boolean)[];
|
||||
@@ -140,7 +144,7 @@ export interface VariableFormModel {
|
||||
textValue: string; // TEXT
|
||||
textConstant: boolean; // TEXT
|
||||
dynamicAttribute: string; // DYNAMIC — the telemetry field name
|
||||
dynamicSignal: DashboardtypesDynamicVariableSignalDTO; // DYNAMIC — the telemetry signal (`all` = every signal)
|
||||
dynamicSignal: DynamicSignalOption; // DYNAMIC — the telemetry signal
|
||||
|
||||
/**
|
||||
* Runtime-selected default, not editable in the management tab yet; carried
|
||||
@@ -162,6 +166,6 @@ export function emptyVariableFormModel(): VariableFormModel {
|
||||
textValue: '',
|
||||
textConstant: false,
|
||||
dynamicAttribute: '',
|
||||
dynamicSignal: DashboardtypesDynamicVariableSignalDTO.all,
|
||||
dynamicSignal: DYNAMIC_SIGNAL_ALL,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
|
||||
[TelemetrytypesSignalDTO.logs]: 'logs',
|
||||
[TelemetrytypesSignalDTO.traces]: 'traces',
|
||||
[TelemetrytypesSignalDTO.metrics]: 'metrics',
|
||||
// The empty "any" signal only appears on field keys, never on a panel query;
|
||||
// mapped for exhaustiveness.
|
||||
[TelemetrytypesSignalDTO['']]: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export const SECTION_REGISTRY: {
|
||||
[SectionKind.ContextLinks]: {
|
||||
Component: ContextLinksSection,
|
||||
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.
|
||||
get: (spec): DashboardtypesLinkDTO[] => spec.links,
|
||||
get: (spec): DashboardtypesLinkDTO[] | undefined => spec.links ?? undefined,
|
||||
update: (spec, links): PanelSpec => ({ ...spec, links }),
|
||||
},
|
||||
// One editor for every threshold variant (label / comparison / table); the kind's
|
||||
|
||||
@@ -49,14 +49,6 @@ describe('newPanelRoute', () => {
|
||||
return { path, params: new URLSearchParams(search) };
|
||||
};
|
||||
|
||||
// Mirrors useGetCompositeQueryParam: it decodes the param twice.
|
||||
const readCompositeQuery = (params: URLSearchParams): unknown =>
|
||||
JSON.parse(
|
||||
decodeURIComponent(
|
||||
(params.get('compositeQuery') as string).replace(/\+/g, ' '),
|
||||
),
|
||||
);
|
||||
|
||||
it.each([
|
||||
[PANEL_TYPES.TIME_SERIES, 'signoz/TimeSeriesPanel'],
|
||||
[PANEL_TYPES.TABLE, 'signoz/TablePanel'],
|
||||
@@ -75,34 +67,16 @@ describe('newPanelRoute', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('carries the query as a compositeQuery param the reader can decode', () => {
|
||||
it('carries the query as a decodable compositeQuery param', () => {
|
||||
const link = buildExportPanelLink({
|
||||
dashboardId: 'dash-1',
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
query,
|
||||
});
|
||||
const { params } = parseLink(link);
|
||||
expect(readCompositeQuery(params)).toStrictEqual(query);
|
||||
});
|
||||
|
||||
// Regression: a bare `%`/`+` must survive the reader's double-decode.
|
||||
it('round-trips a filter expression containing % and + literals', () => {
|
||||
const queryWithLiterals = {
|
||||
id: 'q1',
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
queryData: [
|
||||
{ filter: { expression: "severity_text ILIKE 'Inf%' AND path = 'a+b'" } },
|
||||
],
|
||||
},
|
||||
} as unknown as Query;
|
||||
const link = buildExportPanelLink({
|
||||
dashboardId: 'dash-1',
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
query: queryWithLiterals,
|
||||
});
|
||||
const { params } = parseLink(link);
|
||||
expect(readCompositeQuery(params)).toStrictEqual(queryWithLiterals);
|
||||
expect(JSON.parse(params.get('compositeQuery') as string)).toStrictEqual(
|
||||
query,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for a panel type with no V2 kind', () => {
|
||||
|
||||
@@ -45,11 +45,9 @@ export function parseNewPanelKind(
|
||||
|
||||
/**
|
||||
* New-panel editor link that exports an explorer query into a V2 dashboard. Carries the
|
||||
* raw `Query` as `compositeQuery` (conversion happens in the editor). `null` when the panel
|
||||
* type has no V2 kind, so the caller skips the export instead of landing on an unrelated kind.
|
||||
*
|
||||
* Double-encoded on purpose: `useGetCompositeQueryParam` decodes twice, so a single encode
|
||||
* would let a bare `%`/`+` (e.g. `ILIKE 'Inf%'`) break its second decode and drop the query.
|
||||
* raw `Query` as `compositeQuery` encoded as the V1 link so `useGetCompositeQueryParam`
|
||||
* reads it identically (conversion happens in the editor). `null` when the panel type has
|
||||
* no V2 kind, so the caller skips the export instead of landing on an unrelated kind.
|
||||
*/
|
||||
export function buildExportPanelLink({
|
||||
dashboardId,
|
||||
@@ -70,7 +68,7 @@ export function buildExportPanelLink({
|
||||
});
|
||||
return `${path}${newPanelSearch(kind)}&${
|
||||
QueryParams.compositeQuery
|
||||
}=${encodeURIComponent(encodeURIComponent(JSON.stringify(query)))}`;
|
||||
}=${encodeURIComponent(JSON.stringify(query))}`;
|
||||
}
|
||||
|
||||
/** Target section index for a new panel, or undefined when unset/invalid. */
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { countEnabledQueries } from '../countEnabledQueries';
|
||||
|
||||
function compositeQuery(
|
||||
envelopes: { type: string; disabled?: boolean }[],
|
||||
): DashboardtypesQueryDTO {
|
||||
return {
|
||||
spec: {
|
||||
plugin: {
|
||||
kind: 'signoz/CompositeQuery',
|
||||
spec: {
|
||||
queries: envelopes.map(({ type, disabled }) => ({
|
||||
type,
|
||||
spec: { disabled },
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as DashboardtypesQueryDTO;
|
||||
}
|
||||
|
||||
function bareBuilderQuery(disabled?: boolean): DashboardtypesQueryDTO {
|
||||
return {
|
||||
spec: {
|
||||
plugin: { kind: 'signoz/BuilderQuery', spec: { signal: 'logs', disabled } },
|
||||
},
|
||||
} as unknown as DashboardtypesQueryDTO;
|
||||
}
|
||||
|
||||
describe('countEnabledQueries', () => {
|
||||
it('returns 0 when there are no queries', () => {
|
||||
expect(countEnabledQueries([])).toBe(0);
|
||||
});
|
||||
|
||||
it('counts every enabled envelope inside the composite wrapper', () => {
|
||||
expect(
|
||||
countEnabledQueries([
|
||||
compositeQuery([{ type: 'builder_query' }, { type: 'builder_query' }]),
|
||||
]),
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('does not count disabled envelopes', () => {
|
||||
expect(
|
||||
countEnabledQueries([
|
||||
compositeQuery([
|
||||
{ type: 'builder_query' },
|
||||
{ type: 'builder_query', disabled: true },
|
||||
]),
|
||||
]),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('counts formula envelopes alongside builder queries', () => {
|
||||
expect(
|
||||
countEnabledQueries([
|
||||
compositeQuery([
|
||||
{ type: 'builder_query' },
|
||||
{ type: 'builder_query' },
|
||||
{ type: 'builder_formula' },
|
||||
]),
|
||||
]),
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it('treats an absent disabled flag as enabled', () => {
|
||||
expect(
|
||||
countEnabledQueries([compositeQuery([{ type: 'builder_query' }])]),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('unwraps a bare builder query (List panel shape)', () => {
|
||||
expect(countEnabledQueries([bareBuilderQuery()])).toBe(1);
|
||||
});
|
||||
|
||||
it('does not count a disabled bare builder query', () => {
|
||||
expect(countEnabledQueries([bareBuilderQuery(true)])).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { toQueryEnvelopes } from 'pages/DashboardPageV2/DashboardContainer/queryV5/buildQueryRangeRequest';
|
||||
|
||||
/** Counts enabled queries, unwrapping the single `signoz/CompositeQuery` wrapper. */
|
||||
export function countEnabledQueries(queries: DashboardtypesQueryDTO[]): number {
|
||||
return toQueryEnvelopes(queries).filter((envelope) => !envelope.spec?.disabled)
|
||||
.length;
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { BuilderQuery } from 'types/api/v5/queryRange';
|
||||
|
||||
/**
|
||||
@@ -20,23 +17,3 @@ export function resolveDrilldownSignal(
|
||||
return TelemetrytypesSignalDTO.metrics;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a clicked query's telemetry signal to a dynamic-variable signal (used
|
||||
* when a drilldown seeds a new variable). Concrete signals map 1:1; an unset
|
||||
* signal (no active drilldown) falls back to `all`.
|
||||
*/
|
||||
export function dynamicSignalFromQuerySignal(
|
||||
signal?: TelemetrytypesSignalDTO,
|
||||
): DashboardtypesDynamicVariableSignalDTO {
|
||||
switch (signal) {
|
||||
case TelemetrytypesSignalDTO.traces:
|
||||
return DashboardtypesDynamicVariableSignalDTO.traces;
|
||||
case TelemetrytypesSignalDTO.logs:
|
||||
return DashboardtypesDynamicVariableSignalDTO.logs;
|
||||
case TelemetrytypesSignalDTO.metrics:
|
||||
return DashboardtypesDynamicVariableSignalDTO.metrics;
|
||||
default:
|
||||
return DashboardtypesDynamicVariableSignalDTO.all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import PanelHeaderSearch from './PanelHeaderSearch';
|
||||
import PanelStatusPopover from '../PanelStatus/PanelStatusPopover';
|
||||
import {
|
||||
panelStatusFromError,
|
||||
panelStatusFromMultipleEnabledQueries,
|
||||
panelStatusFromWarning,
|
||||
} from '../PanelStatus/utils';
|
||||
import styles from './PanelHeader.module.scss';
|
||||
@@ -75,24 +74,11 @@ function PanelHeader({
|
||||
[warning],
|
||||
);
|
||||
|
||||
// Client-derived: warn a Number panel that has more than one enabled query (#9512).
|
||||
const multiQueryWarningDetail = useMemo(
|
||||
() => panelStatusFromMultipleEnabledQueries(panel),
|
||||
[panel],
|
||||
);
|
||||
|
||||
/**
|
||||
* Hide the entire header when there's no title, description, or status to show,
|
||||
* and the actions menu is suppressed (editor preview).
|
||||
*/
|
||||
if (
|
||||
!name &&
|
||||
!description &&
|
||||
!errorDetail &&
|
||||
!warningDetail &&
|
||||
!multiQueryWarningDetail &&
|
||||
hideActions
|
||||
) {
|
||||
if (!name && !description && !errorDetail && !warningDetail && hideActions) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
@@ -138,13 +124,6 @@ function PanelHeader({
|
||||
{warningDetail && (
|
||||
<PanelStatusPopover variant="warning" detail={warningDetail} />
|
||||
)}
|
||||
{multiQueryWarningDetail && (
|
||||
<PanelStatusPopover
|
||||
variant="warning"
|
||||
detail={multiQueryWarningDetail}
|
||||
testId="panel-status-config-warning"
|
||||
/>
|
||||
)}
|
||||
{/* Renders nothing when no action survives its gates (kind/role/context). */}
|
||||
{!hideActions && (
|
||||
<PanelActionsMenu
|
||||
|
||||
@@ -17,8 +17,6 @@ const VARIANT_CONFIG: Record<
|
||||
interface PanelStatusPopoverProps {
|
||||
variant: PanelStatusVariant;
|
||||
detail: PanelStatusDetail;
|
||||
/** Overrides the trigger's test id; defaults to `panel-status-<variant>`. */
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +26,6 @@ interface PanelStatusPopoverProps {
|
||||
function PanelStatusPopover({
|
||||
variant,
|
||||
detail,
|
||||
testId,
|
||||
}: PanelStatusPopoverProps): JSX.Element {
|
||||
const { color, ariaLabel } = VARIANT_CONFIG[variant];
|
||||
const Icon = variant === 'error' ? CircleX : TriangleAlert;
|
||||
@@ -44,7 +41,7 @@ function PanelStatusPopover({
|
||||
<span
|
||||
className={styles.trigger}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId ?? `panel-status-${variant}`}
|
||||
data-testid={`panel-status-${variant}`}
|
||||
>
|
||||
<Icon size={16} color={color} />
|
||||
</span>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import type {
|
||||
DashboardtypesPanelDTO,
|
||||
Querybuildertypesv5QueryWarnDataDTO as WarningDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { Querybuildertypesv5QueryWarnDataDTO as WarningDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
|
||||
import {
|
||||
panelStatusFromError,
|
||||
panelStatusFromMultipleEnabledQueries,
|
||||
panelStatusFromWarning,
|
||||
} from '../utils';
|
||||
import { panelStatusFromError, panelStatusFromWarning } from '../utils';
|
||||
|
||||
// The query layer rejects with the raw AxiosError from the generated client
|
||||
// (it is not pre-converted to APIError), so the tests mirror that wire shape.
|
||||
@@ -94,62 +87,3 @@ describe('panelStatusFromWarning', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function panel(
|
||||
kind: string,
|
||||
envelopes: { disabled?: boolean }[],
|
||||
): DashboardtypesPanelDTO {
|
||||
return {
|
||||
spec: {
|
||||
plugin: { kind, spec: {} },
|
||||
queries: [
|
||||
{
|
||||
spec: {
|
||||
plugin: {
|
||||
kind: 'signoz/CompositeQuery',
|
||||
spec: {
|
||||
queries: envelopes.map(({ disabled }) => ({
|
||||
type: 'builder_query',
|
||||
spec: { disabled },
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as DashboardtypesPanelDTO;
|
||||
}
|
||||
|
||||
describe('panelStatusFromMultipleEnabledQueries', () => {
|
||||
it('warns when a Number panel has more than one enabled query', () => {
|
||||
const detail = panelStatusFromMultipleEnabledQueries(
|
||||
panel('signoz/NumberPanel', [{}, {}]),
|
||||
);
|
||||
expect(detail).not.toBeNull();
|
||||
expect(detail?.message).toMatch(/single value/i);
|
||||
expect(detail?.messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('counts only enabled queries (a disabled second query is fine)', () => {
|
||||
expect(
|
||||
panelStatusFromMultipleEnabledQueries(
|
||||
panel('signoz/NumberPanel', [{}, { disabled: true }]),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not warn when a Number panel has a single enabled query', () => {
|
||||
expect(
|
||||
panelStatusFromMultipleEnabledQueries(panel('signoz/NumberPanel', [{}])),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('does not warn for other panel kinds even with multiple enabled queries', () => {
|
||||
expect(
|
||||
panelStatusFromMultipleEnabledQueries(
|
||||
panel('signoz/TimeSeriesPanel', [{}, {}]),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type {
|
||||
DashboardtypesPanelDTO,
|
||||
Querybuildertypesv5QueryWarnDataDTO as WarningDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { countEnabledQueries } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/countEnabledQueries';
|
||||
import type { Querybuildertypesv5QueryWarnDataDTO as WarningDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PanelStatusDetail } from './types';
|
||||
|
||||
@@ -56,25 +52,3 @@ export function panelStatusFromWarning(
|
||||
.filter((message): message is string => Boolean(message)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A Number panel renders only the first query's value, so more than one enabled
|
||||
* query silently hides the rest. Warns for that case; null otherwise.
|
||||
*/
|
||||
export function panelStatusFromMultipleEnabledQueries(
|
||||
panel: DashboardtypesPanelDTO,
|
||||
): PanelStatusDetail | null {
|
||||
if (panel.spec.plugin.kind !== 'signoz/NumberPanel') {
|
||||
return null;
|
||||
}
|
||||
if (countEnabledQueries(panel.spec.queries) <= 1) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
message:
|
||||
'This panel shows a single value, but more than one query is enabled.',
|
||||
messages: [
|
||||
"Disable the queries you don't want to display, keeping only the one whose value you want to show.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,40 +41,6 @@ function makePanel(overrides?: {
|
||||
} as unknown as DashboardtypesPanelDTO;
|
||||
}
|
||||
|
||||
// Blank name so the editor-preview guard test relies on the warning alone.
|
||||
function makePanelWithQueries(
|
||||
kind: string,
|
||||
enabled: number,
|
||||
disabled = 0,
|
||||
): DashboardtypesPanelDTO {
|
||||
const envelope = (isDisabled: boolean): unknown => ({
|
||||
type: 'builder_query',
|
||||
spec: { disabled: isDisabled },
|
||||
});
|
||||
return {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
display: { name: '' },
|
||||
plugin: { kind, spec: {} },
|
||||
queries: [
|
||||
{
|
||||
spec: {
|
||||
plugin: {
|
||||
kind: 'signoz/CompositeQuery',
|
||||
spec: {
|
||||
queries: [
|
||||
...Array.from({ length: enabled }, () => envelope(false)),
|
||||
...Array.from({ length: disabled }, () => envelope(true)),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as DashboardtypesPanelDTO;
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
panel: makePanel(),
|
||||
panelId: 'panel-1',
|
||||
@@ -135,53 +101,6 @@ describe('PanelHeader status indicators', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelHeader multi-query config warning (issue #9512)', () => {
|
||||
it('warns when a Number panel has more than one enabled query', () => {
|
||||
renderWithProvider(
|
||||
<PanelHeader
|
||||
{...baseProps}
|
||||
panel={makePanelWithQueries('signoz/NumberPanel', 2)}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('panel-status-config-warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not warn when only one query is enabled (others disabled)', () => {
|
||||
renderWithProvider(
|
||||
<PanelHeader
|
||||
{...baseProps}
|
||||
panel={makePanelWithQueries('signoz/NumberPanel', 1, 2)}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('panel-status-config-warning'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not warn for non-Number panels with multiple enabled queries', () => {
|
||||
renderWithProvider(
|
||||
<PanelHeader
|
||||
{...baseProps}
|
||||
panel={makePanelWithQueries('signoz/TimeSeriesPanel', 3)}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('panel-status-config-warning'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the warning in the editor preview (hideActions, no title)', () => {
|
||||
renderWithProvider(
|
||||
<PanelHeader
|
||||
{...baseProps}
|
||||
panel={makePanelWithQueries('signoz/NumberPanel', 2)}
|
||||
hideActions
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('panel-status-config-warning')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelHeader search', () => {
|
||||
it('renders no search affordance when the panel is not searchable', () => {
|
||||
renderWithProvider(<PanelHeader {...baseProps} />);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, renderHook, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DashboardtypesDynamicVariableSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { FilterData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
|
||||
import DrilldownDashboardVariablesMenu from '../DrilldownMenu/DrilldownDashboardVariablesMenu';
|
||||
@@ -63,6 +63,7 @@ jest.mock(
|
||||
'pages/DashboardPageV2/DashboardContainer/DashboardSettings/Variables/variableFormModel',
|
||||
() => ({
|
||||
emptyVariableFormModel: (): unknown => ({}),
|
||||
DYNAMIC_SIGNAL_ALL: 'all',
|
||||
}),
|
||||
);
|
||||
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
|
||||
@@ -83,7 +84,7 @@ function renderItems(): void {
|
||||
const { result } = renderHook(() =>
|
||||
useDrilldownDashboardVariables({
|
||||
filters,
|
||||
signal: DashboardtypesDynamicVariableSignalDTO.metrics,
|
||||
signal: TelemetrytypesSignalDTO.metrics,
|
||||
onClose: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { buildAggregateData } from 'pages/DashboardPageV2/DashboardContainer/Pan
|
||||
import { getBuilderQueries } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getBuilderQueries';
|
||||
import { getPanelQueryType } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getPanelQueryType';
|
||||
import { fromPerses } from 'pages/DashboardPageV2/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { dynamicSignalFromQuerySignal } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/drilldown/signal';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
@@ -164,7 +163,7 @@ export function useDrilldown(
|
||||
|
||||
const dashboardVariables = useDrilldownDashboardVariables({
|
||||
filters: context?.filters ?? EMPTY_FILTERS,
|
||||
signal: dynamicSignalFromQuerySignal(context?.signal),
|
||||
signal: context?.signal,
|
||||
onClose: handleClose,
|
||||
});
|
||||
|
||||
@@ -222,7 +221,7 @@ export function useDrilldown(
|
||||
context={context}
|
||||
query={v1Query}
|
||||
isResolving={isResolving}
|
||||
links={panel.spec.links}
|
||||
links={panel.spec.links ?? undefined}
|
||||
canSetDashboardVariables={dashboardVariables.hasFieldVariables}
|
||||
onViewLogs={(): void => navigate('view_logs')}
|
||||
onViewTraces={(): void => navigate('view_traces')}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DashboardtypesDynamicVariableSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type { FilterData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import {
|
||||
dtoToFormModel,
|
||||
formModelToDto,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/DashboardSettings/Variables/variableAdapters';
|
||||
import {
|
||||
DYNAMIC_SIGNAL_ALL,
|
||||
emptyVariableFormModel,
|
||||
type VariableFormModel,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/DashboardSettings/Variables/variableFormModel';
|
||||
@@ -22,8 +23,8 @@ import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
interface UseDrilldownDashboardVariablesArgs {
|
||||
/** Group-by field filters from the clicked point (empty when the click has no group-by). */
|
||||
filters: FilterData[];
|
||||
/** Dynamic-variable signal derived from the clicked query — seeds a created variable's `dynamicSignal`. */
|
||||
signal?: DashboardtypesDynamicVariableSignalDTO;
|
||||
/** Clicked query's telemetry signal — seeds a created variable's `dynamicSignal`. */
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -110,7 +111,8 @@ export function useDrilldownDashboardVariables({
|
||||
type: 'DYNAMIC',
|
||||
multiSelect: true,
|
||||
dynamicAttribute: fieldName,
|
||||
dynamicSignal: signal ?? DashboardtypesDynamicVariableSignalDTO.all,
|
||||
// `||` (not `??`): an empty "any" signal maps to All, same as unset.
|
||||
dynamicSignal: signal || DYNAMIC_SIGNAL_ALL,
|
||||
};
|
||||
try {
|
||||
await patchAsync(
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
DashboardtypesDynamicVariableSignalDTO,
|
||||
type DashboardtypesGettableDashboardV2DTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import type {
|
||||
IDashboardVariable,
|
||||
@@ -11,6 +8,7 @@ import type {
|
||||
|
||||
import { dtoToFormModel } from '../DashboardSettings/Variables/variableAdapters';
|
||||
import {
|
||||
DYNAMIC_SIGNAL_ALL,
|
||||
type VariableFormModel,
|
||||
type VariableType,
|
||||
} from '../DashboardSettings/Variables/variableFormModel';
|
||||
@@ -37,7 +35,7 @@ function toV1Variable(model: VariableFormModel): IDashboardVariable {
|
||||
showALLOption: model.showAllOption,
|
||||
dynamicVariablesAttribute: model.dynamicAttribute,
|
||||
dynamicVariablesSource:
|
||||
model.dynamicSignal === DashboardtypesDynamicVariableSignalDTO.all
|
||||
model.dynamicSignal === DYNAMIC_SIGNAL_ALL
|
||||
? 'all sources'
|
||||
: model.dynamicSignal,
|
||||
};
|
||||
|
||||
@@ -49,7 +49,6 @@ export function createDefaultPanel(
|
||||
spec: pluginSpec,
|
||||
} as DashboardtypesPanelPluginDTO,
|
||||
queries,
|
||||
links: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element {
|
||||
layouts: [],
|
||||
panels: {},
|
||||
variables: [],
|
||||
links: [],
|
||||
},
|
||||
});
|
||||
void logEvent(DashboardListEvents.DashboardCreated, {
|
||||
|
||||
@@ -32,12 +32,11 @@ unaryExpression
|
||||
;
|
||||
|
||||
// Primary constructs: grouped expressions, a comparison (key op value),
|
||||
// a function call, a search() full-text call, or a free-text string
|
||||
// a function call, or a full-text string
|
||||
primary
|
||||
: LPAREN orExpression RPAREN
|
||||
| comparison
|
||||
| functionCall
|
||||
| searchCall
|
||||
| fullText
|
||||
| key
|
||||
| value
|
||||
@@ -111,18 +110,6 @@ functionCall
|
||||
: (HASTOKEN | HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
/*
|
||||
* Full-text search call: search('needle')
|
||||
*
|
||||
* Uses the shared functionParamList so future scoped forms like
|
||||
* search(body, 'abc') / search(attribute, 'abc') need no grammar change. Today
|
||||
* only a single needle is supported. Unlike bare/quoted free text (`fullText`),
|
||||
* which only targets the body column, search() fans out across every field.
|
||||
*/
|
||||
searchCall
|
||||
: SEARCH LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
// Function parameters can be keys, single scalar values, or arrays
|
||||
functionParamList
|
||||
: functionParam ( COMMA functionParam )*
|
||||
@@ -197,7 +184,6 @@ HASTOKEN : [Hh][Aa][Ss][Tt][Oo][Kk][Ee][Nn];
|
||||
HAS : [Hh][Aa][Ss] ;
|
||||
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
|
||||
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
|
||||
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
|
||||
|
||||
// Potential boolean constants
|
||||
BOOL
|
||||
|
||||
@@ -101,12 +101,12 @@ func PrepareFilterExpression(labels map[string]string, whereClause string, group
|
||||
}
|
||||
|
||||
// Visit implements the visitor for the query rule.
|
||||
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) any {
|
||||
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) interface{} {
|
||||
return tree.Accept(r)
|
||||
}
|
||||
|
||||
// VisitQuery visits the query node.
|
||||
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
|
||||
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} {
|
||||
if ctx.Expression() != nil {
|
||||
ctx.Expression().Accept(r)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
|
||||
}
|
||||
|
||||
// VisitExpression visits the expression node.
|
||||
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) interface{} {
|
||||
if ctx.OrExpression() != nil {
|
||||
ctx.OrExpression().Accept(r)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any
|
||||
}
|
||||
|
||||
// VisitOrExpression visits OR expressions.
|
||||
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) interface{} {
|
||||
for i, andExpr := range ctx.AllAndExpression() {
|
||||
if i > 0 {
|
||||
r.rewritten.WriteString(" OR ")
|
||||
@@ -133,7 +133,7 @@ func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext)
|
||||
}
|
||||
|
||||
// VisitAndExpression visits AND expressions.
|
||||
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) interface{} {
|
||||
unaryExprs := ctx.AllUnaryExpression()
|
||||
for i, unaryExpr := range unaryExprs {
|
||||
if i > 0 {
|
||||
@@ -151,7 +151,7 @@ func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContex
|
||||
}
|
||||
|
||||
// VisitUnaryExpression visits unary expressions (with optional NOT).
|
||||
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) any {
|
||||
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) interface{} {
|
||||
if ctx.NOT() != nil {
|
||||
r.rewritten.WriteString("NOT ")
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionCo
|
||||
}
|
||||
|
||||
// VisitPrimary visits primary expressions.
|
||||
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface{} {
|
||||
if ctx.LPAREN() != nil && ctx.RPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
if ctx.OrExpression() != nil {
|
||||
@@ -173,8 +173,6 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
ctx.Comparison().Accept(r)
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
ctx.FunctionCall().Accept(r)
|
||||
} else if ctx.SearchCall() != nil {
|
||||
ctx.SearchCall().Accept(r)
|
||||
} else if ctx.FullText() != nil {
|
||||
ctx.FullText().Accept(r)
|
||||
} else if ctx.Key() != nil {
|
||||
@@ -186,7 +184,7 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
|
||||
}
|
||||
|
||||
// VisitComparison visits comparison expressions.
|
||||
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any {
|
||||
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) interface{} {
|
||||
if ctx.Key() == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -199,7 +197,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
|
||||
if _, partOfGroup := r.groupBySet[key]; partOfGroup {
|
||||
// Case 1: Replace with actual value
|
||||
escapedValue := escapeValueIfNeeded(value)
|
||||
fmt.Fprintf(&r.rewritten, "%s=%s", key, escapedValue)
|
||||
r.rewritten.WriteString(fmt.Sprintf("%s=%s", key, escapedValue))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -307,7 +305,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
|
||||
}
|
||||
|
||||
// VisitInClause visits IN clauses.
|
||||
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
|
||||
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interface{} {
|
||||
r.rewritten.WriteString("IN ")
|
||||
if ctx.LPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
@@ -328,7 +326,7 @@ func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
|
||||
}
|
||||
|
||||
// VisitNotInClause visits NOT IN clauses.
|
||||
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) any {
|
||||
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) interface{} {
|
||||
r.rewritten.WriteString("NOT IN ")
|
||||
if ctx.LPAREN() != nil {
|
||||
r.rewritten.WriteString("(")
|
||||
@@ -349,7 +347,7 @@ func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) a
|
||||
}
|
||||
|
||||
// VisitValueList visits value lists.
|
||||
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
|
||||
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) interface{} {
|
||||
values := ctx.AllValue()
|
||||
for i, val := range values {
|
||||
if i > 0 {
|
||||
@@ -361,20 +359,13 @@ func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
|
||||
}
|
||||
|
||||
// VisitFullText visits full text expressions.
|
||||
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) any {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitSearchCall visits search() calls. It has no keys to rewrite, so it is
|
||||
// preserved verbatim.
|
||||
func (r *WhereClauseRewriter) VisitSearchCall(ctx *parser.SearchCallContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interface{} {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitFunctionCall visits function calls.
|
||||
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} {
|
||||
// Write function name
|
||||
if ctx.HAS() != nil {
|
||||
r.rewritten.WriteString("has")
|
||||
@@ -395,7 +386,7 @@ func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext)
|
||||
}
|
||||
|
||||
// VisitFunctionParamList visits function parameter lists.
|
||||
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) interface{} {
|
||||
params := ctx.AllFunctionParam()
|
||||
for i, param := range params {
|
||||
if i > 0 {
|
||||
@@ -407,7 +398,7 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
|
||||
}
|
||||
|
||||
// VisitFunctionParam visits function parameters.
|
||||
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) any {
|
||||
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) interface{} {
|
||||
if ctx.Key() != nil {
|
||||
ctx.Key().Accept(r)
|
||||
} else if ctx.Value() != nil {
|
||||
@@ -419,7 +410,7 @@ func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContex
|
||||
}
|
||||
|
||||
// VisitArray visits array expressions.
|
||||
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
|
||||
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} {
|
||||
r.rewritten.WriteString("[")
|
||||
if ctx.ValueList() != nil {
|
||||
ctx.ValueList().Accept(r)
|
||||
@@ -429,13 +420,13 @@ func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
|
||||
}
|
||||
|
||||
// VisitValue visits value expressions.
|
||||
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) any {
|
||||
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) interface{} {
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitKey visits key expressions.
|
||||
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) any {
|
||||
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) interface{} {
|
||||
r.keysSeen[ctx.GetText()] = struct{}{}
|
||||
r.rewritten.WriteString(ctx.GetText())
|
||||
return nil
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_ComputeEngine_Color</title><g data-name="Product Icons"><rect class="cls-1" x="9" y="9" width="6" height="6"/><rect class="cls-2" x="11" y="2" width="2" height="4"/><rect class="cls-2" x="7" y="2" width="2" height="4"/><rect class="cls-2" x="15" y="2" width="2" height="4"/><rect class="cls-3" x="11" y="18" width="2" height="4"/><rect class="cls-3" x="7" y="18" width="2" height="4"/><rect class="cls-3" x="15" y="18" width="2" height="4"/><rect class="cls-3" x="19" y="10" width="2" height="4" transform="translate(8 32) rotate(-90)"/><rect class="cls-3" x="19" y="14" width="2" height="4" transform="translate(4 36) rotate(-90)"/><rect class="cls-3" x="19" y="6" width="2" height="4" transform="translate(12 28) rotate(-90)"/><rect class="cls-2" x="3" y="10" width="2" height="4" transform="translate(-8 16) rotate(-90)"/><rect class="cls-2" x="3" y="14" width="2" height="4" transform="translate(-12 20) rotate(-90)"/><rect class="cls-2" x="3" y="6" width="2" height="4" transform="translate(-4 12) rotate(-90)"/><path class="cls-1" d="M5,5V19H19V5ZM17,17H7V7H17Z"/><polygon class="cls-2" points="9 15 15 15 12 12 9 15"/><polygon class="cls-3" points="12 12 15 15 15 9 12 12"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"id": "computeengine",
|
||||
"title": "GCP Compute Engine",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/uptime_total",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/cpu/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/bytes_used",
|
||||
"unit": "Bytes",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/guest/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "agent.googleapis.com/memory/percent_used",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/average_io_latency",
|
||||
"unit": "Microseconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/read_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/write_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/disk/performance_status",
|
||||
"unit": "None",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_bytes_count",
|
||||
"unit": "Bytes",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/received_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/network/sent_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "compute.googleapis.com/instance/firewall/dropped_packets_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "GCP Compute Engine Overview",
|
||||
"description": "Overview of GCP Compute Engine metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor GCP Compute Engine with SigNoz
|
||||
|
||||
Collect key GCP Compute Engine metrics and view them with an out of the box dashboard.
|
||||
@@ -238,7 +238,7 @@
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"title": "Signoz GCP Memorystore Redis overview",
|
||||
"title": "Signoz GCP Memorystory Redis overview",
|
||||
"uploadedGrafana": false,
|
||||
"uuid": "019f85e9-3846-7dc9-9bce-b626b8a3eaff",
|
||||
"variables": {
|
||||
@@ -262,7 +262,7 @@
|
||||
"type": "DYNAMIC"
|
||||
},
|
||||
"dabcf41a-4121-4a03-8738-927576cc90bf": {
|
||||
"allSelected": false,
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"defaultValue": "",
|
||||
"description": "GCP Project ID",
|
||||
@@ -390,7 +390,7 @@
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
"query": "rate({\"redis.googleapis.com/server/uptime\", project_id=\"$project_id\"}[5m])"
|
||||
}
|
||||
],
|
||||
"queryType": "builder",
|
||||
|
||||
@@ -45,17 +45,17 @@ func (m *module) createMany(ctx context.Context, orgID valuer.UUID, kind coretyp
|
||||
return []*tagtypes.Tag{}, nil
|
||||
}
|
||||
|
||||
ordered, toCreate, err := m.resolve(ctx, orgID, kind, postable)
|
||||
toCreate, matched, err := m.resolve(ctx, orgID, kind, postable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// CreateOrGet fills new rows' IDs in place; ordered shares those pointers.
|
||||
if _, err := m.store.CreateOrGet(ctx, toCreate); err != nil {
|
||||
created, err := m.store.CreateOrGet(ctx, toCreate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ordered, nil
|
||||
return append(matched, created...), nil
|
||||
}
|
||||
|
||||
func (m *module) syncLinksForResource(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) error {
|
||||
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
// the existing tags for an org. Lookup is case-insensitive on both key and
|
||||
// value (matching the storage uniqueness rule); when an existing row matches,
|
||||
// its display casing is reused. Inputs are deduped on (LOWER(key), LOWER(value));
|
||||
// the first input's casing wins on collisions. Returns the resolved tags in
|
||||
// request order (deduped) plus the new subset to insert; the new tags share
|
||||
// pointers with the ordered slice, so their IDs populate in place on insert.
|
||||
// the first input's casing wins on collisions. Returns:
|
||||
// - toCreate: new Tag rows the caller should insert (with pre-generated IDs)
|
||||
// - matched: existing rows the caller's input already pointed to. They
|
||||
// already carry authoritative IDs from the store.
|
||||
func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.Kind, postable []tagtypes.PostableTag) ([]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
if len(postable) == 0 {
|
||||
return nil, nil, nil
|
||||
@@ -33,8 +34,8 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
}
|
||||
|
||||
seenInRequestAlready := make(map[string]struct{}, len(postable)) // postable can have the same tag multiple times
|
||||
ordered := make([]*tagtypes.Tag, 0, len(postable))
|
||||
toCreate := make([]*tagtypes.Tag, 0)
|
||||
matched := make([]*tagtypes.Tag, 0)
|
||||
|
||||
for _, p := range postable {
|
||||
key, value, err := tagtypes.ValidatePostableTag(p)
|
||||
@@ -48,13 +49,11 @@ func (m *module) resolve(ctx context.Context, orgID valuer.UUID, kind coretypes.
|
||||
seenInRequestAlready[lookup] = struct{}{}
|
||||
|
||||
if existingTag, ok := lowercaseTagsMap[lookup]; ok {
|
||||
ordered = append(ordered, existingTag)
|
||||
matched = append(matched, existingTag)
|
||||
continue
|
||||
}
|
||||
newTag := tagtypes.NewTag(orgID, kind, key, value)
|
||||
ordered = append(ordered, newTag)
|
||||
toCreate = append(toCreate, newTag)
|
||||
toCreate = append(toCreate, tagtypes.NewTag(orgID, kind, key, value))
|
||||
}
|
||||
|
||||
return ordered, toCreate, nil
|
||||
return toCreate, matched, nil
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
toCreate, matched, err := m.resolve(context.Background(), valuer.GenerateUUID(), testKind, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, ordered)
|
||||
assert.Empty(t, toCreate)
|
||||
assert.Empty(t, matched)
|
||||
assert.Zero(t, store.ListCallCount, "should not hit store when input is empty")
|
||||
})
|
||||
|
||||
t.Run("creates missing pairs and reuses existing, in request order", func(t *testing.T) {
|
||||
t.Run("creates missing pairs and reuses existing", func(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dbTag := tagtypes.NewTag(orgID, testKind, "team", "Pulse")
|
||||
dbTag2 := tagtypes.NewTag(orgID, testKind, "Database", "redis")
|
||||
@@ -35,28 +35,22 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag, dbTag2}
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "events"}, // new
|
||||
{Key: "DATABASE", Value: "REDIS"}, // case-only conflict
|
||||
{Key: "Brand", Value: "New"}, // new
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ordered mirrors the request: new, existing (reused pointer), new.
|
||||
require.Len(t, ordered, 3)
|
||||
assert.Equal(t, "team", ordered[0].Key)
|
||||
assert.Equal(t, "events", ordered[0].Value)
|
||||
assert.Same(t, dbTag2, ordered[1], "case-only conflict reuses the existing pointer with its authoritative ID")
|
||||
assert.Equal(t, "Brand", ordered[2].Key)
|
||||
assert.Equal(t, "New", ordered[2].Value)
|
||||
|
||||
createdLowerKVs := []string{}
|
||||
for _, tg := range toCreate {
|
||||
createdLowerKVs = append(createdLowerKVs, strings.ToLower(tg.Key)+"\x00"+strings.ToLower(tg.Value))
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"team\x00events", "brand\x00new"}, createdLowerKVs,
|
||||
"only the two missing pairs should be returned for insertion")
|
||||
assert.Same(t, ordered[0], toCreate[0], "toCreate shares pointers with ordered so inserted IDs propagate")
|
||||
|
||||
require.Len(t, matched, 1, "DATABASE:REDIS should hit the existing 'Database:redis' tag")
|
||||
assert.Same(t, dbTag2, matched[0], "matched should return the existing pointer with its authoritative ID")
|
||||
})
|
||||
|
||||
t.Run("dedupes inputs that map to the same lower(key)+lower(value)", func(t *testing.T) {
|
||||
@@ -64,14 +58,14 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store := tagtypestest.NewStore()
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "Foo", Value: "Bar"},
|
||||
{Key: "foo", Value: "bar"},
|
||||
{Key: "FOO", Value: "BAR"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ordered, 1, "duplicate inputs must collapse into a single tag")
|
||||
require.Empty(t, matched)
|
||||
require.Len(t, toCreate, 1, "duplicate inputs must collapse into a single insert")
|
||||
assert.Equal(t, "Foo", toCreate[0].Key, "first input's casing wins")
|
||||
assert.Equal(t, "Bar", toCreate[0].Value, "first input's casing wins")
|
||||
@@ -84,15 +78,15 @@ func TestModule_Resolve(t *testing.T) {
|
||||
store.Tags = []*tagtypes.Tag{dbTag}
|
||||
m := &module{store: store}
|
||||
|
||||
ordered, toCreate, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
toCreate, matched, err := m.resolve(context.Background(), orgID, testKind, []tagtypes.PostableTag{
|
||||
{Key: "team", Value: "PULSE"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, toCreate)
|
||||
require.Len(t, ordered, 1)
|
||||
assert.Equal(t, "Team", ordered[0].Key)
|
||||
assert.Equal(t, "Pulse", ordered[0].Value)
|
||||
require.Len(t, matched, 1)
|
||||
assert.Equal(t, "Team", matched[0].Key)
|
||||
assert.Equal(t, "Pulse", matched[0].Value)
|
||||
})
|
||||
|
||||
t.Run("propagates validation error from any input", func(t *testing.T) {
|
||||
|
||||
@@ -44,7 +44,6 @@ func (s *store) ListByResource(ctx context.Context, orgID valuer.UUID, kind core
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id = ?", resourceID).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,7 +71,6 @@ func (s *store) ListByResources(ctx context.Context, orgID valuer.UUID, kind cor
|
||||
Where("tr.kind = ?", kind).
|
||||
Where("tr.resource_id IN (?)", bun.In(resourceIDs)).
|
||||
Where("tag.org_id = ?", orgID).
|
||||
OrderExpr("tr.rank ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -114,14 +112,11 @@ func (s *store) CreateRelations(ctx context.Context, relations []*tagtypes.TagRe
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
// On re-link (same tag, same resource) overwrite rank with the incoming
|
||||
// position, so a reordered tag set persists its new order.
|
||||
_, err := s.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&relations).
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO UPDATE").
|
||||
Set("rank = EXCLUDED.rank").
|
||||
On("CONFLICT (kind, resource_id, tag_id) DO NOTHING").
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -93,12 +93,6 @@ func (s *BaseFilterQueryListener) EnterFunctionCall(ctx *FunctionCallContext) {}
|
||||
// ExitFunctionCall is called when production functionCall is exited.
|
||||
func (s *BaseFilterQueryListener) ExitFunctionCall(ctx *FunctionCallContext) {}
|
||||
|
||||
// EnterSearchCall is called when production searchCall is entered.
|
||||
func (s *BaseFilterQueryListener) EnterSearchCall(ctx *SearchCallContext) {}
|
||||
|
||||
// ExitSearchCall is called when production searchCall is exited.
|
||||
func (s *BaseFilterQueryListener) ExitSearchCall(ctx *SearchCallContext) {}
|
||||
|
||||
// EnterFunctionParamList is called when production functionParamList is entered.
|
||||
func (s *BaseFilterQueryListener) EnterFunctionParamList(ctx *FunctionParamListContext) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -56,10 +56,6 @@ func (v *BaseFilterQueryVisitor) VisitFunctionCall(ctx *FunctionCallContext) int
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitSearchCall(ctx *SearchCallContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitFunctionParamList(ctx *FunctionParamListContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
// Suppress unused import error
|
||||
@@ -50,174 +51,170 @@ func filterquerylexerLexerInit() {
|
||||
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
|
||||
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
|
||||
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
|
||||
"HASALL", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
|
||||
}
|
||||
staticData.RuleNames = []string{
|
||||
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
|
||||
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
|
||||
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
|
||||
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
"HASALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
|
||||
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
}
|
||||
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
|
||||
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
|
||||
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
|
||||
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
|
||||
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
|
||||
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
|
||||
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
|
||||
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
|
||||
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
|
||||
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
|
||||
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
|
||||
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
|
||||
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
|
||||
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
|
||||
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
|
||||
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
|
||||
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
|
||||
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
|
||||
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
|
||||
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
|
||||
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
|
||||
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
|
||||
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
|
||||
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
|
||||
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
|
||||
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
|
||||
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
|
||||
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
|
||||
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
|
||||
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
|
||||
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
|
||||
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
|
||||
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
|
||||
0, 0, 38, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
|
||||
7, 36, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5,
|
||||
1, 5, 1, 5, 3, 5, 89, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1,
|
||||
8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1,
|
||||
12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14,
|
||||
1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1,
|
||||
15, 1, 15, 3, 15, 132, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16,
|
||||
1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 149,
|
||||
8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1,
|
||||
20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22,
|
||||
1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1,
|
||||
24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25,
|
||||
1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 201,
|
||||
8, 26, 1, 27, 1, 27, 1, 28, 3, 28, 206, 8, 28, 1, 28, 4, 28, 209, 8, 28,
|
||||
11, 28, 12, 28, 210, 1, 28, 1, 28, 5, 28, 215, 8, 28, 10, 28, 12, 28, 218,
|
||||
9, 28, 3, 28, 220, 8, 28, 1, 28, 1, 28, 3, 28, 224, 8, 28, 1, 28, 4, 28,
|
||||
227, 8, 28, 11, 28, 12, 28, 228, 3, 28, 231, 8, 28, 1, 28, 3, 28, 234,
|
||||
8, 28, 1, 28, 1, 28, 4, 28, 238, 8, 28, 11, 28, 12, 28, 239, 1, 28, 1,
|
||||
28, 3, 28, 244, 8, 28, 1, 28, 4, 28, 247, 8, 28, 11, 28, 12, 28, 248, 3,
|
||||
28, 251, 8, 28, 3, 28, 253, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 5, 29, 259,
|
||||
8, 29, 10, 29, 12, 29, 262, 9, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 5,
|
||||
29, 269, 8, 29, 10, 29, 12, 29, 272, 9, 29, 1, 29, 3, 29, 275, 8, 29, 1,
|
||||
30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 31, 1, 31,
|
||||
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1,
|
||||
33, 1, 33, 4, 33, 298, 8, 33, 11, 33, 12, 33, 299, 5, 33, 302, 8, 33, 10,
|
||||
33, 12, 33, 305, 9, 33, 1, 34, 4, 34, 308, 8, 34, 11, 34, 12, 34, 309,
|
||||
1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 317, 8, 36, 11, 36, 12, 36, 318,
|
||||
0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
|
||||
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
|
||||
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
|
||||
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
|
||||
75, 33, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0,
|
||||
75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84,
|
||||
84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88,
|
||||
88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71,
|
||||
71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79,
|
||||
111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104,
|
||||
104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102,
|
||||
102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92,
|
||||
4, 0, 35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64,
|
||||
90, 95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
|
||||
8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 353,
|
||||
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
|
||||
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
|
||||
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
|
||||
0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1,
|
||||
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
|
||||
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
|
||||
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
|
||||
0, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
|
||||
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
|
||||
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
|
||||
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
|
||||
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
|
||||
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
|
||||
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
|
||||
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
|
||||
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
|
||||
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
|
||||
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
|
||||
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
|
||||
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
|
||||
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
|
||||
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
|
||||
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
|
||||
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
|
||||
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
|
||||
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
|
||||
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
|
||||
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
|
||||
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
|
||||
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
|
||||
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
|
||||
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
|
||||
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
|
||||
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
|
||||
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
|
||||
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
|
||||
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
|
||||
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
|
||||
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
|
||||
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
|
||||
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
|
||||
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
|
||||
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
|
||||
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
|
||||
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
|
||||
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
|
||||
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
|
||||
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
|
||||
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
|
||||
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
|
||||
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
|
||||
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
|
||||
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
|
||||
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
|
||||
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
|
||||
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
|
||||
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
|
||||
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
|
||||
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
|
||||
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
|
||||
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
|
||||
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
|
||||
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
|
||||
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
|
||||
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
|
||||
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
|
||||
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
|
||||
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
|
||||
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
|
||||
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
|
||||
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
|
||||
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
|
||||
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
|
||||
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
|
||||
3, 73, 36, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1,
|
||||
0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
|
||||
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
|
||||
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
|
||||
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
|
||||
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
|
||||
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
|
||||
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
|
||||
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
|
||||
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
|
||||
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
|
||||
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
|
||||
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
|
||||
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
|
||||
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
|
||||
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
|
||||
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
|
||||
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
|
||||
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
|
||||
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
|
||||
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
|
||||
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
|
||||
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
|
||||
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
|
||||
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
|
||||
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
|
||||
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 0, 0, 316,
|
||||
315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319,
|
||||
1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
|
||||
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
|
||||
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
|
||||
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
|
||||
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
|
||||
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
|
||||
0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69, 31, 71, 0, 73, 32,
|
||||
1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75,
|
||||
107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116,
|
||||
116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120,
|
||||
120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103,
|
||||
103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111,
|
||||
2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2,
|
||||
0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2,
|
||||
0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 35,
|
||||
36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90, 95,
|
||||
95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0,
|
||||
9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 344, 0,
|
||||
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
|
||||
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
|
||||
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0,
|
||||
0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0,
|
||||
0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1,
|
||||
0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47,
|
||||
1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0,
|
||||
57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0,
|
||||
0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0, 5, 79, 1, 0, 0,
|
||||
0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0, 0, 0, 13, 90, 1,
|
||||
0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98, 1, 0, 0, 0, 21,
|
||||
101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 111, 1,
|
||||
0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133, 1, 0, 0, 0,
|
||||
35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0, 0, 41, 157,
|
||||
1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173, 1, 0, 0,
|
||||
0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0, 0, 55, 202,
|
||||
1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276, 1, 0, 0,
|
||||
0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 307,
|
||||
1, 0, 0, 0, 71, 313, 1, 0, 0, 0, 73, 316, 1, 0, 0, 0, 75, 76, 5, 40, 0,
|
||||
0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0, 79, 80, 5,
|
||||
91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1, 0, 0, 0, 83,
|
||||
84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0, 86, 87, 5, 61,
|
||||
0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89,
|
||||
12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0, 92, 14, 1, 0,
|
||||
0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1, 0, 0, 0, 96,
|
||||
97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61,
|
||||
0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 22, 1, 0, 0, 0,
|
||||
103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24, 1, 0, 0, 0, 106,
|
||||
107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2, 0, 0, 109, 110,
|
||||
7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 0,
|
||||
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115, 116, 7, 3, 0, 0,
|
||||
116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7, 3, 0, 0, 119, 120,
|
||||
7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0, 0, 122, 123, 7, 3,
|
||||
0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125, 126, 7, 3, 0, 0,
|
||||
126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129, 7, 9, 0, 0, 129,
|
||||
131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0, 0, 0, 131, 132,
|
||||
1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0, 134, 135, 7, 3,
|
||||
0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 8, 0, 0,
|
||||
138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141, 7, 13, 0, 0, 141,
|
||||
142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7, 5, 0, 0, 144, 145,
|
||||
7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0, 0, 147, 149, 7, 9,
|
||||
0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 36, 1, 0, 0, 0,
|
||||
150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1, 0, 0, 0, 153, 154,
|
||||
7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0, 0, 156, 40, 1, 0,
|
||||
0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159, 160, 7, 16, 0,
|
||||
0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163, 7, 10, 0, 0, 163,
|
||||
44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15, 0, 0, 166, 167,
|
||||
7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0, 169, 170, 7, 2,
|
||||
0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172, 46, 1, 0, 0, 0,
|
||||
173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176,
|
||||
48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
|
||||
7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0, 182, 183, 7, 18,
|
||||
0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185, 186, 7, 15, 0, 0,
|
||||
186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 0, 0, 0, 189,
|
||||
190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5, 0, 0, 192, 193, 7,
|
||||
10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0, 0, 195, 196, 7, 20,
|
||||
0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0, 198, 199, 7, 9, 0, 0,
|
||||
199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200, 195, 1, 0, 0, 0, 201,
|
||||
54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1, 0, 0, 0, 204, 206, 3,
|
||||
55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 208, 1, 0,
|
||||
0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0,
|
||||
0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 219, 1, 0, 0, 0, 212,
|
||||
216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213, 1, 0, 0, 0, 215, 218,
|
||||
1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 220, 1, 0,
|
||||
0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0,
|
||||
220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222, 224, 3, 55, 27, 0, 223,
|
||||
222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 1, 0, 0, 0, 225, 227,
|
||||
3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1,
|
||||
0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0, 0, 230, 221, 1, 0, 0,
|
||||
0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232, 234, 3, 55, 27, 0,
|
||||
233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235,
|
||||
237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1, 0, 0, 0, 238, 239,
|
||||
1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0,
|
||||
0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0, 243, 242, 1, 0, 0,
|
||||
0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 71, 35, 0,
|
||||
246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248,
|
||||
249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251,
|
||||
1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0, 252, 233, 1, 0,
|
||||
0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255, 259, 8, 22, 0, 0,
|
||||
256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255, 1, 0, 0, 0, 258,
|
||||
256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261,
|
||||
1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 275, 5, 34,
|
||||
0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266, 267, 5, 92, 0,
|
||||
0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266, 1, 0, 0, 0, 269,
|
||||
272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273,
|
||||
1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0, 274, 254, 1, 0,
|
||||
0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280, 7, 24, 0, 0,
|
||||
277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280,
|
||||
278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0, 0, 282, 280, 1,
|
||||
0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0, 285, 64, 1, 0, 0,
|
||||
0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288, 289, 5, 93, 0, 0,
|
||||
289, 66, 1, 0, 0, 0, 290, 303, 3, 61, 30, 0, 291, 292, 5, 46, 0, 0, 292,
|
||||
302, 3, 61, 30, 0, 293, 302, 3, 63, 31, 0, 294, 302, 3, 65, 32, 0, 295,
|
||||
297, 5, 46, 0, 0, 296, 298, 3, 71, 35, 0, 297, 296, 1, 0, 0, 0, 298, 299,
|
||||
1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0,
|
||||
0, 0, 301, 291, 1, 0, 0, 0, 301, 293, 1, 0, 0, 0, 301, 294, 1, 0, 0, 0,
|
||||
301, 295, 1, 0, 0, 0, 302, 305, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303,
|
||||
304, 1, 0, 0, 0, 304, 68, 1, 0, 0, 0, 305, 303, 1, 0, 0, 0, 306, 308, 7,
|
||||
26, 0, 0, 307, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 307, 1, 0, 0,
|
||||
0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 6, 34, 0, 0, 312,
|
||||
70, 1, 0, 0, 0, 313, 314, 7, 27, 0, 0, 314, 72, 1, 0, 0, 0, 315, 317, 8,
|
||||
28, 0, 0, 316, 315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0,
|
||||
0, 318, 319, 1, 0, 0, 0, 319, 74, 1, 0, 0, 0, 29, 0, 88, 131, 148, 200,
|
||||
205, 210, 216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260,
|
||||
268, 270, 274, 280, 299, 301, 303, 309, 318, 1, 6, 0, 0,
|
||||
}
|
||||
deserializer := antlr.NewATNDeserializer(nil)
|
||||
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
|
||||
@@ -284,11 +281,10 @@ const (
|
||||
FilterQueryLexerHAS = 24
|
||||
FilterQueryLexerHASANY = 25
|
||||
FilterQueryLexerHASALL = 26
|
||||
FilterQueryLexerSEARCH = 27
|
||||
FilterQueryLexerBOOL = 28
|
||||
FilterQueryLexerNUMBER = 29
|
||||
FilterQueryLexerQUOTED_TEXT = 30
|
||||
FilterQueryLexerKEY = 31
|
||||
FilterQueryLexerWS = 32
|
||||
FilterQueryLexerFREETEXT = 33
|
||||
FilterQueryLexerBOOL = 27
|
||||
FilterQueryLexerNUMBER = 28
|
||||
FilterQueryLexerQUOTED_TEXT = 29
|
||||
FilterQueryLexerKEY = 30
|
||||
FilterQueryLexerWS = 31
|
||||
FilterQueryLexerFREETEXT = 32
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -44,9 +44,6 @@ type FilterQueryListener interface {
|
||||
// EnterFunctionCall is called when entering the functionCall production.
|
||||
EnterFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// EnterSearchCall is called when entering the searchCall production.
|
||||
EnterSearchCall(c *SearchCallContext)
|
||||
|
||||
// EnterFunctionParamList is called when entering the functionParamList production.
|
||||
EnterFunctionParamList(c *FunctionParamListContext)
|
||||
|
||||
@@ -98,9 +95,6 @@ type FilterQueryListener interface {
|
||||
// ExitFunctionCall is called when exiting the functionCall production.
|
||||
ExitFunctionCall(c *FunctionCallContext)
|
||||
|
||||
// ExitSearchCall is called when exiting the searchCall production.
|
||||
ExitSearchCall(c *SearchCallContext)
|
||||
|
||||
// ExitFunctionParamList is called when exiting the functionParamList production.
|
||||
ExitFunctionParamList(c *FunctionParamListContext)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
|
||||
|
||||
package parser // FilterQuery
|
||||
|
||||
@@ -44,9 +44,6 @@ type FilterQueryVisitor interface {
|
||||
// Visit a parse tree produced by FilterQueryParser#functionCall.
|
||||
VisitFunctionCall(ctx *FunctionCallContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#searchCall.
|
||||
VisitSearchCall(ctx *SearchCallContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#functionParamList.
|
||||
VisitFunctionParamList(ctx *FunctionParamListContext) interface{}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ var friendly = map[string]string{
|
||||
"BETWEEN": "BETWEEN", "IN": "IN", "EXISTS": "EXISTS",
|
||||
"REGEXP": "REGEXP", "CONTAINS": "CONTAINS",
|
||||
"HAS": "has()", "HASANY": "hasAny()", "HASALL": "hasAll()",
|
||||
"HASTOKEN": "hasToken()", "SEARCH": "search()",
|
||||
"HASTOKEN": "hasToken()",
|
||||
|
||||
// literals / identifiers
|
||||
"NUMBER": "number",
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
type FieldConstraint struct {
|
||||
Field string
|
||||
Operator qbtypes.FilterOperator
|
||||
Value any
|
||||
Values []any // For IN, NOT IN operations
|
||||
Value interface{}
|
||||
Values []interface{} // For IN, NOT IN operations
|
||||
}
|
||||
|
||||
// ConstraintSet represents a set of constraints that must all be true (AND).
|
||||
@@ -103,7 +103,7 @@ func (d *LogicalContradictionDetector) popNotContext() {
|
||||
}
|
||||
|
||||
// Visit dispatches to the appropriate visit method.
|
||||
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
|
||||
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) interface{} {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
|
||||
}
|
||||
|
||||
// VisitQuery is the entry point.
|
||||
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) interface{} {
|
||||
d.Visit(ctx.Expression())
|
||||
// Check final constraints
|
||||
d.checkContradictions(d.currentConstraints())
|
||||
@@ -119,12 +119,12 @@ func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any
|
||||
}
|
||||
|
||||
// VisitExpression just passes through to OrExpression.
|
||||
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) interface{} {
|
||||
return d.Visit(ctx.OrExpression())
|
||||
}
|
||||
|
||||
// VisitOrExpression handles OR logic.
|
||||
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) interface{} {
|
||||
andExpressions := ctx.AllAndExpression()
|
||||
|
||||
if len(andExpressions) == 1 {
|
||||
@@ -149,7 +149,7 @@ func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressi
|
||||
}
|
||||
|
||||
// VisitAndExpression handles AND logic (including implicit AND).
|
||||
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) interface{} {
|
||||
unaryExpressions := ctx.AllUnaryExpression()
|
||||
|
||||
// Visit each unary expression, accumulating constraints
|
||||
@@ -161,7 +161,7 @@ func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpres
|
||||
}
|
||||
|
||||
// VisitUnaryExpression handles NOT operator.
|
||||
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) interface{} {
|
||||
hasNot := ctx.NOT() != nil
|
||||
|
||||
if hasNot {
|
||||
@@ -180,7 +180,7 @@ func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryEx
|
||||
}
|
||||
|
||||
// VisitPrimary handles different primary expressions.
|
||||
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) interface{} {
|
||||
if ctx.OrExpression() != nil {
|
||||
// Parenthesized expression
|
||||
// If we're in an AND context, we continue with the same constraint set
|
||||
@@ -191,9 +191,6 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
// Handle function calls if needed
|
||||
return nil
|
||||
} else if ctx.SearchCall() != nil {
|
||||
// search() spans all fields; it can never be a logical contradiction
|
||||
return nil
|
||||
} else if ctx.FullText() != nil {
|
||||
// Handle full text search if needed
|
||||
return nil
|
||||
@@ -203,7 +200,7 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
|
||||
}
|
||||
|
||||
// VisitComparison extracts constraints from comparisons.
|
||||
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) interface{} {
|
||||
if ctx.Key() == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -277,7 +274,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
|
||||
constraint := FieldConstraint{
|
||||
Field: field,
|
||||
Operator: operator,
|
||||
Values: []any{val1, val2},
|
||||
Values: []interface{}{val1, val2},
|
||||
}
|
||||
d.addConstraint(constraint)
|
||||
}
|
||||
@@ -346,7 +343,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
|
||||
}
|
||||
|
||||
// extractValue extracts the actual value from a ValueContext.
|
||||
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) any {
|
||||
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) interface{} {
|
||||
if ctx.QUOTED_TEXT() != nil {
|
||||
text := ctx.QUOTED_TEXT().GetText()
|
||||
// Remove quotes
|
||||
@@ -365,12 +362,12 @@ func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) a
|
||||
}
|
||||
|
||||
// extractValueList extracts values from a ValueListContext.
|
||||
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []any {
|
||||
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []interface{} {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := []any{}
|
||||
values := []interface{}{}
|
||||
for _, val := range ctx.AllValue() {
|
||||
values = append(values, d.extractValue(val))
|
||||
}
|
||||
@@ -766,7 +763,7 @@ func (d *LogicalContradictionDetector) checkRangeContradictions(constraints []Fi
|
||||
}
|
||||
|
||||
// valuesSatisfyRanges checks if a value satisfies all range constraints.
|
||||
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraints []FieldConstraint) bool {
|
||||
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value interface{}, constraints []FieldConstraint) bool {
|
||||
val, err := parseNumericValue(value)
|
||||
if err != nil {
|
||||
return true // If not numeric, we can't check
|
||||
@@ -802,7 +799,7 @@ func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraint
|
||||
}
|
||||
|
||||
// valueSatisfiesBetween checks if a value is within a BETWEEN range.
|
||||
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value any, between FieldConstraint) bool {
|
||||
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value interface{}, between FieldConstraint) bool {
|
||||
if len(between.Values) != 2 {
|
||||
return false
|
||||
}
|
||||
@@ -851,7 +848,7 @@ func (d *LogicalContradictionDetector) cloneConstraintSet(set *ConstraintSet) *C
|
||||
}
|
||||
|
||||
// parseNumericValue attempts to parse a value as a number.
|
||||
func parseNumericValue(value any) (float64, error) {
|
||||
func parseNumericValue(value interface{}) (float64, error) {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
|
||||
@@ -203,8 +203,6 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
|
||||
return v.VisitValueList(t)
|
||||
case *grammar.FullTextContext:
|
||||
return v.VisitFullText(t)
|
||||
case *grammar.SearchCallContext:
|
||||
return v.VisitSearchCall(t)
|
||||
case *grammar.FunctionCallContext:
|
||||
return v.VisitFunctionCall(t)
|
||||
case *grammar.FunctionParamListContext:
|
||||
@@ -319,8 +317,6 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
return v.Visit(ctx.Comparison())
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
return v.Visit(ctx.FunctionCall())
|
||||
} else if ctx.SearchCall() != nil {
|
||||
return v.Visit(ctx.SearchCall())
|
||||
} else if ctx.FullText() != nil {
|
||||
return v.Visit(ctx.FullText())
|
||||
}
|
||||
@@ -776,13 +772,6 @@ func normalizeFunctionValue(operator qbtypes.FilterOperator, functionName string
|
||||
return valueParams, nil
|
||||
}
|
||||
|
||||
// VisitSearchCall handles search('needle'). The search() function is parsed but
|
||||
// not yet implemented; reject it with a clear invalid-input error.
|
||||
func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
|
||||
v.errors = append(v.errors, "function `search` is not yet supported")
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
// VisitFunctionParamList handles the parameter list for function calls.
|
||||
func (v *filterExpressionVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
|
||||
params := ctx.AllFunctionParam()
|
||||
|
||||
@@ -221,7 +221,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTagRelationRankFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addTagRelationRank struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddTagRelationRankFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_tag_relation_rank"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTagRelationRank{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("tag_relation"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rankColumn := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("rank"),
|
||||
DataType: sqlschema.DataTypeInteger,
|
||||
Nullable: false,
|
||||
}
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, rankColumn, 0)
|
||||
|
||||
// AddColumn recreates the table for a NOT NULL column on SQLite, which drops
|
||||
// standalone indices. GetTable only reports inline table constraints, so
|
||||
// restore the standalone unique index from migration 082.
|
||||
sqls = append(sqls, migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: sqlschema.TableName("tag_relation"),
|
||||
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
|
||||
})...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTagRelationRank) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Single word",
|
||||
query: "<script>alert('xss')</script>",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '<'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '<'",
|
||||
},
|
||||
|
||||
// Single word searches with spaces
|
||||
@@ -181,7 +181,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Special characters",
|
||||
query: "[tracing]",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '['",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '['",
|
||||
},
|
||||
{
|
||||
category: "Special characters",
|
||||
@@ -211,7 +211,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
category: "Special characters",
|
||||
query: "ERROR: cannot execute update() in a read-only context",
|
||||
shouldPass: false,
|
||||
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got ')'",
|
||||
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got ')'",
|
||||
},
|
||||
{
|
||||
category: "Special characters",
|
||||
@@ -633,7 +633,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -641,7 +641,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -649,7 +649,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF",
|
||||
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -657,7 +657,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'like'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'like'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -665,7 +665,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -673,7 +673,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -681,7 +681,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'exists'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'exists'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -689,7 +689,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'regexp'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'regexp'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -697,7 +697,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'contains'",
|
||||
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'contains'",
|
||||
},
|
||||
{
|
||||
category: "Keyword conflict",
|
||||
@@ -2052,9 +2052,9 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
|
||||
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'AND'"},
|
||||
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'OR'"},
|
||||
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF"},
|
||||
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
|
||||
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'OR'"},
|
||||
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF"},
|
||||
|
||||
{category: "Only functions", query: "has", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
|
||||
{category: "Only functions", query: "hasAny", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
|
||||
@@ -2196,7 +2196,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2204,7 +2204,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2212,7 +2212,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '='",
|
||||
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '='",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2220,7 +2220,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
|
||||
},
|
||||
{
|
||||
category: "Operator keywords as keys",
|
||||
@@ -2228,7 +2228,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
shouldPass: false,
|
||||
expectedQuery: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
|
||||
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
|
||||
},
|
||||
|
||||
// Using function keywords as keys
|
||||
|
||||
@@ -43,6 +43,7 @@ var (
|
||||
// GCP services.
|
||||
GCPServiceCloudSQLPostgres = ServiceID{valuer.NewString("cloudsql_postgres")}
|
||||
GCPServiceMemorystoreRedis = ServiceID{valuer.NewString("memorystore_redis")}
|
||||
GCPServiceComputeEngine = ServiceID{valuer.NewString("computeengine")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -76,6 +77,7 @@ func (ServiceID) Enum() []any {
|
||||
AzureServiceRedis,
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +117,7 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
CloudProviderTypeGCP: {
|
||||
GCPServiceCloudSQLPostgres,
|
||||
GCPServiceMemorystoreRedis,
|
||||
GCPServiceComputeEngine,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -54,12 +54,10 @@ func newTestDashboardV2(t *testing.T, orgID valuer.UUID, source Source) *Dashboa
|
||||
},
|
||||
},
|
||||
},
|
||||
Links: []Link{},
|
||||
},
|
||||
},
|
||||
},
|
||||
Layouts: []Layout{},
|
||||
Links: []Link{},
|
||||
}
|
||||
|
||||
return &DashboardV2{
|
||||
|
||||
@@ -26,7 +26,7 @@ type DashboardSpec struct {
|
||||
Layouts []Layout `json:"layouts" required:"true" nullable:"false"`
|
||||
Duration common.DurationString `json:"duration"`
|
||||
RefreshInterval common.DurationString `json:"refreshInterval"`
|
||||
Links []Link `json:"links" required:"true" nullable:"false"`
|
||||
Links []Link `json:"links,omitzero"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -45,16 +45,6 @@ func (d *DashboardSpec) UnmarshalJSON(data []byte) error {
|
||||
return d.Validate()
|
||||
}
|
||||
|
||||
// validateLinks rejects a missing/null spec.links value: a typed client must
|
||||
// send [] rather than omitting links, so its value round-trips faithfully.
|
||||
// Panel links are the panel spec's concern, validated in validatePanels.
|
||||
func (d *DashboardSpec) validateLinks() error {
|
||||
if d.Links == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.links is required; send [] when there are no links")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Cross-field validation
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -63,9 +53,6 @@ func (d *DashboardSpec) Validate() error {
|
||||
if err := d.Display.Validate("dashboard", "spec.display.name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.validateLinks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.validateVariables(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -117,9 +104,6 @@ func (d *DashboardSpec) validatePanels() error {
|
||||
if err := panel.Spec.Display.Validate("panel", path+".spec.display.name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := panel.Spec.validateLinks(path); err != nil {
|
||||
return err
|
||||
}
|
||||
panelKind := panel.Spec.Plugin.Kind
|
||||
if len(panel.Spec.Queries) != 1 {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query", path)
|
||||
|
||||
@@ -23,7 +23,6 @@ const basePostableJSON = `{
|
||||
"tags": [{"key": "team", "value": "alpha"}, {"key": "env", "value": "prod"}],
|
||||
"spec": {
|
||||
"display": {"name": "Service overview"},
|
||||
"links": [],
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
@@ -42,7 +41,6 @@ const basePostableJSON = `{
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -66,7 +64,6 @@ const basePostableJSON = `{
|
||||
"p2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/NumberPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -185,7 +182,6 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -220,7 +216,6 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/BarChartPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -332,7 +327,7 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
// Appending needs a not-yet-placed panel, so add one in the same patch;
|
||||
// re-placing p1 or p2 would be a duplicate reference.
|
||||
out, err := decode(t, `[
|
||||
{"op": "add", "path": "/spec/panels/p3", "value": {"kind": "Panel", "spec": {"links": [], "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}},
|
||||
{"op": "add", "path": "/spec/panels/p3", "value": {"kind": "Panel", "spec": {"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}},
|
||||
{"op": "add", "path": "/spec/layouts/0/spec/items/-", "value": {"x": 0, "y": 6, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p3"}}}
|
||||
]`).Apply(base)
|
||||
require.NoError(t, err)
|
||||
@@ -351,7 +346,6 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -502,7 +496,7 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"path": "/spec/panels/p1",
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {"links": [], "plugin": {"kind": "signoz/NotAPanel", "spec": {}}}
|
||||
"spec": {"plugin": {"kind": "signoz/NotAPanel", "spec": {}}}
|
||||
}
|
||||
}]`).Apply(base)
|
||||
require.Error(t, err)
|
||||
@@ -518,7 +512,6 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/ListPanel", "spec": {}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
@@ -544,7 +537,6 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
|
||||
"value": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
|
||||
@@ -51,12 +51,10 @@ func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "NonExistentPanel", "spec": {}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
|
||||
@@ -78,7 +76,7 @@ func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) {
|
||||
|
||||
func TestValidateEmptySpec(t *testing.T) {
|
||||
// no variables no panels
|
||||
data := []byte(`{"links": []}`)
|
||||
data := []byte(`{}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
assert.NoError(t, err, "expected valid")
|
||||
}
|
||||
@@ -109,7 +107,6 @@ func TestValidateOnlyVariables(t *testing.T) {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -136,7 +133,6 @@ func TestInvalidateDuplicateVariableNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -161,7 +157,6 @@ func TestInvalidateVariableNameWithInvalidChars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
@@ -191,7 +186,6 @@ func TestInvalidatePanelKey(t *testing.T) {
|
||||
"bad key!": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -202,7 +196,6 @@ func TestInvalidatePanelKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -226,7 +219,6 @@ func TestInvalidateListVariableCrossFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
@@ -290,7 +282,6 @@ func TestInvalidateEmptyVariableName(t *testing.T) {
|
||||
cases := map[string][]byte{
|
||||
"text variable": []byte(`{
|
||||
"variables": [{"kind": "TextVariable", "spec": {"name": "", "value": "x"}}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`),
|
||||
"list variable": []byte(`{
|
||||
@@ -303,7 +294,6 @@ func TestInvalidateEmptyVariableName(t *testing.T) {
|
||||
"plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}
|
||||
}
|
||||
}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`),
|
||||
}
|
||||
@@ -329,12 +319,10 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "NonExistentPanel", "spec": {}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "NonExistentPanel",
|
||||
@@ -350,7 +338,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "unknown panel kind",
|
||||
@@ -362,7 +349,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -373,7 +359,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "FakeQueryPlugin",
|
||||
@@ -385,7 +370,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "TimeSeriesQuery",
|
||||
@@ -396,7 +380,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "unknown request type",
|
||||
@@ -408,7 +391,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "",
|
||||
@@ -419,7 +401,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "unknown request type",
|
||||
@@ -436,7 +417,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"plugin": {"kind": "FakeVariable", "spec": {}}
|
||||
}
|
||||
}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "FakeVariable",
|
||||
@@ -450,7 +430,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
"plugin": {"kind": "FakeDatasource", "spec": {}}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "FakeDatasource",
|
||||
@@ -471,14 +450,13 @@ func TestInvalidateOneInvalidPanel(t *testing.T) {
|
||||
"panels": {
|
||||
"good": {
|
||||
"kind": "Panel",
|
||||
"spec": {"links": [],"plugin": {"kind": "signoz/NumberPanel", "spec": {}}}
|
||||
"spec": {"plugin": {"kind": "signoz/NumberPanel", "spec": {}}}
|
||||
},
|
||||
"bad": {
|
||||
"kind": "Panel",
|
||||
"spec": {"links": [],"plugin": {"kind": "FakePanel", "spec": {}}}
|
||||
"spec": {"plugin": {"kind": "FakePanel", "spec": {}}}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -491,7 +469,6 @@ func TestInvalidateLayoutPanelReferences(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -503,7 +480,7 @@ func TestInvalidateLayoutPanelReferences(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
layout := func(items string) []byte {
|
||||
return []byte(`{` + validPanels + `, "links": [], "layouts": [{"kind": "Grid", "spec": {"items": [` + items + `]}}]}`)
|
||||
return []byte(`{` + validPanels + `, "layouts": [{"kind": "Grid", "spec": {"items": [` + items + `]}}]}`)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -559,7 +536,6 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"bogusField": true}
|
||||
@@ -567,7 +543,6 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "bogusField",
|
||||
@@ -579,7 +554,6 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -593,7 +567,6 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "unknownThing",
|
||||
@@ -613,7 +586,6 @@ func TestRejectUnknownFieldsInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "extraField",
|
||||
@@ -642,7 +614,6 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"visualization": {"fillSpans": "notabool"}}
|
||||
@@ -650,7 +621,6 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "fillSpans",
|
||||
@@ -662,7 +632,6 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{
|
||||
"kind": "time_series",
|
||||
@@ -676,7 +645,6 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "",
|
||||
@@ -696,7 +664,6 @@ func TestInvalidateWrongFieldTypeInPluginSpec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "",
|
||||
@@ -727,7 +694,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {}
|
||||
@@ -744,7 +710,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "signal",
|
||||
@@ -756,7 +721,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"chartAppearance": {"lineInterpolation": "cubic"}}
|
||||
@@ -764,7 +728,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "line interpolation",
|
||||
@@ -776,7 +739,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"chartAppearance": {"lineStyle": "dotted"}}
|
||||
@@ -784,7 +746,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "line style",
|
||||
@@ -796,7 +757,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"chartAppearance": {"fillMode": "striped"}}
|
||||
@@ -804,7 +764,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "fill mode",
|
||||
@@ -816,7 +775,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"chartAppearance": {"spanGaps": {"fillOnlyBelow": true, "fillLessThan": "notaduration"}}}
|
||||
@@ -824,7 +782,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "duration",
|
||||
@@ -836,7 +793,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"visualization": {"timePreference": "last2Hr"}}
|
||||
@@ -844,7 +800,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "timePreference",
|
||||
@@ -856,7 +811,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/BarChartPanel",
|
||||
"spec": {"legend": {"position": "top"}}
|
||||
@@ -864,7 +818,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "legend position",
|
||||
@@ -876,7 +829,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/BarChartPanel",
|
||||
"spec": {"legend": {"mode": "grid"}}
|
||||
@@ -884,7 +836,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "legend mode",
|
||||
@@ -896,7 +847,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {"thresholds": [{"value": 100, "operator": "above", "color": "Red", "format": "Color"}]}
|
||||
@@ -904,7 +854,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "threshold format",
|
||||
@@ -916,7 +865,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {"thresholds": [{"value": 100, "operator": "!=", "color": "Red", "format": "text"}]}
|
||||
@@ -924,7 +872,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "comparison operator",
|
||||
@@ -936,7 +883,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"formatting": {"decimalPrecision": "9"}}
|
||||
@@ -944,7 +890,6 @@ func TestInvalidateBadPanelSpecValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`,
|
||||
wantContain: "precision",
|
||||
@@ -976,13 +921,11 @@ func TestThresholdLabelOptional(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {"thresholds": [` + tt.threshold + `]}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
@@ -1000,10 +943,9 @@ func TestInvalidatePanelWithoutQueries(t *testing.T) {
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {"links": [],"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}}}
|
||||
"spec": {"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}}}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -1017,13 +959,11 @@ func TestInvalidatePanelWithEmptyQueriesArray(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -1039,7 +979,6 @@ func TestInvalidatePanelWithMultipleDirectQueries(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "metrics"}}}},
|
||||
@@ -1048,7 +987,6 @@ func TestInvalidatePanelWithMultipleDirectQueries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
@@ -1068,7 +1006,6 @@ func TestValidateRequiredFields(t *testing.T) {
|
||||
"plugin": {"kind": "` + pluginKind + `", "spec": ` + pluginSpec + `}
|
||||
}
|
||||
}],
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`
|
||||
}
|
||||
@@ -1078,12 +1015,10 @@ func TestValidateRequiredFields(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "` + panelKind + `", "spec": ` + panelSpec + `}
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`
|
||||
}
|
||||
@@ -1148,7 +1083,6 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {}
|
||||
@@ -1157,7 +1091,6 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
@@ -1200,7 +1133,6 @@ func TestNumberPanelDefaults(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {"thresholds": [{"value": 100, "color": "Red"}]}
|
||||
@@ -1209,7 +1141,6 @@ func TestNumberPanelDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
d, err := unmarshalDashboard(data)
|
||||
@@ -1263,7 +1194,6 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {}
|
||||
@@ -1274,7 +1204,6 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
"p2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {"thresholds": [{"value": 100, "color": "Red"}]}
|
||||
@@ -1283,7 +1212,6 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
|
||||
@@ -1344,7 +1272,7 @@ func TestStorageRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPostableDashboardV2GenerateNameFlag(t *testing.T) {
|
||||
const validSpec = `"spec": {"panels": {}, "layouts": [], "links": []}`
|
||||
const validSpec = `"spec": {"panels": {}, "layouts": []}`
|
||||
|
||||
tests := []struct {
|
||||
scenario string
|
||||
@@ -1356,13 +1284,13 @@ func TestPostableDashboardV2GenerateNameFlag(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "flag true with display.name derives name on conversion",
|
||||
body: `{"schemaVersion":"` + SchemaVersion + `","generateName":true,"spec":{"display":{"name":"My Dashboard!"},"panels":{},"layouts":[],"links":[]}}`,
|
||||
body: `{"schemaVersion":"` + SchemaVersion + `","generateName":true,"spec":{"display":{"name":"My Dashboard!"},"panels":{},"layouts":[]}}`,
|
||||
wantName: "",
|
||||
wantDisplay: "My Dashboard!",
|
||||
},
|
||||
{
|
||||
scenario: "flag true with non-empty name is rejected",
|
||||
body: `{"schemaVersion":"` + SchemaVersion + `","name":"already-set","generateName":true,"spec":{"display":{"name":"My Dashboard"},"panels":{},"layouts":[],"links":[]}}`,
|
||||
body: `{"schemaVersion":"` + SchemaVersion + `","name":"already-set","generateName":true,"spec":{"display":{"name":"My Dashboard"},"panels":{},"layouts":[]}}`,
|
||||
wantErr: true,
|
||||
wantErrMatch: "name must be empty when generateName is true",
|
||||
},
|
||||
@@ -1522,24 +1450,20 @@ func TestPanelTypeQueryTypeCompatibility(t *testing.T) {
|
||||
mkQuery := func(panelKind, queryKind, querySpec string) []byte {
|
||||
return []byte(`{
|
||||
"panels": {"p1": {"kind": "Panel", "spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "` + panelKind + `", "spec": {}},
|
||||
"queries": [{"kind": "` + requestKind(panelKind) + `", "spec": {"plugin": {"kind": "` + queryKind + `", "spec": ` + querySpec + `}}}]
|
||||
}}},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
mkComposite := func(panelKind, subType, subSpec string) []byte {
|
||||
return []byte(`{
|
||||
"panels": {"p1": {"kind": "Panel", "spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "` + panelKind + `", "spec": {}},
|
||||
"queries": [{"kind": "` + requestKind(panelKind) + `", "spec": {"plugin": {"kind": "signoz/CompositeQuery", "spec": {
|
||||
"queries": [{"type": "` + subType + `", "spec": ` + subSpec + `}]
|
||||
}}}}]
|
||||
}}},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
@@ -1583,13 +1507,11 @@ func TestCommaSeparatedAggregationRejectedOnWrite(t *testing.T) {
|
||||
buildDashboardWithLogsAggregation := func(aggregationsJSON string) []byte {
|
||||
return []byte(`{
|
||||
"panels": {"p1": {"kind": "Panel", "spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {
|
||||
"name": "A", "signal": "logs", "aggregations": ` + aggregationsJSON + `
|
||||
}}}}]
|
||||
}}},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
@@ -1703,10 +1625,9 @@ func TestValidateGridItemLimit(t *testing.T) {
|
||||
func TestInvalidateLayoutOverlapViaUnmarshal(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"panels": {
|
||||
"p1": {"kind": "Panel", "spec": {"links": [],"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}},
|
||||
"p2": {"kind": "Panel", "spec": {"links": [],"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}
|
||||
"p1": {"kind": "Panel", "spec": {"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}},
|
||||
"p2": {"kind": "Panel", "spec": {"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": [{"kind": "Grid", "spec": {"items": [
|
||||
{"x": 0, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p1"}},
|
||||
{"x": 3, "y": 3, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p2"}}
|
||||
@@ -1723,9 +1644,8 @@ func TestInvalidateLayoutOverlapViaUnmarshal(t *testing.T) {
|
||||
func TestInvalidateDuplicatePanelReference(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"panels": {
|
||||
"p1": {"kind": "Panel", "spec": {"links": [],"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}
|
||||
"p1": {"kind": "Panel", "spec": {"plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}}}]}}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": [{"kind": "Grid", "spec": {"items": [
|
||||
{"x": 0, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p1"}},
|
||||
{"x": 6, "y": 0, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/p1"}}
|
||||
@@ -1755,31 +1675,31 @@ func TestInvalidateDisplayNameTooLong(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scenario: "dashboard display name",
|
||||
dashboardJSON: `{"display": {"name": "` + tooLong + `"}, "links": [], "layouts": []}`,
|
||||
dashboardJSON: `{"display": {"name": "` + tooLong + `"}, "layouts": []}`,
|
||||
expectedLabel: "dashboard",
|
||||
expectedPath: "spec.display.name",
|
||||
},
|
||||
{
|
||||
scenario: "panel display name",
|
||||
dashboardJSON: `{"panels": {"p1": {"kind": "Panel", "spec": {"links": [],"display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "links": [], "layouts": []}`,
|
||||
dashboardJSON: `{"panels": {"p1": {"kind": "Panel", "spec": {"display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "layouts": []}`,
|
||||
expectedLabel: "panel",
|
||||
expectedPath: "spec.panels.p1.spec.display.name",
|
||||
},
|
||||
{
|
||||
scenario: "list variable display name",
|
||||
dashboardJSON: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "links": [], "layouts": []}`,
|
||||
dashboardJSON: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "layouts": []}`,
|
||||
expectedLabel: "variable",
|
||||
expectedPath: "spec.variables[0].spec.display.name",
|
||||
},
|
||||
{
|
||||
scenario: "text variable display name",
|
||||
dashboardJSON: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "` + tooLong + `"}}}], "links": [], "layouts": []}`,
|
||||
dashboardJSON: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "` + tooLong + `"}}}], "layouts": []}`,
|
||||
expectedLabel: "variable",
|
||||
expectedPath: "spec.variables[0].spec.display.name",
|
||||
},
|
||||
{
|
||||
scenario: "layout title",
|
||||
dashboardJSON: `{"links": [], "layouts": [{"kind": "Grid", "spec": {"display": {"title": "` + tooLong + `"}, "items": []}}]}`,
|
||||
dashboardJSON: `{"layouts": [{"kind": "Grid", "spec": {"display": {"title": "` + tooLong + `"}, "items": []}}]}`,
|
||||
expectedLabel: "layout",
|
||||
expectedPath: "spec.layouts[0].spec.display.title",
|
||||
},
|
||||
@@ -1799,7 +1719,7 @@ func TestInvalidateDisplayNameTooLong(t *testing.T) {
|
||||
// A display name at exactly the limit is accepted.
|
||||
func TestValidateDisplayNameAtMaxLength(t *testing.T) {
|
||||
atLimit := strings.Repeat("x", MaxDisplayNameLen)
|
||||
_, err := unmarshalDashboard([]byte(`{"display": {"name": "` + atLimit + `"}, "links": [], "layouts": []}`))
|
||||
_, err := unmarshalDashboard([]byte(`{"display": {"name": "` + atLimit + `"}, "layouts": []}`))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ func TestRedactVariableQueries(t *testing.T) {
|
||||
|
||||
t.Run("leaves non-query variables untouched", func(t *testing.T) {
|
||||
spec := DashboardSpec{Variables: []Variable{
|
||||
{Kind: variable.KindList, Spec: &ListVariableSpec{Name: "signal", Plugin: VariablePlugin{Kind: VariableKindDynamic, Spec: &DynamicVariableSpec{Name: "service.name", Signal: DynamicVariableSignalTraces}}}},
|
||||
{Kind: variable.KindList, Spec: &ListVariableSpec{Name: "signal", Plugin: VariablePlugin{Kind: VariableKindDynamic, Spec: &DynamicVariableSpec{Name: "service.name", Signal: telemetrytypes.SignalTraces}}}},
|
||||
{Kind: variable.KindList, Spec: &ListVariableSpec{Name: "env", Plugin: VariablePlugin{Kind: VariableKindCustom, Spec: &CustomVariableSpec{CustomValue: "prod,staging"}}}},
|
||||
}}
|
||||
|
||||
|
||||
@@ -78,17 +78,7 @@ type PanelSpec struct {
|
||||
Display Display `json:"display" required:"true"`
|
||||
Plugin PanelPlugin `json:"plugin" required:"true"`
|
||||
Queries []Query `json:"queries" required:"true" nullable:"false"`
|
||||
Links []Link `json:"links" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
// validateLinks rejects a missing/null links field, where path is the panel's
|
||||
// location (e.g. "spec.panels.<key>"). A typed client must send [] rather than
|
||||
// omitting links, so its value round-trips faithfully.
|
||||
func (s *PanelSpec) validateLinks(path string) error {
|
||||
if s.Links == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.links is required; send [] when there are no links", path)
|
||||
}
|
||||
return nil
|
||||
Links []Link `json:"links,omitzero"`
|
||||
}
|
||||
|
||||
// Link replicates dashboard.Link (Perses) so its zero-valued fields survive the
|
||||
|
||||
@@ -32,49 +32,7 @@ type DynamicVariableSpec struct {
|
||||
// Name is the name of the attribute being fetched dynamically from the
|
||||
// signal. This could be extended to a richer selector in the future.
|
||||
Name string `json:"name" validate:"required" required:"true"`
|
||||
Signal DynamicVariableSignal `json:"signal" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
// DynamicVariableSignal is the telemetry signal a dynamic variable draws its
|
||||
// values from. Separate from telemetrytypes.Signal because it carries "all"
|
||||
// (values span every signal) rather than "" for an unpinned query signal.
|
||||
type DynamicVariableSignal struct{ valuer.String }
|
||||
|
||||
var (
|
||||
DynamicVariableSignalTraces = DynamicVariableSignal{valuer.NewString("traces")}
|
||||
DynamicVariableSignalLogs = DynamicVariableSignal{valuer.NewString("logs")}
|
||||
DynamicVariableSignalMetrics = DynamicVariableSignal{valuer.NewString("metrics")}
|
||||
DynamicVariableSignalAll = DynamicVariableSignal{valuer.NewString("all")} // default
|
||||
)
|
||||
|
||||
func (DynamicVariableSignal) Enum() []any {
|
||||
return []any{DynamicVariableSignalTraces, DynamicVariableSignalLogs, DynamicVariableSignalMetrics, DynamicVariableSignalAll}
|
||||
}
|
||||
|
||||
func (s DynamicVariableSignal) ValueOrDefault() string {
|
||||
if s.IsZero() {
|
||||
return DynamicVariableSignalAll.StringValue()
|
||||
}
|
||||
return s.StringValue()
|
||||
}
|
||||
|
||||
func (s DynamicVariableSignal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (s *DynamicVariableSignal) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid signal: must be a string, one of `traces`, `logs`, `metrics`, or `all`")
|
||||
}
|
||||
sig := DynamicVariableSignal{valuer.NewString(v)}
|
||||
switch sig {
|
||||
case DynamicVariableSignalTraces, DynamicVariableSignalLogs, DynamicVariableSignalMetrics, DynamicVariableSignalAll:
|
||||
*s = sig
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid signal %q: must be `traces`, `logs`, `metrics`, or `all`", v)
|
||||
}
|
||||
Signal telemetrytypes.Signal `json:"signal"`
|
||||
}
|
||||
|
||||
type QueryVariableSpec struct {
|
||||
|
||||
12
pkg/types/dashboardtypes/testdata/perses.json
vendored
12
pkg/types/dashboardtypes/testdata/perses.json
vendored
@@ -80,7 +80,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"panels": {
|
||||
"24e2697b": {
|
||||
"kind": "Panel",
|
||||
@@ -168,7 +167,6 @@
|
||||
"ff2f72f1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "fraction of calls",
|
||||
"description": ""
|
||||
@@ -255,7 +253,6 @@
|
||||
"011605e7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "total resp size"
|
||||
},
|
||||
@@ -307,7 +304,6 @@
|
||||
"e23516fc": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "num traces for service"
|
||||
},
|
||||
@@ -363,7 +359,6 @@
|
||||
"130c8d6b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "num logs for service"
|
||||
},
|
||||
@@ -403,7 +398,6 @@
|
||||
"246f7c6d": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "num traces for service per resp code"
|
||||
},
|
||||
@@ -457,7 +451,6 @@
|
||||
"21f7d4d0": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "average latency per service"
|
||||
},
|
||||
@@ -500,7 +493,6 @@
|
||||
"ad5fd556": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "logs from service"
|
||||
},
|
||||
@@ -565,7 +557,6 @@
|
||||
"f07b59ee": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "response size buckets"
|
||||
},
|
||||
@@ -605,7 +596,6 @@
|
||||
"e1a41831": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "trace operator",
|
||||
"description": ""
|
||||
@@ -682,7 +672,6 @@
|
||||
"f0d70491": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "no results in this promql",
|
||||
"description": ""
|
||||
@@ -724,7 +713,6 @@
|
||||
"0e6eb4ca": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": "no results in this promql",
|
||||
"description": ""
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"b424e23b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
@@ -60,7 +58,6 @@
|
||||
"251df4d5": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
|
||||
@@ -16,27 +16,23 @@ type TagRelation struct {
|
||||
Kind coretypes.Kind `json:"kind" required:"true" bun:"kind,type:text,notnull"`
|
||||
ResourceID valuer.UUID `json:"resourceId" required:"true" bun:"resource_id,type:text,notnull"`
|
||||
TagID valuer.UUID `json:"tagId" required:"true" bun:"tag_id,type:text,notnull"`
|
||||
// Rank is the tag's position within its resource; reads order by it.
|
||||
Rank int `json:"rank" bun:"rank,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
CreatedAt time.Time `json:"createdAt" bun:"created_at,notnull"`
|
||||
}
|
||||
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID, rank int) *TagRelation {
|
||||
func NewTagRelation(kind coretypes.Kind, resourceID valuer.UUID, tagID valuer.UUID) *TagRelation {
|
||||
return &TagRelation{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
Kind: kind,
|
||||
ResourceID: resourceID,
|
||||
TagID: tagID,
|
||||
Rank: rank,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTagRelations ranks each tag by its position so reads preserve order.
|
||||
func NewTagRelations(kind coretypes.Kind, resourceID valuer.UUID, tagIDs []valuer.UUID) []*TagRelation {
|
||||
relations := make([]*TagRelation, 0, len(tagIDs))
|
||||
for rank, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID, rank))
|
||||
for _, tagID := range tagIDs {
|
||||
relations = append(relations, NewTagRelation(kind, resourceID, tagID))
|
||||
}
|
||||
return relations
|
||||
}
|
||||
|
||||
@@ -109,8 +109,6 @@ func (v *variableReplacementVisitor) Visit(tree antlr.ParseTree) any {
|
||||
return v.VisitValueList(t)
|
||||
case *grammar.FullTextContext:
|
||||
return v.VisitFullText(t)
|
||||
case *grammar.SearchCallContext:
|
||||
return v.VisitSearchCall(t)
|
||||
case *grammar.FunctionCallContext:
|
||||
return v.VisitFunctionCall(t)
|
||||
case *grammar.FunctionParamListContext:
|
||||
@@ -205,8 +203,6 @@ func (v *variableReplacementVisitor) VisitPrimary(ctx *grammar.PrimaryContext) a
|
||||
return v.Visit(ctx.Comparison())
|
||||
} else if ctx.FunctionCall() != nil {
|
||||
return v.Visit(ctx.FunctionCall())
|
||||
} else if ctx.SearchCall() != nil {
|
||||
return v.Visit(ctx.SearchCall())
|
||||
} else if ctx.FullText() != nil {
|
||||
return v.Visit(ctx.FullText())
|
||||
}
|
||||
@@ -411,13 +407,6 @@ func (v *variableReplacementVisitor) VisitFunctionCall(ctx *grammar.FunctionCall
|
||||
return functionName + "(" + params + ")"
|
||||
}
|
||||
|
||||
func (v *variableReplacementVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
|
||||
if ctx.FunctionParamList() == nil {
|
||||
return "search()"
|
||||
}
|
||||
return "search(" + v.Visit(ctx.FunctionParamList()).(string) + ")"
|
||||
}
|
||||
|
||||
func (v *variableReplacementVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
|
||||
params := ctx.AllFunctionParam()
|
||||
parts := make([]string, 0, len(params))
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_create_rejects_unknown_field(
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": "rejects-unknown",
|
||||
"spec": {"display": {"name": "Rejects Unknown"}, "links": []},
|
||||
"spec": {"display": {"name": "Rejects Unknown"}},
|
||||
"tags": [],
|
||||
"unknownfield": "boom",
|
||||
},
|
||||
@@ -209,7 +209,6 @@ def test_create_rejects_invalid_grid_layout(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": name},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TablePanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -250,7 +249,6 @@ def test_create_rejects_invalid_grid_layout(
|
||||
},
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
},
|
||||
"tags": [],
|
||||
},
|
||||
@@ -282,7 +280,6 @@ def test_create_rejects_invalid_grid_layout(
|
||||
},
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
},
|
||||
"tags": [],
|
||||
},
|
||||
@@ -308,7 +305,6 @@ def test_create_rejects_invalid_grid_layout(
|
||||
"spec": {"items": [{"x": 0, "y": 0, "width": 1, "height": 1} for _ in range(101)]},
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
},
|
||||
"tags": [],
|
||||
},
|
||||
@@ -414,7 +410,7 @@ def test_update_missing_dashboard_returns_not_found(
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": "missing-dashboard",
|
||||
"spec": {"display": {"name": "Missing Dashboard"}, "links": []},
|
||||
"spec": {"display": {"name": "Missing Dashboard"}},
|
||||
"tags": [],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -539,7 +535,7 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": name,
|
||||
"spec": {"display": {"name": display}, "links": []},
|
||||
"spec": {"display": {"name": display}},
|
||||
"tags": tags,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -574,11 +570,7 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert alpha["schemaVersion"] == "v6"
|
||||
assert alpha["source"] == "user"
|
||||
assert alpha["locked"] is False
|
||||
# tags round-trip in the order sent — no reordering, no drift
|
||||
assert alpha["tags"] == [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
assert {"key": "team", "value": "pulse"} in alpha["tags"]
|
||||
|
||||
# ── stage 3: list everything ─────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
@@ -598,13 +590,6 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
"Epsilon Metrics",
|
||||
"Zeta Overview",
|
||||
}
|
||||
# per-dashboard tags also round-trip in the order sent
|
||||
delta = next(d for d in body["data"]["dashboards"] if d["spec"]["display"]["name"] == "Delta Storage")
|
||||
assert delta["tags"] == [
|
||||
{"key": "team", "value": "storage"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
# top-level tags = org-wide distinct tag set, sorted case-insensitively
|
||||
# by (key, value). Asserting the exact list (not a set) locks in the sort.
|
||||
assert body["data"]["tags"] == [
|
||||
@@ -901,7 +886,7 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
update_body = {
|
||||
"schemaVersion": "v6",
|
||||
"name": "lc-alpha",
|
||||
"spec": {"display": {"name": "Alpha Overview"}, "links": []},
|
||||
"spec": {"display": {"name": "Alpha Overview"}},
|
||||
"tags": [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
@@ -945,7 +930,7 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
beta_body = {
|
||||
"schemaVersion": "v6",
|
||||
"name": "lc-beta",
|
||||
"spec": {"display": {"name": "Beta Overview"}, "links": []},
|
||||
"spec": {"display": {"name": "Beta Overview"}},
|
||||
"tags": [{"key": "team", "value": "pulse"}, {"key": "env", "value": "dev"}],
|
||||
}
|
||||
response = requests.put(
|
||||
@@ -1028,99 +1013,6 @@ def test_dashboard_v2_lifecycle( # pylint: disable=too-many-locals,too-many-sta
|
||||
assert response.json()["data"]["id"] == clone["id"]
|
||||
|
||||
|
||||
def test_dashboard_v2_tag_order_round_trips(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Tags must round-trip in the order the client sent them across every write
|
||||
path — create, update, patch — so GitOps/Terraform state never drifts."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
def tags_of(response: requests.Response) -> list[dict]:
|
||||
return response.json()["data"]["tags"]
|
||||
|
||||
# ── create with tags in a deliberate order; "env" repeats with two values ─
|
||||
created_order = [
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
]
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}, "links": []}, "tags": created_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
dashboard_id = response.json()["data"]["id"]
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── get echoes the same order ────────────────────────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == created_order
|
||||
|
||||
# ── update reordered; the two "env" values land at non-adjacent positions ─
|
||||
reordered = [
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "prod"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}, "links": []}, "tags": reordered},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered
|
||||
|
||||
# ── patch appends a new tag (a third "env" value); it lands at the end ────
|
||||
response = requests.patch(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json=[{"op": "add", "path": "/tags/-", "value": {"key": "env", "value": "dev"}}],
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == reordered + [{"key": "env", "value": "dev"}]
|
||||
|
||||
# ── update reorders everything and adds another new tag → all in the new order ─
|
||||
# "env" now carries three interleaved values
|
||||
new_order = [
|
||||
{"key": "env", "value": "prod"},
|
||||
{"key": "team", "value": "pulse"},
|
||||
{"key": "env", "value": "dev"},
|
||||
{"key": "tier", "value": "critical"},
|
||||
{"key": "env", "value": "staging"},
|
||||
{"key": "team", "value": "storage"},
|
||||
]
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
json={"schemaVersion": "v6", "name": "tag-order", "spec": {"display": {"name": "Tag Order"}, "links": []}, "tags": new_order},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
# ── and the final order persists on a fresh read ─────────────────────────
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert tags_of(response) == new_order
|
||||
|
||||
|
||||
def test_dashboard_v2_pin_limit(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
@@ -1141,7 +1033,7 @@ def test_dashboard_v2_pin_limit(
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": f"pl-{i}",
|
||||
"spec": {"display": {"name": f"Pin Limit {i}"}, "links": []},
|
||||
"spec": {"display": {"name": f"Pin Limit {i}"}},
|
||||
"tags": [],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -1239,7 +1131,7 @@ def test_dashboard_v2_like_escaping(
|
||||
json={
|
||||
"schemaVersion": "v6",
|
||||
"name": name,
|
||||
"spec": {"display": {"name": display}, "links": []},
|
||||
"spec": {"display": {"name": display}},
|
||||
"tags": [],
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -1330,13 +1222,11 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"name": "by-metric-builder",
|
||||
"spec": {
|
||||
"display": {"name": "by-metric-builder"},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"p-builder": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "D1 builder target"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1381,13 +1271,11 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"name": "by-metric-ch-promql",
|
||||
"spec": {
|
||||
"display": {"name": "by-metric-ch-promql"},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"p-ch": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "D2 clickhouse target"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1409,7 +1297,6 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "D2 promql target"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1446,13 +1333,11 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"name": "by-metric-promql",
|
||||
"spec": {
|
||||
"display": {"name": "by-metric-promql"},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"p-promql": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "D3 promql target"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1491,13 +1376,11 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"name": "by-metric-false-positive",
|
||||
"spec": {
|
||||
"display": {"name": "by-metric-false-positive"},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"p-builder": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": f"{target_metric} builder"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1526,7 +1409,6 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": f"{target_metric} clickhouse"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1548,7 +1430,6 @@ def test_dashboard_v2_get_by_metric_name(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": f"{target_metric} promql"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1622,13 +1503,11 @@ def test_dashboard_v2_rejects_comma_separated_aggregation(
|
||||
"tags": [],
|
||||
"spec": {
|
||||
"display": {"name": "Aggregation"},
|
||||
"links": [],
|
||||
"panels": {
|
||||
"p-agg": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "agg"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1780,7 +1659,6 @@ def test_dashboard_v2_roundtrip_preserves_zero_values(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "timeseries"},
|
||||
"links": [],
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {"thresholds": [{"value": 0, "color": "#c2780b"}]},
|
||||
@@ -1803,7 +1681,6 @@ def test_dashboard_v2_roundtrip_preserves_zero_values(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "promql"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1823,7 +1700,6 @@ def test_dashboard_v2_roundtrip_preserves_zero_values(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "clickhouse"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {}},
|
||||
"queries": [
|
||||
{
|
||||
@@ -1919,9 +1795,11 @@ def test_dashboard_v2_roundtrip_preserves_zero_values(
|
||||
for description, spec, key in absent_cases:
|
||||
assert key not in spec, description
|
||||
|
||||
# links is a required, non-nullable field: an explicit [] round-trips as [],
|
||||
# so a typed client always reads a concrete array (never null or absent).
|
||||
assert panels["timeseries"]["spec"]["links"] == [], "panel links round-trip as []"
|
||||
# A panel with no links comes back with no links value (null or absent);
|
||||
# either is fine for a typed client (an unset optional attribute stays
|
||||
# unset), so this is not a drift. The explicit [] case above is what the
|
||||
# fix guarantees round-trips.
|
||||
assert panels["timeseries"]["spec"].get("links") is None, "unset panel links stays unset"
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{dashboard_id}"),
|
||||
@@ -1973,7 +1851,6 @@ def test_dashboard_v2_omitted_enums_apply_defaults(
|
||||
},
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
},
|
||||
},
|
||||
"num": {
|
||||
@@ -1995,11 +1872,9 @@ def test_dashboard_v2_omitted_enums_apply_defaults(
|
||||
},
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
"links": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ def test_public_dashboard_v2(
|
||||
"spec": {
|
||||
"display": {"name": "Sample Dashboard", "description": "Used for integration tests"},
|
||||
"duration": "1h",
|
||||
"links": [],
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
@@ -78,7 +77,6 @@ def test_public_dashboard_v2(
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {"name": "total"},
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {"visualization": {"fillSpans": True}}},
|
||||
"queries": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user