From e4e01011353972c140470e29f0031862d87f0cd7 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Fri, 14 Jun 2024 14:48:04 -0400 Subject: [PATCH] feat(graph): add copy button for entire target configuration --- .../ui-code-block/src/lib/json-code-block.tsx | 47 +++-------- graph/ui-components/src/index.ts | 1 + .../lib/copy-to-clipboard-button.stories.tsx | 20 +++++ .../src/lib/copy-to-clipboard-button.tsx | 61 ++++++++++++++ .../copy-to-clipboard.stories.tsx | 17 ---- .../copy-to-clipboard/copy-to-clipboard.tsx | 36 --------- .../lib/project-details/project-details.tsx | 81 ++++++++++++++++--- .../target-configuration-details-header.tsx | 19 ++--- .../target-configuration-details.tsx | 60 +++++--------- .../target-executor/target-executor-title.tsx | 28 ++++--- package.json | 2 +- pnpm-lock.yaml | 2 +- 12 files changed, 212 insertions(+), 162 deletions(-) create mode 100644 graph/ui-components/src/lib/copy-to-clipboard-button.stories.tsx create mode 100644 graph/ui-components/src/lib/copy-to-clipboard-button.tsx delete mode 100644 graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx delete mode 100644 graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx diff --git a/graph/ui-code-block/src/lib/json-code-block.tsx b/graph/ui-code-block/src/lib/json-code-block.tsx index 1142025c50157..4f7c7c5077c98 100644 --- a/graph/ui-code-block/src/lib/json-code-block.tsx +++ b/graph/ui-code-block/src/lib/json-code-block.tsx @@ -1,13 +1,8 @@ -import { - ClipboardDocumentCheckIcon, - ClipboardDocumentIcon, -} from '@heroicons/react/24/outline'; -// @ts-ignore -import { CopyToClipboard } from 'react-copy-to-clipboard'; // @ts-ignore import SyntaxHighlighter, { createElement } from 'react-syntax-highlighter'; -import { JSX, ReactNode, useEffect, useMemo, useState } from 'react'; +import { JSX, ReactNode, useMemo } from 'react'; import { twMerge } from 'tailwind-merge'; +import { CopyToClipboardButton } from '@nx/graph/ui-components'; export function JsonCodeBlockPreTag({ children, @@ -30,45 +25,27 @@ export function JsonCodeBlockPreTag({ export interface JsonCodeBlockProps { data: any; renderSource: (propertyName: string) => ReactNode; + copyTooltipText: string; } export function JsonCodeBlock(props: JsonCodeBlockProps): JSX.Element { - const [copied, setCopied] = useState(false); const jsonString = useMemo( () => JSON.stringify(props.data, null, 2), [props.data] ); - useEffect(() => { - if (!copied) return; - const t = setTimeout(() => { - setCopied(false); - }, 3000); - return () => clearTimeout(t); - }, [copied]); return (
- { - setCopied(true); - }} - > - - + tooltipAlignment="right" + tooltipText={props.copyTooltipText} + className={twMerge( + 'not-prose flex', + 'border border-slate-200 bg-slate-50/50 p-2 dark:border-slate-700 dark:bg-slate-800/60', + 'opacity-0 transition-opacity group-hover:opacity-100' + )} + />
= { + component: CopyToClipboardButton, + title: 'CopyToClipboardButton', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + text: 'Hello, world!', + tooltipAlignment: 'left', + } as CopyToClipboardButtonProps, +}; diff --git a/graph/ui-components/src/lib/copy-to-clipboard-button.tsx b/graph/ui-components/src/lib/copy-to-clipboard-button.tsx new file mode 100644 index 0000000000000..0f7b6675084b0 --- /dev/null +++ b/graph/ui-components/src/lib/copy-to-clipboard-button.tsx @@ -0,0 +1,61 @@ +// @ts-ignore +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { JSX, ReactNode, useEffect, useState } from 'react'; +import { + ClipboardDocumentCheckIcon, + ClipboardDocumentIcon, +} from '@heroicons/react/24/outline'; + +export interface CopyToClipboardButtonProps { + text: string; + tooltipText?: string; + tooltipAlignment?: 'left' | 'right'; + className?: string; + children?: ReactNode; +} + +export function CopyToClipboardButton({ + text, + tooltipAlignment, + tooltipText, + className, + children, +}: CopyToClipboardButtonProps) { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const t = setTimeout(() => { + setCopied(false); + }, 3000); + return () => clearTimeout(t); + }, [copied]); + + return ( + { + setCopied(true); + }} + > + + + ); +} diff --git a/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx b/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx deleted file mode 100644 index 8101be70cfbea..0000000000000 --- a/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { CopyToClipboard } from './copy-to-clipboard'; - -const meta: Meta = { - component: CopyToClipboard, - title: 'CopyToClipboard', -}; -export default meta; - -type Story = StoryObj; - -export const Simple: Story = { - args: { - onCopy: () => {}, - tooltipAlignment: 'left', - }, -}; diff --git a/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx b/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx deleted file mode 100644 index e7cbfdd7ffc38..0000000000000 --- a/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ClipboardIcon } from '@heroicons/react/24/outline'; -import { JSX, useEffect, useState } from 'react'; - -interface CopyToClipboardProps { - onCopy: () => void; - tooltipAlignment?: 'left' | 'right'; -} - -export function CopyToClipboard(props: CopyToClipboardProps): JSX.Element { - const [copied, setCopied] = useState(false); - useEffect(() => { - if (copied) { - const timeout = setTimeout(() => { - setCopied(false); - }, 3000); - return () => clearTimeout(timeout); - } - }); - return ( - - { - e.stopPropagation(); - setCopied(true); - props.onCopy(); - }} - > - - ); -} diff --git a/graph/ui-project-details/src/lib/project-details/project-details.tsx b/graph/ui-project-details/src/lib/project-details/project-details.tsx index 39b9508c6c6c6..7d9673b70621a 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -2,12 +2,13 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import type { ProjectGraphProjectNode } from '@nx/devkit'; +import type { ProjectConfiguration, ProjectGraphProjectNode } from '@nx/devkit'; // nx-ignore-next-line import { GraphError } from 'nx/src/command-line/graph/graph'; /* eslint-enable @nx/enforce-module-boundaries */ import { EyeIcon } from '@heroicons/react/24/outline'; import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; +import { CopyToClipboardButton } from '@nx/graph/ui-components'; import { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text'; import { twMerge } from 'tailwind-merge'; import { Pill } from '../pill'; @@ -36,6 +37,18 @@ const typeToProjectType = { e2e: 'E2E', }; +type ProjectConfigurationToCopy = Pick< + ProjectConfiguration, + | 'name' + | 'root' + | 'tags' + | 'targets' + | 'sourceRoot' + | 'implicitDependencies' + | 'release' + | 'projectType' +>; + export const ProjectDetails = ({ project, sourceMap, @@ -48,6 +61,22 @@ export const ProjectDetails = ({ connectedToCloud, }: ProjectDetailsProps) => { const projectData = project.data; + const projectDataToCopy: ProjectConfigurationToCopy = pick( + [ + 'name', + 'root', + 'tags', + 'targets', + 'sourceRoot', + 'implicitDependencies', + 'release', + 'projectType', + ], + projectData + ); + projectDataToCopy.tags = projectDataToCopy?.tags?.filter( + (tag) => !tag.startsWith('npm:') + ); const isCompact = variant === 'compact'; const technologies = [ @@ -71,7 +100,7 @@ export const ProjectDetails = ({ >
@@ -90,17 +119,23 @@ export const ProjectDetails = ({ className="h-6 w-6" />
- - {onViewInProjectGraph && viewInProjectGraphPosition === 'top' && ( + {onViewInProjectGraph && viewInProjectGraphPosition === 'top' && ( +
+ + Copy Project + onViewInProjectGraph({ projectName: project.name }) } /> - )}{' '} - +
+ )}
-
+
{projectData.metadata?.description ? (

@@ -133,16 +168,22 @@ export const ProjectDetails = ({ ) : null}

- - {onViewInProjectGraph && - viewInProjectGraphPosition === 'bottom' && ( + {onViewInProjectGraph && + viewInProjectGraphPosition === 'bottom' && ( +
+ + Copy Project + onViewInProjectGraph({ projectName: project.name }) } /> - )}{' '} - +
+ )}
@@ -186,3 +227,19 @@ function ViewInProjectGraphButton({ callback }: { callback: () => void }) { ); } + +// https://github.com/pnpm/ramda/blob/50c6b57110b2f3631ed8633141f12012b7768d85/source/pick.js#L22 +function pick, N extends keyof T>( + names: Array, + obj: T +): Pick { + var result: Partial = {}; + var idx = 0; + while (idx < names.length) { + if (names[idx] in obj) { + result[names[idx]] = obj[names[idx]]; + } + idx += 1; + } + return result as Pick; +} diff --git a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx index 849fe99200e36..b70dade228ff2 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx @@ -1,6 +1,7 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line import type { TargetConfiguration } from '@nx/devkit'; +import { CopyToClipboardButton } from '@nx/graph/ui-components'; import { ChevronDownIcon, ChevronUpIcon, @@ -17,7 +18,6 @@ import { twMerge } from 'tailwind-merge'; import { Pill } from '../pill'; import { TargetTechnologies } from '../target-technologies/target-technologies'; import { SourceInfo } from '../source-info/source-info'; -import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard'; import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration'; import { TargetExecutor } from '../target-executor/target-executor'; @@ -53,10 +53,6 @@ export const TargetConfigurationDetailsHeader = ({ onViewInTaskGraph, onNxConnect, }: TargetConfigurationDetailsHeaderProps) => { - const handleCopyClick = async (copyText: string) => { - await window.navigator.clipboard.writeText(copyText); - }; - if (!collapsable) { // when collapsable is false, isCollasped should be false isCollasped = false; @@ -156,6 +152,12 @@ export const TargetConfigurationDetailsHeader = ({
+ {onViewInTaskGraph && (