feat(docs): add {% project-details %} as a tag in markdown docs (#21288)

Co-authored-by: Colum Ferry <cferry09@gmail.com>
Co-authored-by: Isaac Mann <isaacplmann@gmail.com>
This commit is contained in:
Jack Hsu 2024-01-24 12:53:03 -05:00 committed by GitHub
parent 292d407536
commit 7b680ec68c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1473 additions and 649 deletions

View File

@ -286,6 +286,49 @@ Have a more decent button-like widget that you can place below sections of a tut
{% video-link link="https://youtu.be/OQ-Zc5tcxJE?t=64" /%}
```
#### Project Details View
Embed a Project Details View that is identical what is shown in Nx Console or `nx show project myproject --web`
````markdown
{% project-details title="Test" height="100px" %}
```json
{
"project": {
"name": "demo",
"data": {
"root": " packages/demo",
"projectType": "application",
"targets": {
"dev": {
"executor": "nx:run-commands",
"options": {
"command": "vite dev"
}
},
"build": {
"executor": "nx:run-commands",
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist"],
"options": {
"command": "vite build"
}
}
}
}
},
"sourceMap": {
"targets": ["packages/demo/vite.config.ts", "@nx/vite"],
"targets.dev": ["packages/demo/vite.config.ts", "@nx/vite"],
"targets.build": ["packages/demo/vite.config.ts", "@nx/vite"]
}
}
```
{% /project-details %}
````
#### Graph
Embed an Nx Graph visualization that can be panned by the user.

View File

@ -5,13 +5,12 @@ import { Shell } from './shell';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
import { ProjectDetailsPage } from '@nx/graph/project-details';
import {
getEnvironmentConfig,
getProjectGraphDataService,
} from '@nx/graph/shared';
import { TasksSidebarErrorBoundary } from './feature-tasks/tasks-sidebar-error-boundary';
import { ProjectDetailsPage } from '@nx/graph/project-details';
const { appConfig } = getEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();

View File

@ -2,7 +2,7 @@
// nx-ignore-next-line
import { useFloating } from '@floating-ui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { ProjectDetails } from '@nx/graph/project-details';
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
@ -46,6 +46,7 @@ export function ProjectDetailsModal() {
setSearchParams(searchParams);
setIsOpen(false);
}
return (
isOpen && (
<div
@ -56,7 +57,7 @@ export function ProjectDetailsModal() {
ref={refs.setFloating}
>
<div className="rounded-md h-full border border-slate-500">
<ProjectDetails project={project} sourceMap={sourceMap} />
<ProjectDetailsWrapper project={project} sourceMap={sourceMap} />
<div className="top-2 right-2 absolute" onClick={onClose}>
<XMarkIcon className="h-4 w-4" />
</div>

View File

@ -1,2 +1,2 @@
export * from './lib/project-details';
export * from './lib/project-details-wrapper';
export * from './lib/project-details-page';

View File

@ -4,12 +4,10 @@ import { ProjectGraphProjectNode } from '@nx/devkit';
import {
Link,
ScrollRestoration,
useLocation,
useNavigate,
useParams,
useRouteLoaderData,
} from 'react-router-dom';
import ProjectDetails from './project-details';
import { ProjectDetailsWrapper } from './project-details-wrapper';
import {
fetchProjectGraph,
getProjectGraphDataService,
@ -54,7 +52,7 @@ export function ProjectDetailsPage() {
<div className="flex flex-col justify-center w-full text-slate-700 dark:text-slate-400">
<ScrollRestoration />
{environment !== 'nx-console' ? (
<header className="flex w-full justify-center items-center px-4 py-2 border-b-2 border-slate-900/10 mb-16 dark:border-slate-300/10">
<header className="flex w-full justify-center items-center px-4 py-2 border-b-2 border-slate-900/10 mb-8 dark:border-slate-300/10">
<div className="flex flex-grow items-center justify-between max-w-6xl">
<svg
className="h-10 w-auto text-slate-900 dark:text-white"
@ -85,10 +83,10 @@ export function ProjectDetailsPage() {
</header>
) : null}
<div className="flex-grow mx-auto w-full max-w-6xl px-8 mb-8">
<ProjectDetails
<ProjectDetailsWrapper
project={project}
sourceMap={sourceMap}
></ProjectDetails>
></ProjectDetailsWrapper>
</div>
</div>
);

View File

@ -0,0 +1,163 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useNavigate, useSearchParams } from 'react-router-dom';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import {
ProjectDetails,
ProjectDetailsImperativeHandle,
} from '@nx/graph/ui-project-details';
import { useCallback, useLayoutEffect, useRef } from 'react';
export interface ProjectDetailsProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
}
export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
const projectDetailsRef = useRef<ProjectDetailsImperativeHandle>(null);
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeConstructor = useRouteConstructor();
const [searchParams, setSearchParams] = useSearchParams();
const handleViewInProjectGraph = useCallback(
(data: { projectName: string }) => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-project-graph',
payload: {
projectName: data.projectName,
},
});
} else {
navigate(
routeConstructor(
`/projects/${encodeURIComponent(data.projectName)}`,
true
)
);
}
},
[externalApiService, routeConstructor, navigate, environment]
);
const handleViewInTaskGraph = useCallback(
(data: { projectName: string; targetName: string }) => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: data.projectName,
targetName: data.targetName,
},
});
} else {
navigate(
routeConstructor(
{
pathname: `/tasks/${encodeURIComponent(data.targetName)}`,
search: `?projects=${encodeURIComponent(data.projectName)}`,
},
true
)
);
}
},
[externalApiService, routeConstructor, navigate, environment]
);
const handleRunTarget = useCallback(
(data: { projectName: string; targetName: string }) => {
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${data.projectName}:${data.targetName}` },
});
},
[externalApiService]
);
const updateSearchParams = (params: URLSearchParams, sections: string[]) => {
if (sections.length === 0) {
params.delete('expanded');
} else {
params.set('expanded', sections.join(','));
}
};
const handleTargetCollapse = useCallback(
(targetName: string) => {
setSearchParams(
(currentSearchParams) => {
const expandedSections =
currentSearchParams.get('expanded')?.split(',') || [];
const newExpandedSections = expandedSections.filter(
(section) => section !== targetName
);
updateSearchParams(currentSearchParams, newExpandedSections);
return currentSearchParams;
},
{
replace: true,
preventScrollReset: true,
}
);
},
[setSearchParams]
);
const handleTargetExpand = useCallback(
(targetName: string) => {
setSearchParams(
(currentSearchParams) => {
const expandedSections =
currentSearchParams.get('expanded')?.split(',') || [];
if (!expandedSections.includes(targetName)) {
expandedSections.push(targetName);
updateSearchParams(currentSearchParams, expandedSections);
}
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
},
[setSearchParams]
);
// On initial render, expand the sections that are included in the URL search params.
const isExpandedHandled = useRef(false);
useLayoutEffect(() => {
if (!props.project.data.targets) return;
if (isExpandedHandled.current) return;
isExpandedHandled.current = true;
const expandedSections = searchParams.get('expanded')?.split(',') || [];
for (const targetName of Object.keys(props.project.data.targets)) {
if (expandedSections.includes(targetName)) {
projectDetailsRef.current?.expandTarget(targetName);
}
}
}, [searchParams, props.project.data.targets, projectDetailsRef]);
return (
<ProjectDetails
ref={projectDetailsRef}
{...props}
onTargetCollapse={handleTargetCollapse}
onTargetExpand={handleTargetExpand}
onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
/>
);
}
export default ProjectDetailsWrapper;

View File

@ -1,114 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useNavigate } from 'react-router-dom';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import { EyeIcon } from '@heroicons/react/24/outline';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { TargetConfigurationDetails } from './target/target-configuration-details';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from './target/ui/tooltip-trigger-text';
export interface ProjectDetailsProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
}
export function ProjectDetails({
project: {
name,
data: { root, ...projectData },
},
sourceMap,
}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeConstructor = useRouteConstructor();
const displayType =
projectData.projectType &&
projectData.projectType?.charAt(0)?.toUpperCase() +
projectData.projectType?.slice(1);
const viewInProjectGraph = () => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-project-graph',
payload: {
projectName: name,
},
});
} else {
navigate(routeConstructor(`/projects/${encodeURIComponent(name)}`, true));
}
};
return (
<>
<header className="border-b border-slate-900/10 mb-4 dark:border-slate-300/10">
<h1 className="text-6xl flex items-center mb-4 gap-2">
{name}{' '}
{environment === 'nx-console' ? (
<EyeIcon className="h-5 w-5" onClick={viewInProjectGraph}></EyeIcon>
) : null}{' '}
</h1>
<div className="p-4">
{projectData.tags ? (
<p>
{projectData.tags?.map((tag) => (
<span className="bg-slate-300 rounded-md p-1 mr-2">{tag}</span>
))}
</p>
) : null}
<p>
<span className="font-bold">Root:</span> {root}
</p>
{displayType ? (
<p>
<span className="font-bold">Type:</span> {displayType}
</p>
) : null}
</div>
</header>
<div>
<h2 className="text-3xl mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="targets" />) as any}
>
<span>
<TooltipTriggerText>Targets</TooltipTriggerText>
</span>
</Tooltip>
</h2>
<ul>
{Object.entries(projectData.targets ?? {}).map(
([targetName, target]) => {
const props = {
projectName: name,
targetName: targetName,
targetConfiguration: target,
sourceMap,
};
return (
<li className="mb-4">
<TargetConfigurationDetails {...props} />
</li>
);
}
)}
</ul>
</div>
</>
);
}
export default ProjectDetails;

View File

@ -1,520 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import {
ChevronDownIcon,
ChevronUpIcon,
EyeIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
// nx-ignore-next-line
import { TargetConfiguration } from '@nx/devkit';
import {
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { JsonCodeBlock } from '@nx/graph/ui-code-block';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { SourceInfo } from './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';
import {
PropertyInfoTooltip,
SourcemapInfoToolTip,
Tooltip,
} from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from './ui/tooltip-trigger-text';
import { ExternalLink } from '@nx/graph/ui-tooltips';
/* eslint-disable-next-line */
export interface TargetProps {
projectName: string;
targetName: string;
targetConfiguration: TargetConfiguration;
sourceMap: Record<string, string[]>;
}
export function TargetConfigurationDetails({
projectName,
targetName,
targetConfiguration,
sourceMap,
}: TargetProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeConstructor = useRouteConstructor();
const [searchParams, setSearchParams] = useSearchParams();
const [collapsed, setCollapsed] = useState(true);
let executorLink: string | null = null;
// TODO: Handle this better because this will not work with labs
if (targetConfiguration.executor?.startsWith('@nx/')) {
const packageName = targetConfiguration.executor
.split('/')[1]
.split(':')[0];
const executorName = targetConfiguration.executor
.split('/')[1]
.split(':')[1];
executorLink = `https://nx.dev/nx-api/${packageName}/executors/${executorName}`;
} else if (targetConfiguration.executor === 'nx:run-commands') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-commands`;
} else if (targetConfiguration.executor === 'nx:run-script') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-script`;
}
useEffect(() => {
const expandedSections = searchParams.get('expanded')?.split(',') || [];
setCollapsed(!expandedSections.includes(targetName));
}, [searchParams, targetName]);
const handleCopyClick = (copyText: string) => {
navigator.clipboard.writeText(copyText);
};
function toggleCollapsed() {
setCollapsed((prevState) => {
const newState = !prevState;
setSearchParams((currentSearchParams) => {
const expandedSections =
currentSearchParams.get('expanded')?.split(',') || [];
if (newState) {
const newExpandedSections = expandedSections.filter(
(section) => section !== targetName
);
updateSearchParams(currentSearchParams, newExpandedSections);
} else {
if (!expandedSections.includes(targetName)) {
expandedSections.push(targetName);
updateSearchParams(currentSearchParams, expandedSections);
}
}
return currentSearchParams;
});
return newState;
});
}
function updateSearchParams(params: URLSearchParams, sections: string[]) {
if (sections.length === 0) {
params.delete('expanded');
} else {
params.set('expanded', sections.join(','));
}
}
const runTarget = () => {
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${projectName}:${targetName}` },
});
};
const viewInTaskGraph = () => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: projectName,
targetName: targetName,
},
});
} else {
navigate(
routeConstructor(
{
pathname: `/tasks/${encodeURIComponent(targetName)}`,
search: `?projects=${encodeURIComponent(projectName)}`,
},
true
)
);
}
};
const singleCommand =
targetConfiguration.executor === 'nx:run-commands'
? targetConfiguration.command ?? targetConfiguration.options?.command
: null;
const options = useMemo(() => {
if (singleCommand) {
const { command, ...rest } = targetConfiguration.options;
return rest;
} else {
return targetConfiguration.options;
}
}, [targetConfiguration.options, singleCommand]);
const configurations = targetConfiguration.configurations;
const shouldRenderOptions =
options &&
(typeof options === 'object' ? Object.keys(options).length : true);
const shouldRenderConfigurations =
configurations &&
(typeof configurations === 'object'
? Object.keys(configurations).length
: true);
return (
<div className="rounded-md border border-slate-500 relative overflow-hidden">
<header
className={`group hover:bg-slate-200 dark:hover:bg-slate-800 p-2 cursor-pointer ${
!collapsed
? 'bg-slate-200 dark:bg-slate-800 border-b-2 border-slate-900/10 dark:border-slate-300/10 '
: ''
}`}
onClick={toggleCollapsed}
>
<div className="flex justify-between items-center">
<div className="flex items-center">
<h3 className="font-bold mr-2">{targetName}</h3>
{collapsed && (
<p className="text-slate-600 mr-2">
{singleCommand ? singleCommand : targetConfiguration.executor}
</p>
)}
</div>
<div className="flex items-center">
<EyeIcon
className={`h-4 w-4 mr-2 ${
collapsed ? 'hidden group-hover:inline-block' : 'inline-block'
}`}
title="View in Task Graph"
onClick={(e) => {
e.stopPropagation();
viewInTaskGraph();
}}
/>
{targetConfiguration.cache && (
<Tooltip
openAction="hover"
strategy="fixed"
content={(<PropertyInfoTooltip type="cacheable" />) as any}
>
<span className="rounded-full inline-block text-xs bg-sky-500 dark:bg-sky-800 px-2 text-slate-50 mr-2">
Cacheable
</span>
</Tooltip>
)}
{environment === 'nx-console' && (
<PlayIcon
className="h-5 w-5 mr-2"
onClick={(e) => {
e.stopPropagation();
runTarget();
}}
/>
)}
{collapsed ? (
<ChevronDownIcon className="h-3 w-3" />
) : (
<ChevronUpIcon className="h-3 w-3" />
)}
</div>
</div>
{!collapsed && (
<div className="flex items-center text-sm mt-2">
<span className="flex-1 flex items-center">
<SourceInfo
data={sourceMap[`targets.${targetName}`]}
propertyKey={`targets.${targetName}`}
/>
</span>
<code className="ml-4 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 font-mono px-2 py-1 rounded">
nx run {projectName}:{targetName}
</code>
<span className="ml-2">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`nx run ${projectName}:${targetName}`)
}
/>
</span>
</div>
)}
</header>
{/* body */}
{!collapsed && (
<div className="p-4 text-base">
<div className="mb-4 group">
<h4 className="mb-4">
{singleCommand ? (
<span className="font-bold">
Command
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`"command": "${singleCommand}"`)
}
/>
</span>
</span>
) : (
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="executors" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Executor</TooltipTriggerText>
</span>
</Tooltip>
)}
</h4>
<p className="pl-5">
{executorLink ? (
<ExternalLink
href={executorLink ?? 'https://nx.dev/nx-api'}
text={
singleCommand ? singleCommand : targetConfiguration.executor
}
title="View Documentation"
/>
) : singleCommand ? (
singleCommand
) : (
targetConfiguration.executor
)}
</p>
</div>
{targetConfiguration.inputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="inputs" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Inputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"inputs": ${JSON.stringify(
targetConfiguration.inputs
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.inputs.map((input) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.inputs`
);
return (
<li className="group/line overflow-hidden whitespace-nowrap">
<TargetConfigurationProperty data={input}>
{sourceInfo && (
<span className="hidden group-hover/line:inline pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.inputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{targetConfiguration.outputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="outputs" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Outputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"outputs": ${JSON.stringify(
targetConfiguration.outputs
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.outputs?.map((output) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.outputs`
);
return (
<li className="group/line overflow-hidden whitespace-nowrap">
<TargetConfigurationProperty data={output}>
{sourceInfo && (
<span className="hidden group-hover/line:inline pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
}) ?? <span>no outputs</span>}
</ul>
</div>
)}
{targetConfiguration.dependsOn && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="dependsOn" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Depends On</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"dependsOn": ${JSON.stringify(
targetConfiguration.dependsOn
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.dependsOn.map((dep) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.dependsOn`
);
return (
<li className="group/line overflow-hidden whitespace-nowrap">
<TargetConfigurationProperty data={dep}>
<span className="hidden group-hover/line:inline pl-4 h-6">
{sourceInfo && (
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`}
/>
)}
</span>
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.options.${propertyName}`
);
return sourceInfo ? (
<span className="pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.options.${propertyName}`}
/>
</span>
) : null;
}}
/>
</FadingCollapsible>
</div>
</>
) : (
''
)}
{shouldRenderConfigurations ? (
<>
<h4 className="py-2 mb-4">
<Tooltip
openAction="hover"
content={
(<PropertyInfoTooltip type="configurations" />) as any
}
>
<span className="font-bold">
<TooltipTriggerText>Configurations</TooltipTriggerText>
</span>
</Tooltip>{' '}
{targetConfiguration.defaultConfiguration && (
<span
className="ml-3 font-bold rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50 mr-6"
title="Default Configuration"
>
{targetConfiguration.defaultConfiguration}
</span>
)}
</h4>
<FadingCollapsible>
<JsonCodeBlock
data={targetConfiguration.configurations}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.configurations.${propertyName}`
);
return sourceInfo ? (
<span className="pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`}
/>{' '}
</span>
) : null;
}}
/>
</FadingCollapsible>
</>
) : (
''
)}
</div>
)}
</div>
);
}
export default TargetConfigurationDetails;

View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,26 @@
/* eslint-disable @nx/enforce-module-boundaries */
import type { StorybookConfig } from '@storybook/react-vite';
// nx-ignore-next-line
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { mergeConfig } from 'vite';
const config: StorybookConfig = {
stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/react-vite',
options: {},
},
viteFinal: async (config) =>
mergeConfig(config, {
plugins: [nxViteTsPaths()],
}),
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs

View File

@ -0,0 +1 @@
import './tailwind.css';

View File

@ -0,0 +1,3 @@
@tailwind components;
@tailwind base;
@tailwind utilities;

View File

@ -0,0 +1,7 @@
# graph-ui-project-details
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test graph-ui-project-details` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -0,0 +1,9 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -0,0 +1,49 @@
{
"name": "graph-ui-project-details",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/ui-project-details/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
},
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {
"port": 4400,
"configDir": "graph/ui-project-details/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nx/storybook:build",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/graph-ui-project-details",
"configDir": "graph/ui-project-details/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"static-storybook": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "graph-ui-project-details:build-storybook",
"staticFilePath": "dist/storybook/graph-ui-project-details"
},
"configurations": {
"ci": {
"buildTarget": "graph-ui-project-details:build-storybook:ci"
}
}
}
}
}

View File

@ -0,0 +1 @@
export * from './lib/project-details/project-details';

View File

@ -0,0 +1,210 @@
import type { Meta } from '@storybook/react';
import { ProjectDetails } from './project-details';
const meta: Meta<typeof ProjectDetails> = {
component: ProjectDetails,
title: 'ProjectDetails',
};
export default meta;
export const Primary = {
args: {
project: {
name: 'jest',
data: {
root: 'packages/jest',
name: 'jest',
targets: {
'nx-release-publish': {
dependsOn: ['^nx-release-publish'],
executor: '@nx/js:release-publish',
options: { packageRoot: 'build/packages/jest' },
configurations: {},
},
test: {
dependsOn: ['test-native', 'build-native', '^build-native'],
inputs: [
'default',
'^production',
'{workspaceRoot}/jest.preset.js',
],
executor: '@nx/jest:jest',
outputs: ['{workspaceRoot}/coverage/{projectRoot}'],
cache: true,
options: {
jestConfig: 'packages/jest/jest.config.ts',
passWithNoTests: true,
},
configurations: {},
},
'build-base': {
dependsOn: ['^build-base', 'build-native'],
inputs: ['production', '^production'],
executor: '@nx/js:tsc',
outputs: ['{options.outputPath}'],
cache: true,
options: {
outputPath: 'build/packages/jest',
tsConfig: 'packages/jest/tsconfig.lib.json',
main: 'packages/jest/index.ts',
assets: [
{
input: 'packages/jest',
glob: '**/@(files|files-angular)/**',
output: '/',
},
{
input: 'packages/jest',
glob: '**/files/**/.gitkeep',
output: '/',
},
{
input: 'packages/jest',
glob: '**/*.json',
ignore: [
'**/tsconfig*.json',
'project.json',
'.eslintrc.json',
],
output: '/',
},
{
input: 'packages/jest',
glob: '**/*.js',
ignore: ['**/jest.config.js'],
output: '/',
},
{ input: 'packages/jest', glob: '**/*.d.ts', output: '/' },
{ input: '', glob: 'LICENSE', output: '/' },
],
},
configurations: {},
},
build: {
dependsOn: ['build-base', 'build-native'],
inputs: ['production', '^production'],
cache: true,
executor: 'nx:run-commands',
outputs: ['{workspaceRoot}/build/packages/jest'],
options: { command: 'node ./scripts/copy-readme.js jest' },
configurations: {},
},
'add-extra-dependencies': {
executor: 'nx:run-commands',
options: {
command:
'node ./scripts/add-dependency-to-build.js jest @nrwl/jest',
},
configurations: {},
},
lint: {
dependsOn: ['build-native', '^build-native'],
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/tools/eslint-rules/**/*',
],
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
cache: true,
options: { lintFilePatterns: ['packages/jest'] },
configurations: {},
},
},
$schema: '../../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'packages/jest',
projectType: 'library',
implicitDependencies: [],
tags: [],
},
},
sourceMap: {
root: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
name: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
targets: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.nx-release-publish': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.dependsOn': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.executor': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.options': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
$schema: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
sourceRoot: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
projectType: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.test': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options.assets': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.outputs': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.lint': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
},
},
};

View File

@ -0,0 +1,163 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import { EyeIcon } from '@heroicons/react/24/outline';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
import {
TargetConfigurationDetails,
TargetConfigurationDetailsHandle,
} from '../target-configuration-details/target-configuration-details';
import { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text';
import {
createRef,
ForwardedRef,
forwardRef,
RefObject,
useImperativeHandle,
useRef,
} from 'react';
import { twMerge } from 'tailwind-merge';
export interface ProjectDetailsProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onTargetCollapse?: (targetName: string) => void;
onTargetExpand?: (targetName: string) => void;
onViewInProjectGraph?: (data: { projectName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
}
export interface ProjectDetailsImperativeHandle {
collapseTarget: (targetName: string) => void;
expandTarget: (targetName: string) => void;
}
export const ProjectDetails = forwardRef(
(
{
project: {
name,
data: { root, ...projectData },
},
sourceMap,
variant,
onTargetCollapse,
onTargetExpand,
onViewInProjectGraph,
onViewInTaskGraph,
onRunTarget,
}: ProjectDetailsProps,
ref: ForwardedRef<ProjectDetailsImperativeHandle>
) => {
const isCompact = variant === 'compact';
const projectTargets = Object.keys(projectData.targets ?? {});
const targetRefs = useRef(
projectTargets.reduce((acc, targetName) => {
acc[targetName] = createRef<TargetConfigurationDetailsHandle>();
return acc;
}, {} as Record<string, RefObject<TargetConfigurationDetailsHandle>>)
);
const displayType =
projectData.projectType &&
projectData.projectType?.charAt(0)?.toUpperCase() +
projectData.projectType?.slice(1);
useImperativeHandle(ref, () => ({
collapseTarget: (targetName: string) => {
targetRefs.current[targetName]?.current?.collapse();
},
expandTarget: (targetName: string) => {
targetRefs.current[targetName]?.current?.expand();
},
}));
return (
<>
<header
className={twMerge(
`border-b border-slate-900/10 dark:border-slate-300/10`,
isCompact ? 'mb-2' : 'mb-4'
)}
>
<h1
className={twMerge(
`flex items-center`,
isCompact ? `text-2xl gap-1` : `text-4xl mb-4 gap-2`
)}
>
{name}{' '}
{onViewInProjectGraph ? (
<EyeIcon
className="h-5 w-5 cursor-pointer"
onClick={() => onViewInProjectGraph({ projectName: name })}
></EyeIcon>
) : null}{' '}
</h1>
<div className={isCompact ? `px-4 py-2` : `p-4`}>
{projectData.tags ? (
<p>
{projectData.tags?.map((tag) => (
<span className="bg-slate-300 rounded-md p-1 mr-2">
{tag}
</span>
))}
</p>
) : null}
<p>
<span className="font-bold">Root:</span> {root}
</p>
{displayType ? (
<p>
<span className="font-bold">Type:</span> {displayType}
</p>
) : null}
</div>
</header>
<div>
<h2 className={isCompact ? `text-lg mb-3` : `text-xl mb-4`}>
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="targets" />) as any}
>
<span>
<TooltipTriggerText>Targets</TooltipTriggerText>
</span>
</Tooltip>
</h2>
<ul>
{projectTargets.map((targetName) => {
const target = projectData.targets?.[targetName];
return target && targetRefs.current[targetName] ? (
<li className="mb-4 last:mb-0" key={`target-${targetName}`}>
<TargetConfigurationDetails
ref={targetRefs.current[targetName]}
variant={variant}
projectName={name}
targetName={targetName}
targetConfiguration={target}
sourceMap={sourceMap}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
onCollapse={onTargetCollapse}
onExpand={onTargetExpand}
/>
</li>
) : null;
})}
</ul>
</div>
</>
);
}
);
export default ProjectDetails;

View File

@ -19,8 +19,8 @@ export function SourceInfo(props: {
(
<SourcemapInfoToolTip
propertyKey={props.propertyKey}
plugin={props.data[1]}
file={props.data[0]}
plugin={props.data?.[1]}
file={props.data?.[0]}
/>
) as any
}
@ -29,7 +29,8 @@ export function SourceInfo(props: {
{/* <InformationCircleIcon className="w-3 h-3" />*/}
{/*</span>*/}
<span className="italic text-gray-500">
{isTarget ? 'Created' : 'Set'} by {props.data[1]} from {props.data[0]}
{isTarget ? 'Created' : 'Set'} by {props.data?.[1]} from{' '}
{props.data?.[0]}
</span>
</Tooltip>
</span>

View File

@ -0,0 +1,514 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import {
ChevronDownIcon,
ChevronUpIcon,
EyeIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
// nx-ignore-next-line
import { TargetConfiguration } from '@nx/devkit';
import { JsonCodeBlock } from '@nx/graph/ui-code-block';
import {
ForwardedRef,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { SourceInfo } from './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';
import {
ExternalLink,
PropertyInfoTooltip,
Tooltip,
} from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from './tooltip-trigger-text';
import { twMerge } from 'tailwind-merge';
/* eslint-disable-next-line */
export interface TargetProps {
projectName: string;
targetName: string;
targetConfiguration: TargetConfiguration;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onCollapse?: (targetName: string) => void;
onExpand?: (targetName: string) => void;
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
}
export interface TargetConfigurationDetailsHandle {
collapse: () => void;
expand: () => void;
}
export const TargetConfigurationDetails = forwardRef(
(
{
variant,
projectName,
targetName,
targetConfiguration,
sourceMap,
onExpand,
onCollapse,
onViewInTaskGraph,
onRunTarget,
}: TargetProps,
ref: ForwardedRef<TargetConfigurationDetailsHandle>
) => {
const isCompact = variant === 'compact';
const [collapsed, setCollapsed] = useState(true);
const handleCopyClick = async (copyText: string) => {
await window.navigator.clipboard.writeText(copyText);
};
const handleCollapseToggle = useCallback(
() => setCollapsed((collapsed) => !collapsed),
[setCollapsed]
);
useEffect(() => {
if (collapsed) {
onCollapse?.(targetName);
} else {
onExpand?.(targetName);
}
}, [collapsed, onCollapse, onExpand, projectName, targetName]);
useImperativeHandle(ref, () => ({
collapse: () => {
!collapsed && setCollapsed(true);
},
expand: () => {
collapsed && setCollapsed(false);
},
}));
let executorLink: string | null = null;
// TODO: Handle this better because this will not work with labs
if (targetConfiguration.executor?.startsWith('@nx/')) {
const packageName = targetConfiguration.executor
.split('/')[1]
.split(':')[0];
const executorName = targetConfiguration.executor
.split('/')[1]
.split(':')[1];
executorLink = `https://nx.dev/nx-api/${packageName}/executors/${executorName}`;
} else if (targetConfiguration.executor === 'nx:run-commands') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-commands`;
} else if (targetConfiguration.executor === 'nx:run-script') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-script`;
}
const singleCommand =
targetConfiguration.executor === 'nx:run-commands'
? targetConfiguration.command ?? targetConfiguration.options?.command
: null;
const options = useMemo(() => {
if (singleCommand) {
const { command, ...rest } = targetConfiguration.options;
return rest;
} else {
return targetConfiguration.options;
}
}, [targetConfiguration.options, singleCommand]);
const configurations = targetConfiguration.configurations;
const shouldRenderOptions =
options &&
(typeof options === 'object' ? Object.keys(options).length : true);
const shouldRenderConfigurations =
configurations &&
(typeof configurations === 'object'
? Object.keys(configurations).length
: true);
return (
<div className="rounded-md border border-slate-500 relative overflow-hidden">
<header
className={twMerge(
`group hover:bg-slate-200 dark:hover:bg-slate-800 cursor-pointer`,
isCompact ? 'px-2 py-1' : 'p-2',
!collapsed
? 'bg-slate-200 dark:bg-slate-800 border-b-2 border-slate-900/10 dark:border-slate-300/10 '
: ''
)}
onClick={handleCollapseToggle}
>
<div className="flex justify-between items-center">
<div className="flex items-center">
<h3 className="font-bold mr-2">{targetName}</h3>
{collapsed && (
<p className="text-slate-600 mr-2">
{singleCommand ? singleCommand : targetConfiguration.executor}
</p>
)}
</div>
<div className="flex items-center">
{onViewInTaskGraph && (
<EyeIcon
className={`h-4 w-4 mr-2 ${
collapsed
? 'hidden group-hover:inline-block'
: 'inline-block'
}`}
title="View in Task Graph"
onClick={(e) => {
e.stopPropagation();
onViewInTaskGraph({ projectName, targetName });
}}
/>
)}
{targetConfiguration.cache && (
<Tooltip
openAction="hover"
strategy="fixed"
content={(<PropertyInfoTooltip type="cacheable" />) as any}
>
<span className="rounded-full inline-block text-xs bg-sky-500 dark:bg-sky-800 px-2 text-slate-50 mr-2">
Cacheable
</span>
</Tooltip>
)}
{onRunTarget && (
<PlayIcon
className="h-5 w-5 mr-2"
onClick={(e) => {
e.stopPropagation();
onRunTarget({ projectName, targetName });
}}
/>
)}
{collapsed ? (
<ChevronDownIcon className="h-3 w-3" />
) : (
<ChevronUpIcon className="h-3 w-3" />
)}
</div>
</div>
{!collapsed && (
<div className="flex items-center text-sm mt-2">
<span className="flex-1 flex items-center">
<SourceInfo
data={sourceMap[`targets.${targetName}`]}
propertyKey={`targets.${targetName}`}
/>
</span>
<code className="ml-4 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300 font-mono px-2 py-1 rounded">
nx run {projectName}:{targetName}
</code>
<span className="ml-2">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`nx run ${projectName}:${targetName}`)
}
/>
</span>
</div>
)}
</header>
{/* body */}
{!collapsed && (
<div className="p-4 text-base">
<div className="mb-4 group">
<h4 className="mb-4">
{singleCommand ? (
<span className="font-bold">
Command
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`"command": "${singleCommand}"`)
}
/>
</span>
</span>
) : (
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="executors" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Executor</TooltipTriggerText>
</span>
</Tooltip>
)}
</h4>
<p className="pl-5">
{executorLink ? (
<ExternalLink
href={executorLink ?? 'https://nx.dev/nx-api'}
text={
singleCommand
? singleCommand
: targetConfiguration.executor
}
title="View Documentation"
/>
) : singleCommand ? (
singleCommand
) : (
targetConfiguration.executor
)}
</p>
</div>
{targetConfiguration.inputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="inputs" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Inputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"inputs": ${JSON.stringify(
targetConfiguration.inputs
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{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="hidden group-hover/line:inline pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.inputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{targetConfiguration.outputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="outputs" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Outputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"outputs": ${JSON.stringify(
targetConfiguration.outputs
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{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="hidden group-hover/line:inline pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
}) ?? <span>no outputs</span>}
</ul>
</div>
)}
{targetConfiguration.dependsOn && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="dependsOn" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Depends On</TooltipTriggerText>
</span>
</Tooltip>
<span className="hidden group-hover:inline ml-2 mb-1">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"dependsOn": ${JSON.stringify(
targetConfiguration.dependsOn
)}`
)
}
/>
</span>
</h4>
<ul className="list-disc pl-5 mb-4">
{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="hidden group-hover/line:inline pl-4 h-6">
{sourceInfo && (
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`}
/>
)}
</span>
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-bold">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.options.${propertyName}`
);
return sourceInfo ? (
<span className="pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.options.${propertyName}`}
/>
</span>
) : null;
}}
/>
</FadingCollapsible>
</div>
</>
) : (
''
)}
{shouldRenderConfigurations ? (
<>
<h4 className="py-2 mb-4">
<Tooltip
openAction="hover"
content={
(<PropertyInfoTooltip type="configurations" />) as any
}
>
<span className="font-bold">
<TooltipTriggerText>Configurations</TooltipTriggerText>
</span>
</Tooltip>{' '}
{targetConfiguration.defaultConfiguration && (
<span
className="ml-3 font-bold rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50 mr-6"
title="Default Configuration"
>
{targetConfiguration.defaultConfiguration}
</span>
)}
</h4>
<FadingCollapsible>
<JsonCodeBlock
data={targetConfiguration.configurations}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.configurations.${propertyName}`
);
return sourceInfo ? (
<span className="pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`}
/>{' '}
</span>
) : null;
}}
/>
</FadingCollapsible>
</>
) : (
''
)}
</div>
)}
</div>
);
}
);
export default TargetConfigurationDetails;

View File

@ -0,0 +1,45 @@
const path = require('path');
// nx-ignore-next-line
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
module.exports = {
content: [
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: 'class', // or 'media' or 'class'
theme: {
extend: {
typography: {
DEFAULT: {
css: {
'code::before': {
content: '',
},
'code::after': {
content: '',
},
'blockquote p:first-of-type::before': {
content: '',
},
'blockquote p:last-of-type::after': {
content: '',
},
},
},
},
},
},
variants: {
extend: {
translate: ['group-hover'],
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')({
strategy: 'class',
}),
],
};

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"lib": ["ES2022", "dom"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.storybook.json"
}
],
"extends": "../../tsconfig.base.json"
}

View File

@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
"**/*.stories.ts",
"**/*.stories.js",
"**/*.stories.jsx",
"**/*.stories.tsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}

View File

@ -0,0 +1,31 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"outDir": ""
},
"files": [
"../../node_modules/@nx/react/typings/styled-jsx.d.ts",
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.jsx",
"src/**/*.test.js"
],
"include": [
"src/**/*.stories.ts",
"src/**/*.stories.js",
"src/**/*.stories.jsx",
"src/**/*.stories.tsx",
"src/**/*.stories.mdx",
".storybook/*.js",
".storybook/*.ts"
]
}

View File

@ -33,6 +33,8 @@ import { InstallNxConsole } from './lib/tags/install-nx-console.component';
import { installNxConsole } from './lib/tags/install-nx-console.schema';
import { Persona, Personas } from './lib/tags/personas.component';
import { persona, personas } from './lib/tags/personas.schema';
import { ProjectDetails } from './lib/tags/project-details.component';
import { projectDetails } from './lib/tags/project-details.schema';
import {
ShortEmbeds,
shortEmbeds,
@ -83,6 +85,7 @@ export const getMarkdocCustomConfig = (
'install-nx-console': installNxConsole,
persona,
personas,
'project-details': projectDetails,
pill,
'short-embeds': shortEmbeds,
'short-video': shortVideo,
@ -112,6 +115,7 @@ export const getMarkdocCustomConfig = (
InstallNxConsole,
Persona,
Personas,
ProjectDetails,
Pill,
ShortEmbeds,
ShortVideo,

View File

@ -91,7 +91,7 @@ export function Graph({
}
return (
<div className="my-6 w-full place-content-center overflow-hidden rounded-md ring-1 ring-slate-100 dark:ring-slate-700">
<div className="my-6 w-full place-content-center overflow-hidden rounded-md ring-1 ring-slate-200 dark:ring-slate-700">
<div className="relative flex justify-center p-2 border-b border-slate-200 bg-slate-100/50 dark:border-slate-700 dark:bg-slate-700/50 font-bold">
{title}
</div>

View File

@ -0,0 +1,93 @@
import { useTheme } from '@nx/nx-dev/ui-theme';
import { JSX, ReactElement, useEffect, useState } from 'react';
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph/ui-project-details';
export function Loading() {
return (
<div className="flex h-[450px] w-full items-center justify-center">
<div
className="spinner-border inline-block h-8 w-8 animate-spin rounded-full border-4 border-slate-200 border-r-slate-400 dark:border-slate-700 dark:border-r-slate-500"
role="status"
>
<span className="sr-only">Loading...</span>
</div>
</div>
);
}
export function ProjectDetails({
height,
title,
jsonFile,
children,
}: {
height: string;
title: string;
jsonFile?: string;
children: ReactElement;
}): JSX.Element {
const [theme] = useTheme();
const [parsedProps, setParsedProps] = useState<any>();
const getData = async (path: string) => {
const response = await fetch('/documentation/' + path, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
setParsedProps(await response.json());
};
useEffect(() => {
if (jsonFile) {
getData(jsonFile);
}
}, [jsonFile, setParsedProps]);
if (!jsonFile && !parsedProps) {
if (!children || !children.hasOwnProperty('props')) {
return (
<div className="no-prose my-6 block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
<p className="mb-4">
No JSON provided for graph, use JSON code fence to embed data for
the graph.
</p>
</div>
);
}
try {
setParsedProps(JSON.parse(children?.props.children as any));
} catch {
return (
<div className="not-prose my-6 block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
<p className="mb-4">Could not parse JSON for graph:</p>
<pre className="p-4 text-sm">{children?.props.children as any}</pre>
</div>
);
}
}
if (!parsedProps) {
return <Loading />;
}
return (
<div className="w-full place-content-center overflow-hidden rounded-md ring-1 ring-slate-200 dark:ring-slate-700">
{title && (
<div className="relative flex justify-center p-2 border-b border-slate-200 bg-slate-100/50 dark:border-slate-700 dark:bg-slate-700/50 font-bold">
{title}
</div>
)}
<div
className={`not-prose ${
height ? `p-4 h-[${height}] overflow-y-auto` : 'p-4'
}`}
>
<ProjectDetailsUi
project={parsedProps.project}
sourceMap={parsedProps.sourceMap}
variant="compact"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
import { Schema } from '@markdoc/markdoc';
export const projectDetails: Schema = {
render: 'ProjectDetails',
children: [],
attributes: {
jsonFile: {
type: 'String',
},
title: {
type: 'String',
},
height: {
type: 'String',
},
},
};

View File

@ -40,6 +40,7 @@
"@nx/graph/ui-code-block": ["graph/ui-code-block/src/index.ts"],
"@nx/graph/ui-components": ["graph/ui-components/src/index.ts"],
"@nx/graph/ui-graph": ["graph/ui-graph/src/index.ts"],
"@nx/graph/ui-project-details": ["graph/ui-project-details/src/index.ts"],
"@nx/graph/ui-theme": ["graph/ui-theme/src/index.ts"],
"@nx/graph/ui-tooltips": ["graph/ui-tooltips/src/index.ts"],
"@nx/jest": ["packages/jest"],