Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Duplicate chart #109

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah it finally made its way into the project... @joelseq I think you'll enjoy this 😂

"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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new pattern learned for me today 🤯

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}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'd prefer a tooltip next to the action button but can see how this is faster to implement rn

<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
Loading