feat: Confirm leaving Dashboard with unsaved changes (#444)

This commit is contained in:
Ernest Iliiasov 2024-06-26 12:21:25 -04:00 committed by GitHub
parent 1751b2e16d
commit 47b758a5f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 453 additions and 372 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Confirm leaving Dashboard with unsaved changes

View file

@ -1,2 +1,3 @@
import DashboardPage from '../../src/DashboardPage';
export default DashboardPage;

View file

@ -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<string, any> = {
'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 <DashboardPage presetConfig={presetConfig} />;
}
DashboardPresetPage.getLayout = withAppNav;

View file

@ -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 (
<Link
href={`/dashboards?config=${encodeURIComponent(JSON.stringify(config))}`}
href={`/dashboards/presets/${presetName}`}
tabIndex={0}
className={cx(styles.listLink, {
[styles.listLinkActive]:
query.config === JSON.stringify(config) && query.dashboardId == null,
'text-muted-hover': query.config !== JSON.stringify(config),
[styles.listLinkActive]: router.query.presetName === presetName,
})}
>
{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 }) {
/>
<Collapse in={!isDashboardsPresetsCollapsed}>
<PresetDashboardLink
query={query}
config={HYPERDX_USAGE_DASHBOARD_CONFIG}
presetName={'hyperdx-usage'}
name="HyperDX Usage"
/>
<PresetDashboardLink
query={query}
config={APP_PERFORMANCE_DASHBOARD_CONFIG}
presetName={'app-performance'}
name="App Performance"
/>
<PresetDashboardLink
query={query}
config={HTTP_SERVER_DASHBOARD_CONFIG}
presetName={'http-server'}
name="HTTP Server"
/>
<PresetDashboardLink
query={query}
config={REDIS_DASHBOARD_CONFIG}
name="Redis"
/>
<PresetDashboardLink
query={query}
config={MONGO_DASHBOARD_CONFIG}
name="Mongo"
/>
<PresetDashboardLink presetName={'redis'} name="Redis" />
<PresetDashboardLink presetName={'mongo'} name="Mongo" />
</Collapse>
</div>
</Collapse>

View file

@ -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(