diff --git a/.changeset/warm-cougars-talk.md b/.changeset/warm-cougars-talk.md new file mode 100644 index 00000000..aa83e47a --- /dev/null +++ b/.changeset/warm-cougars-talk.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +Confirm leaving Dashboard with unsaved changes diff --git a/packages/app/pages/dashboards/[dashboardId].tsx b/packages/app/pages/dashboards/[dashboardId].tsx index 546c210b..80c6d6df 100644 --- a/packages/app/pages/dashboards/[dashboardId].tsx +++ b/packages/app/pages/dashboards/[dashboardId].tsx @@ -1,2 +1,3 @@ import DashboardPage from '../../src/DashboardPage'; + export default DashboardPage; diff --git a/packages/app/pages/dashboards/presets/[presetName].tsx b/packages/app/pages/dashboards/presets/[presetName].tsx new file mode 100644 index 00000000..51eab7cb --- /dev/null +++ b/packages/app/pages/dashboards/presets/[presetName].tsx @@ -0,0 +1,363 @@ +import { useRouter } from 'next/router'; + +import DashboardPage from '../../../src/DashboardPage'; +import { withAppNav } from '../../../src/layout'; + +const APP_PERFORMANCE_DASHBOARD_CONFIG = { + id: '', + name: 'App Performance', + charts: [ + { + id: '1624425', + name: 'P95 Latency by Operation', + x: 0, + y: 0, + w: 8, + h: 3, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: '', + groupBy: ['span_name'], + }, + ], + }, + { + id: '401924', + name: 'Operations with Errors', + x: 8, + y: 0, + w: 4, + h: 3, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'level:err', + groupBy: ['span_name'], + }, + ], + }, + { + id: '883200', + name: 'Count of Operations', + x: 0, + y: 3, + w: 8, + h: 3, + series: [ + { + type: 'time', + aggFn: 'count', + where: '', + groupBy: ['span_name'], + }, + ], + }, + ], +}; +const HTTP_SERVER_DASHBOARD_CONFIG = { + id: '', + name: 'HTTP Server', + charts: [ + { + id: '312739', + name: 'P95 Latency by Endpoint', + x: 0, + y: 0, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: 'span.kind:server', + groupBy: ['http.route'], + }, + ], + }, + { + id: '434437', + name: 'HTTP Status Codes', + x: 0, + y: 2, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'span.kind:server', + groupBy: ['http.status_code'], + }, + ], + }, + { + id: '69137', + name: 'HTTP 4xx, 5xx', + x: 6, + y: 4, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'http.status_code:>=400 span.kind:server', + groupBy: ['http.status_code'], + }, + ], + }, + { + id: '34708', + name: 'HTTP 5xx by Endpoint', + x: 6, + y: 2, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'span.kind:server http.status_code:>=500', + groupBy: ['http.route'], + }, + ], + }, + { + id: '58773', + name: 'Request Volume by Endpoint', + x: 6, + y: 0, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'span.kind:server', + groupBy: ['http.route'], + }, + ], + }, + ], +}; +const REDIS_DASHBOARD_CONFIG = { + id: '', + name: 'Redis', + charts: [ + { + id: '38463', + name: 'GET Operations', + x: 0, + y: 0, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'db.system:"redis" span_name:GET', + groupBy: [], + }, + ], + }, + { + id: '488836', + name: 'P95 GET Latency', + x: 0, + y: 2, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: 'db.system:"redis" span_name:GET', + groupBy: [], + }, + ], + }, + { + id: '8355753', + name: 'SET Operations', + x: 6, + y: 0, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'count', + where: 'db.system:"redis" span_name:SET', + groupBy: [], + }, + ], + }, + { + id: '93278', + name: 'P95 SET Latency', + x: 6, + y: 2, + w: 6, + h: 2, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: 'db.system:"redis" span_name:SET', + groupBy: [], + }, + ], + }, + ], +}; +const MONGO_DASHBOARD_CONFIG = { + id: '', + name: 'MongoDB', + charts: [ + { + id: '98180', + name: 'P95 Read Operation Latency by Collection', + x: 0, + y: 0, + w: 6, + h: 3, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: + 'db.system:mongo (db.operation:"find" OR db.operation:"findOne" OR db.operation:"aggregate")', + groupBy: ['db.mongodb.collection'], + }, + ], + }, + { + id: '28877', + name: 'P95 Write Operation Latency by Collection', + x: 6, + y: 0, + w: 6, + h: 3, + series: [ + { + type: 'time', + aggFn: 'p95', + field: 'duration', + where: + 'db.system:mongo (db.operation:"insert" OR db.operation:"findOneAndUpdate" OR db.operation:"save" OR db.operation:"findAndModify")', + groupBy: ['db.mongodb.collection'], + }, + ], + }, + { + id: '9901546', + name: 'Count of Write Operations by Collection', + x: 6, + y: 3, + w: 6, + h: 3, + series: [ + { + type: 'time', + aggFn: 'count', + where: + 'db.system:mongo (db.operation:"insert" OR db.operation:"findOneAndUpdate" OR db.operation:"save" OR db.operation:"findAndModify")', + groupBy: ['db.mongodb.collection'], + }, + ], + }, + { + id: '6894669', + name: 'Count of Read Operations by Collection', + x: 0, + y: 3, + w: 6, + h: 3, + series: [ + { + type: 'time', + aggFn: 'count', + where: + 'db.system:mongo (db.operation:"find" OR db.operation:"findOne" OR db.operation:"aggregate")', + groupBy: ['db.mongodb.collection'], + }, + ], + }, + ], +}; +const HYPERDX_USAGE_DASHBOARD_CONFIG = { + id: '', + name: 'HyperDX Usage', + charts: [ + { + id: '15gykg', + name: 'Log/Span Usage in Bytes', + x: 0, + y: 0, + w: 3, + h: 2, + series: [ + { + table: 'logs', + type: 'number', + aggFn: 'sum', + field: 'hyperdx_event_size', + where: '', + groupBy: [], + numberFormat: { + output: 'byte', + }, + }, + ], + }, + { + id: '1k5pul', + name: 'Logs/Span Usage over Time', + x: 3, + y: 0, + w: 9, + h: 3, + series: [ + { + table: 'logs', + type: 'time', + aggFn: 'sum', + field: 'hyperdx_event_size', + where: '', + groupBy: [], + numberFormat: { + output: 'byte', + }, + }, + ], + }, + ], +}; + +const PRESETS: Record = { + 'app-performance': APP_PERFORMANCE_DASHBOARD_CONFIG, + 'http-server': HTTP_SERVER_DASHBOARD_CONFIG, + redis: REDIS_DASHBOARD_CONFIG, + mongo: MONGO_DASHBOARD_CONFIG, + 'hyperdx-usage': HYPERDX_USAGE_DASHBOARD_CONFIG, +}; + +export default function DashboardPresetPage() { + const router = useRouter(); + + const presetName = router.query.presetName as string; + + const presetConfig = PRESETS[presetName] as any; + + return ; +} + +DashboardPresetPage.getLayout = withAppNav; diff --git a/packages/app/src/AppNav.tsx b/packages/app/src/AppNav.tsx index dcff113c..2215d5e5 100644 --- a/packages/app/src/AppNav.tsx +++ b/packages/app/src/AppNav.tsx @@ -47,362 +47,20 @@ import styles from '../styles/AppNav.module.scss'; const UNTAGGED_SEARCHES_GROUP_NAME = 'Saved Searches'; const UNTAGGED_DASHBOARDS_GROUP_NAME = 'Saved Dashboards'; -const APP_PERFORMANCE_DASHBOARD_CONFIG = { - id: '', - name: 'App Performance', - charts: [ - { - id: '1624425', - name: 'P95 Latency by Operation', - x: 0, - y: 0, - w: 8, - h: 3, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: '', - groupBy: ['span_name'], - }, - ], - }, - { - id: '401924', - name: 'Operations with Errors', - x: 8, - y: 0, - w: 4, - h: 3, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'level:err', - groupBy: ['span_name'], - }, - ], - }, - { - id: '883200', - name: 'Count of Operations', - x: 0, - y: 3, - w: 8, - h: 3, - series: [ - { - type: 'time', - aggFn: 'count', - where: '', - groupBy: ['span_name'], - }, - ], - }, - ], -}; -const HTTP_SERVER_DASHBOARD_CONFIG = { - id: '', - name: 'HTTP Server', - charts: [ - { - id: '312739', - name: 'P95 Latency by Endpoint', - x: 0, - y: 0, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: 'span.kind:server', - groupBy: ['http.route'], - }, - ], - }, - { - id: '434437', - name: 'HTTP Status Codes', - x: 0, - y: 2, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'span.kind:server', - groupBy: ['http.status_code'], - }, - ], - }, - { - id: '69137', - name: 'HTTP 4xx, 5xx', - x: 6, - y: 4, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'http.status_code:>=400 span.kind:server', - groupBy: ['http.status_code'], - }, - ], - }, - { - id: '34708', - name: 'HTTP 5xx by Endpoint', - x: 6, - y: 2, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'span.kind:server http.status_code:>=500', - groupBy: ['http.route'], - }, - ], - }, - { - id: '58773', - name: 'Request Volume by Endpoint', - x: 6, - y: 0, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'span.kind:server', - groupBy: ['http.route'], - }, - ], - }, - ], -}; -const REDIS_DASHBOARD_CONFIG = { - id: '', - name: 'Redis', - charts: [ - { - id: '38463', - name: 'GET Operations', - x: 0, - y: 0, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'db.system:"redis" span_name:GET', - groupBy: [], - }, - ], - }, - { - id: '488836', - name: 'P95 GET Latency', - x: 0, - y: 2, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: 'db.system:"redis" span_name:GET', - groupBy: [], - }, - ], - }, - { - id: '8355753', - name: 'SET Operations', - x: 6, - y: 0, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'count', - where: 'db.system:"redis" span_name:SET', - groupBy: [], - }, - ], - }, - { - id: '93278', - name: 'P95 SET Latency', - x: 6, - y: 2, - w: 6, - h: 2, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: 'db.system:"redis" span_name:SET', - groupBy: [], - }, - ], - }, - ], -}; -const MONGO_DASHBOARD_CONFIG = { - id: '', - name: 'MongoDB', - charts: [ - { - id: '98180', - name: 'P95 Read Operation Latency by Collection', - x: 0, - y: 0, - w: 6, - h: 3, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: - 'db.system:mongo (db.operation:"find" OR db.operation:"findOne" OR db.operation:"aggregate")', - groupBy: ['db.mongodb.collection'], - }, - ], - }, - { - id: '28877', - name: 'P95 Write Operation Latency by Collection', - x: 6, - y: 0, - w: 6, - h: 3, - series: [ - { - type: 'time', - aggFn: 'p95', - field: 'duration', - where: - 'db.system:mongo (db.operation:"insert" OR db.operation:"findOneAndUpdate" OR db.operation:"save" OR db.operation:"findAndModify")', - groupBy: ['db.mongodb.collection'], - }, - ], - }, - { - id: '9901546', - name: 'Count of Write Operations by Collection', - x: 6, - y: 3, - w: 6, - h: 3, - series: [ - { - type: 'time', - aggFn: 'count', - where: - 'db.system:mongo (db.operation:"insert" OR db.operation:"findOneAndUpdate" OR db.operation:"save" OR db.operation:"findAndModify")', - groupBy: ['db.mongodb.collection'], - }, - ], - }, - { - id: '6894669', - name: 'Count of Read Operations by Collection', - x: 0, - y: 3, - w: 6, - h: 3, - series: [ - { - type: 'time', - aggFn: 'count', - where: - 'db.system:mongo (db.operation:"find" OR db.operation:"findOne" OR db.operation:"aggregate")', - groupBy: ['db.mongodb.collection'], - }, - ], - }, - ], -}; -const HYPERDX_USAGE_DASHBOARD_CONFIG = { - id: '', - name: 'HyperDX Usage', - charts: [ - { - id: '15gykg', - name: 'Log/Span Usage in Bytes', - x: 0, - y: 0, - w: 3, - h: 2, - series: [ - { - table: 'logs', - type: 'number', - aggFn: 'sum', - field: 'hyperdx_event_size', - where: '', - groupBy: [], - numberFormat: { - output: 'byte', - }, - }, - ], - }, - { - id: '1k5pul', - name: 'Logs/Span Usage over Time', - x: 3, - y: 0, - w: 9, - h: 3, - series: [ - { - table: 'logs', - type: 'time', - aggFn: 'sum', - field: 'hyperdx_event_size', - where: '', - groupBy: [], - numberFormat: { - output: 'byte', - }, - }, - ], - }, - ], -}; - function PresetDashboardLink({ - query, - config, name, + presetName, }: { - query: any; - config: any; name: string; + presetName: string; }) { + const router = useRouter(); return ( {name} @@ -1241,13 +899,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { styles.listLink, pathname.includes('/dashboard') && query.dashboardId == null && - query.config != - JSON.stringify(APP_PERFORMANCE_DASHBOARD_CONFIG) && - query.config != - JSON.stringify(HTTP_SERVER_DASHBOARD_CONFIG) && - query.config != - JSON.stringify(REDIS_DASHBOARD_CONFIG) && - query.config != JSON.stringify(MONGO_DASHBOARD_CONFIG) + !pathname.includes('/presets') ? [styles.listLinkActive] : null, )} @@ -1314,30 +966,19 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { /> - - + + diff --git a/packages/app/src/DashboardPage.tsx b/packages/app/src/DashboardPage.tsx index 5e92e24a..4bb613bb 100644 --- a/packages/app/src/DashboardPage.tsx +++ b/packages/app/src/DashboardPage.tsx @@ -81,6 +81,54 @@ const buildAndWhereClause = (query1: string, query2: string) => { } }; +const useConfirmExit = ({ + hasUnsavedChanges, +}: { + hasUnsavedChanges?: boolean; +}) => { + const router = useRouter(); + + const handleBeforeUnload = useCallback( + (e: BeforeUnloadEvent) => { + if (!hasUnsavedChanges) { + return; + } + e.preventDefault(); + e.returnValue = ''; + }, + [hasUnsavedChanges], + ); + + const handleRouteChangeStart = useCallback( + (route: string) => { + console.log(route, router.asPath); + if (!hasUnsavedChanges || route.startsWith('/dashboards')) { + return; + } + if ( + window.confirm( + 'You have unsaved changes. Are you sure you want to leave?', + ) + ) { + return; + } + router.events.emit('routeChangeError'); + throw 'aborted'; + }, + [hasUnsavedChanges, router.asPath, router.events], + ); + + useEffect(() => { + window.addEventListener('beforeunload', handleBeforeUnload); + router.events.on('routeChangeStart', handleRouteChangeStart); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + router.events.off('routeChangeStart', handleRouteChangeStart); + }; + }, [handleBeforeUnload, handleRouteChangeStart, router.events]); +}; + const Tile = forwardRef( ( { @@ -568,7 +616,12 @@ function DashboardFilter({ // TODO: This is a hack to set the default time range const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date]; -export default function DashboardPage() { + +export default function DashboardPage({ + presetConfig, +}: { + presetConfig?: Dashboard; +}) { const { data: dashboardsData, isLoading: isDashboardsLoading } = api.useDashboards(); const { data: meData } = api.useMe(); @@ -578,7 +631,9 @@ export default function DashboardPage() { const deleteAlert = api.useDeleteAlert(); const updateAlert = api.useUpdateAlert(); const router = useRouter(); + const { dashboardId, config } = router.query; + const queryClient = useQueryClient(); const confirm = useConfirm(); @@ -600,6 +655,9 @@ export default function DashboardPage() { dashboardId != null ? dashboardId : hashCode(`${config}`); const dashboard: Dashboard | undefined = useMemo(() => { + if (presetConfig) { + return presetConfig; + } if (isLocalDashboard) { return localDashboard; } @@ -609,7 +667,13 @@ export default function DashboardPage() { ); return matchedDashboard; } - }, [dashboardsData, dashboardId, isLocalDashboard, localDashboard]); + }, [ + presetConfig, + isLocalDashboard, + dashboardsData, + localDashboard, + dashboardId, + ]); // Update dashboard const [isSavedNow, _setSavedNow] = useState(false); @@ -728,6 +792,13 @@ export default function DashboardPage() { } }, [isLocalDashboard, router, dashboard?.charts.length]); + useConfirmExit({ + hasUnsavedChanges: + isLocalDashboard && + (dashboard?.charts.length || 0) > 0 && + presetConfig == null, + }); + const [highlightedChartId] = useQueryParam('highlightedChartId'); const tiles = useMemo(