From 831d6ac7fb89f2ee7f10f107ff650ae861aeb3bf Mon Sep 17 00:00:00 2001 From: Mike Burgess Date: Thu, 19 Dec 2024 10:45:14 +0000 Subject: [PATCH] Improve copy to clipboard for row view --- .../src/components/CopyToClipboard/index.tsx | 15 ++-- ui/dashboard/src/components/Icon/index.tsx | 2 +- .../dashboards/layout/Dashboard/index.tsx | 76 +++++-------------- .../DashboardSidePanel/TableRowSidePanel.tsx | 69 ++++++++++++----- .../layout/DashboardSidePanel/index.tsx | 42 ++++++---- ui/dashboard/src/constants/icons.ts | 4 - ui/dashboard/src/hooks/useCopyToClipboard.ts | 31 ++++++++ 7 files changed, 132 insertions(+), 107 deletions(-) create mode 100644 ui/dashboard/src/hooks/useCopyToClipboard.ts diff --git a/ui/dashboard/src/components/CopyToClipboard/index.tsx b/ui/dashboard/src/components/CopyToClipboard/index.tsx index 6fd4f956..1ba4904c 100644 --- a/ui/dashboard/src/components/CopyToClipboard/index.tsx +++ b/ui/dashboard/src/components/CopyToClipboard/index.tsx @@ -1,9 +1,5 @@ import copy from "copy-to-clipboard"; import { classNames } from "@powerpipe/utils/styles"; -import { - CopyToClipboardIcon, - CopyToClipboardSuccessIcon, -} from "@powerpipe/constants/icons"; import { createContext, useCallback, @@ -11,6 +7,7 @@ import { useEffect, useState, } from "react"; +import Icon from "@powerpipe/components/Icon"; type ICopyToClipboardContext = { doCopy: boolean; @@ -78,13 +75,17 @@ const CopyToClipboard = ({ return ( <> {!copySuccess && ( - handleCopy(e)} /> )} {copySuccess && ( - + )} ); diff --git a/ui/dashboard/src/components/Icon/index.tsx b/ui/dashboard/src/components/Icon/index.tsx index 3237ea9d..536e84a4 100644 --- a/ui/dashboard/src/components/Icon/index.tsx +++ b/ui/dashboard/src/components/Icon/index.tsx @@ -4,7 +4,7 @@ import useDashboardIcons from "@powerpipe/hooks/useDashboardIcons"; type IconProps = { className?: string; icon: string; - onClick?: () => void; + onClick?: (e: any) => void; style?: any; title?: string; }; diff --git a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx index 30e5ab0a..0b9781c5 100644 --- a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx +++ b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx @@ -1,12 +1,10 @@ import Children from "../Children"; import DashboardProgress from "./DashboardProgress"; -import DashboardSidePanel from "@powerpipe/components/dashboards/layout/DashboardSidePanel"; +import DashboardSidePanel from "../DashboardSidePanel"; import DashboardTitle from "@powerpipe/components/dashboards/titles/DashboardTitle"; import Grid from "../Grid"; import PanelDetail from "../PanelDetail"; -import React, { ReactNode } from "react"; import SnapshotRenderComplete from "@powerpipe/components/snapshot/SnapshotRenderComplete"; -import SplitPane from "react-split-pane"; import usePageTitle from "@powerpipe/hooks/usePageTitle"; import { DashboardControlsProvider } from "./DashboardControlsProvider"; import { @@ -15,10 +13,7 @@ import { DashboardDefinition, } from "@powerpipe/types"; import { registerComponent } from "@powerpipe/components/dashboards"; -import { - SidePanelInfo, - useDashboardPanelDetail, -} from "@powerpipe/hooks/useDashboardPanelDetail"; +import { useDashboardPanelDetail } from "@powerpipe/hooks/useDashboardPanelDetail"; import { useDashboardSearch } from "@powerpipe/hooks/useDashboardSearch"; import { useDashboardState } from "@powerpipe/hooks/useDashboardState"; @@ -33,36 +28,6 @@ type DashboardWrapperProps = { showPanelControls?: boolean; }; -const SplitPaneWrapper = ({ - dashboard, - sidePanel, -}: { - dashboard: ReactNode; - sidePanel: SidePanelInfo | null; -}) => { - if (!sidePanel) { - return dashboard; - } - - return ( -
- - {dashboard} - - -
- ); -}; - const Dashboard = ({ definition, isRoot = true, @@ -85,29 +50,24 @@ const Dashboard = ({ /> ); - const renderDashboard = ( - <> - {isRoot ? ( -
- - {dataMode === DashboardDataModeCLISnapshot && ( -
- -
- )} -
{grid}
-
- ) : ( -
{grid}
- )} - - ); return ( - + {dataMode === DashboardDataModeCLISnapshot && ( +
+ +
+ )} +
+ {isRoot ? ( +
+ +
{grid}
+
+ ) : ( +
{grid}
+ )} + +
); }; diff --git a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx index 9d5ef64e..b5a1b326 100644 --- a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx +++ b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx @@ -1,5 +1,8 @@ import CodeBlock from "@powerpipe/components/CodeBlock"; -import CopyToClipboard from "@powerpipe/components/CopyToClipboard"; +import Icon from "@powerpipe/components/Icon"; +import SearchInput from "@powerpipe/components/SearchInput"; +import useCopyToClipboard from "@powerpipe/hooks/useCopyToClipboard"; +import { classNames } from "@powerpipe/utils/styles"; import { LeafNodeData, LeafNodeDataColumn, @@ -7,8 +10,6 @@ import { import { parseDate } from "@powerpipe/utils/date"; import { useDashboardPanelDetail } from "@powerpipe/hooks/useDashboardPanelDetail"; import { useEffect, useMemo, useState } from "react"; -import SearchInput from "@powerpipe/components/SearchInput"; -import Icon from "@powerpipe/components/Icon"; const getNumericValue = (value) => { if ( @@ -46,17 +47,25 @@ const renderValue = (name: string, dataType: string, value: any) => { switch (dataType.toLowerCase()) { case "text": case "varchar": - return {value}; + return ( + + {value} + + ); case "timestamptz": return ( - + {parseDate(value)?.format() || ""} ); case "jsonb": case "varchar[]": return ( - + {JSON.stringify(value, null, 2)} ); @@ -64,38 +73,56 @@ const renderValue = (name: string, dataType: string, value: any) => { case "bigint": { if (name === "timestamp") { return ( - + {parseDate(value)?.format() || ""} ); } - return {getNumericValue(value)}; + return ( + + {getNumericValue(value)} + + ); } default: return ( - {JSON.stringify(value, null, 2)} + + {JSON.stringify(value, null, 2)} + ); } }; const TableRowItem = ({ dataType, name, value }) => { - const [showOptions, setShowOptions] = useState(false); + const [showCopy, setShowCopy] = useState(false); + const { copy, copySuccess } = useCopyToClipboard(); return (
setShowOptions(true)} - onMouseLeave={() => setShowOptions(false)} + className={classNames( + "p-4 space-y-1", + !copySuccess ? "cursor-pointer" : null, + )} + onClick={ + !copySuccess ? () => copy(JSON.stringify(value, null, 2)) : undefined + } + onMouseEnter={() => setShowCopy(true)} + onMouseLeave={() => setShowCopy(false)} > -
+
{name} - {showOptions && ( - <> - - + {showCopy && ( + )}
@@ -183,8 +210,8 @@ const TableRowSidePanel = ({ }, [requestedColumnName]); return ( - <> -
+
+

Row

- +
); }; diff --git a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx index f603c7df..9f5f3578 100644 --- a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx +++ b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx @@ -2,21 +2,31 @@ import FilterAndGroupSidePanel from "@powerpipe/components/dashboards/layout/Das import TableRowSidePanel from "@powerpipe/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel"; import { SidePanelInfo } from "@powerpipe/hooks/useDashboardPanelDetail"; -const DashboardSidePanel = ({ sidePanel }: { sidePanel: SidePanelInfo }) => ( -
- {(sidePanel.panel.panel_type === "benchmark" || - sidePanel.panel.panel_type === "control" || - sidePanel.panel.panel_type === "detection") && ( - - )} - {sidePanel.panel.panel_type === "table" && ( - - )} -
-); +const DashboardSidePanel = ({ + sidePanel, +}: { + sidePanel: SidePanelInfo | null; +}) => { + if (!sidePanel) { + return null; + } + + return ( +
+ {(sidePanel.panel.panel_type === "benchmark" || + sidePanel.panel.panel_type === "control" || + sidePanel.panel.panel_type === "detection") && ( + + )} + {sidePanel.panel.panel_type === "table" && ( + + )} +
+ ); +}; export default DashboardSidePanel; diff --git a/ui/dashboard/src/constants/icons.ts b/ui/dashboard/src/constants/icons.ts index 7d62d4c2..bee922d2 100644 --- a/ui/dashboard/src/constants/icons.ts +++ b/ui/dashboard/src/constants/icons.ts @@ -4,7 +4,6 @@ import { BackwardIcon as BackwardIconOutline, ChevronDownIcon as ChevronDownIconOutline, ChevronUpIcon as ChevronUpIconOutline, - ClipboardDocumentListIcon as ClipboardDocumentListIconOutline, MagnifyingGlassIcon as MagnifyingGlassIconOutline, MinusIcon as MinusIconOutline, PlusIcon as PlusIconOutline, @@ -16,7 +15,6 @@ import { CheckCircleIcon as CheckCircleIconSolid, ChevronDownIcon as ChevronDownIconSolid, ChevronUpIcon as ChevronUpIconSolid, - ClipboardDocumentCheckIcon as ClipboardDocumentCheckIconSolid, ExclamationCircleIcon as ExclamationCircleIconSolid, InformationCircleIcon as InformationCircleIconSolid, NoSymbolIcon as NoSymbolIconSolid, @@ -26,8 +24,6 @@ import { // General export const ClearIcon = XMarkIconOutline; export const CloseIcon = XMarkIconOutline; -export const CopyToClipboardIcon = ClipboardDocumentListIconOutline; -export const CopyToClipboardSuccessIcon = ClipboardDocumentCheckIconSolid; export const ErrorIcon = ExclamationCircleIconSolid; export const SearchIcon = MagnifyingGlassIconOutline; export const SubmitIcon = ArrowDownOnSquareIconOutline; diff --git a/ui/dashboard/src/hooks/useCopyToClipboard.ts b/ui/dashboard/src/hooks/useCopyToClipboard.ts new file mode 100644 index 00000000..536eecb3 --- /dev/null +++ b/ui/dashboard/src/hooks/useCopyToClipboard.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useState } from "react"; +import * as copy from "copy-to-clipboard"; + +const useCopyToClipboard = () => { + const [copySuccess, setCopySuccess] = useState(false); + + const handleCopy = useCallback( + (data) => { + // @ts-ignore + const copyOutput = copy(data); + if (copyOutput) { + setCopySuccess(true); + } + }, + [setCopySuccess], + ); + + useEffect(() => { + let timeoutId; + if (copySuccess) { + timeoutId = setTimeout(() => { + setCopySuccess(false); + }, 1000); + } + return () => clearTimeout(timeoutId); + }, [copySuccess]); + + return { copy: handleCopy, copySuccess }; +}; + +export default useCopyToClipboard;