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/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 ( + + + + {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 DashboardListRow({ + dashboard, + onDelete, +}: { + dashboard: Dashboard; + onDelete: (id: string) => 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, + 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 [viewMode, setViewMode] = useLocalStorage({ + key: 'dashboardsViewMode', + defaultValue: 'grid', + }); + + 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 }} + /> + + + setViewMode('grid')} + aria-label="Grid view" + > + + + setViewMode('list')} + aria-label="List view" + > + + + + + + + + {!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 && ( + + )} + + + ) : viewMode === 'list' ? ( + + + + Name + Tags + Tiles + + + + + {filteredDashboards.map(d => ( + + ))} + +
+ ) : ( + + {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 && (