Skip to content

Commit

Permalink
feat(core): add --help content to project details view (#26629)
Browse files Browse the repository at this point in the history
This PR adds help text for each inferred target that provides
`metadata.help.command`. To pass options/args to the target, users are
directed to open `project.json` and copy the values from `--help` output
into `options` property of the target.

To display the options help section, the inferred target must provide
metadata as follows:

```json5
 metadata: {
      help: {
        command: `foo --help`
        example: {
          options: {
            bar: true
          },
        },
      },
    },
```

The `help.command` value will be used to retrieve help text for the
underlying CLI (e.g. `jest --help`). The `help.example` property
contains sample options and args that users can add to their
`project.json` file -- currently rendered in the hover tooltip of
`project.json` hint text.

---

Example with `vite build --help`:

<img width="1257" alt="Screenshot 2024-06-21 at 3 06 21 PM"
src="https://github.com/nrwl/nx/assets/53559/b94cdcde-80da-4fa5-9f93-11af7fbcaf27">


Result of clicking `Run`:
<img width="1257" alt="Screenshot 2024-06-21 at 3 06 24 PM"
src="https://github.com/nrwl/nx/assets/53559/6803a5a8-9bbd-4510-b9ff-fa895a5b3402">

`project.json` tooltip hint:
<img width="1392" alt="Screenshot 2024-06-25 at 12 44 02 PM"
src="https://github.com/nrwl/nx/assets/53559/565002ae-7993-4dda-ac5d-4b685710f65e">
  • Loading branch information
jaysoo authored Jun 27, 2024
1 parent df83dd4 commit d90a735
Show file tree
Hide file tree
Showing 21 changed files with 1,150 additions and 90 deletions.
7 changes: 4 additions & 3 deletions graph/shared/src/lib/use-environment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function useEnvironmentConfig(): {
watch: boolean;
localMode: 'serve' | 'build';
projectGraphResponse?: ProjectGraphClientResponse;
environment: 'dev' | 'watch' | 'release' | 'nx-console';
environment: 'dev' | 'watch' | 'release' | 'nx-console' | 'docs';
appConfig: AppConfig;
useXstateInspect: boolean;
} {
Expand All @@ -25,13 +25,14 @@ export function getEnvironmentConfig() {
watch: window.watch,
localMode: window.localMode,
projectGraphResponse: window.projectGraphResponse,
environment: window.environment,
// If this was not built into JS or HTML, then it is rendered on docs (nx.dev).
environment: window.environment ?? ('docs' as const),
appConfig: {
...window.appConfig,
showExperimentalFeatures:
localStorage.getItem('showExperimentalFeatures') === 'true'
? true
: window.appConfig.showExperimentalFeatures,
: window.appConfig?.showExperimentalFeatures,
},
useXstateInspect: window.useXstateInspect,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Fragment, ReactNode, useMemo, useState } from 'react';
import { PlayIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { Transition } from '@headlessui/react';
import { getExternalApiService, useEnvironmentConfig } from '@nx/graph/shared';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { TargetConfiguration } from '@nx/devkit';
import { TerminalOutput } from '@nx/nx-dev/ui-fence';
import { Tooltip } from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text';

interface ShowOptionsHelpProps {
projectName: string;
targetName: string;
targetConfiguration: TargetConfiguration;
}

const fallbackHelpExample = {
options: {
silent: true,
},
args: ['foo'],
};

export function ShowOptionsHelp({
projectName,
targetName,
targetConfiguration,
}: ShowOptionsHelpProps) {
const config = useEnvironmentConfig();
const environment = config?.environment;
const localMode = config?.localMode;
const [result, setResult] = useState<{
text: string;
success: boolean;
} | null>(null);
const [isPending, setPending] = useState(false);
const externalApiService = getExternalApiService();

const helpData = targetConfiguration.metadata?.help;
const helpCommand = helpData?.command;
const helpExampleOptions = helpData?.example?.options;
const helpExampleArgs = helpData?.example?.args;

const helpExampleTest = useMemo(() => {
const targetExampleJson =
helpExampleOptions || helpExampleArgs
? {
options: helpExampleOptions,
args: helpExampleArgs,
}
: fallbackHelpExample;
return JSON.stringify(
{
targets: {
[targetName]: targetExampleJson,
},
},
null,
2
);
}, [helpExampleOptions, helpExampleArgs]);

let runHelpActionElement: null | ReactNode;
if (environment === 'docs') {
// Cannot run help command when rendering in docs (e.g. nx.dev).
runHelpActionElement = null;
} else if (environment === 'release' && localMode === 'build') {
// Cannot run help command when statically built via `nx graph --file=graph.html`.
runHelpActionElement = null;
} else if (isPending || !result) {
runHelpActionElement = (
<button
className="flex items-center rounded-md border border-slate-500 px-1 disabled:opacity-75"
disabled={isPending}
onClick={
environment === 'nx-console'
? () => {
externalApiService.postEvent({
type: 'run-help',
payload: {
projectName,
targetName,
helpCommand,
},
});
}
: async () => {
setPending(true);
const result = await fetch(
`/help?project=${encodeURIComponent(
projectName
)}&target=${encodeURIComponent(targetName)}`
).then((resp) => resp.json());
setResult(result);
setPending(false);
}
}
>
<PlayIcon className="mr-1 h-4 w-4" />
Run
</button>
);
} else {
runHelpActionElement = (
<button
className="flex items-center rounded-md border border-slate-500 px-1"
onClick={() => setResult(null)}
>
<XMarkIcon className="mr-1 h-4 w-4" />
Clear output
</button>
);
}

return (
helpCommand && (
<>
<p className="mb-4">
Use <code>--help</code> to see all options for this command, and set
them by{' '}
<a
className="text-blue-500 hover:underline"
target="_blank"
href="https://nx.dev/recipes/running-tasks/pass-args-to-commands#pass-args-to-commands"
>
passing them
</a>{' '}
to the <code>"options"</code> property in{' '}
<Tooltip
openAction="hover"
content={
(
<div className="w-fit max-w-md">
<p className="mb-2">
For example, you can use the following configuration for the{' '}
<code>{targetName}</code> target in the{' '}
<code>project.json</code> file for{' '}
<span className="font-semibold">{projectName}</span>.
</p>
<pre className="mb-2 border border-slate-200 bg-slate-100/50 p-2 p-2 text-slate-400 dark:border-slate-700 dark:bg-slate-700/50 dark:text-slate-500">
{helpExampleTest}
</pre>
{helpExampleOptions && (
<p className="mb-2">
The <code>options</code> are CLI options prefixed by{' '}
<code>--</code>, such as <code>ls --color=never</code>,
where you would use <code>{'"color": "never"'}</code> to
set it in the target configuration.
</p>
)}
{helpExampleArgs && (
<p className="mb-2">
The <code>args</code> are CLI positional arguments, such
as <code>ls somedir</code>, where you would use{' '}
<code>{'"args": ["somedir"]'}</code> to set it in the
target configuration.
</p>
)}
</div>
) as any
}
>
<code>
<TooltipTriggerText>project.json</TooltipTriggerText>
</code>
</Tooltip>
.
</p>
<TerminalOutput
command={helpCommand}
path={targetConfiguration.options.cwd ?? ''}
actionElement={runHelpActionElement}
content={
<div className="relative w-full">
<Transition
show={!!result}
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<pre
className={result && !result.success ? 'text-red-500' : ''}
>
{result?.text}
</pre>
</Transition>
</div>
}
/>
</>
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';
import { ShowOptionsHelp } from '../show-all-options/show-options-help';

interface TargetConfigurationDetailsProps {
projectName: string;
Expand Down Expand Up @@ -85,7 +86,7 @@ export default function TargetConfigurationDetails({
: true);

return (
<div className="relative overflow-hidden rounded-md border border-slate-200 dark:border-slate-700/60">
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
<TargetConfigurationDetailsHeader
isCollasped={collapsed}
toggleCollapse={handleCollapseToggle}
Expand Down Expand Up @@ -141,6 +142,44 @@ export default function TargetConfigurationDetails({
</div>
)}

{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</div>
<div className="mb-4">
<ShowOptionsHelp
targetConfiguration={targetConfiguration}
projectName={projectName}
targetName={targetName}
/>
</div>
</>
) : (
''
)}

{targetConfiguration.inputs && (
<div className="group">
<h4 className="mb-4">
Expand Down Expand Up @@ -265,37 +304,6 @@ export default function TargetConfigurationDetails({
</div>
)}

{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => (
<TargetSourceInfo
className="flex min-w-0 pl-4"
propertyKey={`targets.${targetName}.options.${propertyName}`}
sourceMap={sourceMap}
/>
)}
/>
</FadingCollapsible>
</div>
</>
) : (
''
)}

{shouldRenderConfigurations ? (
<>
<h4 className="mb-4 py-2">
Expand Down
11 changes: 8 additions & 3 deletions nx-dev/ui-fence/src/lib/fences/terminal-output.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { ReactNode } from 'react';
import type { JSX, ReactNode } from 'react';
import { TerminalShellWrapper } from './terminal-shell';

export function TerminalOutput({
content,
command,
path,
actionElement,
}: {
content: ReactNode | null;
content: ReactNode;
command: string;
path: string;
actionElement?: ReactNode;
}): JSX.Element {
const commandLines = command.split('\n').filter(Boolean);
return (
<TerminalShellWrapper>
<div className="overflow-x-auto p-4 pt-2">
<div className="items-left flex flex-col">
<div className="items-left relative flex flex-col">
{commandLines.map((line, index) => {
return (
<div key={index} className="flex">
Expand All @@ -29,6 +31,9 @@ export function TerminalOutput({
</span>
</p>
<p className="typing mt-0.5 flex-1 pl-2">{line}</p>
{actionElement ? (
<div className="sticky top-0 pl-2">{actionElement}</div>
) : null}
</div>
);
})}
Expand Down
Loading

0 comments on commit d90a735

Please sign in to comment.