feat(graph): add source info for command and script (#26162)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

<img width="993" alt="Screenshot 2024-05-28 at 12 19 15 PM"
src="https://github.com/nrwl/nx/assets/16211801/35b95537-72ff-474f-b03a-68e20a7dfe55">
<img width="942" alt="Screenshot 2024-05-28 at 12 19 05 PM"
src="https://github.com/nrwl/nx/assets/16211801/b67d920b-2689-452c-9214-d96ce12331dc">
<img width="728" alt="Screenshot 2024-05-28 at 12 09 15 PM"
src="https://github.com/nrwl/nx/assets/16211801/c6e74976-83b5-44bf-b0b7-c99e22cd6e03">


## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Emily Xiong 2024-06-05 16:56:50 -04:00 committed by GitHub
parent 7e984e11a6
commit 20529d4dc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 144 additions and 112 deletions

View File

@ -4,10 +4,8 @@ import type { TargetConfiguration } from '@nx/devkit';
import { JsonCodeBlock } from '@nx/graph/ui-code-block'; import { JsonCodeBlock } from '@nx/graph/ui-code-block';
import { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { SourceInfo } from '../source-info/source-info';
import { FadingCollapsible } from './fading-collapsible'; import { FadingCollapsible } from './fading-collapsible';
import { TargetConfigurationProperty } from './target-configuration-property'; import { TargetConfigurationProperty } from './target-configuration-property';
import { selectSourceInfo } from './target-configuration-details.util';
import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard'; import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from './tooltip-trigger-text'; import { TooltipTriggerText } from './tooltip-trigger-text';
@ -17,6 +15,8 @@ import { ExpandedTargetsContext } from '@nx/graph/shared';
import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration'; import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration';
import { TargetExecutor } from '../target-executor/target-executor'; import { TargetExecutor } from '../target-executor/target-executor';
import { TargetExecutorTitle } from '../target-executor/target-executor-title'; import { TargetExecutorTitle } from '../target-executor/target-executor-title';
import { TargetSourceInfo } from '../target-source-info/target-source-info';
import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key';
interface TargetConfigurationDetailsProps { interface TargetConfigurationDetailsProps {
projectName: string; projectName: string;
@ -109,7 +109,15 @@ export default function TargetConfigurationDetails({
/> />
</h4> </h4>
<p className="pl-5 font-mono"> <p className="pl-5 font-mono">
<TargetExecutor {...displayHeader} link={link} /> <TargetExecutor {...displayHeader} link={link}>
<TargetSourceInfo
className="pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.${getTargetExecutorSourceMapKey(
targetConfiguration
)}`}
sourceMap={sourceMap}
/>
</TargetExecutor>
</p> </p>
</div> </div>
@ -122,7 +130,13 @@ export default function TargetConfigurationDetails({
/> />
</h4> </h4>
<p className="pl-5 font-mono"> <p className="pl-5 font-mono">
<TargetExecutor script={script} link={link} /> <TargetExecutor script={script} link={link}>
<TargetSourceInfo
className="pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.options.script`}
sourceMap={sourceMap}
/>
</TargetExecutor>
</p> </p>
</div> </div>
)} )}
@ -151,29 +165,20 @@ export default function TargetConfigurationDetails({
</span> </span>
</h4> </h4>
<ul className="mb-4 list-disc pl-5"> <ul className="mb-4 list-disc pl-5">
{targetConfiguration.inputs.map((input, idx) => { {targetConfiguration.inputs.map((input, idx) => (
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.inputs`
);
return (
<li <li
className="group/line overflow-hidden whitespace-nowrap" className="group/line overflow-hidden whitespace-nowrap"
key={`input-${idx}`} key={`input-${idx}`}
> >
<TargetConfigurationProperty data={input}> <TargetConfigurationProperty data={input}>
{sourceInfo && ( <TargetSourceInfo
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"> className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.inputs`} propertyKey={`targets.${targetName}.inputs`}
sourceMap={sourceMap}
/> />
</span>
)}
</TargetConfigurationProperty> </TargetConfigurationProperty>
</li> </li>
); ))}
})}
</ul> </ul>
</div> </div>
)} )}
@ -201,29 +206,20 @@ export default function TargetConfigurationDetails({
</span> </span>
</h4> </h4>
<ul className="mb-4 list-disc pl-5"> <ul className="mb-4 list-disc pl-5">
{targetConfiguration.outputs?.map((output, idx) => { {targetConfiguration.outputs?.map((output, idx) => (
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.outputs`
);
return (
<li <li
className="group/line overflow-hidden whitespace-nowrap" className="group/line overflow-hidden whitespace-nowrap"
key={`output-${idx}`} key={`output-${idx}`}
> >
<TargetConfigurationProperty data={output}> <TargetConfigurationProperty data={output}>
{sourceInfo && ( <TargetSourceInfo
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"> className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`} propertyKey={`targets.${targetName}.outputs`}
sourceMap={sourceMap}
/> />
</span>
)}
</TargetConfigurationProperty> </TargetConfigurationProperty>
</li> </li>
); )) ?? <span>no outputs</span>}
}) ?? <span>no outputs</span>}
</ul> </ul>
</div> </div>
)} )}
@ -251,30 +247,20 @@ export default function TargetConfigurationDetails({
</span> </span>
</h4> </h4>
<ul className="mb-4 list-disc pl-5"> <ul className="mb-4 list-disc pl-5">
{targetConfiguration.dependsOn.map((dep, idx) => { {targetConfiguration.dependsOn.map((dep, idx) => (
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.dependsOn`
);
return (
<li <li
className="group/line overflow-hidden whitespace-nowrap" className="group/line overflow-hidden whitespace-nowrap"
key={`dependsOn-${idx}`} key={`dependsOn-${idx}`}
> >
<TargetConfigurationProperty data={dep}> <TargetConfigurationProperty data={dep}>
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"> <TargetSourceInfo
{sourceInfo && ( className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`} propertyKey={`targets.${targetName}.dependsOn`}
sourceMap={sourceMap}
/> />
)}
</span>
</TargetConfigurationProperty> </TargetConfigurationProperty>
</li> </li>
); ))}
})}
</ul> </ul>
</div> </div>
)} )}
@ -295,20 +281,13 @@ export default function TargetConfigurationDetails({
<FadingCollapsible> <FadingCollapsible>
<JsonCodeBlock <JsonCodeBlock
data={options} data={options}
renderSource={(propertyName: string) => { renderSource={(propertyName: string) => (
const sourceInfo = selectSourceInfo( <TargetSourceInfo
sourceMap, className="flex min-w-0 pl-4"
`targets.${targetName}.options.${propertyName}`
);
return sourceInfo ? (
<span className="flex min-w-0 pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.options.${propertyName}`} propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/> />
</span> )}
) : null;
}}
/> />
</FadingCollapsible> </FadingCollapsible>
</div> </div>
@ -343,20 +322,13 @@ export default function TargetConfigurationDetails({
<FadingCollapsible> <FadingCollapsible>
<JsonCodeBlock <JsonCodeBlock
data={targetConfiguration.configurations} data={targetConfiguration.configurations}
renderSource={(propertyName: string) => { renderSource={(propertyName: string) => (
const sourceInfo = selectSourceInfo( <TargetSourceInfo
sourceMap, className="flex min-w-0 pl-4"
`targets.${targetName}.configurations.${propertyName}`
);
return sourceInfo ? (
<span className="flex min-w-0 pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`} propertyKey={`targets.${targetName}.configurations.${propertyName}`}
/>{' '} sourceMap={sourceMap}
</span> />
) : null; )}
}}
/> />
</FadingCollapsible> </FadingCollapsible>
</> </>

View File

@ -7,6 +7,7 @@ export interface TargetExecutorProps {
executor?: string; executor?: string;
isCompact?: boolean; isCompact?: boolean;
link?: string; link?: string;
children?: React.ReactNode;
} }
export function TargetExecutor({ export function TargetExecutor({
@ -16,9 +17,16 @@ export function TargetExecutor({
executor, executor,
isCompact, isCompact,
link, link,
children,
}: TargetExecutorProps) { }: TargetExecutorProps) {
if (script) { if (script) {
return link ? <ExternalLink href={link}>{script}</ExternalLink> : script; return link ? (
<div className="group/line">
<ExternalLink href={link}>{script}</ExternalLink> {children}
</div>
) : (
script
);
} }
if (commands) { if (commands) {
@ -34,19 +42,22 @@ export function TargetExecutor({
); );
} }
return ( return (
<div className="group/line">
<ul> <ul>
{commands?.map((c) => {commands?.filter(Boolean).map((c) => (
c ? (
<li>{link ? <ExternalLink href={link}>{c}</ExternalLink> : c}</li> <li>{link ? <ExternalLink href={link}>{c}</ExternalLink> : c}</li>
) : null ))}
)}
</ul> </ul>
{children}
</div>
); );
} }
const displayText = command ?? executor ?? ''; const displayText = command ?? executor ?? '';
return link ? ( return link ? (
<ExternalLink href={link}>{displayText}</ExternalLink> <div className="group/line">
<ExternalLink href={link}>{displayText}</ExternalLink> {children}
</div>
) : ( ) : (
displayText displayText
); );

View File

@ -0,0 +1,17 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { TargetConfiguration } from '@nx/devkit';
export function getTargetExecutorSourceMapKey(
targetConfiguration: TargetConfiguration
): string {
if (targetConfiguration.options?.command) {
return 'options.command';
} else if (targetConfiguration.options?.commands) {
return 'options.commands';
} else if (targetConfiguration.options?.script) {
return 'options.script';
} else {
return 'executor';
}
}

View File

@ -1,4 +1,4 @@
import { selectSourceInfo } from './target-configuration-details.util'; import { selectSourceInfo } from './select-source-info';
test('selectSourceInfo', () => { test('selectSourceInfo', () => {
const map = { const map = {

View File

@ -0,0 +1,26 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { SourceInfo } from '../source-info/source-info';
import { selectSourceInfo } from './select-source-info';
export interface TargetSourceInfoProps {
propertyKey: string;
sourceMap: Record<string, string[]>;
className?: string;
}
export function TargetSourceInfo({
propertyKey,
sourceMap,
className,
}: TargetSourceInfoProps) {
const sourceInfo = selectSourceInfo(sourceMap, propertyKey);
if (!sourceInfo) {
return null;
}
return (
<span className={className}>
<SourceInfo data={sourceInfo} propertyKey={propertyKey} />
</span>
);
}

View File

@ -1,19 +1,25 @@
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { twMerge } from 'tailwind-merge';
export function ExternalLink({ export function ExternalLink({
children, children,
href, href,
title, title,
className,
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
href: string; href: string;
className?: string;
title?: string; title?: string;
}) { }) {
return ( return (
<a <a
href={href} href={href}
title={title} title={title}
className="gap-2 text-slate-500 hover:underline dark:text-slate-400" className={twMerge(
'gap-2 text-slate-500 hover:underline dark:text-slate-400',
className
)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >