Compare commits

..

1 Commits

Author SHA1 Message Date
Naman Verma
c8e9e362f7 feat: add spec for text panel (#12711)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
#### Description

Add a new plugin schema for text panel. This also adds a check on the
query count. If the panel is text, then number of queries should be
zero, otherwise it should be 1.

Frontend changes to be built on top of this

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/303
Closes https://github.com/SigNoz/pulse-pod/issues/221

---------

Co-authored-by: Abhi kumar <ahrefabhi@gmail.com>
2026-09-11 13:29:01 +00:00
163 changed files with 5088 additions and 1994 deletions

View File

@@ -3536,6 +3536,11 @@ components:
- tags
- spec
type: object
DashboardtypesHeaderOptions:
properties:
hide:
type: boolean
type: object
DashboardtypesHistogramBuckets:
properties:
bucketCount:
@@ -3893,6 +3898,7 @@ components:
signoz/NumberPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
signoz/PieChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
signoz/TablePanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
signoz/TextPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
signoz/TimeSeriesPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
propertyName: kind
oneOf:
@@ -3903,6 +3909,7 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -3913,6 +3920,7 @@ components:
- signoz/TablePanel
- signoz/HistogramPanel
- signoz/ListPanel
- signoz/TextPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
@@ -3986,6 +3994,18 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec:
properties:
kind:
enum:
- signoz/TextPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesTextPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec:
properties:
kind:
@@ -4277,6 +4297,37 @@ components:
- color
- columnName
type: object
DashboardtypesTextAlign:
enum:
- left
- center
- right
type: string
DashboardtypesTextMode:
enum:
- markdown
type: string
DashboardtypesTextPanelSpec:
properties:
headerOptions:
$ref: '#/components/schemas/DashboardtypesHeaderOptions'
mode:
$ref: '#/components/schemas/DashboardtypesTextMode'
presentation:
$ref: '#/components/schemas/DashboardtypesTextPresentation'
text:
type: string
type: object
DashboardtypesTextPresentation:
properties:
background:
nullable: true
type: string
textAlign:
$ref: '#/components/schemas/DashboardtypesTextAlign'
verticalAlign:
$ref: '#/components/schemas/DashboardtypesVerticalAlign'
type: object
DashboardtypesTextVariableSpec:
properties:
constant:
@@ -4486,6 +4537,12 @@ components:
- kind
- spec
type: object
DashboardtypesVerticalAlign:
enum:
- top
- center
- bottom
type: string
ErrorsJSON:
properties:
code:

View File

@@ -28,10 +28,8 @@ export default defineConfig({
clean: true,
override: {
query: {
// Leave useQuery/useMutation unset. Orval's verb tiebreak
// (`if (verb === GET && isMutation) isQuery = false`) inverts every
// operation when both are forced to true: GET becomes a mutation and
// the write verbs become queries.
useQuery: true,
useMutation: true,
useInvalidate: true,
signal: true,
useOperationIdAsQueryKey: false,

View File

@@ -77,7 +77,7 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.15",
"dompurify": "3.4.12",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
@@ -117,7 +117,7 @@
"react-redux": "^7.2.2",
"react-rnd": "^10.5.3",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.30.6",
"react-router-dom-v5-compat": "6.30.3",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
@@ -198,7 +198,7 @@
"json-schema-to-typescript": "^15.0.4",
"lint-staged": "^17.0.4",
"msw": "1.3.2",
"orval": "8.22.0",
"orval": "8.9.1",
"oxfmt": "0.54.0",
"oxlint": "1.69.0",
"oxlint-tsgolint": "0.23.0",
@@ -207,10 +207,10 @@
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.35.4",
"sharp": "0.35.0",
"storybook": "10.5.9",
"stylelint": "17.15.0",
"svgo": "4.1.0",
"stylelint": "17.7.0",
"svgo": "4.0.2",
"ts-jest": "29.4.9",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
@@ -230,5 +230,18 @@
"*.(scss|css)": [
"stylelint"
]
},
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
"js-cookie@<=3.0.5": ">=3.0.7 <4",
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}

1064
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,7 @@ overrides:
'@babel/core@<=7.29.0': '>=7.29.6 <8'
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
'@istanbuljs/load-nyc-config>js-yaml': '>=4.3.2 <5'
# via: msw@1.3.2 (devDep) > @mswjs/interceptors@0.17.10 (@xmldom/xmldom ^0.8.3)
# remove: upgrade msw to >=2 (drops the 0.8.x interceptor). Do NOT open the cap:
# 0.9.x is ESM-only and changes the DOMParser error contract
'@xmldom/xmldom@<0.8.15': '>=0.8.15 <0.9'
'@istanbuljs/load-nyc-config>js-yaml': '>=4.3.1 <5'
# via: babel-plugin-istanbul > test-exclude@6 > minimatch@3.1.5 (^1.1.7); also glob@7
# remove: blocked — babel-plugin-istanbul pins test-exclude@6, which pins minimatch@3
brace-expansion@<1.1.18: '>=1.1.18 <2'
@@ -27,39 +23,32 @@ overrides:
# via: eslint-plugin-sonarjs@4.0.2 (minimatch ^10.2.4) > minimatch@10.2.5 (^5.0.5)
# remove: blocked — minimatch@10.2.6 (latest) only widens to ^5.0.8, still vulnerable
'brace-expansion@>=5.0.0 <5.0.9': '>=5.0.9 <6'
# via: direct devDep @babel/core > @babel/helper-compilation-targets (browserslist ^4.24.0)
# remove: blocked, the range is open so the floor is what pulls the fix.
# Also lifts baseline-browser-mapping, which browserslist@4.28.7 floors at ^2.11.20
browserslist@<4.28.7: '>=4.28.7 <5'
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
cookie@<0.7.0: '>=0.7.1 <1'
# via: @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
# remove: blocked. The direct dep is already on 3.4.15, but @grafana/data
# (latest 13.1.0) and monaco-editor (latest 0.55.1) still pin vulnerable versions
dompurify@<=3.4.12: '>=3.4.13 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); ts-jest@29.4.9 (~0.27.4)
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
dompurify@<=3.4.10: '>=3.4.11 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
# via: @commitlint/cli > @commitlint/config-validator > ajv@8 (fast-uri ^3.0.1)
# remove: blocked — ajv@8 caps fast-uri at ^3, and only 3.1.6 carries the fix
fast-uri@<3.1.6: '>=3.1.6 <4'
# via: direct dep posthog-js@1.298.0 (fflate ^0.4.8)
# remove: blocked, posthog-js 1.430.2 (latest) is still on ^0.4.8
fflate@<0.4.9: '>=0.4.9 <0.5'
# remove: blocked — ajv@8 caps fast-uri at ^3, and only 3.1.5 carries the fix
fast-uri@<3.1.5: '>=3.1.5 <4'
# via: direct devDep sass@1.97.3 (immutable ^5.0.2)
# remove: blocked — plain sass bumps stay within ^5, so the floor is what pulls 5.1.8
immutable@<5.1.8: '>=5.1.8 <6'
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
js-cookie@<=3.0.5: '>=3.0.7 <4'
# via: json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0);
# @commitlint/cli > @commitlint/load > cosmiconfig@9 (^4.1.0)
# remove: blocked, both consumers cap js-yaml at ^4 and only 4.3.2 carries the fix
'js-yaml@>=4.0.0 <4.3.2': '>=4.3.2 <5'
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
'js-yaml@>=4.0.0 <4.3.1': '>=4.3.1 <5'
# via: typescript-plugin-css-modules@5.2.0 (less ^4.2.0) > less@4.4.0 (image-size ~0.5.0)
# remove: bump typescript-plugin-css-modules once it floors less itself; image-size has
# no patched release at all, so dropping the dep is the only fix — less@4.5.0 did
@@ -70,15 +59,10 @@ overrides:
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
prismjs@<1.30.0: '>=1.30.0 <2'
# via: typescript-plugin-css-modules@5.2.0 > postcss-modules-local-by-default@4.2.0
# (postcss-selector-parser ^7.0.0)
# remove: blocked, 4.2.0 is latest and the range is open, so the floor is what pulls the fix
postcss-selector-parser@>=7.1.0 <7.1.3: '>=7.1.3 <8'
# via: @signozhq/ui > nuqs@2 (react-router ^6.4.0 || ^7)
# remove: blocked. GHSA-wrjc-x8rr-h8h6 and the deserializeErrors advisory are patched
# only in 7.18.0. Do NOT open the cap: react-router >=7 requires React 19 and breaks
# the app-wide CompatRouter, so those two moderates stay until the app moves to React 19
react-router@>=6.7.0 <6.30.6: '>=6.30.6 <7'
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
tmp@<0.2.6: '>=0.2.6 <0.3.0'

View File

@@ -14,6 +14,15 @@ done
echo "\n✅ Tag files renamed to index.ts"
# Format generated files
echo "\n\n---\nRunning prettier...\n"
if ! pnpm prettify src/api/generated; then
echo "Formatting failed!"
exit 1
fi
echo "\n✅ Formatting successful"
# Fix linting issues
echo "\n\n---\nRunning lint...\n"
if ! pnpm lint:generated; then
@@ -23,15 +32,6 @@ fi
echo "\n✅ Lint check successful"
# Format generated files (must run after lint: oxlint --fix writes unformatted autofixes)
echo "\n\n---\nRunning prettier...\n"
if ! pnpm prettify src/api/generated; then
echo "Formatting failed!"
exit 1
fi
echo "\n✅ Formatting successful"
# Check for type errors
echo "\n\n---\nChecking for type errors...\n"
if ! tsc --noEmit; then

View File

@@ -25,26 +25,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns the field keys the AI observability explorer can filter on, including the computed per-trace aggregates
* @summary Get AI observability field keys
@@ -131,7 +111,7 @@ export function useGetAIObservabilityFieldsKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -236,7 +216,7 @@ export function useGetAIObservabilityFieldsValues<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -19,26 +19,6 @@ import type { GetAlerts200, RenderErrorResponseDTO } from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns alerts for the organization
* @summary Get alerts
@@ -97,7 +77,7 @@ export function useGetAlerts<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -32,26 +32,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all auth domains
* @summary List all auth domains
@@ -118,7 +98,7 @@ export function useListAuthDomains<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -343,7 +323,7 @@ export const getGetAuthDomainQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAuthDomain>>,
@@ -380,7 +360,7 @@ export function useGetAuthDomain<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -43,26 +43,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all notification channels for the organization
* @summary List notification channels
@@ -129,7 +109,7 @@ export function useListChannels<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -354,7 +334,7 @@ export const getGetChannelByIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
@@ -391,7 +371,7 @@ export function useGetChannelByID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -758,7 +738,7 @@ export function useListNotificationChannels<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -987,7 +967,7 @@ export const getGetNotificationChannelQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getNotificationChannel>>,
@@ -1025,7 +1005,7 @@ export function useGetNotificationChannel<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -51,26 +51,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* [Deprecated] This endpoint is called by the deployed agent to check in
* @deprecated
@@ -219,7 +199,7 @@ export const getListAccountsQueryOptions = <
return {
queryKey,
queryFn,
enabled: cloudProvider !== null && cloudProvider !== undefined,
enabled: !!cloudProvider,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listAccounts>>,
@@ -256,7 +236,7 @@ export function useListAccounts<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -500,11 +480,7 @@ export const getGetAccountQueryOptions = <
return {
queryKey,
queryFn,
enabled:
cloudProvider !== null &&
cloudProvider !== undefined &&
id !== null &&
id !== undefined,
enabled: !!(cloudProvider && id),
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getAccount>>, TError, TData> & {
queryKey: QueryKey;
@@ -539,7 +515,7 @@ export function useGetAccount<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -707,11 +683,7 @@ export const getListAccountServicesMetadataQueryOptions = <
return {
queryKey,
queryFn,
enabled:
cloudProvider !== null &&
cloudProvider !== undefined &&
id !== null &&
id !== undefined,
enabled: !!(cloudProvider && id),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listAccountServicesMetadata>>,
@@ -752,7 +724,7 @@ export function useListAccountServicesMetadata<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -823,13 +795,7 @@ export const getGetAccountServiceQueryOptions = <
return {
queryKey,
queryFn,
enabled:
cloudProvider !== null &&
cloudProvider !== undefined &&
id !== null &&
id !== undefined &&
serviceId !== null &&
serviceId !== undefined,
enabled: !!(cloudProvider && id && serviceId),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getAccountService>>,
@@ -869,7 +835,7 @@ export function useGetAccountService<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1133,7 +1099,7 @@ export const getGetConnectionCredentialsQueryOptions = <
return {
queryKey,
queryFn,
enabled: cloudProvider !== null && cloudProvider !== undefined,
enabled: !!cloudProvider,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getConnectionCredentials>>,
@@ -1174,7 +1140,7 @@ export function useGetConnectionCredentials<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1239,7 +1205,7 @@ export const getListServicesMetadataQueryOptions = <
return {
queryKey,
queryFn,
enabled: cloudProvider !== null && cloudProvider !== undefined,
enabled: !!cloudProvider,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1279,7 +1245,7 @@ export function useListServicesMetadata<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1347,11 +1313,7 @@ export const getGetServiceQueryOptions = <
return {
queryKey,
queryFn,
enabled:
cloudProvider !== null &&
cloudProvider !== undefined &&
serviceId !== null &&
serviceId !== undefined,
enabled: !!(cloudProvider && serviceId),
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getService>>, TError, TData> & {
queryKey: QueryKey;
@@ -1389,7 +1351,7 @@ export function useGetService<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -72,26 +72,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
* @summary Delete public dashboard
@@ -218,7 +198,7 @@ export const getGetPublicDashboardQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboard>>,
@@ -255,7 +235,7 @@ export function useGetPublicDashboard<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -520,7 +500,7 @@ export const getGetPublicDashboardDataQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
@@ -558,7 +538,7 @@ export function useGetPublicDashboardData<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -625,7 +605,7 @@ export const getGetPublicDashboardWidgetQueryRangeQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined && idx !== null && idx !== undefined,
enabled: !!(id && idx),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
@@ -666,7 +646,7 @@ export function useGetPublicDashboardWidgetQueryRange<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -751,7 +731,7 @@ export function useListDashboardViews<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1111,7 +1091,7 @@ export function useListDashboardsV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1337,7 +1317,7 @@ export const getGetDashboardV2QueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getDashboardV2>>,
@@ -1374,7 +1354,7 @@ export function useGetDashboardV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1953,7 +1933,7 @@ export const getGetSystemDashboardQueryOptions = <
return {
queryKey,
queryFn,
enabled: name !== null && name !== undefined,
enabled: !!name,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
@@ -1990,7 +1970,7 @@ export function useGetSystemDashboard<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -2055,7 +2035,7 @@ export const getGetPublicDashboardDataV2QueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
@@ -2093,7 +2073,7 @@ export function useGetPublicDashboardDataV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -2167,7 +2147,7 @@ export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined && key !== null && key !== undefined,
enabled: !!(id && key),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
@@ -2210,7 +2190,7 @@ export function useGetPublicDashboardPanelQueryRangeV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -2315,7 +2295,7 @@ export function useListDashboardsForUserV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -32,26 +32,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all planned maintenance / downtime schedules
* @summary List downtime schedules
@@ -131,7 +111,7 @@ export function useListDowntimeSchedules<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -360,7 +340,7 @@ export const getGetDowntimeScheduleByIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
@@ -398,7 +378,7 @@ export function useGetDowntimeScheduleByID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -19,26 +19,6 @@ import type { GetFeatures200, RenderErrorResponseDTO } from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns the supported features and their details
* @summary Get features
@@ -105,7 +85,7 @@ export function useGetFeatures<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -25,26 +25,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns field keys
* @summary Get field keys
@@ -121,7 +101,7 @@ export function useGetFieldsKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -216,7 +196,7 @@ export function useGetFieldsValues<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -48,26 +48,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace
@@ -149,7 +129,7 @@ export function useGetIngestionKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -376,7 +356,7 @@ export const getGetIngestionKeyQueryOptions = <
return {
queryKey,
queryFn,
enabled: keyId !== null && keyId !== undefined,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
@@ -413,7 +393,7 @@ export function useGetIngestionKey<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -577,7 +557,7 @@ export const getGetIngestionKeyLimitsQueryOptions = <
return {
queryKey,
queryFn,
enabled: keyId !== null && keyId !== undefined,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
@@ -614,7 +594,7 @@ export function useGetIngestionKeyLimits<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1001,7 +981,7 @@ export function useSearchIngestionKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1230,7 +1210,7 @@ export const getGetIngestionLimitQueryOptions = <
return {
queryKey,
queryFn,
enabled: limitId !== null && limitId !== undefined,
enabled: !!limitId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
@@ -1267,7 +1247,7 @@ export function useGetIngestionLimit<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -22,26 +22,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns global config
* @summary Get global config
@@ -108,7 +88,7 @@ export function useGetGlobalConfig<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -26,26 +26,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* @summary Health check
*/
@@ -103,7 +83,7 @@ export function useHealthz<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -176,7 +156,7 @@ export function useLivez<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -246,7 +226,7 @@ export function useReadyz<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -48,26 +48,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Checks whether the metrics and attributes required to power the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes) are being received. For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.
* @summary Run Infra Monitoring Setup Checks
@@ -136,7 +116,7 @@ export function useGetChecks<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -32,26 +32,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @deprecated
@@ -281,7 +261,7 @@ export function useListLicenses<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -504,7 +484,7 @@ export const getGetLicenseQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getLicense>>, TError, TData> & {
queryKey: QueryKey;
@@ -539,7 +519,7 @@ export function useGetLicense<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -703,7 +683,7 @@ export function useGetActiveLicense<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -31,26 +31,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Returns all LLM pricing rules for the authenticated org, with pagination.
* @summary List pricing rules
@@ -130,7 +110,7 @@ export function useListLLMPricingRules<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -359,7 +339,7 @@ export const getGetLLMPricingRuleQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getLLMPricingRule>>,
@@ -396,7 +376,7 @@ export function useGetLLMPricingRule<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -481,7 +461,7 @@ export function useListUnmappedLLMModels<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -28,26 +28,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoints allows complex query exporting raw data for traces and logs
* @summary Export raw data
@@ -217,7 +197,7 @@ export function useListPromotedAndIndexedPaths<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -60,26 +60,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Returns active metric volume-control (label reduction) rules.
* @summary List metric reduction rules
@@ -163,7 +143,7 @@ export function useListMetricReductionRules<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -392,7 +372,7 @@ export const getGetMetricReductionRuleByIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
@@ -433,7 +413,7 @@ export function useGetMetricReductionRuleByID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -704,7 +684,7 @@ export function useGetMetricReductionRuleStats<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -790,7 +770,7 @@ export function useGetMetricReductionRuleTimeseries<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -884,7 +864,7 @@ export function useListMetrics<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -979,7 +959,7 @@ export function useGetMetricAlerts<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1077,7 +1057,7 @@ export function useGetMetricAttributes<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1175,7 +1155,7 @@ export function useGetMetricDashboards<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1273,7 +1253,7 @@ export function useGetMetricHighlights<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1454,7 +1434,7 @@ export function useGetMetricMetadata<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1625,7 +1605,7 @@ export function useGetMetricsOnboardingStatus<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1888,7 +1868,7 @@ export function useGetMetricDashboardsV2<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -26,26 +26,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns the organization I belong to
* @summary Get my organization
@@ -112,7 +92,7 @@ export function useGetMyOrganization<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -33,26 +33,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all org preferences
* @summary List org preferences
@@ -119,7 +99,7 @@ export function useListOrgPreferences<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -183,7 +163,7 @@ export const getGetOrgPreferenceQueryOptions = <
return {
queryKey,
queryFn,
enabled: name !== null && name !== undefined,
enabled: !!name,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getOrgPreference>>,
@@ -220,7 +200,7 @@ export function useGetOrgPreference<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -405,7 +385,7 @@ export function useListUserPreferences<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -469,7 +449,7 @@ export const getGetUserPreferenceQueryOptions = <
return {
queryKey,
queryFn,
enabled: name !== null && name !== undefined,
enabled: !!name,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserPreference>>,
@@ -506,7 +486,7 @@ export function useGetUserPreference<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -30,26 +30,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
* @summary Prometheus instant query
@@ -128,7 +108,7 @@ export function usePrometheusQuery<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -313,7 +293,7 @@ export function usePrometheusQueryRange<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -29,26 +29,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Returns the org's quick filters for every source, each filter as a telemetry field key.
* @summary List quick filters
@@ -115,7 +95,7 @@ export function useListQuickFilters<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -179,7 +159,7 @@ export const getGetQuickFiltersQueryOptions = <
return {
queryKey,
queryFn,
enabled: source !== null && source !== undefined,
enabled: !!source,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
@@ -216,7 +196,7 @@ export function useGetQuickFilters<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -32,26 +32,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all roles
* @summary List roles
@@ -110,7 +90,7 @@ export function useListRoles<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -329,7 +309,7 @@ export const getGetRoleQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData> & {
queryKey: QueryKey;
@@ -360,7 +340,7 @@ export function useGetRole<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -32,26 +32,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all route policies for the organization
* @summary List route policies
@@ -118,7 +98,7 @@ export function useGetAllRoutePolicies<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -345,7 +325,7 @@ export const getGetRoutePolicyByIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
@@ -382,7 +362,7 @@ export function useGetRoutePolicyByID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -52,26 +52,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint lists all alert rules with their current evaluation state
* @summary List alert rules
@@ -130,7 +110,7 @@ export function useListRules<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -353,7 +333,7 @@ export const getGetRuleByIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleByID>>,
@@ -390,7 +370,7 @@ export function useGetRuleByID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -660,7 +640,7 @@ export const getGetRuleHistoryFilterKeysQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
@@ -703,7 +683,7 @@ export function useGetRuleHistoryFilterKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -777,7 +757,7 @@ export const getGetRuleHistoryFilterValuesQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
@@ -820,7 +800,7 @@ export function useGetRuleHistoryFilterValues<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -894,7 +874,7 @@ export const getGetRuleHistoryOverallStatusQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
@@ -937,7 +917,7 @@ export function useGetRuleHistoryOverallStatus<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1010,7 +990,7 @@ export const getGetRuleHistoryStatsQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
@@ -1052,7 +1032,7 @@ export function useGetRuleHistoryStats<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1125,7 +1105,7 @@ export const getGetRuleHistoryTimelineQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
@@ -1168,7 +1148,7 @@ export function useGetRuleHistoryTimeline<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1242,7 +1222,7 @@ export const getGetRuleHistoryTopContributorsQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
@@ -1285,7 +1265,7 @@ export function useGetRuleHistoryTopContributors<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -33,26 +33,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Returns saved views, optionally filtered by source and name.
* @summary List saved views
@@ -129,7 +109,7 @@ export function useListSavedViews<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -353,7 +333,7 @@ export const getGetSavedViewQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
@@ -390,7 +370,7 @@ export function useGetSavedView<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -47,26 +47,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint assigns a role to a service account
* @summary Create service account role
@@ -277,7 +257,7 @@ export const getGetServiceAccountRoleQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
@@ -314,7 +294,7 @@ export function useGetServiceAccountRole<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -399,7 +379,7 @@ export function useListServiceAccounts<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -627,7 +607,7 @@ export const getGetServiceAccountQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccount>>,
@@ -664,7 +644,7 @@ export function useGetServiceAccount<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -829,7 +809,7 @@ export const getListServiceAccountKeysQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listServiceAccountKeys>>,
@@ -867,7 +847,7 @@ export function useListServiceAccountKeys<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1212,7 +1192,7 @@ export const getGetServiceAccountRolesQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRoles>>,
@@ -1250,7 +1230,7 @@ export function useGetServiceAccountRoles<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1335,7 +1315,7 @@ export function useGetMyServiceAccount<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -34,26 +34,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint creates a session for a user using google callback
* @summary Create session by google callback
@@ -123,7 +103,7 @@ export function useCreateSessionByGoogleCallback<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -210,7 +190,7 @@ export function useCreateSessionByOIDCCallback<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -484,7 +464,7 @@ export function useGetSessionContext<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -3220,30 +3220,33 @@ export interface CloudintegrationtypesAWSServiceConfigDTO {
metrics?: CloudintegrationtypesAWSServiceMetricsConfigDTO;
}
export type CloudintegrationtypesAgentReportDTODataAnyOf = {
export type CloudintegrationtypesAgentReportDTOAnyOfDataAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type CloudintegrationtypesAgentReportDTOData =
CloudintegrationtypesAgentReportDTODataAnyOf | null;
export type CloudintegrationtypesAgentReportDTOAnyOfData =
CloudintegrationtypesAgentReportDTOAnyOfDataAnyOf | null;
/**
* @nullable
*/
export type CloudintegrationtypesAgentReportDTO = {
export type CloudintegrationtypesAgentReportDTOAnyOf = {
/**
* @type object,null
*/
data: CloudintegrationtypesAgentReportDTOData;
data: CloudintegrationtypesAgentReportDTOAnyOfData;
/**
* @type integer
* @format int64
*/
timestampMillis: number;
} | null;
};
/**
* @nullable
*/
export type CloudintegrationtypesAgentReportDTO =
CloudintegrationtypesAgentReportDTOAnyOf | null;
export interface CloudintegrationtypesAzureAccountConfigDTO {
/**
@@ -3442,10 +3445,7 @@ export enum CloudintegrationtypesServiceIDDTO {
cloudstorage = 'cloudstorage',
cloudsql_mysql = 'cloudsql_mysql',
}
/**
* @nullable
*/
export type CloudintegrationtypesCloudIntegrationServiceDTO = {
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
* @type string
*/
@@ -3466,7 +3466,13 @@ export type CloudintegrationtypesCloudIntegrationServiceDTO = {
* @format date-time
*/
updatedAt?: string;
} | null;
};
/**
* @nullable
*/
export type CloudintegrationtypesCloudIntegrationServiceDTO =
CloudintegrationtypesCloudIntegrationServiceDTOAnyOf | null;
export interface CloudintegrationtypesCollectedLogAttributeDTO {
/**
@@ -3620,16 +3626,19 @@ export interface CloudintegrationtypesOldAWSCollectionStrategyDTO {
s3_buckets?: CloudintegrationtypesOldAWSCollectionStrategyDTOS3Buckets;
}
/**
* @nullable
*/
export type CloudintegrationtypesIntegrationConfigDTO = {
export type CloudintegrationtypesIntegrationConfigDTOAnyOf = {
/**
* @type array
*/
enabled_regions: string[];
telemetry: CloudintegrationtypesOldAWSCollectionStrategyDTO;
} | null;
};
/**
* @nullable
*/
export type CloudintegrationtypesIntegrationConfigDTO =
CloudintegrationtypesIntegrationConfigDTOAnyOf | null;
export interface CloudintegrationtypesProviderIntegrationConfigDTO {
aws?: CloudintegrationtypesAWSIntegrationConfigDTO;
@@ -5010,6 +5019,57 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesListPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTOKind {
'signoz/TextPanel' = 'signoz/TextPanel',
}
export interface DashboardtypesHeaderOptionsDTO {
/**
* @type boolean
*/
hide?: boolean;
}
export enum DashboardtypesTextModeDTO {
markdown = 'markdown',
}
export enum DashboardtypesTextAlignDTO {
left = 'left',
center = 'center',
right = 'right',
}
export enum DashboardtypesVerticalAlignDTO {
top = 'top',
center = 'center',
bottom = 'bottom',
}
export interface DashboardtypesTextPresentationDTO {
/**
* @type string,null
*/
background?: string | null;
textAlign?: DashboardtypesTextAlignDTO;
verticalAlign?: DashboardtypesVerticalAlignDTO;
}
export interface DashboardtypesTextPanelSpecDTO {
headerOptions?: DashboardtypesHeaderOptionsDTO;
mode?: DashboardtypesTextModeDTO;
presentation?: DashboardtypesTextPresentationDTO;
/**
* @type string
*/
text?: string;
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTO {
/**
* @enum signoz/TextPanel
* @type string
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTOKind;
spec: DashboardtypesTextPanelSpecDTO;
}
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
@@ -5017,7 +5077,8 @@ export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO;
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTO;
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
@@ -5941,6 +6002,7 @@ export enum DashboardtypesPanelPluginKindDTO {
'signoz/TablePanel' = 'signoz/TablePanel',
'signoz/HistogramPanel' = 'signoz/HistogramPanel',
'signoz/ListPanel' = 'signoz/ListPanel',
'signoz/TextPanel' = 'signoz/TextPanel',
}
/**
* @nullable
@@ -9397,10 +9459,7 @@ export interface TelemetrystoretypesMergeTreeReadDTO {
table: string;
}
/**
* @nullable
*/
export type TelemetrystoretypesGranulesDTO = {
export type TelemetrystoretypesGranulesDTOAnyOf = {
/**
* @type integer
* @format int64
@@ -9420,7 +9479,13 @@ export type TelemetrystoretypesGranulesDTO = {
* @format int64
*/
skipped: number;
} | null;
};
/**
* @nullable
*/
export type TelemetrystoretypesGranulesDTO =
TelemetrystoretypesGranulesDTOAnyOf | null;
export interface Querybuildertypesv5PreviewStatementDTO {
/**
@@ -10618,10 +10683,7 @@ export interface SpantypesGettableFlamegraphTraceDTO {
startTimestampMillis: number;
}
/**
* @nullable
*/
export type SpantypesSpanMapperGroupConditionDTO = {
export type SpantypesSpanMapperGroupConditionDTOAnyOf = {
/**
* @type array,null
*/
@@ -10630,7 +10692,13 @@ export type SpantypesSpanMapperGroupConditionDTO = {
* @type array,null
*/
resource: string[] | null;
} | null;
};
/**
* @nullable
*/
export type SpantypesSpanMapperGroupConditionDTO =
SpantypesSpanMapperGroupConditionDTOAnyOf | null;
export interface SpantypesSpanMapperGroupDTO {
condition: SpantypesSpanMapperGroupConditionDTO | null;

View File

@@ -41,26 +41,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Returns all span attribute mapping groups for the authenticated org.
* @summary List span attribute mapping groups
@@ -140,7 +120,7 @@ export function useListSpanMapperGroups<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -469,7 +449,7 @@ export const getListSpanMappersQueryOptions = <
return {
queryKey,
queryFn,
enabled: groupId !== null && groupId !== undefined,
enabled: !!groupId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listSpanMappers>>,
@@ -506,7 +486,7 @@ export function useListSpanMappers<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -19,26 +19,6 @@ import type { GetStats200, RenderErrorResponseDTO } from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint returns the collected stats for the organization
* @summary Get stats
@@ -97,7 +77,7 @@ export function useGetStats<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -28,26 +28,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint gets the organization's subscription along with its usage and billing details.
* @summary Get the subscription.
@@ -114,7 +94,7 @@ export function useGetSubscription<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -50,26 +50,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint is deprecated and always fails. Use GET /api/v2/users/me instead.
* @deprecated
@@ -138,7 +118,7 @@ export function useGetMyUserDeprecated<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -452,7 +432,7 @@ export const getGetUsersByRoleIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUsersByRoleID>>,
@@ -489,7 +469,7 @@ export function useGetUsersByRoleID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -713,7 +693,7 @@ export const getGetUserRoleQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
@@ -750,7 +730,7 @@ export function useGetUserRole<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -827,7 +807,7 @@ export function useListUsers<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1046,7 +1026,7 @@ export const getGetUserQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getUser>>, TError, TData> & {
queryKey: QueryKey;
@@ -1077,7 +1057,7 @@ export function useGetUser<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1241,7 +1221,7 @@ export const getGetResetPasswordTokenQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getResetPasswordToken>>,
@@ -1278,7 +1258,7 @@ export function useGetResetPasswordToken<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1422,7 +1402,7 @@ export const getGetRolesByUserIDQueryOptions = <
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRolesByUserID>>,
@@ -1459,7 +1439,7 @@ export function useGetRolesByUserID<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**
@@ -1536,7 +1516,7 @@ export function useGetMyUser<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -27,26 +27,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* This endpoint gets the host info from zeus.
* @summary Get host info from Zeus.
@@ -105,7 +85,7 @@ export function useGetHosts<
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
return { ...query, queryKey: queryOptions.queryKey };
}
/**

View File

@@ -31,6 +31,8 @@ export const getComponentForPanelType = (
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.HISTOGRAM]: Uplot,
// Dashboards v2 renders this kind; nothing reaches the V1 chart map for it.
[PANEL_TYPES.TEXT]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@@ -376,6 +376,7 @@ export enum PANEL_TYPES {
BAR = 'bar',
PIE = 'pie',
HISTOGRAM = 'histogram',
TEXT = 'text',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}

View File

@@ -29,5 +29,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
BAR: true,
PIE: false,
HISTOGRAM: false,
TEXT: false,
EMPTY_WIDGET: false,
};

View File

@@ -14,6 +14,8 @@ export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.LIST]: ListPanelWrapper,
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
[PANEL_TYPES.TRACE]: null,
// Dashboards v2 renders this kind; the V1 wrapper map is never asked for it.
[PANEL_TYPES.TEXT]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
[PANEL_TYPES.PIE]: PiePanelWrapper,
[PANEL_TYPES.BAR]: BarPanel,

View File

@@ -19,6 +19,7 @@ const KIND_LABEL: Record<VariableUsage['kind'], string> = {
promql: 'PromQL',
clickhouse: 'ClickHouse',
variable: 'Variable',
text: 'Markdown body',
};
interface VariableImpactDialogProps {

View File

@@ -0,0 +1,49 @@
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { buildVariableImpactPatch } from '../utils/variableImpactPatch';
import type { VariableUsage } from '../utils/variableUsages';
jest.mock('../variableAdapters', () => ({
formModelToDto: (model: unknown): unknown => model,
}));
const dashboard = {
spec: {
panels: {
runbook: {
spec: {
display: { name: 'Runbook' },
plugin: { kind: 'signoz/TextPanel', spec: { text: 'env {{svc}}' } },
queries: [],
},
},
},
variables: [],
},
} as unknown as DashboardtypesGettableDashboardV2DTO;
const textUsage: VariableUsage = {
id: 'panel:runbook:0',
sourceType: 'panel',
sourceId: 'runbook',
sourceLabel: 'Runbook',
kind: 'text',
envelopeIndex: 0,
currentText: 'env {{svc}}',
resultingText: 'env {{zone}}',
};
describe('buildVariableImpactPatch — text panel bodies', () => {
it('replaces the plugin-spec text, never the (empty) queries', () => {
const ops = buildVariableImpactPatch(dashboard, [], [textUsage]);
const panelOps = ops.filter((op) => op.path.includes('/panels/'));
expect(panelOps).toStrictEqual([
{
op: 'replace',
path: '/spec/panels/runbook/spec/plugin/spec/text',
value: 'env {{zone}}',
},
]);
});
});

View File

@@ -45,6 +45,16 @@ function promqlPanel(name: string, query: string): unknown {
};
}
function textPanel(name: string, text: string): unknown {
return {
spec: {
display: { name },
plugin: { kind: 'signoz/TextPanel', spec: { text } },
queries: [],
},
};
}
function dashboard(
panels: Record<string, unknown>,
variables: VariableFormModel[],
@@ -99,6 +109,38 @@ describe('findVariableUsages', () => {
it('returns nothing for an unreferenced variable', () => {
expect(findVariableUsages(dash, 'nope', 'delete')).toStrictEqual([]);
});
describe('text panel bodies (TDD D5)', () => {
const textDash = dashboard(
{
runbook: textPanel(
'Runbook',
'env {{svc}} / {{.svc}} / [[svc]] / $svc / {{svcx}}',
),
unrelated: textPanel('Plain', 'no tokens here'),
},
[variable({ name: 'svc', type: 'QUERY' })],
);
it('finds the body usage and skips bodies without the token', () => {
const usages = findVariableUsages(textDash, 'svc', 'rename', 'zone');
expect(usages.map((u) => u.id)).toStrictEqual(['panel:runbook:0']);
expect(usages[0].kind).toBe('text');
expect(usages[0].sourceLabel).toBe('Runbook');
});
it('rewrites all four syntaxes on rename, leaving other names alone', () => {
const [usage] = findVariableUsages(textDash, 'svc', 'rename', 'zone');
expect(usage.resultingText).toBe(
'env {{zone}} / {{.zone}} / [[zone]] / $zone / {{svcx}}',
);
});
it('leaves the body for review on delete', () => {
const [usage] = findVariableUsages(textDash, 'svc', 'delete');
expect(usage.resultingText).toBe(usage.currentText);
});
});
});
describe('findApplyUsages', () => {

View File

@@ -0,0 +1,16 @@
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { isStaticPanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
/**
* The markdown body of a static (query-less) panel, or null for query kinds.
* One localized cast: the plugin-spec union can't be narrowed by a dynamic kind.
*/
export function getTextPanelBody(
spec: DashboardtypesPanelSpecDTO | undefined,
): string | null {
if (!spec?.plugin || !isStaticPanelKind(spec.plugin.kind)) {
return null;
}
const { text } = spec.plugin.spec as { text?: string };
return typeof text === 'string' ? text : null;
}

View File

@@ -107,6 +107,18 @@ export function buildVariableImpactPatch(
byPanel.forEach((list, panelId) => {
const panel = panels[panelId];
// A static kind's usage edits its markdown body, not a query.
const textUsage = list.find((usage) => usage.kind === 'text');
if (textUsage) {
ops.push({
op: 'replace' as DashboardtypesJSONPatchOperationDTO['op'],
path: `/spec/panels/${panelId}/spec/plugin/spec/text`,
value: textUsage.resultingText,
});
return;
}
if (!panel?.spec?.queries?.length) {
return;
}

View File

@@ -12,6 +12,7 @@ import {
} from 'lib/dashboardVariables/variableReference';
import { toQueryEnvelopes } from '../../../queryV5/buildQueryRangeRequest';
import { getTextPanelBody } from './getTextPanelBody';
import { dtoToFormModel } from '../variableAdapters';
/** The kind of query text a variable is referenced from. */
@@ -19,7 +20,8 @@ export type VariableUsageKind =
| 'builder'
| 'promql'
| 'clickhouse'
| 'variable';
| 'variable'
| 'text';
export type VariableImpactMode = 'rename' | 'delete' | 'apply';
@@ -81,7 +83,7 @@ function computeResultingText(
return rewriteVariableReferences(text, variableName, newName);
}
// delete: only builder filter clauses can be safely auto-stripped; raw PromQL/
// ClickHouse and variable queries are left for the user to edit.
// ClickHouse, markdown bodies and variable queries are left for the user to edit.
return kind === 'builder'
? removeVariableFromExpression(text, variableName)
: text;
@@ -106,6 +108,31 @@ export function findVariableUsages(
const spec = dashboard.spec;
Object.entries(spec.panels ?? {}).forEach(([panelId, panel]) => {
// A static kind references variables from its body, not a query (TDD D5 —
// rename must rewrite text bodies too, or it silently orphans the tokens).
const textBody = getTextPanelBody(panel?.spec);
if (typeof textBody === 'string') {
if (textContainsVariableReference(textBody, variableName)) {
usages.push({
id: `panel:${panelId}:0`,
sourceType: 'panel',
sourceId: panelId,
sourceLabel: panel.spec?.display?.name || panelId,
kind: 'text',
envelopeIndex: 0,
currentText: textBody,
resultingText: computeResultingText(
'text',
textBody,
variableName,
mode,
newName,
),
});
}
return;
}
const queries = panel?.spec?.queries;
if (!queries?.length) {
return;

View File

@@ -5,6 +5,7 @@ import type {
DashboardtypesPanelSpecDTO,
} from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import { SectionKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { getSupportedSignals } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { resolveSignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
import type { EQueryType } from 'types/common/dashboard';
@@ -66,7 +67,14 @@ function ConfigPane({
}: ConfigPaneProps): JSX.Element {
const panelKind = spec.plugin.kind;
const definition = getPanelDefinition(panelKind);
const sections = definition.sections;
// The header toggle belongs with the title and description it hides, so the kind's
// declaration still gates it but it renders above, out of the display options.
const headerSection = definition.sections.find(
(config) => config.kind === SectionKind.PanelHeader,
);
const sections = definition.sections.filter(
(config) => config.kind !== SectionKind.PanelHeader,
);
const signal = resolveSignal(spec.queries, getSupportedSignals(panelKind)[0]);
@@ -105,6 +113,23 @@ function ConfigPane({
onChange={(e): void => setDisplayField('description', e.target.value)}
/>
</div>
{headerSection && (
<SectionSlot
bare
config={headerSection}
spec={spec}
onChangeSpec={onChangeSpec}
legendSeries={legendSeries}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
/>
)}
</div>
{sections.length > 0 && (

View File

@@ -2,6 +2,7 @@ import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import {
isStaticPanelKind,
isQueryTypeSupportedByPanelKind,
isSignalSupported,
} from '../../../Panels/capabilities';
@@ -37,6 +38,12 @@ export function getPanelTypeDisabledReason({
signal?: TelemetrytypesSignalDTO;
label: string;
}): string | undefined {
// A kind that renders without a query pairs with anything — it declares no
// query types or signals, and the checks below would read that as "supports
// nothing" and disable it everywhere.
if (isStaticPanelKind(kind)) {
return undefined;
}
if (!isQueryTypeSupportedByPanelKind(kind, queryType)) {
return `${label} isn't available for ${QUERY_TYPE_LABEL[queryType]} queries`;
}

View File

@@ -16,6 +16,8 @@ type SectionSlotProps = {
config: SectionConfig;
spec: DashboardtypesPanelSpecDTO;
onChangeSpec: (next: DashboardtypesPanelSpecDTO) => void;
/** Renders the editor alone, for a section promoted into the Panel Details fields. */
bare?: boolean;
} & Omit<SectionEditorContext, 'yAxisUnit' | 'registerHeaderAction'>;
// Per-section header content; `trigger` expands the section and runs the editor's handler.
@@ -50,6 +52,7 @@ function SectionSlot({
config,
spec,
onChangeSpec,
bare,
legendSeries,
tableColumns,
signal,
@@ -110,6 +113,28 @@ function SectionSlot({
const headerSlot = SECTION_HEADER_SLOT[config.kind]?.(triggerHeaderAction);
const editorElement = (
<Component
value={get(spec)}
controls={controls}
onChange={(next): void => onChangeSpec(update(spec, next))}
legendSeries={legendSeries}
yAxisUnit={yAxisUnit}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
registerHeaderAction={registerHeaderAction}
/>
);
if (bare) {
return editorElement;
}
return (
<SettingsSection
title={title}
@@ -118,21 +143,7 @@ function SectionSlot({
onOpenChange={setOpen}
headerSlot={headerSlot}
>
<Component
value={get(spec)}
controls={controls}
onChange={(next): void => onChangeSpec(update(spec, next))}
legendSeries={legendSeries}
yAxisUnit={yAxisUnit}
tableColumns={tableColumns}
signal={signal}
panelKind={panelKind}
onChangePanelKind={onChangePanelKind}
queryType={queryType}
stepInterval={stepInterval}
metricUnit={metricUnit}
registerHeaderAction={registerHeaderAction}
/>
{editorElement}
</SettingsSection>
);
}

View File

@@ -23,6 +23,14 @@ jest.mock(
}),
);
function textSpec(): DashboardtypesPanelSpecDTO {
return {
display: { name: 'Runbook', description: 'steps' },
plugin: { kind: 'signoz/TextPanel', spec: { text: '' } },
queries: [],
} as unknown as DashboardtypesPanelSpecDTO;
}
function spec(unit?: string): DashboardtypesPanelSpecDTO {
return {
display: { name: 'CPU', description: 'usage' },
@@ -93,6 +101,24 @@ describe('ConfigPane', () => {
);
});
// It hides the title strip, so it sits with the title rather than under the
// display options — and only a kind whose spec accepts `headerOptions` shows it.
it('renders the hide-header toggle among the Panel Details fields', () => {
renderConfigPane({ spec: textSpec() });
const toggle = screen.getByTestId('panel-header-hide');
expect(toggle).toBeInTheDocument();
expect(screen.getByText('Hide header')).toBeInTheDocument();
// No collapsible wrapper of its own.
expect(screen.queryByText('Panel header')).not.toBeInTheDocument();
});
it('omits the hide-header toggle for a kind that has no header options', () => {
renderConfigPane();
expect(screen.queryByTestId('panel-header-hide')).not.toBeInTheDocument();
});
it('renders the Formatting section for a kind that declares it', () => {
renderConfigPane();
// The TimeSeries kind declares a Formatting section; its collapsible header shows.

View File

@@ -0,0 +1,76 @@
.row {
display: flex;
align-items: center;
gap: 6px;
}
.swatch {
flex: none;
width: 26px;
height: 26px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--l2-border);
border-radius: 4px;
background: none;
cursor: pointer;
position: relative;
// The input carries focus, so the ring is drawn on the swatch around it.
&:has(.input:focus-visible) {
outline: 2px solid var(--bg-robin-400);
outline-offset: 1px;
}
}
// The real control, sized to the swatch and invisible over it: clicks and focus
// land on the radio, the swatch is what the user sees.
.input {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
cursor: pointer;
}
.selected {
box-shadow: 0 0 0 2px var(--bg-robin-500);
}
// Transparency has no colour to show, so it reads as the conventional checkerboard.
.checkerboard {
background-color: var(--l2-background);
background-image:
linear-gradient(
45deg,
var(--l2-border) 25%,
transparent 25%,
transparent 75%,
var(--l2-border) 75%
),
linear-gradient(
45deg,
var(--l2-border) 25%,
transparent 25%,
transparent 75%,
var(--l2-border) 75%
);
background-size: 8px 8px;
background-position:
0 0,
4px 4px;
}
.defaultSurface {
background: var(--l2-background);
}
.divider {
flex: none;
width: 1px;
height: 18px;
margin: 0 2px;
background: var(--l2-border);
}

View File

@@ -0,0 +1,119 @@
import { Fragment } from 'react';
import { Check } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import {
TEXT_BACKGROUND_PAIRS,
TEXT_BACKGROUND_PRESETS,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/presets';
import type {
PanelTheme,
TextBackgroundPreset,
TextBackgroundSelection,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/types';
import { TextBackgroundKind } from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/types';
import styles from './BackgroundSwatches.module.scss';
const PRESET_TITLES: Record<TextBackgroundPreset, string> = {
robin: 'Robin',
purple: 'Purple',
sakura: 'Sakura',
cherry: 'Cherry',
amber: 'Amber',
forest: 'Forest',
sienna: 'Sienna',
slate: 'Slate',
};
type BaseSelection = TextBackgroundKind.None | TextBackgroundKind.Default;
const BASE_TITLES: Record<BaseSelection, string> = {
none: 'Transparent',
default: 'Default panel',
};
/** Neither base swatch shows a colour, so its tooltip says what it does. */
const BASE_TOOLTIPS: Record<BaseSelection, string> = {
none: 'Transparent — no card, border or title bar',
default: 'Default panel colour',
};
const OPTIONS: TextBackgroundSelection[] = [
TextBackgroundKind.None,
TextBackgroundKind.Default,
...TEXT_BACKGROUND_PRESETS,
];
const DIVIDER_AFTER = 1;
interface BackgroundSwatchesProps {
testId: string;
/** Names the group for assistive tech — the row carries no visible label. */
label: string;
/** `undefined` while a custom colour is active: no swatch is selected. */
value: TextBackgroundSelection | undefined;
/** Swatches paint in this theme's pair, so what the user picks is what they see. */
theme: PanelTheme;
onChange: (value: TextBackgroundSelection) => void;
}
/**
* The Text panel's background choices as one radio group. Native radios sharing a
* `name`, so arrow-key movement, the single tab stop and selection-follows-focus
* are the platform's; each input is transparent and fills its swatch.
*/
function BackgroundSwatches({
testId,
label,
value,
theme,
onChange,
}: BackgroundSwatchesProps): JSX.Element {
return (
<div
className={styles.row}
role="radiogroup"
aria-label={label}
data-testid={testId}
>
{OPTIONS.map((option, index) => {
const isBase =
option === TextBackgroundKind.None ||
option === TextBackgroundKind.Default;
const pair = isBase ? undefined : TEXT_BACKGROUND_PAIRS[option][theme];
const title = isBase ? BASE_TITLES[option] : PRESET_TITLES[option];
return (
<Fragment key={option}>
<TooltipSimple title={isBase ? BASE_TOOLTIPS[option] : title} arrow>
<label
className={cx(styles.swatch, {
[styles.checkerboard]: option === TextBackgroundKind.None,
[styles.defaultSurface]: option === TextBackgroundKind.Default,
[styles.selected]: option === value,
})}
style={pair ? { background: pair.surface, color: pair.ink } : undefined}
data-testid={`${testId}-${option}`}
>
<input
type="radio"
className={styles.input}
name={testId}
value={option}
checked={option === value}
aria-label={title}
onChange={(): void => onChange(option)}
/>
{option === value && <Check size={14} />}
</label>
</TooltipSimple>
{index === DIVIDER_AFTER && <span className={styles.divider} />}
</Fragment>
);
})}
</div>
);
}
export default BackgroundSwatches;

View File

@@ -0,0 +1,67 @@
.row {
display: flex;
width: 100%;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--l2-border);
border-radius: 4px;
background: transparent;
cursor: pointer;
text-align: left;
}
.active {
border-color: var(--bg-robin-500);
}
.chip {
flex: none;
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--l2-border);
border-radius: 4px;
}
// No colour to show yet, so the chip advertises that it opens a picker.
.chipEmpty {
background: conic-gradient(
from 0deg,
var(--bg-cherry-400),
var(--bg-amber-400),
var(--bg-forest-400),
var(--bg-robin-400),
var(--bg-sakura-400),
var(--bg-cherry-400)
);
}
.label {
flex: 1;
font-size: 12px;
color: var(--l2-foreground);
}
.hex {
font-family: var(--font-family-sf-mono);
font-size: 12px;
color: var(--text-vanilla-400);
letter-spacing: 0.02em;
}
// Appended under the picker's own panel.
.contrast {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 4px 2px;
font-size: 12px;
color: var(--text-vanilla-400);
}
.warning {
color: var(--bg-amber-400);
}

View File

@@ -0,0 +1,91 @@
import type { ReactNode } from 'react';
import { Check, ChevronDown, TriangleAlert } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { ColorPicker } from 'antd';
import cx from 'classnames';
import {
contrastRatio,
inkForSurface,
MIN_CONTRAST_RATIO,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/contrast';
import styles from './CustomBackgroundRow.module.scss';
const HEX_PLACEHOLDER = '#______';
/** What the picker opens on before a colour is chosen. */
const INITIAL_COLOR = '#3A2A63';
interface CustomBackgroundRowProps {
testId: string;
/** The stored hex while a custom colour is active; `undefined` otherwise. */
value: string | undefined;
onChange: (hex: string) => void;
}
/**
* The custom colour, as a row rather than a swatch: it opens a picker instead of
* committing a value in one click. The picker warns below the contrast floor but
* never blocks the choice.
*/
function CustomBackgroundRow({
testId,
value,
onChange,
}: CustomBackgroundRowProps): JSX.Element {
const color = value ?? INITIAL_COLOR;
const ratio = contrastRatio(inkForSurface(color), color);
const isLegible = ratio >= MIN_CONTRAST_RATIO;
const contrastMessage = isLegible
? `Contrast ${ratio.toFixed(1)}:1`
: `Contrast ${ratio.toFixed(1)}:1 — below ${MIN_CONTRAST_RATIO}:1`;
function renderPanel(panel: ReactNode): ReactNode {
return (
<>
{panel}
<div
className={cx(styles.contrast, { [styles.warning]: !isLegible })}
data-testid={`${testId}-contrast`}
>
{!isLegible && <TriangleAlert size={12} />}
<span className="translate-safe">{contrastMessage}</span>
</div>
</>
);
}
return (
<ColorPicker
value={color}
size="small"
showText={false}
trigger="click"
panelRender={renderPanel}
onChangeComplete={(next): void => onChange(next.toHexString())}
>
<button
type="button"
className={cx(styles.row, { [styles.active]: value !== undefined })}
data-testid={testId}
>
<span
className={cx(styles.chip, { [styles.chipEmpty]: value === undefined })}
style={
value ? { background: value, color: inkForSurface(value) } : undefined
}
>
{value !== undefined && <Check size={14} />}
</span>
<Typography.Text className={styles.label}>Custom</Typography.Text>
<span className={cx(styles.hex, 'translate-safe')}>
{value ?? HEX_PLACEHOLDER}
</span>
<ChevronDown size={14} />
</button>
</ColorPicker>
);
}
export default CustomBackgroundRow;

View File

@@ -0,0 +1,118 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { TEXT_BACKGROUND_PAIRS } from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/presets';
import {
PanelTheme,
TextBackgroundKind,
TextBackgroundPreset,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/types';
import BackgroundSwatches from '../BackgroundSwatches';
function renderRow(
props: Partial<React.ComponentProps<typeof BackgroundSwatches>> = {},
): jest.Mock {
const onChange = jest.fn();
render(
<TooltipProvider>
<BackgroundSwatches
testId="background"
label="Panel background"
theme={PanelTheme.Dark}
value={TextBackgroundKind.Default}
onChange={onChange}
{...props}
/>
</TooltipProvider>,
);
return onChange;
}
describe('BackgroundSwatches', () => {
it('offers transparent, the default surface and the eight presets in order', () => {
renderRow();
expect(
screen
.getAllByRole('radio')
.map((swatch) => swatch.getAttribute('aria-label')),
).toStrictEqual([
'Transparent',
'Default panel',
'Robin',
'Purple',
'Sakura',
'Cherry',
'Amber',
'Forest',
'Sienna',
'Slate',
]);
});
it('is one labelled group', () => {
renderRow();
expect(
screen.getByRole('radiogroup', { name: 'Panel background' }),
).toBeInTheDocument();
});
it.each([
['background-none', 'Transparent — no card, border or title bar'],
['background-default', 'Default panel colour'],
['background-sakura', 'Sakura'],
])('explains %s on hover', async (swatchId, copy) => {
renderRow();
fireEvent.focus(screen.getByTestId(swatchId));
await waitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent(copy);
});
});
it('paints each preset in the given theme', () => {
renderRow({ theme: PanelTheme.Light });
expect(screen.getByTestId('background-amber')).toHaveStyle({
background: TEXT_BACKGROUND_PAIRS.amber.light.surface,
color: TEXT_BACKGROUND_PAIRS.amber.light.ink,
});
});
it('marks only the selected swatch, and checks it', () => {
renderRow({ value: TextBackgroundPreset.Forest });
expect(screen.getByRole('radio', { name: 'Forest' })).toBeChecked();
expect(
screen.getByRole('radio', { name: 'Default panel' }),
).not.toBeChecked();
expect(
screen.getByTestId('background-forest').querySelector('svg'),
).toBeInTheDocument();
expect(
screen.getByTestId('background-default').querySelector('svg'),
).not.toBeInTheDocument();
});
it('reports the swatch that was clicked', () => {
const onChange = renderRow();
fireEvent.click(screen.getByRole('radio', { name: 'Sienna' }));
expect(onChange).toHaveBeenCalledWith('sienna');
});
// jsdom does not implement radio arrow navigation, so the shared name — what
// makes them one group — is what there is to assert.
it('groups every swatch under one radio name', () => {
renderRow();
const names = new Set(
screen.getAllByRole('radio').map((swatch) => swatch.getAttribute('name')),
);
expect(names).toStrictEqual(new Set(['background']));
});
});

View File

@@ -0,0 +1,77 @@
import { fireEvent, render, screen } from '@testing-library/react';
import CustomBackgroundRow from '../CustomBackgroundRow';
function renderRow(value?: string): jest.Mock {
const onChange = jest.fn();
render(
<CustomBackgroundRow testId="custom" value={value} onChange={onChange} />,
);
return onChange;
}
describe('CustomBackgroundRow', () => {
it('stands in for the hex while no custom colour is set', () => {
renderRow();
expect(screen.getByTestId('custom')).toHaveTextContent('#______');
});
it('shows the stored hex once one is set', () => {
renderRow('#3A2A63');
expect(screen.getByTestId('custom')).toHaveTextContent('#3A2A63');
});
it('checks the chip only while the custom colour is the selection', () => {
renderRow('#3A2A63');
expect(screen.getByTestId('custom').querySelector('svg')).toBeInTheDocument();
});
it('leaves the chip unchecked while no custom colour is set', () => {
renderRow();
expect(screen.getByTestId('custom').querySelectorAll('svg')).toHaveLength(1);
});
describe('the picker', () => {
it('opens on the row', () => {
renderRow('#3A2A63');
fireEvent.click(screen.getByTestId('custom'));
expect(screen.getByTestId('custom-contrast')).toBeInTheDocument();
});
it('reports the contrast the colour achieves', () => {
renderRow('#3A2A63');
fireEvent.click(screen.getByTestId('custom'));
// The derived ink is pure white, not purple's paired ink.
expect(screen.getByTestId('custom-contrast')).toHaveTextContent(
'Contrast 12.4:1',
);
});
it('warns when no ink clears the floor, without disabling anything', () => {
renderRow('#808080');
fireEvent.click(screen.getByTestId('custom'));
expect(screen.getByTestId('custom-contrast')).toHaveTextContent(
'below 4.5:1',
);
expect(screen.getByTestId('custom')).toBeEnabled();
});
it('says nothing about the floor when the colour clears it', () => {
renderRow('#111111');
fireEvent.click(screen.getByTestId('custom'));
expect(screen.getByTestId('custom-contrast')).not.toHaveTextContent('below');
});
});
});

View File

@@ -23,6 +23,8 @@ import ChartAppearanceSection from './sections/ChartAppearanceSection/ChartAppea
import ContextLinksSection from './sections/ContextLinksSection/ContextLinksSection';
import FormattingSection from './sections/FormattingSection/FormattingSection';
import LegendSection from './sections/LegendSection/LegendSection';
import PanelHeaderSection from './sections/PanelHeaderSection/PanelHeaderSection';
import TextLayoutSection from './sections/TextLayoutSection/TextLayoutSection';
import ThresholdsSection from './sections/ThresholdsSection/ThresholdsSection';
import VisualizationSection from './sections/VisualizationSection/VisualizationSection';
@@ -117,6 +119,23 @@ export const SECTION_REGISTRY: {
update: (spec, buckets): PanelSpec =>
updatePluginSlice(spec, 'histogramBuckets', buckets),
},
[SectionKind.TextLayout]: {
Component: TextLayoutSection,
get: (spec): SectionSpecMap[SectionKind.TextLayout] | undefined =>
getPluginSlice<SectionSpecMap[SectionKind.TextLayout]>(spec, 'presentation'),
update: (spec, presentation): PanelSpec =>
updatePluginSlice(spec, 'presentation', presentation),
},
[SectionKind.PanelHeader]: {
Component: PanelHeaderSection,
get: (spec): SectionSpecMap[SectionKind.PanelHeader] | undefined =>
getPluginSlice<SectionSpecMap[SectionKind.PanelHeader]>(
spec,
'headerOptions',
),
update: (spec, headerOptions): PanelSpec =>
updatePluginSlice(spec, 'headerOptions', headerOptions),
},
[SectionKind.ContextLinks]: {
Component: ContextLinksSection,
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.

View File

@@ -0,0 +1,24 @@
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
/** Edits the Text panel's `headerOptions` slice: the panel card's title strip. */
function PanelHeaderSection({
value,
onChange,
}: SectionEditorProps<SectionKind.PanelHeader>): JSX.Element {
return (
<ConfigSwitch
testId="panel-header-hide"
title="Hide header"
description="Drop the title strip on the dashboard; hovering the panel shows controls for drag and actions."
value={value?.hide === true}
onChange={(hide): void => onChange({ ...value, hide })}
/>
);
}
export default PanelHeaderSection;

View File

@@ -0,0 +1,29 @@
import { fireEvent, render, screen } from '@testing-library/react';
import PanelHeaderSection from '../PanelHeaderSection';
describe('PanelHeaderSection', () => {
it('toggles hide on', () => {
const onChange = jest.fn();
render(<PanelHeaderSection value={undefined} onChange={onChange} />);
fireEvent.click(screen.getByTestId('panel-header-hide'));
expect(onChange).toHaveBeenCalledWith({ hide: true });
});
it('toggles hide back off', () => {
const onChange = jest.fn();
render(<PanelHeaderSection value={{ hide: true }} onChange={onChange} />);
fireEvent.click(screen.getByTestId('panel-header-hide'));
expect(onChange).toHaveBeenCalledWith({ hide: false });
});
it('shows the header by default when the slice is empty', () => {
render(<PanelHeaderSection value={undefined} onChange={jest.fn()} />);
expect(screen.getByTestId('panel-header-hide')).not.toBeChecked();
});
});

View File

@@ -0,0 +1,11 @@
.section {
display: flex;
flex-direction: column;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}

View File

@@ -0,0 +1,99 @@
import {
DashboardtypesTextAlignDTO,
DashboardtypesVerticalAlignDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
resolveTextBackground,
selectionFromResolved,
storedFromSelection,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/resolveTextBackground';
import type { TextBackgroundSelection } from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/types';
import {
PanelTheme,
TextBackgroundKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/types';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import BackgroundSwatches from '../../controls/BackgroundSwatches/BackgroundSwatches';
import CustomBackgroundRow from '../../controls/BackgroundSwatches/CustomBackgroundRow';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import styles from './TextLayoutSection.module.scss';
const HORIZONTAL_OPTIONS = [
{ value: DashboardtypesTextAlignDTO.left, label: 'Left' },
{ value: DashboardtypesTextAlignDTO.center, label: 'Center' },
{ value: DashboardtypesTextAlignDTO.right, label: 'Right' },
];
const VERTICAL_OPTIONS = [
{ value: DashboardtypesVerticalAlignDTO.top, label: 'Top' },
{ value: DashboardtypesVerticalAlignDTO.center, label: 'Middle' },
{ value: DashboardtypesVerticalAlignDTO.bottom, label: 'Bottom' },
];
/**
* Edits the Text panel's `presentation` slice: body alignment and the card
* background (TDD D7 — scoped to the text spec, not the panel envelope).
*/
function TextLayoutSection({
value,
onChange,
}: SectionEditorProps<SectionKind.TextLayout>): JSX.Element {
const theme = useIsDarkMode() ? PanelTheme.Dark : PanelTheme.Light;
const background = resolveTextBackground(value?.background, theme);
return (
<div className={styles.section}>
<div className={styles.field}>
<Typography.Text>Horizontal alignment</Typography.Text>
<ConfigSegmented
testId="text-layout-horizontal-align"
items={HORIZONTAL_OPTIONS}
value={value?.textAlign ?? DashboardtypesTextAlignDTO.left}
onChange={(textAlign): void => onChange({ ...value, textAlign })}
/>
</div>
<div className={styles.field}>
<Typography.Text>Vertical alignment</Typography.Text>
<ConfigSegmented
testId="text-layout-vertical-align"
items={VERTICAL_OPTIONS}
value={value?.verticalAlign ?? DashboardtypesVerticalAlignDTO.top}
onChange={(verticalAlign): void => onChange({ ...value, verticalAlign })}
/>
</div>
<div className={styles.field}>
<Typography.Text>Background</Typography.Text>
<BackgroundSwatches
testId="text-layout-background"
label="Panel background"
theme={theme}
value={selectionFromResolved(background)}
onChange={(selection: TextBackgroundSelection): void =>
onChange({
...value,
background: storedFromSelection(selection, theme),
})
}
/>
<CustomBackgroundRow
testId="text-layout-background-custom"
value={
background.kind === TextBackgroundKind.Custom
? background.surface
: undefined
}
onChange={(hex): void => onChange({ ...value, background: hex })}
/>
</div>
</div>
);
}
export default TextLayoutSection;

View File

@@ -0,0 +1,144 @@
import type { ReactElement } from 'react';
import {
fireEvent,
render as rtlRender,
type RenderResult,
screen,
} from '@testing-library/react';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import {
DashboardtypesTextAlignDTO,
DashboardtypesVerticalAlignDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
TEXT_BACKGROUND_PAIRS,
TRANSPARENT_BACKGROUND,
} from 'pages/DashboardPage/DashboardContainer/Panels/kinds/TextPanel/background/presets';
import TextLayoutSection from '../TextLayoutSection';
const value = {
textAlign: DashboardtypesTextAlignDTO.left,
verticalAlign: DashboardtypesVerticalAlignDTO.top,
};
// The swatch tooltips need a provider; AppLayout supplies one at runtime.
function render(ui: ReactElement): RenderResult {
return rtlRender(<TooltipProvider>{ui}</TooltipProvider>);
}
// The theme context defaults to dark, so the swatches paint the dark pairs.
describe('TextLayoutSection', () => {
it('changes horizontal alignment', () => {
const onChange = jest.fn();
render(<TextLayoutSection value={value} onChange={onChange} />);
fireEvent.click(screen.getByText('Center'));
expect(onChange).toHaveBeenCalledWith({
...value,
textAlign: DashboardtypesTextAlignDTO.center,
});
});
it('changes vertical alignment', () => {
const onChange = jest.fn();
render(<TextLayoutSection value={value} onChange={onChange} />);
fireEvent.click(screen.getByText('Bottom'));
expect(onChange).toHaveBeenCalledWith({
...value,
verticalAlign: DashboardtypesVerticalAlignDTO.bottom,
});
});
it('stores the surface of the theme a preset was picked in', () => {
const onChange = jest.fn();
render(<TextLayoutSection value={value} onChange={onChange} />);
fireEvent.click(screen.getByRole('radio', { name: 'Amber' }));
expect(onChange).toHaveBeenCalledWith({
...value,
background: TEXT_BACKGROUND_PAIRS.amber.dark.surface,
});
});
it('stores a zero-alpha colour for transparent', () => {
const onChange = jest.fn();
render(<TextLayoutSection value={value} onChange={onChange} />);
fireEvent.click(screen.getByRole('radio', { name: 'Transparent' }));
expect(onChange).toHaveBeenCalledWith({
...value,
background: TRANSPARENT_BACKGROUND,
});
});
it('unsets the background for the default panel surface', () => {
const onChange = jest.fn();
render(
<TextLayoutSection
value={{ ...value, background: TRANSPARENT_BACKGROUND }}
onChange={onChange}
/>,
);
fireEvent.click(screen.getByRole('radio', { name: 'Default panel' }));
expect(onChange).toHaveBeenCalledWith({ ...value, background: undefined });
});
it('lights up the swatch the stored surface belongs to', () => {
render(
<TextLayoutSection
value={{ ...value, background: TEXT_BACKGROUND_PAIRS.sakura.light.surface }}
onChange={jest.fn()}
/>,
);
expect(screen.getByRole('radio', { name: 'Sakura' })).toBeChecked();
});
it('stores a custom colour straight from the picker', () => {
const onChange = jest.fn();
render(<TextLayoutSection value={value} onChange={onChange} />);
fireEvent.click(screen.getByTestId('text-layout-background-custom'));
fireEvent.change(screen.getByRole('textbox'), {
target: { value: '3A2A64' },
});
expect(onChange).toHaveBeenCalledWith({
...value,
background: '#3a2a64',
});
});
it('shows a stored custom colour on the custom row alone', () => {
render(
<TextLayoutSection
value={{ ...value, background: '#3A2A64' }}
onChange={jest.fn()}
/>,
);
expect(screen.getByTestId('text-layout-background-custom')).toHaveTextContent(
'#3A2A64',
);
expect(
screen
.getAllByRole<HTMLInputElement>('radio')
.filter((swatch) => swatch.checked),
).toHaveLength(0);
});
it('selects the default surface when nothing is stored', () => {
render(<TextLayoutSection value={undefined} onChange={jest.fn()} />);
expect(screen.getByRole('radio', { name: 'Default panel' })).toBeChecked();
expect(screen.getByRole('radio', { name: 'Transparent' })).not.toBeChecked();
});
});

View File

@@ -26,16 +26,3 @@
background: var(--l2-border);
}
}
// The static editor's preview: the panel card the grid shows, minus actions.
.staticPreviewSurface {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
margin: 12px;
border: 1px solid var(--l2-border);
border-radius: 4px;
background: var(--l2-background);
overflow: hidden;
}

View File

@@ -23,7 +23,7 @@ import { EQueryType } from 'types/common/dashboard';
import { mergeQueryBuilderFieldRule } from '../../Panels/types/panelCapabilities';
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
import { PANEL_KIND_TO_PANEL_TYPE } from '../../Panels/types/panelKind';
import { toPanelType } from '../../Panels/types/panelKind';
import styles from './PanelEditorQueryBuilder.module.scss';
@@ -60,7 +60,7 @@ function PanelEditorQueryBuilder({
}: PanelEditorQueryBuilderProps): JSX.Element {
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
// builder offers for this kind comes from the kind's own declaration.
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelDefinition.kind];
const panelType = toPanelType(panelDefinition.kind);
// Raw rows: the builder drops its aggregation controls, and with them the trace
// operator that combines aggregated trace queries (V1 parity).
const isListViewPanel = panelDefinition.kind === 'signoz/ListPanel';

View File

@@ -9,6 +9,13 @@
border-bottom: 1px solid var(--l1-border);
}
// A static pane never scrolls — the panel card clips, and the renderer scrolls its
// own body when the content outgrows it, as on the grid.
.previewStatic {
box-sizing: border-box;
overflow: hidden;
}
.header {
width: 100%;
box-sizing: border-box;
@@ -56,6 +63,14 @@
overflow: visible;
}
// A static kind's card takes its colours from the background the panel declares,
// falling back to the same tokens the query surface uses.
.surfaceStatic {
border-color: var(--text-panel-border, var(--l2-border));
background: var(--text-panel-surface, var(--l2-background));
color: var(--text-panel-ink, inherit);
}
.state {
flex: 1;
display: flex;

View File

@@ -5,10 +5,16 @@ import { PanelMode } from 'lib/visualization/panels/types';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import PanelBody from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelBody/PanelBody';
import PanelHeader from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelHeader/PanelHeader';
import StaticPanelBody from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/StaticPanelBody/StaticPanelBody';
import { useTextBackground } from 'pages/DashboardPage/DashboardContainer/Panels/hooks/useTextBackground';
import type { AnyPanelInteractionProps } from 'pages/DashboardPage/DashboardContainer/Panels/types/interactions';
import type { RenderableQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import type {
RenderableQueryPanelDefinition,
RenderableStaticPanelDefinition,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import type { DashboardPreference } from 'pages/DashboardPage/DashboardContainer/Panels/types/rendererProps';
import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getPanelQueryType';
import { isPanelHeaderHidden } from 'pages/DashboardPage/DashboardContainer/Panels/utils/isPanelHeaderHidden';
import type {
PanelPagination,
PanelQueryData,
@@ -17,9 +23,15 @@ import type {
import PlotTag from './PlotTag';
import styles from './PreviewPane.module.scss';
interface PreviewPaneProps {
interface PreviewPaneBaseProps {
panelId: string;
panel: DashboardtypesPanelDTO;
/** Render context — defaults to the editor's DASHBOARD_EDIT; the View modal passes STANDALONE_VIEW. */
panelMode?: PanelMode;
}
interface QueryPreviewPaneProps extends PreviewPaneBaseProps {
mode: 'query';
/** The kind's definition, narrowed to the query arm — this preview is the query render path. */
panelDefinition: RenderableQueryPanelDefinition;
data: PanelQueryData;
@@ -34,8 +46,6 @@ interface PreviewPaneProps {
onDragSelect: (start: number, end: number) => void;
/** Server-side pager for raw/list panels; absent for non-paginated panels. */
pagination?: PanelPagination;
/** Render context — defaults to the editor's DASHBOARD_EDIT; the View modal passes STANDALONE_VIEW. */
panelMode?: PanelMode;
/** Hide the preview's top row entirely (query-type badge + time picker) — the View modal has its own header. */
hideHeader?: boolean;
/** Dashboard-wide preferences (cursor sync, …) forwarded to the body; the modal isolates cursor-sync. */
@@ -48,41 +58,43 @@ interface PreviewPaneProps {
enableDrillDown?: boolean;
}
interface StaticPreviewPaneProps extends PreviewPaneBaseProps {
mode: 'static';
/** The kind's definition, narrowed to the static arm — no query, no Run step. */
panelDefinition: RenderableStaticPanelDefinition;
/** Saves an edit made from the rendered body into the draft; absent = read-only. */
onChangeText?: (text: string) => void;
}
type PreviewPaneProps = QueryPreviewPaneProps | StaticPreviewPaneProps;
/**
* Live preview for the panel editor: renders the draft through the same `PanelBody`
* the dashboard grid uses (only `panelMode` differs), so the preview is the
* production render path. The query result is owned by the editor root.
* Live preview for the panel editor and the View modal: the draft rendered through
* the same body the dashboard grid uses (only `panelMode` differs), so the preview
* is the production render path. A query draft's result is owned by the editor
* root; a static draft re-renders straight from the spec on every edit.
*/
function PreviewPane({
panelId,
panel,
panelDefinition,
data,
isFetching,
isPreviousData,
error,
refetch,
onDragSelect,
pagination,
panelMode = PanelMode.DASHBOARD_EDIT,
hideHeader = false,
dashboardPreference,
onCloseStandaloneView,
onClick,
enableDrillDown,
}: PreviewPaneProps): JSX.Element {
const queryType = getPanelQueryType(panel);
function PreviewPane(props: PreviewPaneProps): JSX.Element {
const { panelId, panel, panelMode = PanelMode.DASHBOARD_EDIT } = props;
const query = props.mode === 'query' ? props : null;
const staticDraft = props.mode === 'static' ? props : null;
const background = useTextBackground(panel.spec);
// Search term is ephemeral preview state, threaded to header + renderer but
// not persisted to the draft spec. Only kinds that declare it render the box.
const searchable = !!panelDefinition.actions.search;
const searchable = !!query?.panelDefinition.actions.search;
const [searchTerm, setSearchTerm] = useState('');
return (
<div className={styles.preview}>
{!hideHeader && (
<div
className={cx(styles.preview, { [styles.previewStatic]: !!staticDraft })}
>
{query && !query.hideHeader && (
<div className={styles.header}>
<PlotTag queryType={queryType} className={styles.queryType} />
<PlotTag
queryType={getPanelQueryType(panel)}
className={styles.queryType}
/>
<div className={styles.dateTimeSelector}>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
</div>
@@ -91,39 +103,67 @@ function PreviewPane({
<div className={styles.container}>
<div
className={cx(styles.surface, {
[styles.surfaceStacked]: panelMode === PanelMode.STANDALONE_VIEW,
[styles.surfaceStacked]:
!!query && panelMode === PanelMode.STANDALONE_VIEW,
[styles.surfaceStatic]: !!staticDraft,
})}
style={background.style}
>
<PanelHeader
panelId={panelId}
panel={panel}
data={data}
isFetching={isFetching}
error={error}
warning={data.response?.data?.warning}
searchable={searchable}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
hideActions
/>
<PanelBody
Renderer={panelDefinition.Renderer}
panel={panel}
panelId={panelId}
data={data}
isFetching={isFetching}
isPreviousData={isPreviousData}
error={error}
refetch={refetch}
onDragSelect={onDragSelect}
panelMode={panelMode}
dashboardPreference={dashboardPreference}
searchTerm={searchable ? searchTerm : undefined}
pagination={pagination}
onCloseStandaloneView={onCloseStandaloneView}
onClick={onClick}
enableDrillDown={enableDrillDown}
/>
{query ? (
<>
<PanelHeader
mode="query"
panelId={panelId}
panel={panel}
data={query.data}
isFetching={query.isFetching}
error={query.error}
warning={query.data.response?.data?.warning}
searchable={searchable}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
hideActions
/>
<PanelBody
Renderer={query.panelDefinition.Renderer}
panel={panel}
panelId={panelId}
data={query.data}
isFetching={query.isFetching}
isPreviousData={query.isPreviousData}
error={query.error}
refetch={query.refetch}
onDragSelect={query.onDragSelect}
panelMode={panelMode}
dashboardPreference={query.dashboardPreference}
searchTerm={searchable ? searchTerm : undefined}
pagination={query.pagination}
onCloseStandaloneView={query.onCloseStandaloneView}
onClick={query.onClick}
enableDrillDown={query.enableDrillDown}
/>
</>
) : (
staticDraft && (
<>
{!isPanelHeaderHidden(panel.spec) && (
<PanelHeader
mode="static"
panelId={panelId}
panel={panel}
hideActions
/>
)}
<StaticPanelBody
Renderer={staticDraft.panelDefinition.Renderer}
panel={panel}
panelId={panelId}
panelMode={panelMode}
onChangeText={staticDraft.onChangeText}
/>
</>
)
)}
</div>
</div>
</div>

View File

@@ -8,7 +8,7 @@ import {
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { RenderableQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import {
type SectionConfig,
type SectionControls,
@@ -217,7 +217,7 @@ function QueryEditorBody({
const onSwitchToView = useSwitchToViewMode({
dashboardId,
panelId,
panelType: PANEL_KIND_TO_PANEL_TYPE[panelKind],
panelType: toPanelType(panelKind),
query: currentQuery,
spec: draft.spec,
});
@@ -286,6 +286,7 @@ function QueryEditorBody({
}
preview={
<PreviewPane
mode="query"
panelId={panelId}
panel={draft}
panelDefinition={panelDefinition}

View File

@@ -1,11 +1,8 @@
import { useCallback } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { PanelMode } from 'lib/visualization/panels/types';
import StaticPanelBody from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/StaticPanelBody/StaticPanelBody';
import PanelHeader from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelHeader/PanelHeader';
import type { RenderableStaticPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { EMPTY_PANEL_QUERY_DATA } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import { EQueryType } from 'types/common/dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
@@ -16,12 +13,12 @@ import Header from './Header/Header';
import PanelEditorLayout, {
PANE_SPLIT,
} from './PanelEditorLayout/PanelEditorLayout';
import PreviewPane from './PreviewPane/PreviewPane';
import type { PanelEditorContainerProps } from './index';
import type { PanelEditorDraftApi } from './types';
import { withPanelText } from '../Panels/utils/withPanelText';
import { usePanelEditorSave } from './hooks/usePanelEditorSave';
import styles from './PanelEditor.module.scss';
interface StaticEditorBodyProps extends PanelEditorContainerProps {
draftApi: PanelEditorDraftApi;
panelDefinition: RenderableStaticPanelDefinition;
@@ -51,7 +48,7 @@ function StaticEditorBody({
useDashboardEditContext();
const { draft, spec, setSpec, isSpecDirty } = draftApi;
const { EditorPane, Renderer } = panelDefinition;
const { EditorPane } = panelDefinition;
const { save, isSaving } = usePanelEditorSave({
dashboardId,
@@ -80,6 +77,11 @@ function StaticEditorBody({
}
}, [isEditable, save, draft.spec, setScrollTargetId, onSaved, showErrorModal]);
const onChangeText = useCallback(
(text: string): void => setSpec(withPanelText(spec, text)),
[spec, setSpec],
);
const onCloseEditor = useCallback((): void => {
if (!isNew) {
setScrollTargetId(panelId);
@@ -103,22 +105,14 @@ function StaticEditorBody({
/>
}
preview={
<div className={styles.staticPreviewSurface}>
<PanelHeader
panelId={panelId}
panel={draft}
data={EMPTY_PANEL_QUERY_DATA}
isFetching={false}
error={null}
hideActions
/>
<StaticPanelBody
Renderer={Renderer}
panel={draft}
panelId={panelId}
panelMode={PanelMode.DASHBOARD_EDIT}
/>
</div>
<PreviewPane
mode="static"
panelId={panelId}
panel={draft}
panelDefinition={panelDefinition}
panelMode={PanelMode.DASHBOARD_EDIT}
onChangeText={isEditable ? onChangeText : undefined}
/>
}
editor={<EditorPane spec={spec} onChangeSpec={setSpec} />}
config={

View File

@@ -0,0 +1,142 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import type { RenderableStaticPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import StaticEditorBody from '../StaticEditorBody';
import type { PanelEditorDraftApi } from '../types';
import { usePanelEditorSave } from '../hooks/usePanelEditorSave';
jest.mock('../hooks/usePanelEditorSave', () => ({
usePanelEditorSave: jest.fn(),
}));
// Chrome + collaborators stubbed: this suite asserts the static body's wiring —
// the save shape above all — not their internals.
jest.mock('../Header/Header', () => ({
__esModule: true,
default: ({ onSave }: { onSave: () => void }): JSX.Element => (
<button type="button" data-testid="header-save" onClick={onSave}>
Save
</button>
),
}));
jest.mock('../ConfigPane/ConfigPane', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="config-pane" />,
}));
jest.mock(
'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelHeader/PanelHeader',
() => ({ __esModule: true, default: (): null => null }),
);
jest.mock(
'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/StaticPanelBody/StaticPanelBody',
() => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="static-preview-body" />,
}),
);
jest.mock('@signozhq/ui/sonner', () => ({ toast: { success: jest.fn() } }));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): unknown => ({ showErrorModal: jest.fn() }),
}));
// The derivation has its own suite (useDashboardEditContext.authz); these cases are
// about what the static body does with a given edit context, so control it directly.
let editContext = { isEditable: true, editChecks: [], editDisabledTooltip: '' };
jest.mock(
'pages/DashboardPage/DashboardContainer/hooks/useDashboardEditContext',
() => ({
useDashboardEditContext: (): typeof editContext => editContext,
}),
);
const mockUseSave = usePanelEditorSave as jest.Mock;
// The draft deliberately carries a stray query: Save must strip it — the API
// rejects anything but [] for a static kind.
const draft = {
kind: 'Panel',
spec: {
display: { name: 'Runbook' },
plugin: { kind: 'signoz/TextPanel', spec: { text: '# hi' } },
queries: [{ spec: {} }],
},
} as unknown as DashboardtypesPanelDTO;
const draftApi: PanelEditorDraftApi = {
draft,
spec: draft.spec,
setSpec: jest.fn(),
isSpecDirty: false,
reset: jest.fn(),
};
const definition = {
kind: 'signoz/TextPanel',
displayName: 'Text',
sections: [],
actions: {},
mode: 'static',
Renderer: (): null => null,
EditorPane: (): JSX.Element => <div data-testid="editor-pane" />,
} as unknown as RenderableStaticPanelDefinition;
function renderBody(): void {
render(
<StaticEditorBody
dashboardId="d1"
panelId="p1"
panel={draft}
onClose={jest.fn()}
onSaved={jest.fn()}
draftApi={draftApi}
panelDefinition={definition}
onChangePanelKind={jest.fn()}
/>,
);
}
describe('StaticEditorBody', () => {
beforeEach(() => {
mockUseSave.mockReset();
editContext = { isEditable: true, editChecks: [], editDisabledTooltip: '' };
mockUseSave.mockReturnValue({
save: jest.fn().mockResolvedValue('p1'),
isSaving: false,
});
});
it('renders the editor pane and the live preview, no query builder', () => {
renderBody();
expect(screen.getByTestId('editor-pane')).toBeInTheDocument();
expect(screen.getByTestId('static-preview-body')).toBeInTheDocument();
expect(
screen.queryByTestId('panel-editor-v2-query-builder'),
).not.toBeInTheDocument();
});
it('saves the spec with queries forced to [] — the only shape the API accepts', async () => {
const save = jest.fn().mockResolvedValue('p1');
mockUseSave.mockReturnValue({ save, isSaving: false });
renderBody();
fireEvent.click(screen.getByTestId('header-save'));
await waitFor(() => expect(save).toHaveBeenCalledTimes(1));
expect(save).toHaveBeenCalledWith({ ...draft.spec, queries: [] });
});
it('does not save when the dashboard is not editable', () => {
const save = jest.fn();
mockUseSave.mockReturnValue({ save, isSaving: false });
editContext = {
isEditable: false,
editChecks: [],
editDisabledTooltip: 'Dashboard is locked',
};
renderBody();
fireEvent.click(screen.getByTestId('header-save'));
expect(save).not.toHaveBeenCalled();
});
});

View File

@@ -115,3 +115,15 @@ describe('newPanelRoute', () => {
});
});
});
describe('parseNewPanelKind — kinds without a legacy panel type', () => {
it('accepts a registered static kind', () => {
expect(parseNewPanelKind('new', '?panelKind=signoz%2FTextPanel')).toBe(
'signoz/TextPanel',
);
});
it('still rejects a kind that is not registered', () => {
expect(parseNewPanelKind('new', '?panelKind=signoz%2FNopePanel')).toBeNull();
});
});

View File

@@ -7,7 +7,7 @@ import type { PANEL_TYPES } from 'constants/queryBuilder';
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { isPanelKindSupported } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import type { RenderableQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelDefinition';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import {
usePanelQuery,
type PanelQueryTimeOverride,
@@ -90,7 +90,7 @@ export function usePanelEditSession({
// Hosts fork on `definition.mode` before mounting this session (the editor and
// View modal shells) — asserted rather than assumed.
const panelDefinition = requireQueryPanelDefinition(panelKind);
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
const panelType = toPanelType(panelKind);
const defaultSignal = panelDefinition.supportedSignals[0];
const query = usePanelQuery({

View File

@@ -19,10 +19,7 @@ import type {
} from 'types/api/queryBuilder/queryBuilderData';
import { isStaticPanelKind, resolveQueryType } from '../../Panels/capabilities';
import {
PANEL_KIND_TO_PANEL_TYPE,
type PanelKind,
} from '../../Panels/types/panelKind';
import { toPanelType, type PanelKind } from '../../Panels/types/panelKind';
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../queryV5/persesQueryAdapters';
import {
@@ -110,7 +107,7 @@ export function usePanelTypeSwitch({
builderQuery: query,
});
const newPanelType = PANEL_KIND_TO_PANEL_TYPE[newKind];
const newPanelType = toPanelType(newKind);
// Only `plugin` needs a cast: it's a discriminated union over `kind`, and a
// dynamically-chosen kind can't be correlated with its spec statically (as in

View File

@@ -1,6 +1,6 @@
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import QueryEditorBody from './QueryEditorBody';
import StaticEditorBody from './StaticEditorBody';
@@ -41,7 +41,7 @@ function PanelEditorContainer(props: PanelEditorContainerProps): JSX.Element {
const { onChangePanelKind } = usePanelTypeSwitch({
spec: draftApi.draft.spec,
panelType: PANEL_KIND_TO_PANEL_TYPE[panelKind],
panelType: toPanelType(panelKind),
setSpec: draftApi.setSpec,
});

View File

@@ -4,8 +4,8 @@ import type { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PANELS } from '../Panels/registry';
import {
PANEL_KIND_TO_PANEL_TYPE,
PANEL_TYPE_TO_PANEL_KIND,
type PanelKind,
} from '../Panels/types/panelKind';
@@ -40,7 +40,9 @@ export function parseNewPanelKind(
return null;
}
const kind = new URLSearchParams(search).get(PANEL_KIND_PARAM);
return kind && kind in PANEL_KIND_TO_PANEL_TYPE ? (kind as PanelKind) : null;
// Gated on the registry, not the legacy map — a static kind has no legacy
// panel type, and the map would reject its route as a stale link.
return kind && kind in PANELS ? (kind as PanelKind) : null;
}
/**

View File

@@ -34,6 +34,8 @@ const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/ListPanel': [QUERY_BUILDER],
// Static kind: no query surface at all.
'signoz/TextPanel': [],
};
const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
@@ -45,11 +47,16 @@ const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
'signoz/TablePanel': [metrics, logs, traces],
// List renders raw rows; metrics produce no row data.
'signoz/ListPanel': [logs, traces],
'signoz/TextPanel': [],
};
// Exhaustive over PanelKind, so a new kind can't ship without stating how its request is
// shaped — the check that used to be implicit in a legacy PANEL_TYPES switch.
const EXPECTED_QUERY_CAPABILITIES: Record<PanelKind, PanelQueryCapabilities> = {
// Partial: a static kind declares no query capabilities — the lookup below
// resolves undefined on both sides for it.
const EXPECTED_QUERY_CAPABILITIES: Partial<
Record<PanelKind, PanelQueryCapabilities>
> = {
'signoz/TimeSeriesPanel': {
requestType: time_series,
formatTableResultForUI: false,

View File

@@ -8,7 +8,7 @@ import {
selectViewPanelExtendWindow,
useViewPanelStore,
} from '../../../store/useViewPanelStore';
import { PANEL_KIND_TO_PANEL_TYPE } from '../../types/panelKind';
import { toPanelType } from '../../types/panelKind';
import PanelLoader from '../PanelLoader/PanelLoader';
import PanelMessage, { PanelMessageAction } from '../PanelMessage/PanelMessage';
import { useExtendTimeWindow } from './useExtendTimeWindow';
@@ -57,7 +57,7 @@ function NoData({
// `panelType` stays on the event so existing reports keep resolving; `panelKind` is the
// V2 identity, and the only one that can tell two kinds sharing a panel type apart.
const panelKind = panel.spec.plugin.kind;
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
const panelType = toPanelType(panelKind);
const extendAction: PanelMessageAction | undefined =
activeExtend?.canExtend && activeExtend.actionLabel

View File

@@ -0,0 +1,147 @@
import { renderHook } from '@testing-library/react';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { rgbaFromHex } from '../../kinds/TextPanel/background/contrast';
import {
INK_ALPHAS,
SECONDARY_INK_OPACITY,
TEXT_BACKGROUND_PAIRS,
TRANSPARENT_BACKGROUND,
} from '../../kinds/TextPanel/background/presets';
import { useTextBackground } from '../useTextBackground';
const isDarkMode = jest.fn<boolean, []>(() => true);
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => isDarkMode(),
}));
function textPanel(background?: string): DashboardtypesPanelSpecDTO {
return {
display: { name: 'Panel' },
plugin: {
kind: 'signoz/TextPanel',
spec: { text: '', presentation: { background } },
},
queries: [],
} as unknown as DashboardtypesPanelSpecDTO;
}
describe('useTextBackground', () => {
beforeEach(() => {
isDarkMode.mockReturnValue(true);
});
it('sets no custom properties for the default surface', () => {
const { result } = renderHook(() => useTextBackground(textPanel()));
expect(result.current).toStrictEqual({ kind: 'default', style: {} });
});
// The card, its border and the header's divider all read these.
it('paints a zero-alpha background transparent rather than dropping the card', () => {
const { result } = renderHook(() =>
useTextBackground(textPanel(TRANSPARENT_BACKGROUND)),
);
expect(result.current).toStrictEqual({
kind: 'none',
style: {
'--text-panel-surface': 'transparent',
'--text-panel-border': 'transparent',
},
});
});
it('exposes the preset pair for the current theme', () => {
const { result } = renderHook(() =>
useTextBackground(textPanel(TEXT_BACKGROUND_PAIRS.amber.dark.surface)),
);
const { ink } = TEXT_BACKGROUND_PAIRS.amber.dark;
expect(result.current.style).toStrictEqual({
'--text-panel-surface': TEXT_BACKGROUND_PAIRS.amber.dark.surface,
'--text-panel-ink': ink,
'--text-panel-border': 'rgba(255, 255, 255, 0.09)',
'--text-panel-link-decoration': 'underline',
'--text-panel-ink-secondary': rgbaFromHex(ink, SECONDARY_INK_OPACITY),
'--text-panel-grip': rgbaFromHex(ink, INK_ALPHAS['--text-panel-grip']),
'--scrollbar-thumb': rgbaFromHex(ink, INK_ALPHAS['--scrollbar-thumb']),
'--scrollbar-thumb-hover': rgbaFromHex(
ink,
INK_ALPHAS['--scrollbar-thumb-hover'],
),
'--text-panel-pill-surface': rgbaFromHex(
ink,
INK_ALPHAS['--text-panel-pill-surface'],
),
});
});
it('draws the surface chrome from the ink', () => {
const { result } = renderHook(() =>
useTextBackground(textPanel(TEXT_BACKGROUND_PAIRS.amber.light.surface)),
);
Object.keys(INK_ALPHAS).forEach((name) => {
expect(result.current.style).toHaveProperty(name);
});
});
// Stored light, read in dark: the ink is the dark pair's.
it('carries the secondary ink and the link underline', () => {
const { result } = renderHook(() =>
useTextBackground(textPanel(TEXT_BACKGROUND_PAIRS.sakura.light.surface)),
);
expect(result.current.style).toMatchObject({
'--text-panel-ink-secondary': `rgba(253, 232, 242, ${SECONDARY_INK_OPACITY})`,
'--text-panel-link-decoration': 'underline',
});
});
it('re-resolves a stored surface when the theme changes', () => {
const spec = textPanel(TEXT_BACKGROUND_PAIRS.forest.dark.surface);
const { result, rerender } = renderHook(() => useTextBackground(spec));
expect(result.current.style).toMatchObject({
'--text-panel-surface': TEXT_BACKGROUND_PAIRS.forest.dark.surface,
});
isDarkMode.mockReturnValue(false);
rerender();
expect(result.current.style).toMatchObject({
'--text-panel-surface': TEXT_BACKGROUND_PAIRS.forest.light.surface,
'--text-panel-ink': TEXT_BACKGROUND_PAIRS.forest.light.ink,
'--text-panel-border': 'rgba(0, 0, 0, 0.07)',
});
});
it('paints a custom colour the same in both themes', () => {
const spec = textPanel('#3A2A64');
const { result, rerender } = renderHook(() => useTextBackground(spec));
const inDark = result.current.style;
isDarkMode.mockReturnValue(false);
rerender();
expect(result.current.style).toMatchObject({
'--text-panel-surface': '#3A2A64',
'--text-panel-ink': inDark['--text-panel-ink' as keyof typeof inDark],
});
});
it('leaves a kind without a presentation slice alone', () => {
const { result } = renderHook(() =>
useTextBackground({
display: { name: 'Panel' },
plugin: { kind: 'signoz/TimeSeriesPanel', spec: {} },
queries: [],
} as unknown as DashboardtypesPanelSpecDTO),
);
expect(result.current).toStrictEqual({ kind: 'default', style: {} });
});
});

View File

@@ -0,0 +1,73 @@
import { renderHook } from '@testing-library/react';
import { useUpdatePanelText } from '../useUpdatePanelText';
const patchAsync = jest.fn<Promise<unknown>, [unknown]>(() =>
Promise.resolve(undefined),
);
const showErrorModal = jest.fn();
let store = { dashboardId: 'dash-1' };
let editContext = { isEditable: true };
jest.mock('../../../hooks/useOptimisticPatch', () => ({
useOptimisticPatch: (): unknown => ({ patchAsync }),
}));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): unknown => ({ showErrorModal }),
}));
jest.mock('../../../store/useDashboardStore', () => ({
useDashboardStore: (select: (s: typeof store) => unknown): unknown =>
select(store),
}));
jest.mock('../../../hooks/useDashboardEditContext', () => ({
useDashboardEditContext: (): typeof editContext => editContext,
}));
describe('useUpdatePanelText', () => {
beforeEach(() => {
jest.clearAllMocks();
store = { dashboardId: 'dash-1' };
editContext = { isEditable: true };
});
it('patches the panel body', () => {
const { result } = renderHook(() => useUpdatePanelText('p1'));
result.current?.('- [x] done');
expect(patchAsync).toHaveBeenCalledWith([
{
op: 'add',
path: '/spec/panels/p1/spec/plugin/spec/text',
value: '- [x] done',
},
]);
});
it('gives no callback when the viewer cannot edit', () => {
editContext = { isEditable: false };
const { result } = renderHook(() => useUpdatePanelText('p1'));
expect(result.current).toBeUndefined();
});
it('gives no callback outside a dashboard', () => {
store = { dashboardId: '' };
const { result } = renderHook(() => useUpdatePanelText('p1'));
expect(result.current).toBeUndefined();
});
it('surfaces a failed save', async () => {
const failure = new Error('locked');
patchAsync.mockRejectedValueOnce(failure);
const { result } = renderHook(() => useUpdatePanelText('p1'));
result.current?.('- [x] done');
await Promise.resolve();
expect(showErrorModal).toHaveBeenCalledWith(failure);
});
});

View File

@@ -0,0 +1,67 @@
import {
type RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
// Within this distance of the end counts as "at the bottom", so the pill isn't
// kept alive by sub-pixel rounding or a trailing margin.
const BOTTOM_EPSILON_PX = 16;
interface UseOverflowBelowResult<T extends HTMLElement> {
scrollRef: RefObject<T>;
/** Content extends below the fold and the user isn't at the bottom yet. */
hasMoreBelow: boolean;
scrollToBottom: () => void;
}
/**
* Tracks whether a scroll container has unseen content below the fold. Re-measures
* on scroll, on container resize, and on every commit — the cheap way to follow
* content growth (a live preview re-rendering as the body is typed) without
* observing the subtree.
*/
export function useOverflowBelow<
T extends HTMLElement,
>(): UseOverflowBelowResult<T> {
const scrollRef = useRef<T>(null);
const [hasMoreBelow, setHasMoreBelow] = useState(false);
const measure = useCallback((): void => {
const el = scrollRef.current;
if (!el) {
return;
}
const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
setHasMoreBelow(remaining > BOTTOM_EPSILON_PX);
}, []);
// No deps on purpose: runs after every commit. setState bails on unchanged
// values, so this settles instead of looping.
useEffect(() => {
measure();
});
useEffect(() => {
const el = scrollRef.current;
if (!el) {
return undefined;
}
el.addEventListener('scroll', measure, { passive: true });
const observer = new ResizeObserver(measure);
observer.observe(el);
return (): void => {
el.removeEventListener('scroll', measure);
observer.disconnect();
};
}, [measure]);
const scrollToBottom = useCallback((): void => {
const el = scrollRef.current;
el?.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
}, []);
return { scrollRef, hasMoreBelow, scrollToBottom };
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { isLanguageRegistered, loadLanguage } from './syntaxLanguages';
import { isLanguageRegistered, loadLanguage } from '../utils/syntaxLanguages';
/**
* Registers `language` with Prism on demand, reporting when it is ready to

View File

@@ -0,0 +1,83 @@
import { useMemo } from 'react';
import type { CSSProperties } from 'react';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { rgbaFromHex } from '../kinds/TextPanel/background/contrast';
import {
INK_ALPHAS,
PRESET_BORDER,
} from '../kinds/TextPanel/background/presets';
import { resolveTextBackground } from '../kinds/TextPanel/background/resolveTextBackground';
import {
PanelTheme,
TextBackgroundKind,
} from '../kinds/TextPanel/background/types';
export interface TextBackground {
kind: TextBackgroundKind;
/**
* Custom properties for the card root. Empty for `default`, so the stylesheet's
* own fallbacks decide — nothing here hardcodes a surface.
*/
style: CSSProperties;
}
const NO_STYLE: CSSProperties = {};
// D7: `None` drops the card so the body sits on the dashboard canvas. It rides the
// same properties as a colour, which takes the header's divider with it.
const CARDLESS_STYLE = {
'--text-panel-surface': 'transparent',
'--text-panel-border': 'transparent',
} as CSSProperties;
function inkShares(ink: string): Record<string, string> {
return Object.fromEntries(
Object.entries(INK_ALPHAS).map(([name, alpha]) => [
name,
rgbaFromHex(ink, alpha) ?? ink,
]),
);
}
/**
* The card is an ancestor of the renderer, so the host owns these properties and
* everything below inherits them.
*
* Reading one plugin-spec field off the kind union is the accepted smell (TDD
* D7): a dynamic kind can't narrow it, hence one localized cast per host.
*/
export function useTextBackground(
spec: DashboardtypesPanelSpecDTO,
): TextBackground {
const isDarkMode = useIsDarkMode();
const background = (
spec.plugin.spec as {
presentation?: { background?: string | null };
}
).presentation?.background;
return useMemo(() => {
const theme = isDarkMode ? PanelTheme.Dark : PanelTheme.Light;
const resolved = resolveTextBackground(background, theme);
if (resolved.kind === TextBackgroundKind.None) {
return { kind: resolved.kind, style: CARDLESS_STYLE };
}
return {
kind: resolved.kind,
style:
resolved.surface && resolved.ink
? ({
'--text-panel-surface': resolved.surface,
'--text-panel-ink': resolved.ink,
'--text-panel-border': PRESET_BORDER[theme],
'--text-panel-link-decoration': 'underline',
...inkShares(resolved.ink),
} as CSSProperties)
: NO_STYLE,
};
}, [background, isDarkMode]);
}

View File

@@ -0,0 +1,33 @@
import { useCallback } from 'react';
import { useErrorModal } from 'providers/ErrorModalProvider';
import type APIError from 'types/api/error';
import { useDashboardEditContext } from '../../hooks/useDashboardEditContext';
import { useOptimisticPatch } from '../../hooks/useOptimisticPatch';
import { setPanelTextOp } from '../../patchOps';
import { useDashboardStore } from '../../store/useDashboardStore';
/**
* Saves a panel's authored body, or `undefined` when the viewer cannot edit it —
* the absent callback is the read-only gate, so nothing downstream re-checks.
* The patch is optimistic: the edit shows at once and rolls back if it fails.
*/
export function useUpdatePanelText(
panelId: string,
): ((text: string) => void) | undefined {
const dashboardId = useDashboardStore((s) => s.dashboardId);
const { isEditable } = useDashboardEditContext();
const { patchAsync } = useOptimisticPatch();
const { showErrorModal } = useErrorModal();
const save = useCallback(
(text: string): void => {
patchAsync([setPanelTextOp(panelId, text)]).catch((error) => {
showErrorModal(error as APIError);
});
},
[panelId, patchAsync, showErrorModal],
);
return dashboardId && isEditable ? save : undefined;
}

View File

@@ -0,0 +1,69 @@
@use '../../../../../../styles/scrollbar' as *;
.panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 8px 12px;
overflow: auto;
@include custom-scrollbar;
}
// Horizontal alignment inherits into the rendered body, which deliberately leaves
// `text-align` alone so the panel can own it.
.alignLeft {
text-align: left;
}
.alignCenter {
text-align: center;
}
.alignRight {
text-align: right;
}
// `text-align` moves only inline content: the table is a block box inside its
// scroll wrapper and stays put, and list markers hang at the list's left edge.
// (0,2,1) beats the body reset at (0,2,0); `list-style-position` goes on the
// `li` directly because the body's `list-style` shorthand on `ul`/`ol` resets
// the inherited position.
.panel.alignRight table {
margin-left: auto;
}
.panel.alignCenter table {
margin-left: auto;
margin-right: auto;
}
.panel.alignRight li,
.panel.alignCenter li {
list-style-position: inside;
}
.alignTop {
justify-content: flex-start;
}
// Auto margins, not `justify-content`: when the body overflows, an auto margin
// resolves to zero so the content's top stays scrollable — `center`/`flex-end`
// push the overflow above the scrollport, where no scroll position reaches it.
// Specificity (0,3,0): the body root's `all: revert` reset sits at (0,2,0) and
// would strip a tied margin rule.
.panel.alignMiddle > *:first-child {
margin-top: auto;
margin-bottom: auto;
}
.panel.alignBottom > *:first-child {
margin-top: auto;
}
// Positioning context for the scroll-to-bottom pill floating over the body.
.host {
position: relative;
height: 100%;
min-height: 0;
}

View File

@@ -0,0 +1,101 @@
import { useMemo } from 'react';
import { Pencil } from '@signozhq/icons';
import cx from 'classnames';
import {
DashboardtypesTextAlignDTO,
DashboardtypesVerticalAlignDTO,
} from 'api/generated/services/sigNoz.schemas';
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
import PanelMessage from '../../components/PanelMessage/PanelMessage';
import type { StaticRendererProps } from '../../types/rendererProps';
import { interpolateVariables } from '../../utils/interpolateVariables';
import MarkdownContent from './components/MarkdownContent/MarkdownContent';
import ScrollToBottomPill from './components/ScrollToBottomPill/ScrollToBottomPill';
import { useOverflowBelow } from '../../hooks/useOverflowBelow';
import styles from './Renderer.module.scss';
const HORIZONTAL_ALIGN_CLASS: Record<DashboardtypesTextAlignDTO, string> = {
[DashboardtypesTextAlignDTO.left]: styles.alignLeft,
[DashboardtypesTextAlignDTO.center]: styles.alignCenter,
[DashboardtypesTextAlignDTO.right]: styles.alignRight,
};
// Static, so it is not rebuilt on every variable tick.
const EMPTY_STATE = (
<PanelMessage
icon={<Pencil size={18} />}
title="Nothing written yet"
description="Add Markdown to this panel to show content."
data-testid="text-panel-empty"
/>
);
const VERTICAL_ALIGN_CLASS: Record<DashboardtypesVerticalAlignDTO, string> = {
[DashboardtypesVerticalAlignDTO.top]: styles.alignTop,
[DashboardtypesVerticalAlignDTO.center]: styles.alignMiddle,
[DashboardtypesVerticalAlignDTO.bottom]: styles.alignBottom,
};
/**
* Renders the panel's own Markdown body. The first kind that issues no query, so it
* reads nothing from `data` and has no loading or error state — malformed Markdown
* renders as literal text rather than throwing.
*/
function Renderer({
panel,
dashboardId,
onChangeText,
}: StaticRendererProps<'signoz/TextPanel'>): JSX.Element {
const { text, presentation } = panel.spec.plugin.spec;
const variables = useDashboardStore(
selectResolvedVariables(dashboardId ?? ''),
);
// Interpolate and parse together: a dashboard re-renders on every variable tick,
// and re-parsing every text panel on each one is the cost worth avoiding.
const body = useMemo(
() => interpolateVariables(text ?? '', variables),
[text, variables],
);
// The authored body, not the interpolated one: an edit lands on what is saved.
const interactive = useMemo(
() =>
onChangeText
? { source: text ?? '', onChangeSource: onChangeText }
: undefined,
[onChangeText, text],
);
const { scrollRef, hasMoreBelow, scrollToBottom } =
useOverflowBelow<HTMLDivElement>();
return (
<div className={styles.host}>
<div
ref={scrollRef}
className={cx(
styles.panel,
HORIZONTAL_ALIGN_CLASS[
presentation?.textAlign ?? DashboardtypesTextAlignDTO.left
],
VERTICAL_ALIGN_CLASS[
presentation?.verticalAlign ?? DashboardtypesVerticalAlignDTO.top
],
)}
data-testid="text-panel"
>
<MarkdownContent interactive={interactive} emptyState={EMPTY_STATE}>
{body}
</MarkdownContent>
</div>
{hasMoreBelow && <ScrollToBottomPill onClick={scrollToBottom} />}
</div>
);
}
export default Renderer;

View File

@@ -1,7 +1,8 @@
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import MarkdownContent from '../MarkdownContent';
import { loadLanguage } from '../syntaxLanguages';
import MarkdownContent from '../components/MarkdownContent/MarkdownContent';
import { loadLanguage } from '../../../utils/syntaxLanguages';
describe('MarkdownContent', () => {
describe('security', () => {
@@ -234,6 +235,23 @@ describe('MarkdownContent — interactive task lists', () => {
expect(second).toBeChecked();
});
it('warns on hover that a tick edits the panel', async () => {
const user = userEvent.setup();
render(
<MarkdownContent interactive={{ source, onChangeSource: jest.fn() }}>
{source}
</MarkdownContent>,
);
await user.hover(screen.getAllByRole('checkbox')[0]);
await waitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent(
'Toggling this updates the panel spec',
);
});
});
it('checking one rewrites its marker in the source', () => {
const onChangeSource = jest.fn();
render(

View File

@@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import { PanelMode } from 'lib/visualization/panels/types';
import type { PanelOfKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/rendererProps';
import Renderer from '../Renderer';
function textPanel(text?: string): PanelOfKind<'signoz/TextPanel'> {
return {
kind: 'Panel',
spec: {
display: { name: 'Runbook' },
plugin: { kind: 'signoz/TextPanel', spec: { text } },
queries: [],
},
} as unknown as PanelOfKind<'signoz/TextPanel'>;
}
function renderPanel(text?: string): void {
render(
<Renderer
panelId="p1"
panel={textPanel(text)}
panelMode={PanelMode.DASHBOARD_VIEW}
/>,
);
}
describe('Text panel empty state', () => {
it.each([undefined, '', ' \n\t'])('stands in for a body of %j', (text) => {
renderPanel(text);
expect(screen.getByTestId('text-panel-empty')).toBeInTheDocument();
expect(screen.getByText('Nothing written yet')).toBeInTheDocument();
});
it('gives way to the body once there is one', () => {
renderPanel('# Runbook');
expect(screen.queryByTestId('text-panel-empty')).not.toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Runbook' })).toBeInTheDocument();
});
// An undefined variable renders literally, as queries treat one, so the body
// is not empty and the panel shows it rather than the empty state.
it('does not stand in for an unresolved variable', () => {
renderPanel('$missing');
expect(screen.queryByTestId('text-panel-empty')).not.toBeInTheDocument();
expect(screen.getByText('$missing')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,81 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { PanelOfKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/rendererProps';
import { PanelMode } from 'lib/visualization/panels/types';
import Renderer from '../Renderer';
const panel = {
kind: 'Panel',
spec: {
display: { name: 'Runbook' },
plugin: { kind: 'signoz/TextPanel', spec: { text: '# hello' } },
queries: [],
},
} as unknown as PanelOfKind<'signoz/TextPanel'>;
/** jsdom has no layout: stub the scroll geometry the hook reads. */
function setScrollGeometry(
el: HTMLElement,
{ scrollHeight, clientHeight }: { scrollHeight: number; clientHeight: number },
): void {
Object.defineProperty(el, 'scrollHeight', {
configurable: true,
value: scrollHeight,
});
Object.defineProperty(el, 'clientHeight', {
configurable: true,
value: clientHeight,
});
}
function renderPanel(): HTMLElement {
render(
<Renderer panelId="p1" panel={panel} panelMode={PanelMode.DASHBOARD_VIEW} />,
);
return screen.getByTestId('text-panel');
}
describe('Text panel scroll-to-bottom pill', () => {
it('is absent when the body fits', () => {
const scroller = renderPanel();
setScrollGeometry(scroller, { scrollHeight: 100, clientHeight: 100 });
fireEvent.scroll(scroller);
expect(
screen.queryByTestId('text-panel-scroll-more'),
).not.toBeInTheDocument();
});
it('appears when content extends below the fold and jumps to the end on click', () => {
const scroller = renderPanel();
setScrollGeometry(scroller, { scrollHeight: 400, clientHeight: 100 });
act(() => {
fireEvent.scroll(scroller);
});
const pill = screen.getByTestId('text-panel-scroll-more');
const scrollTo = jest.fn();
scroller.scrollTo = scrollTo;
fireEvent.click(pill);
expect(scrollTo).toHaveBeenCalledWith({ top: 400, behavior: 'smooth' });
});
it('hides once the user reaches the bottom', () => {
const scroller = renderPanel();
setScrollGeometry(scroller, { scrollHeight: 400, clientHeight: 100 });
act(() => {
fireEvent.scroll(scroller);
});
expect(screen.getByTestId('text-panel-scroll-more')).toBeInTheDocument();
scroller.scrollTop = 300;
act(() => {
fireEvent.scroll(scroller);
});
expect(
screen.queryByTestId('text-panel-scroll-more'),
).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,69 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { PanelMode } from 'lib/visualization/panels/types';
import type { PanelOfKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/rendererProps';
import Renderer from '../Renderer';
const SOURCE = ['- [ ] first', '- [x] second'].join('\n');
function textPanel(text: string): PanelOfKind<'signoz/TextPanel'> {
return {
kind: 'Panel',
spec: {
display: { name: 'Runbook' },
plugin: { kind: 'signoz/TextPanel', spec: { text } },
queries: [],
},
} as unknown as PanelOfKind<'signoz/TextPanel'>;
}
describe('Text panel task lists', () => {
it('renders them read-only without a write channel', () => {
render(
<Renderer
panelId="p1"
panel={textPanel(SOURCE)}
panelMode={PanelMode.DASHBOARD_VIEW}
/>,
);
screen.getAllByRole('checkbox').forEach((box) => expect(box).toBeDisabled());
});
it('reports the rewritten body when a host can save it', () => {
const onChangeText = jest.fn();
render(
<Renderer
panelId="p1"
panel={textPanel(SOURCE)}
panelMode={PanelMode.DASHBOARD_VIEW}
onChangeText={onChangeText}
/>,
{ wrapper: TooltipProvider },
);
fireEvent.click(screen.getAllByRole('checkbox')[0]);
expect(onChangeText).toHaveBeenCalledWith(
['- [x] first', '- [x] second'].join('\n'),
);
});
it('edits the authored body, not the interpolated one', () => {
const onChangeText = jest.fn();
render(
<Renderer
panelId="p1"
panel={textPanel('- [ ] deploy $service')}
panelMode={PanelMode.DASHBOARD_VIEW}
onChangeText={onChangeText}
/>,
{ wrapper: TooltipProvider },
);
fireEvent.click(screen.getByRole('checkbox'));
expect(onChangeText).toHaveBeenCalledWith('- [x] deploy $service');
});
});

View File

@@ -0,0 +1,265 @@
import {
contrastRatio,
inkForSurface,
meetsContrast,
MIN_CONTRAST_RATIO,
normalizeHex,
parseHex,
rgbaFromHex,
} from '../contrast';
import {
CUSTOM_INK,
TEXT_BACKGROUND_PAIRS,
TEXT_BACKGROUND_PRESETS,
TRANSPARENT_BACKGROUND,
} from '../presets';
import {
presetSurface,
resolveTextBackground,
selectionFromResolved,
storedFromSelection,
toStoredBackground,
} from '../resolveTextBackground';
import type { ResolvedTextBackground } from '../types';
import { PanelTheme, TextBackgroundKind, TextBackgroundPreset } from '../types';
const THEMES: PanelTheme[] = Object.values(PanelTheme);
describe('preset tokens', () => {
const pairs = TEXT_BACKGROUND_PRESETS.flatMap((preset) =>
THEMES.map((theme) => ({
preset,
theme,
...TEXT_BACKGROUND_PAIRS[preset][theme],
})),
);
it('covers all eight presets in both themes', () => {
expect(pairs).toHaveLength(16);
});
it.each(pairs)(
'$preset/$theme clears the contrast floor',
({ surface, ink }) => {
expect(contrastRatio(ink, surface)).toBeGreaterThanOrEqual(
MIN_CONTRAST_RATIO,
);
},
);
// A repeated surface would make the hex → preset lookup ambiguous.
it('keeps all sixteen surfaces distinct', () => {
const surfaces = pairs.map(({ surface }) => surface.toUpperCase());
expect(new Set(surfaces).size).toBe(16);
});
});
describe('parseHex', () => {
it('expands shorthand digits', () => {
expect(parseHex('#abc')).toStrictEqual({ r: 170, g: 187, b: 204, a: 1 });
});
it('reads the alpha channel from the four- and eight-digit forms', () => {
expect(parseHex('#0000')?.a).toBe(0);
expect(parseHex('#00000000')?.a).toBe(0);
expect(parseHex('#aabbccff')?.a).toBe(1);
});
it('treats a form without an alpha channel as opaque', () => {
expect(parseHex('#aabbcc')?.a).toBe(1);
});
it.each(['', 'aabbcc', 'red', '#abcde', '#gggggg'])('rejects %s', (color) => {
expect(parseHex(color)).toBeUndefined();
});
it('normalises to the uppercase six-digit form', () => {
expect(normalizeHex('#dce4ff')).toBe('#DCE4FF');
expect(normalizeHex('#abc')).toBe('#AABBCC');
expect(normalizeHex('#dce4ffcc')).toBe('#DCE4FF');
});
});
describe('rgbaFromHex', () => {
it('takes a share of the colour', () => {
expect(rgbaFromHex('#DCE4FF', 0.82)).toBe('rgba(220, 228, 255, 0.82)');
});
it('expands shorthand and ignores the source alpha', () => {
expect(rgbaFromHex('#abc', 1)).toBe('rgba(170, 187, 204, 1)');
expect(rgbaFromHex('#aabbcc00', 0.5)).toBe('rgba(170, 187, 204, 0.5)');
});
it('returns nothing for a colour it cannot read', () => {
expect(rgbaFromHex('red', 0.82)).toBeUndefined();
});
});
describe('inkForSurface', () => {
it('puts light ink on a dark surface and dark ink on a light one', () => {
expect(inkForSurface('#101010')).toBe(CUSTOM_INK.light);
expect(inkForSurface('#F5F5F5')).toBe(CUSTOM_INK.dark);
});
it('reports a mid surface as short of the floor without failing', () => {
const surface = '#808080';
expect(meetsContrast(surface, inkForSurface(surface))).toBe(false);
});
});
describe('resolveTextBackground', () => {
it.each([undefined, null, ''])('reads %s as the default surface', (stored) => {
expect(resolveTextBackground(stored, PanelTheme.Dark)).toStrictEqual({
kind: TextBackgroundKind.Default,
});
});
it.each([TRANSPARENT_BACKGROUND, '#0000'])(
'reads the zero-alpha colour %s as no card',
(stored) => {
expect(resolveTextBackground(stored, PanelTheme.Dark)).toStrictEqual({
kind: TextBackgroundKind.None,
});
},
);
it('resolves a surface stored in one theme to the pair of the other', () => {
const storedInLight = presetSurface(
TextBackgroundPreset.Amber,
PanelTheme.Light,
);
expect(resolveTextBackground(storedInLight, PanelTheme.Dark)).toStrictEqual({
kind: TextBackgroundKind.Preset,
preset: TextBackgroundPreset.Amber,
...TEXT_BACKGROUND_PAIRS.amber.dark,
});
});
it('recognises a preset surface whatever its case', () => {
const stored = presetSurface(
TextBackgroundPreset.Forest,
PanelTheme.Dark,
).toLowerCase();
expect(resolveTextBackground(stored, PanelTheme.Light)).toMatchObject({
kind: TextBackgroundKind.Preset,
preset: TextBackgroundPreset.Forest,
});
});
it('reads a hex that is not a preset surface as a custom colour', () => {
expect(resolveTextBackground('#3A2A64', PanelTheme.Dark)).toStrictEqual({
kind: TextBackgroundKind.Custom,
surface: '#3A2A64',
ink: CUSTOM_INK.light,
});
});
it('holds a custom colour steady across a theme switch', () => {
expect(resolveTextBackground('#3A2A64', PanelTheme.Light)).toStrictEqual(
resolveTextBackground('#3A2A64', PanelTheme.Dark),
);
});
// The enum the API used to accept; neither value is a hex.
it.each([
['solid', TextBackgroundKind.Default],
['transparent', TextBackgroundKind.None],
])('migrates the legacy %s value to %s', (stored, kind) => {
expect(resolveTextBackground(stored, PanelTheme.Dark)).toStrictEqual({
kind,
});
});
it('falls back to the default surface for an unreadable value', () => {
expect(resolveTextBackground('rgb(1, 2, 3)', PanelTheme.Dark)).toStrictEqual({
kind: TextBackgroundKind.Default,
});
});
});
describe('editor adapters', () => {
it.each([
[undefined, TextBackgroundKind.Default],
[TRANSPARENT_BACKGROUND, TextBackgroundKind.None],
['solid', TextBackgroundKind.Default],
])('lights up the %s swatch', (stored, selection) => {
expect(
selectionFromResolved(resolveTextBackground(stored, PanelTheme.Dark)),
).toBe(selection);
});
it('lights up the preset a stored surface belongs to', () => {
expect(
selectionFromResolved(
resolveTextBackground(
presetSurface(TextBackgroundPreset.Slate, PanelTheme.Light),
PanelTheme.Dark,
),
),
).toBe(TextBackgroundPreset.Slate);
});
it('lights up nothing for a custom colour', () => {
expect(
selectionFromResolved(resolveTextBackground('#3A2A64', PanelTheme.Dark)),
).toBeUndefined();
});
it('stores what each swatch means', () => {
expect(storedFromSelection(TextBackgroundKind.None, PanelTheme.Dark)).toBe(
TRANSPARENT_BACKGROUND,
);
expect(
storedFromSelection(TextBackgroundKind.Default, PanelTheme.Dark),
).toBeUndefined();
expect(
storedFromSelection(TextBackgroundPreset.Cherry, PanelTheme.Light),
).toBe(presetSurface(TextBackgroundPreset.Cherry, PanelTheme.Light));
});
});
describe('round trip', () => {
const cases: ResolvedTextBackground[] = [
{ kind: TextBackgroundKind.None },
{ kind: TextBackgroundKind.Default },
{
kind: TextBackgroundKind.Custom,
surface: '#3A2A64',
ink: CUSTOM_INK.light,
},
...TEXT_BACKGROUND_PRESETS.map((preset) => ({
kind: TextBackgroundKind.Preset,
preset,
})),
];
it.each(cases)(
'preserves $kind $preset through a save and load',
(resolved) => {
THEMES.forEach((theme) => {
const stored = toStoredBackground(resolved, theme);
const reread = resolveTextBackground(stored, theme);
expect(reread.kind).toBe(resolved.kind);
expect(reread.preset).toBe(resolved.preset);
});
},
);
it.each([
undefined,
TRANSPARENT_BACKGROUND,
'#3A2A64',
'solid',
'transparent',
])('re-reading %s changes nothing', (stored) => {
THEMES.forEach((theme) => {
const once = resolveTextBackground(stored, theme);
const twice = resolveTextBackground(toStoredBackground(once, theme), theme);
expect(twice).toStrictEqual(once);
});
});
});

View File

@@ -0,0 +1,91 @@
import { CUSTOM_INK } from './presets';
interface Channels {
r: number;
g: number;
b: number;
a: number;
}
const HEX_PATTERN = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
export function isHexColor(color: string): boolean {
return HEX_PATTERN.test(color);
}
/**
* Splits `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa` — shorthand digits double,
* and a form without an alpha channel is opaque. `undefined` for anything else.
*/
export function parseHex(color: string): Channels | undefined {
if (!isHexColor(color)) {
return undefined;
}
const hex = color.slice(1);
const short = hex.length <= 4;
const step = short ? 1 : 2;
const channel = (index: number): number => {
const digits = hex.slice(index * step, index * step + step);
return parseInt(short ? digits + digits : digits, 16);
};
return {
r: channel(0),
g: channel(1),
b: channel(2),
a: hex.length === 4 || hex.length === 8 ? channel(3) / 255 : 1,
};
}
/** The uppercase 6-digit form used as the preset lookup key. */
export function normalizeHex(color: string): string | undefined {
const channels = parseHex(color);
if (!channels) {
return undefined;
}
const pad = (value: number): string =>
value.toString(16).padStart(2, '0').toUpperCase();
return `#${pad(channels.r)}${pad(channels.g)}${pad(channels.b)}`;
}
/** The colour at a given alpha; the source's own alpha is ignored. */
export function rgbaFromHex(color: string, alpha: number): string | undefined {
const channels = parseHex(color);
if (!channels) {
return undefined;
}
return `rgba(${channels.r}, ${channels.g}, ${channels.b}, ${alpha})`;
}
/** WCAG 2.1 relative luminance; alpha is ignored. */
export function relativeLuminance(color: string): number {
const channels = parseHex(color);
if (!channels) {
return 0;
}
const linear = ([channels.r, channels.g, channels.b] as const).map((value) => {
const srgb = value / 255;
return srgb <= 0.03928 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
}
/** WCAG 2.1 contrast ratio, 1 to 21. */
export function contrastRatio(foreground: string, background: string): number {
const a = relativeLuminance(foreground);
const b = relativeLuminance(background);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
export const MIN_CONTRAST_RATIO = 4.5;
/** Whichever fixed ink contrasts further, so a custom colour needs none of its own. */
export function inkForSurface(surface: string): string {
return contrastRatio(CUSTOM_INK.light, surface) >=
contrastRatio(CUSTOM_INK.dark, surface)
? CUSTOM_INK.light
: CUSTOM_INK.dark;
}
export function meetsContrast(surface: string, ink: string): boolean {
return contrastRatio(ink, surface) >= MIN_CONTRAST_RATIO;
}

View File

@@ -0,0 +1,92 @@
import type { PanelTheme, TextBackgroundPair } from './types';
import { TextBackgroundPreset } from './types';
/** Declaration order is the swatch row order, after Transparent and Default panel. */
export const TEXT_BACKGROUND_PRESETS: readonly TextBackgroundPreset[] =
Object.values(TextBackgroundPreset);
/**
* The sixteen surfaces must stay distinct — `resolveTextBackground` recovers a
* preset name from a stored hex — and every pair must clear 4.5:1. Both are
* asserted in `__tests__/textBackground.test.ts`.
*/
export const TEXT_BACKGROUND_PAIRS: Record<
TextBackgroundPreset,
Record<PanelTheme, TextBackgroundPair>
> = {
robin: {
light: { surface: '#DCE4FF', ink: '#16224D' },
dark: { surface: '#24356E', ink: '#EDF1FF' },
},
purple: {
light: { surface: '#E8DEFB', ink: '#2B1B4D' },
dark: { surface: '#3A2A63', ink: '#F1EAFE' },
},
sakura: {
light: { surface: '#FBDCEB', ink: '#4A1730' },
dark: { surface: '#5F2342', ink: '#FDE8F2' },
},
cherry: {
light: { surface: '#FBDCDC', ink: '#4C1717' },
dark: { surface: '#63262A', ink: '#FDE9E9' },
},
amber: {
light: { surface: '#FBEECC', ink: '#45320A' },
dark: { surface: '#5B4415', ink: '#FDF3DC' },
},
forest: {
light: { surface: '#D6F2E2', ink: '#0F3A25' },
dark: { surface: '#1D4A33', ink: '#E3F7EC' },
},
sienna: {
light: { surface: '#F0E4D8', ink: '#40301F' },
dark: { surface: '#56412C', ink: '#F5EADF' },
},
slate: {
light: { surface: '#E4E6EA', ink: '#1D212D' },
dark: { surface: '#2C3140', ink: '#EDEEF0' },
},
};
/** How `TextBackgroundKind.None` survives a string-only schema. */
export const TRANSPARENT_BACKGROUND = '#00000000';
/** The theme's own overlay ink, so a note keeps the edge weight of its neighbours. */
export const PRESET_BORDER: Record<PanelTheme, string> = {
light: 'rgba(0, 0, 0, 0.07)',
dark: 'rgba(255, 255, 255, 0.09)',
};
export const SECONDARY_INK_OPACITY = 0.82;
/**
* Surface chrome, as a share of the pair's ink. Each name falls back to its
* original token in the stylesheet, so a panel with no background is untouched.
* `--scrollbar-thumb*` are unscoped on purpose: they override the shared
* scrollbar mixin, which any surface may want to retint.
*/
export const INK_ALPHAS: Record<string, number> = {
'--text-panel-ink-secondary': SECONDARY_INK_OPACITY,
'--text-panel-grip': 0.28,
'--scrollbar-thumb': 0.24,
'--scrollbar-thumb-hover': 0.4,
'--text-panel-pill-surface': 0.16,
};
/** The two inks a custom surface picks between. */
export const CUSTOM_INK: Record<PanelTheme, string> = {
light: '#FFFFFF',
dark: '#1D212D',
};
/**
* Normalised surface hex to the preset that owns it, both themes: a panel saved
* in dark mode resolves to its preset in light mode, with no re-save.
*/
export const PRESET_BY_SURFACE: Record<string, TextBackgroundPreset> =
Object.fromEntries(
TEXT_BACKGROUND_PRESETS.flatMap((preset) => [
[TEXT_BACKGROUND_PAIRS[preset].light.surface.toUpperCase(), preset],
[TEXT_BACKGROUND_PAIRS[preset].dark.surface.toUpperCase(), preset],
]),
);

View File

@@ -0,0 +1,122 @@
import { inkForSurface, normalizeHex, parseHex } from './contrast';
import {
PRESET_BY_SURFACE,
TEXT_BACKGROUND_PAIRS,
TRANSPARENT_BACKGROUND,
} from './presets';
import type {
PanelTheme,
ResolvedTextBackground,
TextBackgroundPreset,
TextBackgroundSelection,
} from './types';
import { TextBackgroundKind } from './types';
/** `presentation.background` before it was a hex string; the API rejects both now. */
const LEGACY_VALUES: Record<string, TextBackgroundKind> = {
solid: TextBackgroundKind.Default,
transparent: TextBackgroundKind.None,
};
const DEFAULT_BACKGROUND: ResolvedTextBackground = {
kind: TextBackgroundKind.Default,
};
/**
* A stored preset surface resolves to its pair in the *current* theme, so a panel
* follows a theme switch with no re-save; any other hex is a custom colour, which
* does not adapt.
*/
export function resolveTextBackground(
background: string | null | undefined,
theme: PanelTheme,
): ResolvedTextBackground {
if (!background) {
return DEFAULT_BACKGROUND;
}
const legacy = LEGACY_VALUES[background];
if (legacy) {
return legacy === TextBackgroundKind.None
? { kind: TextBackgroundKind.None }
: DEFAULT_BACKGROUND;
}
const channels = parseHex(background);
if (!channels) {
return DEFAULT_BACKGROUND;
}
if (channels.a === 0) {
return { kind: TextBackgroundKind.None };
}
const normalized = normalizeHex(background);
const preset = normalized ? PRESET_BY_SURFACE[normalized] : undefined;
if (preset) {
return {
kind: TextBackgroundKind.Preset,
preset,
...TEXT_BACKGROUND_PAIRS[preset][theme],
};
}
return {
kind: TextBackgroundKind.Custom,
surface: background,
ink: inkForSurface(background),
};
}
/** A preset stores the current theme's surface; `default` stores nothing. */
export function toStoredBackground(
resolved: ResolvedTextBackground,
theme: PanelTheme,
): string | undefined {
switch (resolved.kind) {
case TextBackgroundKind.None:
return TRANSPARENT_BACKGROUND;
case TextBackgroundKind.Preset:
return resolved.preset
? TEXT_BACKGROUND_PAIRS[resolved.preset][theme].surface
: undefined;
case TextBackgroundKind.Custom:
return resolved.surface;
default:
return undefined;
}
}
/** Which swatch lights up; `undefined` for a custom colour, which has no swatch. */
export function selectionFromResolved(
resolved: ResolvedTextBackground,
): TextBackgroundSelection | undefined {
if (resolved.kind === TextBackgroundKind.Preset) {
return resolved.preset;
}
return resolved.kind === TextBackgroundKind.Custom ? undefined : resolved.kind;
}
/** What a swatch click stores. */
export function storedFromSelection(
selection: TextBackgroundSelection,
theme: PanelTheme,
): string | undefined {
if (
selection === TextBackgroundKind.None ||
selection === TextBackgroundKind.Default
) {
return toStoredBackground({ kind: selection }, theme);
}
return toStoredBackground(
{ kind: TextBackgroundKind.Preset, preset: selection },
theme,
);
}
/** The hex a swatch paints in the given theme. */
export function presetSurface(
preset: TextBackgroundPreset,
theme: PanelTheme,
): string {
return TEXT_BACKGROUND_PAIRS[preset][theme].surface;
}

View File

@@ -0,0 +1,41 @@
export enum TextBackgroundPreset {
Robin = 'robin',
Purple = 'purple',
Sakura = 'sakura',
Cherry = 'cherry',
Amber = 'amber',
Forest = 'forest',
Sienna = 'sienna',
Slate = 'slate',
}
export enum TextBackgroundKind {
None = 'none',
Default = 'default',
Preset = 'preset',
Custom = 'custom',
}
/** `Custom` is absent: it opens a picker, so it has its own row. */
export type TextBackgroundSelection =
| TextBackgroundKind.None
| TextBackgroundKind.Default
| TextBackgroundPreset;
export enum PanelTheme {
Light = 'light',
Dark = 'dark',
}
export interface TextBackgroundPair {
surface: string;
ink: string;
}
/** `None` and `Default` carry no colours: the card keeps or drops its own. */
export interface ResolvedTextBackground {
kind: TextBackgroundKind;
preset?: TextBackgroundPreset;
surface?: string;
ink?: string;
}

View File

@@ -2,8 +2,10 @@ import type { CodeProps } from 'react-markdown/lib/ast-to-react';
import CopyButton from 'periscope/components/CopyButton/CopyButton';
import SyntaxHighlighter, { resolveLanguage } from './syntaxLanguages';
import { usePrismLanguage } from './usePrismLanguage';
import SyntaxHighlighter, {
resolveLanguage,
} from '../../../../utils/syntaxLanguages';
import { usePrismLanguage } from '../../../../hooks/usePrismLanguage';
import styles from './CodeBlock.module.scss';

View File

@@ -1,4 +1,4 @@
@use '../../../../../../styles/scrollbar' as *;
@use '../../../../../../../../styles/scrollbar' as *;
// Style isolation: the subtree is rolled back to user-agent styling, so no global
// rule reaches the rendered body and the rules below are the only author styles that
@@ -15,12 +15,20 @@
box-sizing: border-box;
}
// Sourced from the panel's ink when it has one — the Robin link colour and the
// neutral muted tone hold no contrast on a coloured surface and from the theme
// token when it does not, which is byte-identical to before.
.content.content {
--md-foreground: var(--text-vanilla-100);
--md-muted: var(--text-neutral-dark-100);
--md-link: var(--text-robin-400);
--md-border: var(--l1-border);
--md-surface: color-mix(in srgb, var(--l1-foreground) 6%, transparent);
--md-foreground: var(--text-panel-ink, var(--text-vanilla-100));
--md-muted: var(--text-panel-ink-secondary, var(--text-neutral-dark-100));
--md-link: var(--text-panel-ink, var(--text-robin-400));
--md-link-decoration: var(--text-panel-link-decoration, none);
--md-border: var(--text-panel-border, var(--l1-border));
--md-surface: color-mix(
in srgb,
var(--text-panel-ink, var(--l1-foreground)) 6%,
transparent
);
--md-code-comment: var(--text-neutral-dark-200);
--md-code-punctuation: var(--text-neutral-dark-100);
--md-code-keyword: var(--text-sakura-400);
@@ -41,9 +49,9 @@
}
:global(body.lightMode) .content.content {
--md-foreground: var(--text-ink-400);
--md-muted: var(--text-neutral-light-100);
--md-link: var(--text-robin-500);
--md-foreground: var(--text-panel-ink, var(--text-ink-400));
--md-muted: var(--text-panel-ink-secondary, var(--text-neutral-light-100));
--md-link: var(--text-panel-ink, var(--text-robin-500));
--md-code-comment: var(--text-neutral-light-100);
--md-code-punctuation: var(--text-neutral-light-100);
--md-code-keyword: var(--text-sakura-600);
@@ -166,7 +174,7 @@
.content.content a {
color: var(--md-link);
text-decoration: none;
text-decoration: var(--md-link-decoration);
&:hover,
&:focus-visible {

View File

@@ -7,10 +7,10 @@ import remarkGfm from 'remark-gfm';
import {
editRenderedOccurrence,
type EditableConstruct,
} from '../../utils/markdownSource';
import { TASK_LIST } from '../../utils/taskList';
import CodeBlock from './CodeBlock';
import TaskCheckbox from './TaskCheckbox';
} from '../../../../utils/markdownSource';
import { TASK_LIST } from '../../../../utils/taskList';
import CodeBlock from '../CodeBlock/CodeBlock';
import TaskCheckbox from '../TaskCheckbox/TaskCheckbox';
import { TaskItemOffsetContext } from './taskItemOffset';
import styles from './MarkdownContent.module.scss';
@@ -75,7 +75,6 @@ function MarkdownContent({
className,
testId = 'markdown-content',
}: MarkdownContentProps): JSX.Element | null {
// Dashboards re-render on every variable tick; parsing is the expensive half.
// Element overrides that write back to the source: one entry per interactive
// construct, pairing an `EditableConstruct` with the element it renders as.
const components = useMemo<Components>(() => {
@@ -103,10 +102,10 @@ function MarkdownContent({
return {
...READ_ONLY_COMPONENTS,
li: ({ node, children: items, ...props }): JSX.Element => (
li: ({ node, children, ...props }): JSX.Element => (
<li {...props}>
<TaskItemOffsetContext.Provider value={node.position?.start.offset}>
{items}
{children}
</TaskItemOffsetContext.Provider>
</li>
),
@@ -124,6 +123,7 @@ function MarkdownContent({
};
}, [interactive, children]);
// Dashboards re-render on every variable tick; parsing is the expensive half.
const body = useMemo(
() =>
children.trim() ? (

Some files were not shown because too many files have changed in this diff Show More