mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-01 18:10:21 +01:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc6ffb0ea | ||
|
|
9343fb65b3 | ||
|
|
67fb4cf8a5 | ||
|
|
11f9daf2a7 | ||
|
|
01cc509c1b | ||
|
|
c17021aade | ||
|
|
d9961fe527 | ||
|
|
5035390236 |
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trailingComma": "none"
|
||||
}
|
||||
44
.vscode/settings.json
vendored
44
.vscode/settings.json
vendored
@@ -1,23 +1,25 @@
|
||||
{
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
"eslint.workingDirectories": ["./frontend"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": [],
|
||||
"editor.tokenColorCustomizations": {
|
||||
"comments": "",
|
||||
"textMateRules": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,11 +577,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
open={isPanelNameModalOpen}
|
||||
title="New Section"
|
||||
rootClassName="section-naming"
|
||||
onOk={(): void => handleAddRow()}
|
||||
onCancel={(): void => {
|
||||
setIsPanelNameModalOpen(false);
|
||||
setSectionName(DEFAULT_ROW_NAME);
|
||||
}}
|
||||
footer={
|
||||
<div className="dashboard-rename">
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MemoryRouter } from 'react-router-dom-v5-compat';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { IAppContext, IUser } from 'providers/App/types';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import reducers from 'store/reducers';
|
||||
import { act, render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { LicenseResModel } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../../constants';
|
||||
|
||||
const SERVER_URL = 'http://localhost/api';
|
||||
|
||||
// jsdom does not implement IntersectionObserver — provide a no-op stub
|
||||
const mockObserver = {
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
global.IntersectionObserver = jest
|
||||
.fn()
|
||||
.mockImplementation(() => mockObserver) as any;
|
||||
|
||||
const mockVolume = {
|
||||
persistentVolumeClaimName: 'test-pvc',
|
||||
volumeAvailable: 1000000,
|
||||
volumeCapacity: 5000000,
|
||||
volumeInodes: 100,
|
||||
volumeInodesFree: 50,
|
||||
volumeInodesUsed: 50,
|
||||
volumeUsage: 4000000,
|
||||
meta: {
|
||||
k8s_cluster_name: 'test-cluster',
|
||||
k8s_namespace_name: 'test-namespace',
|
||||
k8s_node_name: 'test-node',
|
||||
k8s_persistentvolumeclaim_name: 'test-pvc',
|
||||
k8s_pod_name: 'test-pod',
|
||||
k8s_pod_uid: 'test-pod-uid',
|
||||
k8s_statefulset_name: '',
|
||||
},
|
||||
};
|
||||
|
||||
const mockVolumesResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: '',
|
||||
records: [mockVolume],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: false,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
};
|
||||
|
||||
/** Renders K8sVolumesList with a real Redux store so dispatched actions affect state. */
|
||||
function renderWithRealStore(
|
||||
initialEntries?: Record<string, any>,
|
||||
): { testStore: ReturnType<typeof createStore> } {
|
||||
const testStore = createStore(reducers, applyMiddleware(thunk as any));
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter searchParams={initialEntries}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryBuilderProvider>
|
||||
<MemoryRouter>
|
||||
<K8sVolumesList
|
||||
isFiltersVisible={false}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
quickFiltersLastUpdated={-1}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</QueryBuilderProvider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
return { testStore };
|
||||
}
|
||||
|
||||
describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
|
||||
let requestsMade: Array<{
|
||||
url: string;
|
||||
params: URLSearchParams;
|
||||
body?: any;
|
||||
}> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
requestsMade = [];
|
||||
|
||||
server.use(
|
||||
rest.get(`${SERVER_URL}/v3/autocomplete/attribute_keys`, (req, res, ctx) => {
|
||||
const url = req.url.toString();
|
||||
const params = req.url.searchParams;
|
||||
|
||||
requestsMade.push({
|
||||
url,
|
||||
params,
|
||||
});
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
attributeKeys: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.post(`${SERVER_URL}/v1/pvcs/list`, (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [],
|
||||
groups: null,
|
||||
total: 0,
|
||||
sentAnyHostMetricsData: false,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('should call aggregate keys API with k8s_volume_capacity', async () => {
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestsMade.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find the attribute_keys request
|
||||
const attributeKeysRequest = requestsMade.find((req) =>
|
||||
req.url.includes('/autocomplete/attribute_keys'),
|
||||
);
|
||||
|
||||
expect(attributeKeysRequest).toBeDefined();
|
||||
|
||||
const aggregateAttribute = attributeKeysRequest?.params.get(
|
||||
'aggregateAttribute',
|
||||
);
|
||||
|
||||
expect(aggregateAttribute).toBe('k8s_volume_capacity');
|
||||
});
|
||||
|
||||
it('should call aggregate keys API with k8s.volume.capacity when dotMetrics enabled', async () => {
|
||||
jest
|
||||
.spyOn(await import('providers/App/App'), 'useAppContext')
|
||||
.mockReturnValue({
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.DOT_METRICS_ENABLED,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: 0,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
user: { role: 'ADMIN' } as IUser,
|
||||
activeLicense: (null as unknown) as LicenseResModel,
|
||||
} as IAppContext);
|
||||
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestsMade.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const attributeKeysRequest = requestsMade.find((req) =>
|
||||
req.url.includes('/autocomplete/attribute_keys'),
|
||||
);
|
||||
|
||||
expect(attributeKeysRequest).toBeDefined();
|
||||
|
||||
const aggregateAttribute = attributeKeysRequest?.params.get(
|
||||
'aggregateAttribute',
|
||||
);
|
||||
|
||||
expect(aggregateAttribute).toBe('k8s.volume.capacity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('K8sVolumesList', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockVolumesResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: { attributeKeys: [] } })),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders volume rows from API response', async () => {
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(async () => {
|
||||
const elements = await screen.findAllByText('test-pvc');
|
||||
|
||||
expect(elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens VolumeDetails when a volume row is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRealStore();
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
expect(pvcCells.length).toBeGreaterThan(0);
|
||||
|
||||
const row = pvcCells[0].closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
await user.click(row!);
|
||||
|
||||
await waitFor(async () => {
|
||||
const cells = await screen.findAllByText('test-pvc');
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('closes VolumeDetails when the close button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRealStore();
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
expect(pvcCells.length).toBeGreaterThan(0);
|
||||
|
||||
const row = pvcCells[0].closest('tr');
|
||||
await user.click(row!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Close' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Close' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not re-fetch the volumes list when time range changes after selecting a volume', async () => {
|
||||
const user = userEvent.setup();
|
||||
let apiCallCount = 0;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', async (_req, res, ctx) => {
|
||||
apiCallCount += 1;
|
||||
return res(ctx.status(200), ctx.json(mockVolumesResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
const { testStore } = renderWithRealStore();
|
||||
|
||||
await waitFor(() => expect(apiCallCount).toBe(1));
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
const row = pvcCells[0].closest('tr');
|
||||
await user.click(row!);
|
||||
await waitFor(async () => {
|
||||
const cells = await screen.findAllByText('test-pvc');
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
// Wait for nuqs URL state to fully propagate to the component
|
||||
// The selectedVolumeUID is managed via nuqs (async URL state),
|
||||
// so we need to ensure the state has settled before dispatching time changes
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
});
|
||||
|
||||
const countAfterClick = apiCallCount;
|
||||
|
||||
// There's a specific component causing the min/max time to be updated
|
||||
// After the volume loads, it triggers the change again
|
||||
// And then the query to fetch data for the selected volume enters in a loop
|
||||
act(() => {
|
||||
testStore.dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
|
||||
maxTime: Date.now() * 1000000,
|
||||
selectedTime: '30m',
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Allow any potential re-fetch to settle
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
expect(apiCallCount).toBe(countAfterClick);
|
||||
});
|
||||
|
||||
it('does not re-fetch groupedByRowData when time range changes after expanding a volume row with groupBy', async () => {
|
||||
const user = userEvent.setup();
|
||||
const groupByValue = [{ key: 'k8s_namespace_name' }];
|
||||
|
||||
let groupedByRowDataCallCount = 0;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
// Check for both underscore and dot notation keys since dotMetricsEnabled
|
||||
// may be true or false depending on test order
|
||||
const isGroupedByRowDataRequest = body.filters?.items?.some(
|
||||
(item: { key?: { key?: string }; value?: string }) =>
|
||||
(item.key?.key === 'k8s_namespace_name' ||
|
||||
item.key?.key === 'k8s.namespace.name') &&
|
||||
item.value === 'test-namespace',
|
||||
);
|
||||
if (isGroupedByRowDataRequest) {
|
||||
groupedByRowDataCallCount += 1;
|
||||
}
|
||||
return res(ctx.status(200), ctx.json(mockVolumesResponse));
|
||||
}),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
attributeKeys: [{ key: 'k8s_namespace_name', dataType: 'string' }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { testStore } = renderWithRealStore({
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupByValue),
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const elements = await screen.findAllByText('test-namespace');
|
||||
return expect(elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const row = (await screen.findAllByText('test-namespace'))[0].closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
user.click(row as HTMLElement);
|
||||
await waitFor(() => expect(groupedByRowDataCallCount).toBe(1));
|
||||
|
||||
const countAfterExpand = groupedByRowDataCallCount;
|
||||
|
||||
act(() => {
|
||||
testStore.dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
|
||||
maxTime: Date.now() * 1000000,
|
||||
selectedTime: '30m',
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Allow any potential re-fetch to settle
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
expect(groupedByRowDataCallCount).toBe(countAfterExpand);
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,10 @@ export interface NewWidgetProps {
|
||||
}
|
||||
|
||||
export interface WidgetGraphProps {
|
||||
// this should be inside the querier plugin definition
|
||||
selectedLogFields: Widgets['selectedLogFields'];
|
||||
setSelectedLogFields?: Dispatch<SetStateAction<Widgets['selectedLogFields']>>;
|
||||
// this should be inside the querier plugin definition
|
||||
selectedTracesFields: Widgets['selectedTracesFields'];
|
||||
setSelectedTracesFields?: Dispatch<
|
||||
SetStateAction<Widgets['selectedTracesFields']>
|
||||
|
||||
@@ -118,7 +118,6 @@ export interface IBaseWidget {
|
||||
opacity: string;
|
||||
nullZeroValues: string;
|
||||
timePreferance: timePreferenceType;
|
||||
stepSize?: number;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption; // number of decimals or 'full precision'
|
||||
stackedBarChart?: boolean;
|
||||
|
||||
@@ -29,6 +29,8 @@ package common
|
||||
|
||||
#PrecisionOption: *2 | 0 | 1 | 3 | 4 | "full"
|
||||
|
||||
// how is this undefined and null? Would like to avoid undefined if possible
|
||||
// also bools can required always?
|
||||
#Axes: {
|
||||
softMin?: number | *null
|
||||
softMax?: number | *null
|
||||
@@ -41,10 +43,12 @@ package common
|
||||
value: number
|
||||
unit?: string
|
||||
color: string
|
||||
// What is this?
|
||||
format: "Text" | "Background"
|
||||
label?: string
|
||||
}
|
||||
|
||||
// where is this used
|
||||
#ComparisonThreshold: {
|
||||
value: number
|
||||
operator: ">" | "<" | ">=" | "<=" | "="
|
||||
@@ -70,6 +74,7 @@ package common
|
||||
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
|
||||
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
|
||||
reduceTo?: #ReduceTo
|
||||
// should unspecified be the default?
|
||||
temporality?: "delta" | "cumulative" | "unspecified"
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
perses/schemas-wrapper/schemas/Image.jpeg
Normal file
BIN
perses/schemas-wrapper/schemas/Image.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 711 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
20
perses/schemas-wrapper/schemas/review-panels.md
Normal file
20
perses/schemas-wrapper/schemas/review-panels.md
Normal file
@@ -0,0 +1,20 @@
|
||||
which panel type only caters to specific signals?
|
||||
metrics has no option for list panel type. also, no other querying types
|
||||
|
||||
should the panel definition be linked to the query type?
|
||||
|
||||
| type | QB | P | CH |
|
||||
| ----------- | --- | --- | --- |
|
||||
| Time Series | ✅ | ✅ | ✅ |
|
||||
| Number | ✅ | ✅ | ✅ |
|
||||
| Table | ✅ | ❌ | ✅ |
|
||||
| List | ✅ | ❌ | ❌ |
|
||||
| Bar | ✅ | ✅ | ✅ |
|
||||
| Pie | ✅ | ❌ | ✅ |
|
||||
| Histogram | ✅ | ✅ | ✅ |
|
||||
|
||||
- unsure where this is used
|
||||
stepSize - can be removed
|
||||
renderColumnCell - used though - only table
|
||||
customColumTitles - used though - only table
|
||||
hiddenColumns - used though - only table
|
||||
1
perses/schemas-wrapper/schemas/review.md
Normal file
1
perses/schemas-wrapper/schemas/review.md
Normal file
@@ -0,0 +1 @@
|
||||
creating folders based on type is helpful instead of a flat folder structure. It is how you would visualise the dashboard as components
|
||||
@@ -4,6 +4,7 @@ import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozBarChartPanel"
|
||||
spec: close({
|
||||
// need to put more thought into this being optional?
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
axes?: common.#Axes
|
||||
@@ -23,6 +24,7 @@ spec: close({
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
// can this be more composable?
|
||||
#Legend: {
|
||||
position?: common.#LegendPosition
|
||||
customColors?: [string]: string
|
||||
|
||||
@@ -7,6 +7,7 @@ kind: "SigNozClickHouseSQL"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
query: string & !=""
|
||||
// required / optional
|
||||
disabled?: bool | *false
|
||||
legend?: string
|
||||
})
|
||||
|
||||
@@ -22,6 +22,8 @@ spec: close({
|
||||
})
|
||||
|
||||
// QueryEnvelope wraps a single query plugin with a type discriminator.
|
||||
|
||||
// need to verify the types internally are marked required where necessary, so that we can make them optional here and not have to worry about it at the top level
|
||||
#QueryEnvelope:
|
||||
close({
|
||||
type: "builder_query",
|
||||
|
||||
@@ -7,3 +7,5 @@ kind: "SigNozDatasource"
|
||||
// Add fields here if SigNoz ever supports multiple backends or
|
||||
// configurable API versions.
|
||||
spec: close({})
|
||||
|
||||
// this is required to override the default that always requires a spec
|
||||
|
||||
@@ -8,4 +8,5 @@ spec: close({
|
||||
name: string
|
||||
source: string
|
||||
sort?: common.#VariableSortOrder
|
||||
// where does attribute go here?
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ spec: close({
|
||||
stepSize?: number
|
||||
columnWidths?: [string]: number // not a config choice — persisted user-resized column widths
|
||||
// "Chart Appearance" section in UI — could not find this section in the app.
|
||||
// This is behind a boolean flag right now. Can help reproduce locally
|
||||
// These 4 fields are gated behind panelTypeVs* constants (TIME_SERIES only).
|
||||
lineInterpolation?: #LineInterpolation
|
||||
showPoints?: bool
|
||||
|
||||
@@ -14,7 +14,10 @@ spec: close({
|
||||
timePreference?: common.#TimePreference
|
||||
}
|
||||
|
||||
// where to store if alerts can be added or not?
|
||||
|
||||
#Formatting: {
|
||||
// mainly need to discuss optional fields
|
||||
unit?: string | *""
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ spec: close({
|
||||
name: common.#QueryName
|
||||
query: string & !=""
|
||||
disabled?: bool | *false
|
||||
legend?: string
|
||||
// where are the below items coming from? Don't see types for it. How to arrive at this?
|
||||
step?: =~"^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" | number
|
||||
stats?: bool
|
||||
legend?: string
|
||||
})
|
||||
|
||||
@@ -7,4 +7,7 @@ kind: "SigNozQueryVariable"
|
||||
spec: close({
|
||||
queryValue: string
|
||||
sort?: common.#VariableSortOrder
|
||||
// This should not be optional
|
||||
// what happens to the existing https://perses.dev/perses/docs/dac/go/variable/#sortingby inside the ListVariable spec? Do we move that to the plugin spec level?
|
||||
// same for other list variable cues
|
||||
})
|
||||
|
||||
@@ -23,3 +23,9 @@ spec: close({
|
||||
common.#ComparisonThreshold
|
||||
tableOptions: string
|
||||
}
|
||||
|
||||
// missing
|
||||
columnWidths
|
||||
customColumnTitles
|
||||
hiddenColumns
|
||||
renderColumnCell - can be FE
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
// Shouldn't the below line be TextVariable?
|
||||
// are any of the variables inside this required? How would that validation work?
|
||||
// defaultValue lives on the Perses ListVariable wrapper (spec level).
|
||||
kind: "SigNozTextboxVariable"
|
||||
spec: close({})
|
||||
|
||||
@@ -24,5 +24,13 @@ spec: close({
|
||||
|
||||
#Legend: {
|
||||
position?: common.#LegendPosition
|
||||
// why call it customColors?
|
||||
customColors?: [string]: string
|
||||
}
|
||||
|
||||
// chart appearance
|
||||
- fill mode
|
||||
- line style
|
||||
- line interpolation
|
||||
- show points
|
||||
- span gaps
|
||||
|
||||
Reference in New Issue
Block a user