Skip to content

Commit

Permalink
Improve copy to clipboard for row view
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelBurgess committed Dec 19, 2024
1 parent 97aff70 commit 831d6ac
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 107 deletions.
15 changes: 8 additions & 7 deletions ui/dashboard/src/components/CopyToClipboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import copy from "copy-to-clipboard";
import { classNames } from "@powerpipe/utils/styles";
import {
CopyToClipboardIcon,
CopyToClipboardSuccessIcon,
} from "@powerpipe/constants/icons";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import Icon from "@powerpipe/components/Icon";

type ICopyToClipboardContext = {
doCopy: boolean;
Expand Down Expand Up @@ -78,13 +75,17 @@ const CopyToClipboard = ({
return (
<>
{!copySuccess && (
<CopyToClipboardIcon
<Icon
icon="content_copy"
className={classNames("h-5 w-5 cursor-pointer", className)}
onClick={handleCopy}
onClick={(e) => handleCopy(e)}
/>
)}
{copySuccess && (
<CopyToClipboardSuccessIcon className="h-5 w-5 text-ok" />
<Icon
icon="materialsymbols-solid:content_copy"
className={classNames("h-5 w-5 text-ok", className)}
/>
)}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion ui/dashboard/src/components/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
76 changes: 18 additions & 58 deletions ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";

Expand All @@ -33,36 +28,6 @@ type DashboardWrapperProps = {
showPanelControls?: boolean;
};

const SplitPaneWrapper = ({
dashboard,
sidePanel,
}: {
dashboard: ReactNode;
sidePanel: SidePanelInfo | null;
}) => {
if (!sidePanel) {
return dashboard;
}

return (
<div className="relative h-full">
<SplitPane
className="flex flex-col-reverse md:flex-row w-full h-full overflow-y-hidden"
split="vertical"
// defaultSize={sidePanel?.panel?.panel_type === "table" ? "75%" : "60%"}
minSize={sidePanel?.panel?.panel_type === "table" ? 300 : 500}
primary="second"
>
{dashboard}
<DashboardSidePanel
key={sidePanel?.panel?.panel_type}
sidePanel={sidePanel}
/>
</SplitPane>
</div>
);
};

const Dashboard = ({
definition,
isRoot = true,
Expand All @@ -85,29 +50,24 @@ const Dashboard = ({
/>
</Grid>
);
const renderDashboard = (
<>
{isRoot ? (
<div className="flex flex-col flex-1 h-full overflow-y-hidden">
<DashboardProgress />
{dataMode === DashboardDataModeCLISnapshot && (
<div className="p-4">
<SnapshotHeader />
</div>
)}
<div className="h-full w-full overflow-y-auto p-4">{grid}</div>
</div>
) : (
<div className="w-full">{grid}</div>
)}
</>
);
return (
<DashboardControlsProvider>
<SplitPaneWrapper
dashboard={renderDashboard}
sidePanel={selectedSidePanel}
/>
{dataMode === DashboardDataModeCLISnapshot && (
<div className="p-4">
<SnapshotHeader />
</div>
)}
<div className="flex flex-col-reverse md:flex-row w-full h-full overflow-y-hidden">
{isRoot ? (
<div className="flex flex-col flex-1 h-full overflow-y-hidden">
<DashboardProgress />
<div className="h-full w-full overflow-y-auto p-4">{grid}</div>
</div>
) : (
<div className="w-full">{grid}</div>
)}
<DashboardSidePanel sidePanel={selectedSidePanel} />
</div>
</DashboardControlsProvider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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,
} from "@powerpipe/components/dashboards/common";
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 (
Expand Down Expand Up @@ -46,56 +47,82 @@ const renderValue = (name: string, dataType: string, value: any) => {
switch (dataType.toLowerCase()) {
case "text":
case "varchar":
return <CodeBlock language="yaml">{value}</CodeBlock>;
return (
<CodeBlock copyToClipboard={false} language="yaml">
{value}
</CodeBlock>
);
case "timestamptz":
return (
<CodeBlock language="yaml">
<CodeBlock copyToClipboard={false} language="yaml">
{parseDate(value)?.format() || ""}
</CodeBlock>
);
case "jsonb":
case "varchar[]":
return (
<CodeBlock language="json" style={{ fontSize: "12px" }}>
<CodeBlock
copyToClipboard={false}
language="json"
style={{ fontSize: "12px" }}
>
{JSON.stringify(value, null, 2)}
</CodeBlock>
);
case "numeric":
case "bigint": {
if (name === "timestamp") {
return (
<CodeBlock language="yaml">
<CodeBlock copyToClipboard={false} language="yaml">
{parseDate(value)?.format() || ""}
</CodeBlock>
);
}
return <CodeBlock language="json">{getNumericValue(value)}</CodeBlock>;
return (
<CodeBlock copyToClipboard={false} language="json">
{getNumericValue(value)}
</CodeBlock>
);
}
default:
return (
<CodeBlock language="json">{JSON.stringify(value, null, 2)}</CodeBlock>
<CodeBlock copyToClipboard={false} language="json">
{JSON.stringify(value, null, 2)}
</CodeBlock>
);
}
};

const TableRowItem = ({ dataType, name, value }) => {
const [showOptions, setShowOptions] = useState(false);
const [showCopy, setShowCopy] = useState(false);
const { copy, copySuccess } = useCopyToClipboard();
return (
<div
key={name}
id={name}
className="p-4 space-y-1"
onMouseEnter={() => 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)}
>
<div className="flex icon-spacer items-center text-sm">
<div className="flex space-x-1 items-center">
<span className="block font-light tracking-wider text-table-head">
{name}
</span>
{showOptions && (
<>
<CopyToClipboard data={JSON.stringify(value, null, 2)} />
</>
{showCopy && (
<Icon
icon={
copySuccess
? "materialsymbols-solid:content_copy"
: "content_copy"
}
className={classNames("h-5 w-5", copySuccess ? "text-ok" : null)}
/>
)}
</div>
<div>
Expand Down Expand Up @@ -183,8 +210,8 @@ const TableRowSidePanel = ({
}, [requestedColumnName]);

return (
<>
<div className="flex items-center justify-between p-4 min-w-[300px]">
<div className="min-w-[300px] max-w-[400px]">
<div className="flex items-center justify-between p-4">
<h3>Row</h3>
<Icon
className="w-5 h-5 text-foreground cursor-pointer hover:text-foreground-light shrink-0"
Expand Down Expand Up @@ -212,7 +239,7 @@ const TableRowSidePanel = ({
))}
</div>
</div>
</>
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<div className="h-full w-full bg-dashboard-panel divide-y divide-divide print:hidden overflow-y-auto">
{(sidePanel.panel.panel_type === "benchmark" ||
sidePanel.panel.panel_type === "control" ||
sidePanel.panel.panel_type === "detection") && (
<FilterAndGroupSidePanel panelName={sidePanel.panel.name} />
)}
{sidePanel.panel.panel_type === "table" && (
<TableRowSidePanel
data={sidePanel.panel.data}
requestedColumnName={sidePanel.context.requestedColumnName}
rowIndex={sidePanel.context.rowIndex}
/>
)}
</div>
);
const DashboardSidePanel = ({
sidePanel,
}: {
sidePanel: SidePanelInfo | null;
}) => {
if (!sidePanel) {
return null;
}

return (
<div className="h-full bg-dashboard-panel divide-y divide-divide print:hidden overflow-y-auto">
{(sidePanel.panel.panel_type === "benchmark" ||
sidePanel.panel.panel_type === "control" ||
sidePanel.panel.panel_type === "detection") && (
<FilterAndGroupSidePanel panelName={sidePanel.panel.name} />
)}
{sidePanel.panel.panel_type === "table" && (
<TableRowSidePanel
data={sidePanel.panel.data}
requestedColumnName={sidePanel.context.requestedColumnName}
rowIndex={sidePanel.context.rowIndex}
/>
)}
</div>
);
};

export default DashboardSidePanel;
4 changes: 0 additions & 4 deletions ui/dashboard/src/constants/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions ui/dashboard/src/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 831d6ac

Please sign in to comment.