From b58c54c741a1e8ffbea5ac96d376c6d1aee65f22 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Wed, 25 Feb 2026 17:52:46 +0000 Subject: [PATCH 1/2] refactor: update dashboard page and clean up AppNav component Replaced DBDashboardPage with DashboardsListPage in the dashboard index. Removed unused imports and components related to dashboard creation and management in AppNav, streamlining the navigation component for better performance and maintainability. --- packages/app/pages/dashboards/index.tsx | 4 +- packages/app/src/DashboardsListPage.tsx | 299 ++++++++++++++++++ packages/app/src/components/AppNav/AppNav.tsx | 230 +------------- 3 files changed, 303 insertions(+), 230 deletions(-) create mode 100644 packages/app/src/DashboardsListPage.tsx diff --git a/packages/app/pages/dashboards/index.tsx b/packages/app/pages/dashboards/index.tsx index 18f0f8157b..4c2426c98f 100644 --- a/packages/app/pages/dashboards/index.tsx +++ b/packages/app/pages/dashboards/index.tsx @@ -1,2 +1,2 @@ -import DBDashboardPage from '@/DBDashboardPage'; -export default DBDashboardPage; +import DashboardsListPage from '@/DashboardsListPage'; +export default DashboardsListPage; diff --git a/packages/app/src/DashboardsListPage.tsx b/packages/app/src/DashboardsListPage.tsx new file mode 100644 index 0000000000..50f4f908d1 --- /dev/null +++ b/packages/app/src/DashboardsListPage.tsx @@ -0,0 +1,299 @@ +import { useCallback, useMemo, useState } from 'react'; +import Head from 'next/head'; +import Link from 'next/link'; +import Router from 'next/router'; +import { + ActionIcon, + Badge, + Button, + Card, + Container, + Flex, + Group, + Menu, + SimpleGrid, + Stack, + Text, + TextInput, +} from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import { + IconDots, + IconLayoutGrid, + IconPlus, + IconSearch, + IconServer, + IconSettings, + IconTrash, +} from '@tabler/icons-react'; + +import { PageHeader } from '@/components/PageHeader'; +import { IS_K8S_DASHBOARD_ENABLED, IS_LOCAL_MODE } from '@/config'; +import { + useCreateDashboard, + useDashboards, + useDeleteDashboard, +} from '@/dashboard'; +import { useBrandDisplayName } from '@/theme/ThemeProvider'; + +import type { Dashboard } from './dashboard'; +import { withAppNav } from './layout'; + +const PRESET_DASHBOARDS = [ + { + name: 'Services', + href: '/services', + description: 'Monitor HTTP endpoints, latency, and error rates', + icon: IconServer, + }, + { + name: 'ClickHouse', + href: '/clickhouse', + description: 'ClickHouse cluster health and query performance', + icon: IconSettings, + }, +]; + +function DashboardCard({ + dashboard, + onDelete, +}: { + dashboard: Dashboard; + onDelete: (id: string) => void; +}) { + return ( + + + + {dashboard.name} + + + + e.preventDefault()} + > + + + + + } + onClick={e => { + e.preventDefault(); + onDelete(dashboard.id); + }} + > + Delete + + + + + + + + {dashboard.tiles.length}{' '} + {dashboard.tiles.length === 1 ? 'tile' : 'tiles'} + + {dashboard.tags.map(tag => ( + + {tag} + + ))} + + + ); +} + +function PresetDashboardCard({ + name, + href, + description, + icon: Icon, +}: { + name: string; + href: string; + description: string; + icon: React.ElementType; +}) { + return ( + + + + {name} + + + {description} + + + ); +} + +export default function DashboardsListPage() { + const brandName = useBrandDisplayName(); + const { data: dashboards, isLoading } = useDashboards(); + const createDashboard = useCreateDashboard(); + const deleteDashboard = useDeleteDashboard(); + const [search, setSearch] = useState(''); + + const presets = useMemo(() => { + if (IS_K8S_DASHBOARD_ENABLED) { + return [ + ...PRESET_DASHBOARDS, + { + name: 'Kubernetes', + href: '/kubernetes', + description: 'Kubernetes cluster monitoring and pod health', + icon: IconLayoutGrid, + }, + ]; + } + return PRESET_DASHBOARDS; + }, []); + + const filteredDashboards = useMemo(() => { + if (!dashboards) return []; + if (!search.trim()) return dashboards; + const q = search.toLowerCase(); + return dashboards.filter( + d => + d.name.toLowerCase().includes(q) || + d.tags.some(t => t.toLowerCase().includes(q)), + ); + }, [dashboards, search]); + + const handleCreate = useCallback(() => { + if (IS_LOCAL_MODE) { + Router.push('/dashboards'); + return; + } + createDashboard.mutate( + { name: 'My Dashboard', tiles: [], tags: [] }, + { + onSuccess: data => { + Router.push(`/dashboards/${data.id}`); + }, + }, + ); + }, [createDashboard]); + + const handleDelete = useCallback( + (id: string) => { + deleteDashboard.mutate(id, { + onSuccess: () => { + notifications.show({ + message: 'Dashboard deleted', + color: 'green', + }); + }, + onError: () => { + notifications.show({ + message: 'Failed to delete dashboard', + color: 'red', + }); + }, + }); + }, + [deleteDashboard], + ); + + return ( +
+ + Dashboards - {brandName} + + Dashboards + + + } + value={search} + onChange={e => setSearch(e.currentTarget.value)} + style={{ flex: 1, maxWidth: 400 }} + /> + + + + {!IS_LOCAL_MODE && ( + <> + + Preset Dashboards + + + {presets.map(p => ( + + ))} + + + )} + + + {search + ? `Results (${filteredDashboards.length})` + : 'Your Dashboards'} + + + {isLoading ? ( + + Loading dashboards... + + ) : filteredDashboards.length === 0 ? ( + + + + + {search + ? `No dashboards matching "${search}"` + : 'No dashboards yet. Create one to get started.'} + + {!search && ( + + )} + + + ) : ( + + {filteredDashboards.map(d => ( + + ))} + + )} + +
+ ); +} + +DashboardsListPage.getLayout = withAppNav; diff --git a/packages/app/src/components/AppNav/AppNav.tsx b/packages/app/src/components/AppNav/AppNav.tsx index 279abb2452..78f4d21833 100644 --- a/packages/app/src/components/AppNav/AppNav.tsx +++ b/packages/app/src/components/AppNav/AppNav.tsx @@ -41,17 +41,12 @@ import { } from '@tabler/icons-react'; import api from '@/api'; -import { IS_K8S_DASHBOARD_ENABLED, IS_LOCAL_MODE } from '@/config'; -import { - useCreateDashboard, - useDashboards, - useUpdateDashboard, -} from '@/dashboard'; +import { IS_LOCAL_MODE } from '@/config'; import InstallInstructionModal from '@/InstallInstructionsModal'; import OnboardingChecklist from '@/OnboardingChecklist'; import { useSavedSearches, useUpdateSavedSearch } from '@/savedSearch'; import { useLogomark, useWordmark } from '@/theme/ThemeProvider'; -import type { SavedSearch, ServerDashboard } from '@/types'; +import type { SavedSearch } from '@/types'; import { UserPreferencesModal } from '@/UserPreferencesModal'; import { useUserPreferences } from '@/useUserPreferences'; import { useWindowSize } from '@/utils'; @@ -73,7 +68,6 @@ const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION ?? packageJson.version ?? 'dev'; const UNTAGGED_SEARCHES_GROUP_NAME = 'Saved Searches'; -const UNTAGGED_DASHBOARDS_GROUP_NAME = 'Saved Dashboards'; // Navigation link configuration type NavLinkConfig = { @@ -114,54 +108,6 @@ const NAV_LINKS: NavLinkConfig[] = [ }, ]; -function NewDashboardButton() { - const createDashboard = useCreateDashboard(); - - if (IS_LOCAL_MODE) { - return ( - - ); - } - - return ( - - ); -} - function SearchInput({ placeholder, value, @@ -418,16 +364,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { } = useSavedSearches(); const logViews = useMemo(() => logViewsData ?? [], [logViewsData]); - const updateDashboard = useUpdateDashboard(); const updateLogView = useUpdateSavedSearch(); - const { - data: dashboardsData, - isLoading: isDashboardsLoading, - refetch: refetchDashboards, - } = useDashboards(); - const dashboards = useMemo(() => dashboardsData ?? [], [dashboardsData]); - const router = useRouter(); const { pathname, query } = router; @@ -446,11 +384,6 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { key: 'isSearchExpanded', defaultValue: true, }); - const [isDashboardsExpanded, setIsDashboardExpanded] = - useLocalStorage({ - key: 'isDashboardsExpanded', - defaultValue: true, - }); const { width } = useWindowSize(); const [isPreferCollapsed, setIsPreferCollapsed] = useLocalStorage({ @@ -491,24 +424,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { untaggedGroupName: UNTAGGED_SEARCHES_GROUP_NAME, }); - const { - q: dashboardsListQ, - setQ: setDashboardsListQ, - filteredList: filteredDashboardsList, - groupedFilteredList: groupedFilteredDashboardsList, - } = useSearchableList({ - items: dashboards, - untaggedGroupName: UNTAGGED_DASHBOARDS_GROUP_NAME, - }); - - const [isDashboardsPresetsCollapsed, setDashboardsPresetsCollapsed] = - useLocalStorage({ - key: 'isDashboardsPresetsCollapsed', - defaultValue: false, - }); - const savedSearchesResultsRef = useRef(null); - const dashboardsResultsRef = useRef(null); const renderLogViewLink = useCallback( (savedSearch: SavedSearch) => ( @@ -587,50 +503,6 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { [logViews, refetchLogViews, updateLogView], ); - const renderDashboardLink = useCallback( - (dashboard: ServerDashboard) => ( - - {dashboard.name} - - ), - [query.dashboardId], - ); - - const handleDashboardDragEnd = useCallback( - (target: HTMLElement | null, name: string | null) => { - if (!target?.dataset.dashboardid || name == null) { - return; - } - const dashboard = dashboards.find( - d => d.id === target.dataset.dashboardid, - ); - if (dashboard?.tags?.includes(name)) { - return; - } - updateDashboard.mutate( - { - id: target.dataset.dashboardid, - tags: name === UNTAGGED_DASHBOARDS_GROUP_NAME ? [] : [name], - }, - { - onSuccess: () => { - refetchDashboards(); - }, - }, - ); - }, - [dashboards, refetchDashboards, updateDashboard], - ); - const [ UserPreferencesOpen, { close: closeUserPreferences, open: openUserPreferences }, @@ -792,106 +664,8 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { label="Dashboards" href="/dashboards" icon={} - isExpanded={isDashboardsExpanded} - onToggle={() => setIsDashboardExpanded(!isDashboardsExpanded)} /> - {!isCollapsed && ( - -
- - - {isDashboardsLoading ? ( - - ) : ( - !IS_LOCAL_MODE && ( - <> - { - ( - dashboardsResultsRef?.current - ?.firstChild as HTMLAnchorElement - )?.focus?.(); - }} - /> - - - - {dashboards.length === 0 && ( -
- No saved dashboards -
- )} - - {dashboardsListQ && - filteredDashboardsList.length === 0 ? ( -
- No results matching {dashboardsListQ} -
- ) : null} - - ) - )} - - - setDashboardsPresetsCollapsed( - !isDashboardsPresetsCollapsed, - ) - } - /> - - - ClickHouse - - - Services - - {IS_K8S_DASHBOARD_ENABLED && ( - - Kubernetes - - )} - -
-
- )} - {/* Team Settings (Cloud only) */} {!IS_LOCAL_MODE && ( Date: Wed, 25 Feb 2026 19:18:14 +0000 Subject: [PATCH 2/2] feat: enhance DashboardsListPage with list view and dashboard row component Added a new list view for dashboards in DashboardsListPage, allowing users to toggle between grid and list layouts. Introduced DashboardListRow component for displaying individual dashboard details, including name, tags, and a delete option. Updated UI elements for better interaction and organization. --- packages/app/src/DBDashboardPage.tsx | 13 ++- packages/app/src/DashboardsListPage.tsx | 132 ++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index 3f4bb8ae43..ece4c1865c 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -8,6 +8,7 @@ import { } from 'react'; import dynamic from 'next/dynamic'; import Head from 'next/head'; +import Link from 'next/link'; import { useRouter } from 'next/router'; import { formatRelative } from 'date-fns'; import produce from 'immer'; @@ -33,7 +34,9 @@ import { } from '@hyperdx/common-utils/dist/types'; import { ActionIcon, + Anchor, Box, + Breadcrumbs, Button, Flex, Group, @@ -1062,7 +1065,15 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) { )} - + + + Dashboards + + + {dashboard?.name ?? 'Untitled'} + + + void; +}) { + return ( + Router.push(`/dashboards/${dashboard.id}`)} + > + + e.stopPropagation()} + > + {dashboard.name} + + + + + {dashboard.tags.map(tag => ( + + {tag} + + ))} + + + + + {dashboard.tiles.length} + + + + + + e.stopPropagation()} + > + + + + + } + onClick={e => { + e.stopPropagation(); + onDelete(dashboard.id); + }} + > + Delete + + + + + + ); +} + function PresetDashboardCard({ name, href, @@ -151,6 +221,10 @@ export default function DashboardsListPage() { const createDashboard = useCreateDashboard(); const deleteDashboard = useDeleteDashboard(); const [search, setSearch] = useState(''); + const [viewMode, setViewMode] = useLocalStorage({ + key: 'dashboardsViewMode', + defaultValue: 'grid', + }); const presets = useMemo(() => { if (IS_K8S_DASHBOARD_ENABLED) { @@ -228,15 +302,35 @@ export default function DashboardsListPage() { onChange={e => setSearch(e.currentTarget.value)} style={{ flex: 1, maxWidth: 400 }} /> - + + + setViewMode('grid')} + aria-label="Grid view" + > + + + setViewMode('list')} + aria-label="List view" + > + + + + + {!IS_LOCAL_MODE && ( @@ -284,6 +378,26 @@ export default function DashboardsListPage() { )} + ) : viewMode === 'list' ? ( + + + + Name + Tags + Tiles + + + + + {filteredDashboards.map(d => ( + + ))} + +
) : ( {filteredDashboards.map(d => (