mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Duplicate chart (#109)
* 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
This commit is contained in:
parent
283f32ac9c
commit
fe41b150de
6 changed files with 130 additions and 7 deletions
5
.changeset/chilled-icons-lay.md
Normal file
5
.changeset/chilled-icons-lay.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': minor
|
||||
---
|
||||
|
||||
feat: Add dashboard delete confirmations and duplicate chart button
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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')
|
||||
|
|
@ -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" />
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
import HDXNumberChart from './HDXNumberChart';
|
||||
import GranularityPicker from './GranularityPicker';
|
||||
import HDXTableChart from './HDXTableChart';
|
||||
import { useConfirm } from './useConfirm';
|
||||
|
||||
import type { Chart } from './EditChartForm';
|
||||
|
||||
|
|
@ -54,6 +55,8 @@ 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 = {
|
||||
|
|
@ -81,6 +84,7 @@ const Tile = forwardRef(
|
|||
{
|
||||
chart,
|
||||
dateRange,
|
||||
onDuplicateClick,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
query,
|
||||
|
|
@ -99,6 +103,7 @@ const Tile = forwardRef(
|
|||
}: {
|
||||
chart: Chart;
|
||||
dateRange: [Date, Date];
|
||||
onDuplicateClick: () => void;
|
||||
onEditClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
query: string;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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, {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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),
|
||||
|
|
@ -713,10 +752,11 @@ export default function DashboardPage() {
|
|||
}),
|
||||
[
|
||||
dashboard,
|
||||
searchedTimeRange,
|
||||
setDashboard,
|
||||
dashboardQuery,
|
||||
searchedTimeRange,
|
||||
granularityQuery,
|
||||
confirm,
|
||||
setDashboard,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -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}`,
|
||||
|
|
|
|||
63
packages/app/src/useConfirm.tsx
Normal file
63
packages/app/src/useConfirm.tsx
Normal file
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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==
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue