diff --git a/.changeset/hot-chicken-jam.md b/.changeset/hot-chicken-jam.md
new file mode 100644
index 00000000..8ad76552
--- /dev/null
+++ b/.changeset/hot-chicken-jam.md
@@ -0,0 +1,5 @@
+---
+"@hyperdx/app": patch
+---
+
+feat: Add alert icons to dashboard list page
diff --git a/packages/app/src/components/AlertStatusIcon.tsx b/packages/app/src/components/AlertStatusIcon.tsx
new file mode 100644
index 00000000..31031115
--- /dev/null
+++ b/packages/app/src/components/AlertStatusIcon.tsx
@@ -0,0 +1,31 @@
+import { AlertState } from '@hyperdx/common-utils/dist/types';
+import { Tooltip } from '@mantine/core';
+import { IconBell, IconBellFilled } from '@tabler/icons-react';
+
+export function AlertStatusIcon({
+ alerts,
+}: {
+ alerts?: { state?: AlertState }[];
+}) {
+ if (!Array.isArray(alerts) || alerts.length === 0) return null;
+ const alertingCount = alerts.filter(a => a.state === AlertState.ALERT).length;
+ return (
+ 0
+ ? `${alertingCount} alert${alertingCount > 1 ? 's' : ''} triggered`
+ : 'Alerts configured'
+ }
+ >
+ {alertingCount > 0 ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/packages/app/src/components/AppNav/AppNav.tsx b/packages/app/src/components/AppNav/AppNav.tsx
index f6bf1445..35dc7dbd 100644
--- a/packages/app/src/components/AppNav/AppNav.tsx
+++ b/packages/app/src/components/AppNav/AppNav.tsx
@@ -3,7 +3,7 @@ import Link from 'next/link';
import Router, { useRouter } from 'next/router';
import cx from 'classnames';
import HyperDX from '@hyperdx/browser';
-import { AlertState } from '@hyperdx/common-utils/dist/types';
+import { isBuilderSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
import {
ActionIcon,
Anchor,
@@ -18,7 +18,6 @@ import { useDisclosure, useLocalStorage } from '@mantine/hooks';
import {
IconArrowBarToLeft,
IconBell,
- IconBellFilled,
IconChartDots,
IconDeviceFloppy,
IconDeviceLaptop,
@@ -29,6 +28,7 @@ import {
} from '@tabler/icons-react';
import api from '@/api';
+import { AlertStatusIcon } from '@/components/AlertStatusIcon';
import { IS_LOCAL_MODE } from '@/config';
import { Dashboard, useDashboards } from '@/dashboard';
import { useFavorites } from '@/favorites';
@@ -217,26 +217,9 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
>
{savedSearch.name}
- {Array.isArray(savedSearch.alerts) &&
- savedSearch.alerts.length > 0 ? (
- savedSearch.alerts.some(a => a.state === AlertState.ALERT) ? (
-
-
-
- ) : (
-
-
-
- )
- ) : null}
+
+
+
),
@@ -244,18 +227,31 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
);
const renderDashboardLink = useCallback(
- (dashboard: Dashboard) => (
-
-
{dashboard.name}
-
- ),
+ (dashboard: Dashboard) => {
+ const alerts = dashboard.tiles
+ .map(t =>
+ isBuilderSavedChartConfig(t.config) ? t.config.alert : undefined,
+ )
+ .filter(a => a != null);
+ return (
+
+
+ {dashboard.name}
+
+
+
+
+
+ );
+ },
[query.dashboardId],
);
diff --git a/packages/app/src/components/Dashboards/DashboardsListPage.tsx b/packages/app/src/components/Dashboards/DashboardsListPage.tsx
index 3c131ce1..2d97df2b 100644
--- a/packages/app/src/components/Dashboards/DashboardsListPage.tsx
+++ b/packages/app/src/components/Dashboards/DashboardsListPage.tsx
@@ -3,10 +3,10 @@ import Head from 'next/head';
import Link from 'next/link';
import Router from 'next/router';
import { useQueryState } from 'nuqs';
+import { isBuilderSavedChartConfig } from '@hyperdx/common-utils/dist/guards';
import {
ActionIcon,
Anchor,
- Box,
Button,
Container,
Flex,
@@ -31,12 +31,14 @@ import {
IconUpload,
} from '@tabler/icons-react';
+import { AlertStatusIcon } from '@/components/AlertStatusIcon';
import { FavoriteButton } from '@/components/FavoriteButton';
import { ListingCard } from '@/components/ListingCard';
import { ListingRow } from '@/components/ListingListRow';
import { PageHeader } from '@/components/PageHeader';
import { IS_K8S_DASHBOARD_ENABLED } from '@/config';
import {
+ type Dashboard,
useCreateDashboard,
useDashboards,
useDeleteDashboard,
@@ -48,6 +50,14 @@ import { groupByTags } from '@/utils/groupByTags';
import { withAppNav } from '../../layout';
+function getDashboardAlerts(tiles: Dashboard['tiles']) {
+ return tiles
+ .map(t =>
+ isBuilderSavedChartConfig(t.config) ? t.config.alert : undefined,
+ )
+ .filter(a => a != null);
+}
+
const PRESET_DASHBOARDS = [
{
name: 'Services',
@@ -209,6 +219,9 @@ export default function DashboardsListPage() {
tags={d.tags}
description={`${d.tiles.length} ${d.tiles.length === 1 ? 'tile' : 'tiles'}`}
onDelete={() => handleDelete(d.id)}
+ statusIcon={
+
+ }
resourceId={d.id}
resourceType="dashboard"
/>
@@ -351,7 +364,7 @@ export default function DashboardsListPage() {
-
+
Name
Tags
@@ -367,13 +380,14 @@ export default function DashboardsListPage() {
tags={d.tags}
onDelete={handleDelete}
leftSection={
-
+
-
+
+
}
/>
))}
@@ -395,6 +409,9 @@ export default function DashboardsListPage() {
tags={d.tags}
description={`${d.tiles.length} ${d.tiles.length === 1 ? 'tile' : 'tiles'}`}
onDelete={() => handleDelete(d.id)}
+ statusIcon={
+
+ }
resourceId={d.id}
resourceType="dashboard"
/>
diff --git a/packages/app/src/components/SavedSearches/SavedSearchesListPage.tsx b/packages/app/src/components/SavedSearches/SavedSearchesListPage.tsx
index e3b39e49..e34f7d56 100644
--- a/packages/app/src/components/SavedSearches/SavedSearchesListPage.tsx
+++ b/packages/app/src/components/SavedSearches/SavedSearchesListPage.tsx
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react';
import Head from 'next/head';
import Router from 'next/router';
import { useQueryState } from 'nuqs';
-import { AlertState } from '@hyperdx/common-utils/dist/types';
import {
ActionIcon,
Button,
@@ -15,19 +14,17 @@ import {
Table,
Text,
TextInput,
- Tooltip,
} from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import {
- IconBell,
- IconBellFilled,
IconLayoutGrid,
IconList,
IconSearch,
IconTable,
} from '@tabler/icons-react';
+import { AlertStatusIcon } from '@/components/AlertStatusIcon';
import { FavoriteButton } from '@/components/FavoriteButton';
import { ListingCard } from '@/components/ListingCard';
import { ListingRow } from '@/components/ListingListRow';
@@ -35,36 +32,11 @@ import { PageHeader } from '@/components/PageHeader';
import { useFavorites } from '@/favorites';
import { useDeleteSavedSearch, useSavedSearches } from '@/savedSearch';
import { useBrandDisplayName } from '@/theme/ThemeProvider';
-import type { SavedSearchWithEnhancedAlerts } from '@/types';
import { useConfirm } from '@/useConfirm';
import { groupByTags } from '@/utils/groupByTags';
import { withAppNav } from '../../layout';
-function AlertStatusIcon({
- alerts,
-}: {
- alerts?: SavedSearchWithEnhancedAlerts['alerts'];
-}) {
- if (!Array.isArray(alerts) || alerts.length === 0) return null;
- const alertingCount = alerts.filter(a => a.state === AlertState.ALERT).length;
- return (
- 0
- ? `${alertingCount} alert${alertingCount > 1 ? 's' : ''} triggered`
- : 'Alerts configured'
- }
- >
- {alertingCount > 0 ? (
-
- ) : (
-
- )}
-
- );
-}
-
export default function SavedSearchesListPage() {
const brandName = useBrandDisplayName();
const { data: savedSearches, isLoading, isError } = useSavedSearches();
diff --git a/packages/app/tests/e2e/features/dashboards-list.spec.ts b/packages/app/tests/e2e/features/dashboards-list.spec.ts
index 0d0c9dd3..9f5813ef 100644
--- a/packages/app/tests/e2e/features/dashboards-list.spec.ts
+++ b/packages/app/tests/e2e/features/dashboards-list.spec.ts
@@ -237,4 +237,73 @@ test.describe('Dashboards Listing Page', { tag: ['@dashboard'] }, () => {
});
},
);
+
+ test(
+ 'should show alert icon on dashboard card when tile has alert configured',
+ { tag: ['@full-stack'] },
+ async ({ page }) => {
+ const ts = Date.now();
+ const dashboardName = `E2E Alert Icon Dashboard ${ts}`;
+ const tileName = `E2E Alert Tile ${ts}`;
+ const webhookName = `E2E Webhook Alert Icon ${ts}`;
+ const webhookUrl = `https://example.com/alert-icon-${ts}`;
+
+ await test.step('Create a new saved dashboard', async () => {
+ await dashboardsListPage.goto();
+ await dashboardsListPage.createNewDashboard();
+ });
+
+ await test.step('Rename the dashboard', async () => {
+ await dashboardPage.editDashboardName(dashboardName);
+ });
+
+ await test.step('Add a tile with a chart', async () => {
+ await dashboardPage.addTile();
+ await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
+ await dashboardPage.chartEditor.waitForDataToLoad();
+ await dashboardPage.chartEditor.setChartName(tileName);
+ await dashboardPage.chartEditor.runQuery();
+ });
+
+ await test.step('Configure an alert on the tile', async () => {
+ await expect(dashboardPage.chartEditor.alertButton).toBeVisible();
+ await dashboardPage.chartEditor.clickAddAlert();
+ await expect(
+ dashboardPage.chartEditor.addNewWebhookButton,
+ ).toBeVisible();
+ await dashboardPage.chartEditor.addNewWebhookButton.click();
+ await expect(page.getByTestId('webhook-name-input')).toBeVisible();
+ await dashboardPage.chartEditor.webhookAlertModal.addWebhook(
+ 'Generic',
+ webhookName,
+ webhookUrl,
+ );
+ await expect(page.getByTestId('alert-modal')).toBeHidden();
+ await dashboardPage.chartEditor.save();
+ await expect(dashboardPage.getTiles()).toHaveCount(1, {
+ timeout: 10000,
+ });
+ });
+
+ await test.step('Verify alert icon in grid view', async () => {
+ await dashboardsListPage.goto();
+ await expect(
+ dashboardsListPage.getDashboardCard(dashboardName),
+ ).toBeVisible();
+ await expect(
+ dashboardsListPage.getAlertStatusIcon(dashboardName),
+ ).toBeVisible();
+ });
+
+ await test.step('Verify alert icon in list view', async () => {
+ await dashboardsListPage.switchToListView();
+ await expect(
+ dashboardsListPage.getDashboardRow(dashboardName),
+ ).toBeVisible();
+ await expect(
+ dashboardsListPage.getAlertStatusIconInRow(dashboardName),
+ ).toBeVisible();
+ });
+ },
+ );
});
diff --git a/packages/app/tests/e2e/page-objects/DashboardsListPage.ts b/packages/app/tests/e2e/page-objects/DashboardsListPage.ts
index 0b0b357d..bc126bf5 100644
--- a/packages/app/tests/e2e/page-objects/DashboardsListPage.ts
+++ b/packages/app/tests/e2e/page-objects/DashboardsListPage.ts
@@ -154,4 +154,14 @@ export class DashboardsListPage {
const card = this.getFavoritedDashboardCard(name);
await card.getByTestId('favorite-button').click();
}
+
+ getAlertStatusIcon(name: string) {
+ const card = this.getDashboardCard(name);
+ return card.getByTestId(/^alert-status-icon-/);
+ }
+
+ getAlertStatusIconInRow(name: string) {
+ const row = this.getDashboardRow(name);
+ return row.getByTestId(/^alert-status-icon-/);
+ }
}