mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: Add error message and edit button when tile source is missing (#2063)
## Summary This PR updates dashboard tiles so that 1. When a tile references a source that no longer exists, there is an appropriate error message 2. When a tile references a source that no longer exists, the user is able to click the edit tile button to fix the issue ### Screenshots or video <img width="887" height="429" alt="Screenshot 2026-04-07 at 9 40 53 AM" src="https://github.com/user-attachments/assets/ae0f77bc-3fcc-40c3-bf65-9ed454f31a4b" /> ### How to test locally or on Vercel This can be tested in the preview environment by creating a tile and then deleting the associated source. ### References - Linear Issue: HDX-3926 - Related PRs:
This commit is contained in:
parent
3ffafced5e
commit
ffc961c621
4 changed files with 223 additions and 106 deletions
5
.changeset/tiny-spiders-smell.md
Normal file
5
.changeset/tiny-spiders-smell.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: Add error message and edit button when tile source is missing
|
||||
|
|
@ -23,6 +23,8 @@ Delegate to the **`playwright-test-generator`** agent (via the Agent tool). Pass
|
|||
|
||||
The agent will drive a real browser, execute the steps live, and produce spec code that follows HyperDX conventions. Review the output before proceeding.
|
||||
|
||||
NOTE: When there is an existing spec file covering the feature, add new tests to the existing file instead of creating a new one. This keeps related tests together and avoids fragmentation.
|
||||
|
||||
### 2. Test Execution
|
||||
After the generator agent writes the file, run the test:
|
||||
|
||||
|
|
|
|||
|
|
@ -50,15 +50,14 @@ import {
|
|||
Flex,
|
||||
Group,
|
||||
Indicator,
|
||||
Input,
|
||||
Menu,
|
||||
Modal,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useHotkeys, useHover } from '@mantine/hooks';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconArrowsMaximize,
|
||||
|
|
@ -220,10 +219,17 @@ const Tile = forwardRef(
|
|||
ChartConfigWithDateRange | undefined
|
||||
>(undefined);
|
||||
|
||||
const { data: source } = useSource({
|
||||
const { data: source, isFetched: isSourceFetched } = useSource({
|
||||
id: chart.config.source,
|
||||
});
|
||||
|
||||
const isSourceMissing =
|
||||
!!chart.config.source && isSourceFetched && source == null;
|
||||
const isSourceUnset =
|
||||
!!chart.config &&
|
||||
isBuilderSavedChartConfig(chart.config) &&
|
||||
!chart.config.source;
|
||||
|
||||
useEffect(() => {
|
||||
if (isRawSqlSavedChartConfig(chart.config)) {
|
||||
// Some raw SQL charts don't have a source
|
||||
|
|
@ -364,6 +370,7 @@ const Tile = forwardRef(
|
|||
gap="0px"
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
key="hover-toolbar"
|
||||
my={4} // Margin to ensure that the Alert Indicator doesn't clip on non-Line/Bar display types
|
||||
style={{ visibility: hovered ? 'visible' : 'hidden' }}
|
||||
>
|
||||
{(chart.config.displayType === DisplayType.Line ||
|
||||
|
|
@ -510,6 +517,26 @@ const Tile = forwardRef(
|
|||
</div>
|
||||
}
|
||||
>
|
||||
{isSourceMissing ? (
|
||||
<ChartContainer title={title} toolbarItems={toolbar}>
|
||||
<Stack align="center" justify="center" h="100%" p="md">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
The data source for this tile no longer exists. Edit the
|
||||
tile to select a new source.
|
||||
</Text>
|
||||
</Stack>
|
||||
</ChartContainer>
|
||||
) : isSourceUnset ? (
|
||||
<ChartContainer title={title} toolbarItems={toolbar}>
|
||||
<Stack align="center" justify="center" h="100%" p="md">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
The data source for this tile is not set. Edit the tile to
|
||||
select a data source.
|
||||
</Text>
|
||||
</Stack>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<>
|
||||
{(queriedConfig?.displayType === DisplayType.Line ||
|
||||
queriedConfig?.displayType === DisplayType.StackedBar) && (
|
||||
<DBTimeChart
|
||||
|
|
@ -569,7 +596,8 @@ const Tile = forwardRef(
|
|||
config={queriedConfig}
|
||||
/>
|
||||
)}
|
||||
{effectiveMarkdownConfig?.displayType === DisplayType.Markdown &&
|
||||
{effectiveMarkdownConfig?.displayType ===
|
||||
DisplayType.Markdown &&
|
||||
'markdown' in effectiveMarkdownConfig && (
|
||||
<HDXMarkdownChart
|
||||
key={`${keyPrefix}-${chart.id}`}
|
||||
|
|
@ -617,6 +645,8 @@ const Tile = forwardRef(
|
|||
/>
|
||||
</ChartContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
|
|
@ -630,6 +660,8 @@ const Tile = forwardRef(
|
|||
source,
|
||||
dateRange,
|
||||
filterWarning,
|
||||
isSourceMissing,
|
||||
isSourceUnset,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DisplayType } from '@hyperdx/common-utils/dist/types';
|
|||
import { AlertsPage } from '../page-objects/AlertsPage';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage';
|
||||
import { DashboardsListPage } from '../page-objects/DashboardsListPage';
|
||||
import { getApiUrl, getSources } from '../utils/api-helpers';
|
||||
import { expect, test } from '../utils/base-test';
|
||||
import {
|
||||
DEFAULT_LOGS_SOURCE_NAME,
|
||||
|
|
@ -821,6 +822,83 @@ test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
|
|||
},
|
||||
);
|
||||
|
||||
test('should show error message and allow editing when tile source is missing', async ({
|
||||
page,
|
||||
}) => {
|
||||
const apiUrl = getApiUrl();
|
||||
const DELETABLE_SOURCE_NAME = `E2E Deletable Source ${Date.now()}`;
|
||||
|
||||
// Get an existing log source to copy its connection
|
||||
const logSources = await getSources(page, 'log');
|
||||
const { connection, from } = logSources[0];
|
||||
|
||||
// Create a dedicated source for this test via the API
|
||||
const createResponse = await page.request.post(`${apiUrl}/sources`, {
|
||||
data: {
|
||||
kind: 'log',
|
||||
name: DELETABLE_SOURCE_NAME,
|
||||
connection,
|
||||
from,
|
||||
timestampValueExpression: 'TimestampTime',
|
||||
defaultTableSelectExpression:
|
||||
'Timestamp, ServiceName, SeverityText, Body',
|
||||
serviceNameExpression: 'ServiceName',
|
||||
implicitColumnExpression: 'Body',
|
||||
},
|
||||
});
|
||||
expect(createResponse.ok()).toBeTruthy();
|
||||
const createdSource = await createResponse.json();
|
||||
|
||||
await test.step('Create dashboard with tile using the deletable source', async () => {
|
||||
await dashboardPage.goto();
|
||||
await dashboardPage.createNewDashboard();
|
||||
|
||||
await dashboardPage.addTile();
|
||||
await dashboardPage.chartEditor.waitForDataToLoad();
|
||||
await dashboardPage.chartEditor.setChartName('Missing Source Tile');
|
||||
await dashboardPage.chartEditor.selectSource(DELETABLE_SOURCE_NAME);
|
||||
await dashboardPage.chartEditor.runQuery();
|
||||
await dashboardPage.saveTile();
|
||||
|
||||
await expect(dashboardPage.getTiles()).toHaveCount(1, {
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Delete the source and reload the dashboard', async () => {
|
||||
const dashboardUrl = page.url();
|
||||
|
||||
const deleteResponse = await page.request.delete(
|
||||
`${apiUrl}/sources/${createdSource.id}`,
|
||||
);
|
||||
expect(deleteResponse.ok()).toBeTruthy();
|
||||
|
||||
await page.goto(dashboardUrl);
|
||||
await expect(dashboardPage.getTiles()).toHaveCount(1, {
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Verify tile shows error message for missing source', async () => {
|
||||
const tile = dashboardPage.getTiles().first();
|
||||
await expect(tile).toContainText(
|
||||
'The data source for this tile no longer exists',
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Verify tile can be edited when source is missing', async () => {
|
||||
await dashboardPage.hoverOverTile(0);
|
||||
|
||||
const editButton = dashboardPage.getTileButton('edit');
|
||||
await expect(editButton).toBeVisible();
|
||||
await editButton.click();
|
||||
|
||||
await expect(dashboardPage.chartEditor.nameInput).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'should clear saved query when WHERE input is cleared and saved',
|
||||
{},
|
||||
|
|
|
|||
Loading…
Reference in a new issue