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-/); + } }