mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-05 20:50:45 +01:00
Compare commits
1 Commits
test/dashb
...
test/dashb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fea77fa345 |
@@ -87,7 +87,7 @@ function AddColumnDropdown({
|
||||
value={field.name}
|
||||
isSelected={selectedNames.has(field.name)}
|
||||
onSelect={(): void => onToggle(field)}
|
||||
data-testid="list-columns-suggestion"
|
||||
data-testid={`list-columns-suggestion-${field.name}`}
|
||||
>
|
||||
{field.name}
|
||||
</ComboboxItem>
|
||||
|
||||
@@ -35,7 +35,12 @@ function SortableColumnChip({
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className={styles.chip}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={styles.chip}
|
||||
data-testid={`list-column-chip-${name}`}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -58,7 +63,7 @@ function SortableColumnChip({
|
||||
size="icon"
|
||||
className={styles.remove}
|
||||
aria-label={`Remove ${name}`}
|
||||
testId="list-column-remove"
|
||||
testId={`list-column-remove-${name}`}
|
||||
onClick={(): void => onRemove(name)}
|
||||
>
|
||||
<X size={12} />
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { expect, test } from '../../../../fixtures/dashboards';
|
||||
import { PanelKind } from '../../../../helpers/dashboard-v2-spec';
|
||||
import { getDashboardV2ViaApi } from '../../../../helpers/dashboards-v2';
|
||||
import {
|
||||
Section,
|
||||
editor,
|
||||
expandSection,
|
||||
savePanel,
|
||||
sectionToggle,
|
||||
selectOption,
|
||||
setSegment,
|
||||
} from '../../../../helpers/panel-editor-v2';
|
||||
import {
|
||||
QueryRange,
|
||||
mockQueryRange,
|
||||
ramp,
|
||||
} from '../../../../helpers/query-range-mock';
|
||||
import {
|
||||
LOG_DISTR,
|
||||
previewState,
|
||||
uplotState,
|
||||
yScale,
|
||||
} from '../../../../helpers/uplot';
|
||||
import {
|
||||
SINGLE_PANEL_ID,
|
||||
singlePanelDashboard,
|
||||
} from '../../../../testdata/v2/panels-dashboard';
|
||||
|
||||
// Pinned so the chart is guaranteed to exist with a known series count.
|
||||
const SERIES = [
|
||||
{ labels: { 'service.name': 'adservice' }, points: ramp(24, 10, 80) },
|
||||
{ labels: { 'service.name': 'cartservice' }, points: ramp(24, 20, 60) },
|
||||
];
|
||||
|
||||
// Scope: the ConfigPane sections a TimeSeries / Bar panel declares —
|
||||
// Visualization, Axes, Legend and Chart Appearance — and that each control
|
||||
// round-trips into the persisted spec.
|
||||
//
|
||||
// Each control is checked twice: it round-trips into the saved spec, and the
|
||||
// rendered chart honours it (read from the live uPlot instance). The spec alone
|
||||
// would pass even if the renderer ignored the setting.
|
||||
|
||||
/** The plugin spec of the single fixture panel, straight from the API. */
|
||||
async function savedSpec(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
dashboardId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const after = await getDashboardV2ViaApi(page, dashboardId);
|
||||
return after.spec.panels[SINGLE_PANEL_ID].spec.plugin
|
||||
.spec as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
test.describe('Dashboards V2 — editor sections (TimeSeries / Bar)', () => {
|
||||
test('TC-01 a TimeSeries panel declares its expected sections', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
for (const title of [
|
||||
Section.visualization,
|
||||
Section.formatting,
|
||||
Section.axes,
|
||||
Section.legend,
|
||||
Section.chartAppearance,
|
||||
Section.thresholds,
|
||||
Section.contextLinks,
|
||||
]) {
|
||||
await expect(sectionToggle(page, title)).toBeVisible();
|
||||
}
|
||||
// Buckets belongs to Histogram only.
|
||||
await expect(sectionToggle(page, Section.buckets)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-02 sections start collapsed and toggle open', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
const axes = sectionToggle(page, Section.axes);
|
||||
await expect(axes).toHaveAttribute('aria-expanded', 'false');
|
||||
await expandSection(page, Section.axes);
|
||||
await expect(page.getByTestId('panel-editor-v2-soft-min')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-03 axis bounds reach the chart', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await mockQueryRange(page, QueryRange.timeSeries(SERIES));
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.axes);
|
||||
await page.getByTestId('panel-editor-v2-soft-min').fill('5');
|
||||
await page.getByTestId('panel-editor-v2-soft-max').fill('95');
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
axes: { softMin: 5, softMax: 95 },
|
||||
});
|
||||
|
||||
// …and the chart is actually bounded by them.
|
||||
await expect
|
||||
.poll(async () => (await yScale(page, SINGLE_PANEL_ID)).min)
|
||||
.toBeLessThanOrEqual(5);
|
||||
await expect
|
||||
.poll(async () => (await yScale(page, SINGLE_PANEL_ID)).max)
|
||||
.toBeGreaterThanOrEqual(95);
|
||||
});
|
||||
|
||||
test('TC-03b log scale reaches the chart, not just the spec', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await mockQueryRange(page, QueryRange.timeSeries(SERIES));
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.axes);
|
||||
await setSegment(page, 'panel-editor-v2-log-scale', 'Log');
|
||||
|
||||
// Live, before saving — uPlot encodes a log scale as distr 3.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const scales = (await previewState(page)).scales;
|
||||
return (scales.y ?? Object.values(scales)[1])?.distr;
|
||||
})
|
||||
.toBe(LOG_DISTR);
|
||||
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
axes: { isLogScale: true },
|
||||
});
|
||||
// And on the saved panel.
|
||||
await expect
|
||||
.poll(async () => (await yScale(page, SINGLE_PANEL_ID)).distr)
|
||||
.toBe(LOG_DISTR);
|
||||
});
|
||||
|
||||
test('TC-04 clearing an axis bound stores null, not zero', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ pluginSpec: { axes: { softMin: 5 } } }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.axes);
|
||||
// Must clear the bound, not pin the axis to 0.
|
||||
await page.getByTestId('panel-editor-v2-soft-min').fill('');
|
||||
await savePanel(page);
|
||||
|
||||
const spec = await savedSpec(page, id);
|
||||
expect((spec.axes as { softMin?: number | null }).softMin ?? null).toBeNull();
|
||||
});
|
||||
|
||||
test('TC-05 legend position persists', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.legend);
|
||||
await setSegment(page, 'panel-editor-v2-legend-position', 'Right');
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
legend: { position: 'right' },
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-06 chart appearance controls reach the chart', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await mockQueryRange(page, QueryRange.timeSeries(SERIES));
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.chartAppearance);
|
||||
await setSegment(page, 'panel-editor-v2-line-style', 'Dashed');
|
||||
await setSegment(page, 'panel-editor-v2-fill-mode', 'Gradient');
|
||||
await selectOption(page, 'panel-editor-v2-line-interpolation', 'Step before');
|
||||
await page.getByTestId('panel-editor-v2-show-points').click();
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
chartAppearance: {
|
||||
lineStyle: 'dashed',
|
||||
fillMode: 'gradient',
|
||||
lineInterpolation: 'step_before',
|
||||
showPoints: true,
|
||||
},
|
||||
});
|
||||
|
||||
// The rendered series carry the styling.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [first] = (await uplotState(page, SINGLE_PANEL_ID)).series;
|
||||
return {
|
||||
dashed: (first?.dash?.length ?? 0) > 0,
|
||||
filled: first?.hasFill ?? false,
|
||||
};
|
||||
})
|
||||
.toEqual({ dashed: true, filled: true });
|
||||
|
||||
// "Show points" is not asserted against the chart: uPlot installs a
|
||||
// predicate for `points.show` either way, so the seam can't distinguish on
|
||||
// from off. The spec round-trip above is the available coverage.
|
||||
});
|
||||
|
||||
test('TC-07 the panel time preference persists and drives the header pill', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.visualization);
|
||||
await selectOption(page, 'panel-editor-v2-time-preference', 'Last 15 min');
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
visualization: { timePreference: 'last_15_min' },
|
||||
});
|
||||
// The pill is the user-visible consequence.
|
||||
await expect(page.getByTestId('panel-time-preference')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-08 Fill gaps is offered on TimeSeries but Stack series is not', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.visualization);
|
||||
await expect(page.getByTestId('panel-editor-v2-fill-spans')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('panel-editor-v2-stacked-bar-chart'),
|
||||
).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('panel-editor-v2-fill-spans').click();
|
||||
await savePanel(page);
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
visualization: { fillSpans: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-09 Stack series is offered on Bar but Fill gaps is not', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.BarChart }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.visualization);
|
||||
await expect(
|
||||
page.getByTestId('panel-editor-v2-stacked-bar-chart'),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('panel-editor-v2-fill-spans')).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('panel-editor-v2-stacked-bar-chart').click();
|
||||
await savePanel(page);
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
visualization: { stackedBarChart: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-10 a section edit marks the editor dirty', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
await expect(editor.unsavedBadge(page)).toHaveCount(0);
|
||||
|
||||
await expandSection(page, Section.legend);
|
||||
await setSegment(page, 'panel-editor-v2-legend-position', 'Right');
|
||||
await expect(editor.unsavedBadge(page)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { expect, test } from '../../../../fixtures/dashboards';
|
||||
import { PanelKind } from '../../../../helpers/dashboard-v2-spec';
|
||||
import { getDashboardV2ViaApi } from '../../../../helpers/dashboards-v2';
|
||||
import {
|
||||
Section,
|
||||
expandSection,
|
||||
savePanel,
|
||||
searchAndSelectOption,
|
||||
sectionToggle,
|
||||
selectOption,
|
||||
} from '../../../../helpers/panel-editor-v2';
|
||||
import {
|
||||
QueryRange,
|
||||
mockQueryRange,
|
||||
} from '../../../../helpers/query-range-mock';
|
||||
import {
|
||||
SINGLE_PANEL_ID,
|
||||
singlePanelDashboard,
|
||||
} from '../../../../testdata/v2/panels-dashboard';
|
||||
|
||||
// Scope: the sections that differ across the non-chart kinds.
|
||||
//
|
||||
// The asymmetry: TimeSeries/Number/Pie have one `unit`; Table has none and
|
||||
// carries `columnUnits` per column (hence the fan-out in 30-type-switch TC-07).
|
||||
|
||||
async function savedSpec(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
dashboardId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const after = await getDashboardV2ViaApi(page, dashboardId);
|
||||
return after.spec.panels[SINGLE_PANEL_ID].spec.plugin
|
||||
.spec as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
test.describe('Dashboards V2 — editor sections (Number / Table / Pie / Histogram)', () => {
|
||||
test('TC-01 Number declares formatting but not axes or chart appearance', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Number }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(sectionToggle(page, Section.formatting)).toBeVisible();
|
||||
await expect(sectionToggle(page, Section.visualization)).toBeVisible();
|
||||
await expect(sectionToggle(page, Section.axes)).toHaveCount(0);
|
||||
await expect(sectionToggle(page, Section.chartAppearance)).toHaveCount(0);
|
||||
await expect(sectionToggle(page, Section.legend)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-02 a panel-wide unit and decimals persist', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Number }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.formatting);
|
||||
// Virtualised list — search first.
|
||||
await searchAndSelectOption(
|
||||
page,
|
||||
'panel-editor-v2-unit',
|
||||
'Milliseconds',
|
||||
'Milliseconds',
|
||||
);
|
||||
await selectOption(page, 'panel-editor-v2-decimals', '3 decimals');
|
||||
await savePanel(page);
|
||||
|
||||
const spec = await savedSpec(page, id);
|
||||
expect(
|
||||
(spec.formatting as { decimalPrecision?: string }).decimalPrecision,
|
||||
).toBe('3');
|
||||
expect((spec.formatting as { unit?: string }).unit).toBeTruthy();
|
||||
});
|
||||
|
||||
test('TC-03 Table offers per-column units instead of a panel-wide unit', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
// Golden data: the column key is derived from the resolved result
|
||||
// (`column.id || column.name`), which a hand-rolled payload must match
|
||||
// exactly or the column renders unnamed.
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('table-panel-renderer')).toBeVisible();
|
||||
await expandSection(page, Section.formatting);
|
||||
// Table declares `columnUnits`, never `unit`.
|
||||
await expect(page.getByTestId('panel-editor-v2-unit')).toHaveCount(0);
|
||||
await expect(
|
||||
page.locator('[data-testid^="panel-editor-v2-column-unit-"]').first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-04 the column-units editor explains itself before the panel has run', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
// No result means no columns, so the section shows a hint.
|
||||
await mockQueryRange(page, QueryRange.empty());
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.formatting);
|
||||
await expect(
|
||||
page.getByText('Run the panel to set per-column units.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-05 a per-column unit persists', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('table-panel-renderer')).toBeVisible();
|
||||
await expandSection(page, Section.formatting);
|
||||
|
||||
// Read the key off the control rather than assuming the derivation.
|
||||
const selector = page
|
||||
.locator('[data-testid^="panel-editor-v2-column-unit-"]')
|
||||
.first();
|
||||
const testId = (await selector.getAttribute('data-testid')) ?? '';
|
||||
const columnKey = testId.replace('panel-editor-v2-column-unit-', '');
|
||||
expect(columnKey).not.toBe('');
|
||||
|
||||
await searchAndSelectOption(page, testId, 'Milliseconds', 'Milliseconds');
|
||||
await savePanel(page);
|
||||
|
||||
const spec = await savedSpec(page, id);
|
||||
const columnUnits = (
|
||||
spec.formatting as { columnUnits?: Record<string, string> }
|
||||
).columnUnits;
|
||||
expect(columnUnits?.[columnKey]).toBeTruthy();
|
||||
});
|
||||
|
||||
test('TC-06 Histogram declares Buckets and only a minimal Visualization', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Histogram }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(sectionToggle(page, Section.buckets)).toBeVisible();
|
||||
// Histogram's Visualization declares only the type switcher.
|
||||
await expandSection(page, Section.visualization);
|
||||
await expect(page.getByTestId('panel-editor-v2-time-preference')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-07 bucket count and width persist', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Histogram }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.buckets);
|
||||
await page.getByTestId('panel-editor-v2-bucket-count').fill('40');
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
histogramBuckets: { bucketCount: 40 },
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-08 merging active queries hides the Legend section', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Histogram }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
// One merged distribution has no per-series legend to configure.
|
||||
await expect(sectionToggle(page, Section.legend)).toBeVisible();
|
||||
await expandSection(page, Section.buckets);
|
||||
await page.getByTestId('panel-editor-v2-merge-queries').click();
|
||||
await expect(sectionToggle(page, Section.legend)).toHaveCount(0);
|
||||
|
||||
await savePanel(page);
|
||||
expect(await savedSpec(page, id)).toMatchObject({
|
||||
histogramBuckets: { mergeAllActiveQueries: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-09 Pie declares legend and formatting but no axes', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.PieChart }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(sectionToggle(page, Section.legend)).toBeVisible();
|
||||
await expect(sectionToggle(page, Section.formatting)).toBeVisible();
|
||||
await expect(sectionToggle(page, Section.axes)).toHaveCount(0);
|
||||
|
||||
await expect(sectionToggle(page, Section.thresholds)).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
424
tests/e2e/tests/dashboards/v2/editor/66-thresholds.spec.ts
Normal file
424
tests/e2e/tests/dashboards/v2/editor/66-thresholds.spec.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../../../../fixtures/dashboards';
|
||||
import { PanelKind } from '../../../../helpers/dashboard-v2-spec';
|
||||
import { getDashboardV2ViaApi } from '../../../../helpers/dashboards-v2';
|
||||
import {
|
||||
Section,
|
||||
collapseSection,
|
||||
expandSection,
|
||||
savePanel,
|
||||
sectionToggle,
|
||||
selectOption,
|
||||
} from '../../../../helpers/panel-editor-v2';
|
||||
import {
|
||||
QueryRange,
|
||||
mockQueryRange,
|
||||
} from '../../../../helpers/query-range-mock';
|
||||
import {
|
||||
SINGLE_PANEL_ID,
|
||||
singlePanelDashboard,
|
||||
} from '../../../../testdata/v2/panels-dashboard';
|
||||
|
||||
/** A Number panel whose value is pinned, so threshold crossings are exact. */
|
||||
const PINNED_VALUE = 100;
|
||||
|
||||
function pinnedNumberValue(page: Parameters<typeof mockQueryRange>[0]) {
|
||||
return mockQueryRange(
|
||||
page,
|
||||
QueryRange.scalar({ aggregationColumns: ['A'], rows: [[PINNED_VALUE]] }),
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline background on a table cell — set only while a background threshold matches. */
|
||||
async function cellBackground(cell: Locator): Promise<string> {
|
||||
return cell.evaluate((node) => (node as HTMLElement).style.backgroundColor);
|
||||
}
|
||||
|
||||
/** The threshold's target column is derived from the live result, so don't assume its name. */
|
||||
async function pickFirstOption(
|
||||
page: Page,
|
||||
triggerTestId: string,
|
||||
): Promise<void> {
|
||||
await page.getByTestId(triggerTestId).click();
|
||||
await page
|
||||
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
|
||||
.locator('.ant-select-item-option')
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
/** Inline colour on the rendered value — set only while a threshold matches. */
|
||||
async function renderedValueColor(
|
||||
page: Parameters<typeof mockQueryRange>[0],
|
||||
): Promise<string> {
|
||||
return page
|
||||
.getByTestId('number-panel-value')
|
||||
.evaluate((node) => (node as HTMLElement).style.color);
|
||||
}
|
||||
|
||||
// Scope: the Thresholds section across its three variants, and the row
|
||||
// lifecycle.
|
||||
//
|
||||
// Variant follows panel kind, not a user control: label → TimeSeries/Bar,
|
||||
// comparison → Number, table → Table. The add button's testid varies with it.
|
||||
|
||||
const AddButton = {
|
||||
label: 'panel-editor-v2-add-threshold',
|
||||
comparison: 'panel-editor-v2-add-comparison-threshold',
|
||||
table: 'panel-editor-v2-add-table-threshold',
|
||||
} as const;
|
||||
|
||||
async function savedThresholds(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
dashboardId: string,
|
||||
): Promise<{ value?: number; color?: string; label?: string }[]> {
|
||||
const after = await getDashboardV2ViaApi(page, dashboardId);
|
||||
const spec = after.spec.panels[SINGLE_PANEL_ID].spec.plugin.spec as {
|
||||
thresholds?: { value?: number; color?: string; label?: string }[];
|
||||
};
|
||||
return spec.thresholds ?? [];
|
||||
}
|
||||
|
||||
test.describe('Dashboards V2 — editor thresholds', () => {
|
||||
test('TC-01 a TimeSeries panel gets the label variant', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await expect(page.getByTestId(AddButton.label)).toBeVisible();
|
||||
await expect(page.getByTestId(AddButton.comparison)).toHaveCount(0);
|
||||
await expect(page.getByTestId(AddButton.table)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-02 a Number panel gets the comparison variant', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Number }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await expect(page.getByTestId(AddButton.comparison)).toBeVisible();
|
||||
await expect(page.getByTestId(AddButton.label)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-03 a Table panel gets the table variant', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await expect(page.getByTestId(AddButton.table)).toBeVisible();
|
||||
await expect(page.getByTestId(AddButton.label)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-04 adding a threshold and saving persists it', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.label).click();
|
||||
|
||||
await page.getByTestId('threshold-value-0').fill('42');
|
||||
await page.getByTestId('threshold-label-0').fill('Too high');
|
||||
await page.getByTestId('threshold-save-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
const thresholds = await savedThresholds(page, id);
|
||||
expect(thresholds).toHaveLength(1);
|
||||
expect(thresholds[0]).toMatchObject({ value: 42, label: 'Too high' });
|
||||
});
|
||||
|
||||
test('TC-05 the header quick-add expands a collapsed section and adds a row', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
// One click must both expand and add (SectionSlot's pendingAction hop).
|
||||
await collapseSection(page, Section.thresholds);
|
||||
await page.getByTestId('panel-editor-v2-add-threshold-header').click();
|
||||
|
||||
await expect(sectionToggle(page, Section.thresholds)).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
);
|
||||
await expect(page.getByTestId('threshold-value-0')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-06 only one row is editable at a time', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.label).click();
|
||||
await page.getByTestId('threshold-value-0').fill('10');
|
||||
await page.getByTestId('threshold-save-0').click();
|
||||
|
||||
// The first row must fall back to its summary.
|
||||
await page.getByTestId(AddButton.label).click();
|
||||
await expect(page.getByTestId('threshold-value-1')).toBeVisible();
|
||||
await expect(page.getByTestId('threshold-value-0')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-07 discarding a freshly added row removes it entirely', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.label).click();
|
||||
await page.getByTestId('threshold-value-0').fill('7');
|
||||
|
||||
// Discard on a NEW row deletes it.
|
||||
await page.getByTestId('threshold-discard-0').click();
|
||||
await expect(page.getByTestId('threshold-value-0')).toHaveCount(0);
|
||||
await expect(page.getByTestId('threshold-edit-0')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-08 discarding an existing row restores its previous value', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
pluginSpec: { thresholds: [{ value: 11, color: 'Red' }] },
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId('threshold-edit-0').click();
|
||||
await page.getByTestId('threshold-value-0').fill('999');
|
||||
|
||||
// Discard on an EXISTING row restores the snapshot.
|
||||
await page.getByTestId('threshold-discard-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
const thresholds = await savedThresholds(page, id);
|
||||
expect(thresholds).toHaveLength(1);
|
||||
expect(thresholds[0].value).toBe(11);
|
||||
});
|
||||
|
||||
test('TC-09 a threshold can be removed', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
pluginSpec: { thresholds: [{ value: 11, color: 'Red' }] },
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId('threshold-remove-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedThresholds(page, id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('TC-11 a crossed threshold colours the rendered value', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
// A renderer ignoring `thresholds` would still save the right JSON, so
|
||||
// assert the PANEL: value pinned at 100, threshold fires above 50.
|
||||
await pinnedNumberValue(page);
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
kind: PanelKind.Number,
|
||||
pluginSpec: {
|
||||
thresholds: [
|
||||
{
|
||||
value: 50,
|
||||
color: 'Red',
|
||||
operator: 'above',
|
||||
format: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('number-panel-value')).toBeVisible();
|
||||
await expect.poll(() => renderedValueColor(page)).not.toBe('');
|
||||
});
|
||||
|
||||
test('TC-12 raising the threshold past the value clears the colour live', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await pinnedNumberValue(page);
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
kind: PanelKind.Number,
|
||||
pluginSpec: {
|
||||
thresholds: [
|
||||
{ value: 50, color: 'Red', operator: 'above', format: 'text' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
await expect.poll(() => renderedValueColor(page)).not.toBe('');
|
||||
|
||||
// Edits stream into the preview as you type — no Run, no Save.
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId('comparison-threshold-edit-0').click();
|
||||
await page.getByTestId('comparison-threshold-value-0').fill('500');
|
||||
|
||||
await expect.poll(() => renderedValueColor(page)).toBe('');
|
||||
});
|
||||
|
||||
test('TC-13 the threshold colour survives save and shows on the dashboard', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await pinnedNumberValue(page);
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Number }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.comparison).click();
|
||||
await page.getByTestId('comparison-threshold-value-0').fill('50');
|
||||
await selectOption(page, 'comparison-threshold-operator-0', 'Above (>)');
|
||||
await page.getByTestId('comparison-threshold-save-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
// End-to-end: the saved panel on the dashboard renders the colour.
|
||||
await page.waitForURL(new RegExp(`/dashboard/${id}(\\?|$)`));
|
||||
await expect(page.getByTestId('number-panel-value')).toBeVisible();
|
||||
await expect.poll(() => renderedValueColor(page)).not.toBe('');
|
||||
});
|
||||
|
||||
test('TC-14 a table threshold paints the targeted cell background', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
// The column key is derived from the live result, so pick it from the
|
||||
// dropdown; ">= 0" fires on any non-negative value.
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
await expect(page.getByTestId('table-panel-renderer')).toBeVisible();
|
||||
|
||||
const valueCell = page
|
||||
.getByTestId('table-panel-renderer')
|
||||
.locator('tbody tr.ant-table-row')
|
||||
.first()
|
||||
.locator('td')
|
||||
.last();
|
||||
await expect(valueCell).toBeVisible();
|
||||
expect(await cellBackground(valueCell)).toBe('');
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.table).click();
|
||||
await pickFirstOption(page, 'table-threshold-column-0');
|
||||
await page.getByTestId('table-threshold-value-0').fill('0');
|
||||
await selectOption(page, 'table-threshold-operator-0', 'Above or equal');
|
||||
await selectOption(page, 'table-threshold-format-0', 'Background');
|
||||
await page.getByTestId('table-threshold-save-0').click();
|
||||
|
||||
// Live in the preview, before saving.
|
||||
await expect.poll(() => cellBackground(valueCell)).not.toBe('');
|
||||
|
||||
await savePanel(page);
|
||||
|
||||
// And on the saved panel back on the dashboard.
|
||||
await page.waitForURL(new RegExp(`/dashboard/${id}(\\?|$)`));
|
||||
const savedCell = page
|
||||
.getByTestId('table-panel-renderer')
|
||||
.locator('tbody tr.ant-table-row')
|
||||
.first()
|
||||
.locator('td')
|
||||
.last();
|
||||
await expect(savedCell).toBeVisible();
|
||||
await expect.poll(() => cellBackground(savedCell)).not.toBe('');
|
||||
});
|
||||
|
||||
test('TC-15 the text format colours the value, not the cell', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Table }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
await expect(page.getByTestId('table-panel-renderer')).toBeVisible();
|
||||
|
||||
const valueCell = page
|
||||
.getByTestId('table-panel-renderer')
|
||||
.locator('tbody tr.ant-table-row')
|
||||
.first()
|
||||
.locator('td')
|
||||
.last();
|
||||
await expect(valueCell).toBeVisible();
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.table).click();
|
||||
await pickFirstOption(page, 'table-threshold-column-0');
|
||||
await page.getByTestId('table-threshold-value-0').fill('0');
|
||||
await selectOption(page, 'table-threshold-operator-0', 'Above or equal');
|
||||
await selectOption(page, 'table-threshold-format-0', 'Text');
|
||||
await page.getByTestId('table-threshold-save-0').click();
|
||||
|
||||
// Text recolours the value; background paints the cell. Not interchangeable.
|
||||
await expect
|
||||
.poll(async () => valueCell.locator('span[style*="color"]').count())
|
||||
.toBeGreaterThan(0);
|
||||
expect(await cellBackground(valueCell)).toBe('');
|
||||
});
|
||||
|
||||
test('TC-10 the comparison variant persists its operator and display mode', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({ kind: PanelKind.Number }),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.thresholds);
|
||||
await page.getByTestId(AddButton.comparison).click();
|
||||
|
||||
await page.getByTestId('comparison-threshold-value-0').fill('5');
|
||||
await selectOption(page, 'comparison-threshold-operator-0', 'Below (<)');
|
||||
await selectOption(page, 'comparison-threshold-format-0', 'Background');
|
||||
await page.getByTestId('comparison-threshold-save-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
const after = await getDashboardV2ViaApi(page, id);
|
||||
const thresholds = (
|
||||
after.spec.panels[SINGLE_PANEL_ID].spec.plugin.spec as {
|
||||
thresholds?: { operator?: string; format?: string; value?: number }[];
|
||||
}
|
||||
).thresholds;
|
||||
expect(thresholds?.[0]).toMatchObject({
|
||||
value: 5,
|
||||
operator: 'below',
|
||||
format: 'background',
|
||||
});
|
||||
});
|
||||
});
|
||||
204
tests/e2e/tests/dashboards/v2/editor/75-context-links.spec.ts
Normal file
204
tests/e2e/tests/dashboards/v2/editor/75-context-links.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { expect, test } from '../../../../fixtures/dashboards';
|
||||
import { getDashboardV2ViaApi } from '../../../../helpers/dashboards-v2';
|
||||
import {
|
||||
Section,
|
||||
collapseSection,
|
||||
expandSection,
|
||||
savePanel,
|
||||
sectionToggle,
|
||||
} from '../../../../helpers/panel-editor-v2';
|
||||
import {
|
||||
SINGLE_PANEL_ID,
|
||||
singlePanelDashboard,
|
||||
} from '../../../../testdata/v2/panels-dashboard';
|
||||
|
||||
// Scope: the Context Links dialog and that a saved link reaches
|
||||
// `panel.spec.links` — where it surfaces as a `drilldown-context-link`
|
||||
// (covered in panels/57-drilldown).
|
||||
|
||||
/**
|
||||
* Close the URL field's suggestion popover, which otherwise keeps the dialog
|
||||
* reflowing so buttons never settle. Escape would dismiss the whole dialog.
|
||||
*/
|
||||
async function blurDialogFields(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
): Promise<void> {
|
||||
await page.getByTestId('context-link-label').focus();
|
||||
}
|
||||
|
||||
async function savedLinks(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
dashboardId: string,
|
||||
): Promise<{ name?: string; url?: string }[]> {
|
||||
const after = await getDashboardV2ViaApi(page, dashboardId);
|
||||
return after.spec.panels[SINGLE_PANEL_ID].spec.links ?? [];
|
||||
}
|
||||
|
||||
test.describe('Dashboards V2 — editor context links', () => {
|
||||
test('TC-01 the section is offered and opens an empty dialog', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link').click();
|
||||
|
||||
const dialog = page.getByTestId('context-link-dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page.getByTestId('context-link-label')).toHaveValue('');
|
||||
await expect(page.getByTestId('context-link-url')).toHaveValue('');
|
||||
});
|
||||
|
||||
test('TC-02 Save stays disabled until the link is valid', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link').click();
|
||||
|
||||
// A link with no URL would be a dead menu entry.
|
||||
await expect(page.getByTestId('context-link-save')).toBeDisabled();
|
||||
await page.getByTestId('context-link-label').fill('Runbook');
|
||||
await expect(page.getByTestId('context-link-save')).toBeDisabled();
|
||||
|
||||
await page
|
||||
.getByTestId('context-link-url')
|
||||
.fill('https://example.com/runbook');
|
||||
await expect(page.getByTestId('context-link-save')).toBeEnabled();
|
||||
});
|
||||
|
||||
test('TC-03 a saved link persists into panel.spec.links', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link').click();
|
||||
await page.getByTestId('context-link-label').fill('Runbook');
|
||||
await page
|
||||
.getByTestId('context-link-url')
|
||||
.fill('https://example.com/runbook');
|
||||
await blurDialogFields(page);
|
||||
await page.getByTestId('context-link-save').click();
|
||||
|
||||
await expect(page.getByTestId('context-link-item-0')).toBeVisible();
|
||||
await savePanel(page);
|
||||
|
||||
const links = await savedLinks(page, id);
|
||||
expect(links).toHaveLength(1);
|
||||
// "Label" persists as `name` (Perses link model).
|
||||
expect(links[0]).toMatchObject({
|
||||
name: 'Runbook',
|
||||
url: 'https://example.com/runbook',
|
||||
});
|
||||
});
|
||||
|
||||
test('TC-04 Cancel discards the dialog without adding a link', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link').click();
|
||||
await page.getByTestId('context-link-label').fill('Throwaway');
|
||||
await page.getByTestId('context-link-url').fill('https://example.com');
|
||||
await blurDialogFields(page);
|
||||
await page.getByTestId('context-link-cancel').click();
|
||||
|
||||
await expect(page.getByTestId('context-link-dialog')).toHaveCount(0);
|
||||
await expect(page.getByTestId('context-link-item-0')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-05 an existing link can be edited', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
links: [{ name: 'Original', url: 'https://example.com/one' }],
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('context-link-edit-0').click();
|
||||
await page.getByTestId('context-link-label').fill('Renamed');
|
||||
await blurDialogFields(page);
|
||||
await page.getByTestId('context-link-save').click();
|
||||
await savePanel(page);
|
||||
|
||||
const links = await savedLinks(page, id);
|
||||
expect(links[0]).toMatchObject({ name: 'Renamed' });
|
||||
});
|
||||
|
||||
test('TC-06 a link can be removed', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
links: [{ name: 'Original', url: 'https://example.com/one' }],
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('context-link-remove-0').click();
|
||||
await savePanel(page);
|
||||
|
||||
expect(await savedLinks(page, id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('TC-07 URL parameters can be added to a link', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
singlePanelDashboard(),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expandSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link').click();
|
||||
await page.getByTestId('context-link-label').fill('With params');
|
||||
await page.getByTestId('context-link-url').fill('https://example.com/search');
|
||||
|
||||
await blurDialogFields(page);
|
||||
await page.getByTestId('context-link-add-param').click();
|
||||
await page.getByTestId('context-link-param-key-0').fill('service');
|
||||
await page.getByTestId('context-link-param-value-0').fill('adservice');
|
||||
await blurDialogFields(page);
|
||||
await page.getByTestId('context-link-save').click();
|
||||
await savePanel(page);
|
||||
|
||||
const links = await savedLinks(page, id);
|
||||
expect(links).toHaveLength(1);
|
||||
// Params fold into the persisted URL.
|
||||
expect(JSON.stringify(links[0])).toContain('service');
|
||||
});
|
||||
|
||||
test('TC-08 the header quick-add opens the dialog from a collapsed section', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
|
||||
// One click must expand AND add (pendingAction hop).
|
||||
await collapseSection(page, Section.contextLinks);
|
||||
await page.getByTestId('panel-editor-v2-add-link-header').click();
|
||||
|
||||
await expect(sectionToggle(page, Section.contextLinks)).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
);
|
||||
await expect(page.getByTestId('context-link-dialog')).toBeVisible();
|
||||
});
|
||||
});
|
||||
180
tests/e2e/tests/dashboards/v2/editor/84-list-columns.spec.ts
Normal file
180
tests/e2e/tests/dashboards/v2/editor/84-list-columns.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { expect, test } from '../../../../fixtures/dashboards';
|
||||
import {
|
||||
PanelKind,
|
||||
logsCountQuery,
|
||||
rawQuery,
|
||||
} from '../../../../helpers/dashboard-v2-spec';
|
||||
import { getDashboardV2ViaApi } from '../../../../helpers/dashboards-v2';
|
||||
import { editor, savePanel } from '../../../../helpers/panel-editor-v2';
|
||||
import {
|
||||
SINGLE_PANEL_ID,
|
||||
singlePanelDashboard,
|
||||
} from '../../../../testdata/v2/panels-dashboard';
|
||||
|
||||
// Scope: the List columns editor — the one per-kind control that lives in the
|
||||
// query builder's footer rather than the ConfigPane. Persists to
|
||||
// `plugin.spec.selectFields`.
|
||||
|
||||
async function savedFields(
|
||||
page: Parameters<typeof getDashboardV2ViaApi>[0],
|
||||
dashboardId: string,
|
||||
): Promise<{ name?: string }[]> {
|
||||
const after = await getDashboardV2ViaApi(page, dashboardId);
|
||||
const spec = after.spec.panels[SINGLE_PANEL_ID].spec.plugin.spec as {
|
||||
selectFields?: { name?: string }[];
|
||||
};
|
||||
return spec.selectFields ?? [];
|
||||
}
|
||||
|
||||
function listDashboard(fields?: { name: string; signal?: 'logs' }[]) {
|
||||
return singlePanelDashboard({
|
||||
kind: PanelKind.List,
|
||||
query: rawQuery({ signal: 'logs' }),
|
||||
...(fields ? { pluginSpec: { selectFields: fields } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('Dashboards V2 — editor list columns', () => {
|
||||
test('TC-01 the columns editor renders only for List panels', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(listDashboard(), SINGLE_PANEL_ID);
|
||||
await expect(page.getByTestId('list-columns-editor')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-02 a TimeSeries panel has no columns editor', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(singlePanelDashboard(), SINGLE_PANEL_ID);
|
||||
await expect(editor.queryBuilder(page)).toBeVisible();
|
||||
await expect(page.getByTestId('list-columns-editor')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-03 seeded columns render as chips', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
listDashboard([
|
||||
{ name: 'timestamp', signal: 'logs' },
|
||||
{ name: 'body', signal: 'logs' },
|
||||
]),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId('list-column-chip-timestamp')).toBeVisible();
|
||||
await expect(page.getByTestId('list-column-chip-body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-04 a column can be removed and the removal persists', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
listDashboard([
|
||||
{ name: 'timestamp', signal: 'logs' },
|
||||
{ name: 'body', signal: 'logs' },
|
||||
]),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await page.getByTestId('list-column-remove-body').click();
|
||||
await expect(page.getByTestId('list-column-chip-body')).toHaveCount(0);
|
||||
await savePanel(page);
|
||||
|
||||
const fields = await savedFields(page, id);
|
||||
expect(fields.map((field) => field.name)).not.toContain('body');
|
||||
});
|
||||
|
||||
test('TC-05 a custom column can be added by free text', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
listDashboard([{ name: 'timestamp', signal: 'logs' }]),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await page.getByTestId('list-columns-add').click();
|
||||
await page.getByTestId('list-columns-search').fill('my_custom_field');
|
||||
|
||||
// Explicit "Add …" entry, so a typo can't be committed by blurring.
|
||||
await page.getByTestId('list-columns-add-custom').click();
|
||||
await expect(
|
||||
page.getByTestId('list-column-chip-my_custom_field'),
|
||||
).toBeVisible();
|
||||
|
||||
await savePanel(page);
|
||||
const fields = await savedFields(page, id);
|
||||
expect(fields.map((field) => field.name)).toContain('my_custom_field');
|
||||
});
|
||||
|
||||
test('TC-06 a suggested field can be added from the dropdown', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
const id = await dashboards.seedAndEdit(
|
||||
listDashboard([{ name: 'timestamp', signal: 'logs' }]),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await page.getByTestId('list-columns-add').click();
|
||||
await page.getByTestId('list-columns-search').fill('service');
|
||||
|
||||
// The list repaints as backend results land; waiting for the loading row
|
||||
// avoids resolving `.first()` against a node about to be replaced.
|
||||
await expect(page.getByText('Loading…')).toHaveCount(0);
|
||||
const suggestion = page
|
||||
.locator('[data-testid^="list-columns-suggestion-"]')
|
||||
.first();
|
||||
await expect(suggestion).toBeVisible();
|
||||
const testId = (await suggestion.getAttribute('data-testid')) ?? '';
|
||||
const fieldName = testId.replace('list-columns-suggestion-', '');
|
||||
await suggestion.click();
|
||||
|
||||
await savePanel(page);
|
||||
const fields = await savedFields(page, id);
|
||||
expect(fields.map((field) => field.name)).toContain(fieldName);
|
||||
});
|
||||
|
||||
test('TC-07 an empty column set is allowed and explains itself', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
await dashboards.seedAndEdit(
|
||||
listDashboard([{ name: 'timestamp', signal: 'logs' }]),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
|
||||
await page.getByTestId('list-column-remove-timestamp').click();
|
||||
// Empty means "show everything the query returns".
|
||||
await expect(
|
||||
page.getByText('Leave empty to show all fields returned by the query.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-08 switching a List panel to Table drops the columns editor', async ({
|
||||
authedPage: page,
|
||||
dashboards,
|
||||
}) => {
|
||||
// Logs-shaped so Table stays a legal target.
|
||||
await dashboards.seedAndEdit(
|
||||
singlePanelDashboard({
|
||||
kind: PanelKind.List,
|
||||
query: logsCountQuery(),
|
||||
}),
|
||||
SINGLE_PANEL_ID,
|
||||
);
|
||||
await expect(page.getByTestId('list-columns-editor')).toBeVisible();
|
||||
|
||||
await page.getByTestId('panel-editor-v2-type-switcher').click();
|
||||
await page
|
||||
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
|
||||
.getByText('Table', { exact: true })
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('list-columns-editor')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user