Skip to content

Commit

Permalink
feat: Duplicate chart (#109)
Browse files Browse the repository at this point in the history
* Add duplicate chart button
* Add confirm modal for delete chart and delete dashboard actions

`docker compose -f docker-compose.dev.yml up -d --no-deps --build app` after pulling this branch

https://github.com/hyperdxio/hyperdx/assets/149748269/68f8facc-9d8f-4ebf-9dc2-937d65dbc89f
  • Loading branch information
svc-shorpo committed Nov 17, 2023
1 parent 283f32a commit fe41b15
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-icons-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': minor
---

feat: Add dashboard delete confirmations and duplicate chart button
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"esbuild": "^0.14.47",
"fuse.js": "^6.6.2",
"immer": "^9.0.21",
"jotai": "^2.5.1",
"ky": "^0.30.0",
"ky-universal": "^0.10.1",
"lodash": "^4.17.21",
Expand Down
6 changes: 5 additions & 1 deletion packages/app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ReactQueryDevtools } from 'react-query/devtools';
import { ToastContainer } from 'react-toastify';
import { NextAdapter } from 'next-query-params';
import { QueryParamProvider } from 'use-query-params';
import { useConfirmModal } from '../src/useConfirm';

import * as config from '../src/config';
import { QueryParamProvider as HDXQueryParamProvider } from '../src/useQueryParam';
Expand All @@ -23,6 +24,8 @@ const queryClient = new QueryClient();
import HyperDX from '@hyperdx/browser';

export default function MyApp({ Component, pageProps }: AppProps) {
const confirmModal = useConfirmModal();

// port to react query ? (needs to wrap with QueryClientProvider)
useEffect(() => {
fetch('/api/config')
Expand Down Expand Up @@ -58,7 +61,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
<Head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
/>
<link rel="icon" type="image/png" sizes="32x32" href="/Icon32.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
Expand All @@ -85,6 +88,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
<ToastContainer position="bottom-right" theme="dark" />
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
{confirmModal}
</UserPreferencesProvider>
</QueryClientProvider>
</QueryParamProvider>
Expand Down
55 changes: 50 additions & 5 deletions packages/app/src/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ import {
import HDXNumberChart from './HDXNumberChart';
import GranularityPicker from './GranularityPicker';
import HDXTableChart from './HDXTableChart';
import { useConfirm } from './useConfirm';

import type { Chart } from './EditChartForm';

import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { ZIndexContext } from './zIndex';

const makeId = () => Math.floor(100000000 * Math.random()).toString(36);

const ReactGridLayout = WidthProvider(RGL);

type Dashboard = {
Expand Down Expand Up @@ -81,6 +84,7 @@ const Tile = forwardRef(
{
chart,
dateRange,
onDuplicateClick,
onEditClick,
onDeleteClick,
query,
Expand All @@ -99,6 +103,7 @@ const Tile = forwardRef(
}: {
chart: Chart;
dateRange: [Date, Date];
onDuplicateClick: () => void;
onEditClick: () => void;
onDeleteClick: () => void;
query: string;
Expand Down Expand Up @@ -203,11 +208,21 @@ const Tile = forwardRef(
<span className="bi bi-bell" />
</div>
)}
<Button
variant="link"
className="text-muted-hover p-0"
size="sm"
onClick={onDuplicateClick}
title="Duplicate"
>
<i className="bi bi-copy fs-8"></i>
</Button>
<Button
variant="link"
className="text-muted-hover p-0"
size="sm"
onClick={onEditClick}
title="Edit"
>
<i className="bi bi-pencil"></i>
</Button>
Expand All @@ -216,6 +231,7 @@ const Tile = forwardRef(
className="text-muted-hover p-0"
size="sm"
onClick={onDeleteClick}
title="Edit"
>
<i className="bi bi-trash"></i>
</Button>
Expand Down Expand Up @@ -566,6 +582,8 @@ export default function DashboardPage() {
const { dashboardId, config } = router.query;
const queryClient = useQueryClient();

const confirm = useConfirm();

const [localDashboard, setLocalDashboard] = useQueryParam<Dashboard>(
'config',
withDefault(JsonParam, {
Expand Down Expand Up @@ -662,7 +680,7 @@ export default function DashboardPage() {

const onAddChart = () => {
setEditedChart({
id: Math.floor(100000000 * Math.random()).toString(36),
id: makeId(),
name: 'My New Chart',
x: 0,
y: 0,
Expand Down Expand Up @@ -700,8 +718,29 @@ export default function DashboardPage() {
onEditClick={() => setEditedChart(chart)}
granularity={granularityQuery}
hasAlert={dashboard?.alerts?.some(a => a.chartId === chart.id)}
onDeleteClick={() => {
onDuplicateClick={async () => {
if (dashboard != null) {
if (!(await confirm(`Duplicate ${chart.name}?`, 'Duplicate'))) {
return;
}
setDashboard({
...dashboard,
charts: [
...dashboard.charts,
{
...chart,
id: makeId(),
name: `${chart.name} (Copy)`,
},
],
});
}
}}
onDeleteClick={async () => {
if (dashboard != null) {
if (!(await confirm(`Delete ${chart.name}?`, 'Delete'))) {
return;
}
setDashboard({
...dashboard,
charts: dashboard.charts.filter(c => c.id !== chart.id),
Expand All @@ -713,10 +752,11 @@ export default function DashboardPage() {
}),
[
dashboard,
searchedTimeRange,
setDashboard,
dashboardQuery,
searchedTimeRange,
granularityQuery,
confirm,
setDashboard,
],
);

Expand Down Expand Up @@ -920,7 +960,12 @@ export default function DashboardPage() {
variant="dark"
className="text-muted-hover text-nowrap"
size="sm"
onClick={() => {
onClick={async () => {
if (
!(await confirm(`Delete ${dashboard?.name}?`, 'Delete'))
) {
return;
}
deleteDashboard.mutate(
{
id: `${dashboardId}`,
Expand Down
63 changes: 63 additions & 0 deletions packages/app/src/useConfirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';

type ConfirmAtom = {
message: string;
confirmLabel?: string;
onConfirm: () => void;
onClose?: () => void;
} | null;

const confirmAtom = atom<ConfirmAtom>(null);

export const useConfirm = () => {
const setConfirm = useSetAtom(confirmAtom);

return React.useCallback(
async (message: string, confirmLabel?: string): Promise<boolean> => {
return new Promise(resolve => {
setConfirm({
message,
confirmLabel,
onConfirm: () => {
resolve(true);
setConfirm(null);
},
onClose: () => {
resolve(false);
setConfirm(null);
},
});
});
},
[setConfirm],
);
};

export const useConfirmModal = () => {
const confirm = useAtomValue(confirmAtom);
const setConfirm = useSetAtom(confirmAtom);

const handleClose = React.useCallback(() => {
confirm?.onClose?.();
setConfirm(null);
}, [confirm, setConfirm]);

return confirm ? (
<Modal show onHide={handleClose}>
<Modal.Body className="bg-hdx-dark">
{confirm.message}
<div className="mt-3 d-flex justify-content-end gap-2">
<Button variant="secondary" onClick={handleClose} size="sm">
Cancel
</Button>
<Button variant="success" onClick={confirm.onConfirm} size="sm">
{confirm.confirmLabel || 'OK'}
</Button>
</div>
</Modal.Body>
</Modal>
) : null;
};
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4655,7 +4655,7 @@
dependencies:
"@types/react" "*"

"@types/react@*", "@types/react@>=16", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.52":
"@types/react@*", "@types/react@17.0.52", "@types/react@>=16", "@types/react@>=16.9.11", "@types/react@^17", "@types/react@^17.0.52":
version "17.0.52"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b"
integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==
Expand Down Expand Up @@ -10218,6 +10218,11 @@ joi@^17.3.0:
"@sideway/formula" "^3.0.1"
"@sideway/pinpoint" "^2.0.0"

jotai@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.5.1.tgz#eed05a32a4ac1264c531a77e86478f7ad3197ca3"
integrity sha512-vanPCCSuHczUXNbVh/iUunuMfrWRL4FdBtAbTRmrfqezJcKb8ybBTg8iivyYuUHapjcDETyJe1E4inlo26bVHA==

js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
Expand Down

0 comments on commit fe41b15

Please sign in to comment.