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 { useCallback, useContext, useEffect, useState } from 'react';
import { SourceInfo } from '../source-info/source-info';
import { FadingCollapsible } from './fading-collapsible';
import { TargetConfigurationProperty } from './target-configuration-property';
import { selectSourceInfo } from './target-configuration-details.util';
import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
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 { TargetExecutor } from '../target-executor/target-executor';
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 {
projectName: string;
@ -109,7 +109,15 @@ export default function TargetConfigurationDetails({
/>
</h4>
<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>
</div>
@ -122,7 +130,13 @@ export default function TargetConfigurationDetails({
/>
</h4>
<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>
</div>
)}
@ -151,29 +165,20 @@ export default function TargetConfigurationDetails({
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.inputs.map((input, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.inputs`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`input-${idx}`}
>
<TargetConfigurationProperty data={input}>
{sourceInfo && (
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.inputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
})}
{targetConfiguration.inputs.map((input, idx) => (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`input-${idx}`}
>
<TargetConfigurationProperty data={input}>
<TargetSourceInfo
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.inputs`}
sourceMap={sourceMap}
/>
</TargetConfigurationProperty>
</li>
))}
</ul>
</div>
)}
@ -201,29 +206,20 @@ export default function TargetConfigurationDetails({
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.outputs?.map((output, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.outputs`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`output-${idx}`}
>
<TargetConfigurationProperty data={output}>
{sourceInfo && (
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
}) ?? <span>no outputs</span>}
{targetConfiguration.outputs?.map((output, idx) => (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`output-${idx}`}
>
<TargetConfigurationProperty data={output}>
<TargetSourceInfo
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.outputs`}
sourceMap={sourceMap}
/>
</TargetConfigurationProperty>
</li>
)) ?? <span>no outputs</span>}
</ul>
</div>
)}
@ -251,30 +247,20 @@ export default function TargetConfigurationDetails({
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.dependsOn.map((dep, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.dependsOn`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`dependsOn-${idx}`}
>
<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">
{sourceInfo && (
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`}
/>
)}
</span>
</TargetConfigurationProperty>
</li>
);
})}
{targetConfiguration.dependsOn.map((dep, idx) => (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`dependsOn-${idx}`}
>
<TargetConfigurationProperty data={dep}>
<TargetSourceInfo
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.dependsOn`}
sourceMap={sourceMap}
/>
</TargetConfigurationProperty>
</li>
))}
</ul>
</div>
)}
@ -295,20 +281,13 @@ export default function TargetConfigurationDetails({
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.options.${propertyName}`
);
return sourceInfo ? (
<span className="flex min-w-0 pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.options.${propertyName}`}
/>
</span>
) : null;
}}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</div>
@ -343,20 +322,13 @@ export default function TargetConfigurationDetails({
<FadingCollapsible>
<JsonCodeBlock
data={targetConfiguration.configurations}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.configurations.${propertyName}`
);
return sourceInfo ? (
<span className="flex min-w-0 pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`}
/>{' '}
</span>
) : null;
}}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.configurations.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</>

View File

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