diff --git a/docs/README.md b/docs/README.md index 01e830e7f40c6..0c5d1301ca025 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. diff --git a/graph/client/src/app/routes.tsx b/graph/client/src/app/routes.tsx index 0dbb4ec218841..5f9b99851245e 100644 --- a/graph/client/src/app/routes.tsx +++ b/graph/client/src/app/routes.tsx @@ -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(); diff --git a/graph/client/src/app/ui-components/project-details-modal.tsx b/graph/client/src/app/ui-components/project-details-modal.tsx index c2ee74de7d7e5..7ee2a4ca8a9bb 100644 --- a/graph/client/src/app/ui-components/project-details-modal.tsx +++ b/graph/client/src/app/ui-components/project-details-modal.tsx @@ -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 && (
- +
diff --git a/graph/project-details/src/index.ts b/graph/project-details/src/index.ts index 8bb12e02f3ec9..2ae2b30601c45 100644 --- a/graph/project-details/src/index.ts +++ b/graph/project-details/src/index.ts @@ -1,2 +1,2 @@ -export * from './lib/project-details'; +export * from './lib/project-details-wrapper'; export * from './lib/project-details-page'; diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx index 78ee2fde2d249..2c429ab826e84 100644 --- a/graph/project-details/src/lib/project-details-page.tsx +++ b/graph/project-details/src/lib/project-details-page.tsx @@ -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() {
{environment !== 'nx-console' ? ( -
+
) : null}
- + >
); diff --git a/graph/project-details/src/lib/project-details-wrapper.tsx b/graph/project-details/src/lib/project-details-wrapper.tsx new file mode 100644 index 0000000000000..d42f5acccc89b --- /dev/null +++ b/graph/project-details/src/lib/project-details-wrapper.tsx @@ -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; +} + +export function ProjectDetailsWrapper(props: ProjectDetailsProps) { + const projectDetailsRef = useRef(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 ( + + ); +} + +export default ProjectDetailsWrapper; diff --git a/graph/project-details/src/lib/project-details.tsx b/graph/project-details/src/lib/project-details.tsx deleted file mode 100644 index 7a4cbf97887b5..0000000000000 --- a/graph/project-details/src/lib/project-details.tsx +++ /dev/null @@ -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; -} - -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 ( - <> -
-

- {name}{' '} - {environment === 'nx-console' ? ( - - ) : null}{' '} -

-
- {projectData.tags ? ( -

- {projectData.tags?.map((tag) => ( - {tag} - ))} -

- ) : null} -

- Root: {root} -

- {displayType ? ( -

- Type: {displayType} -

- ) : null} -
-
-
-

- ) as any} - > - - Targets - - -

-
    - {Object.entries(projectData.targets ?? {}).map( - ([targetName, target]) => { - const props = { - projectName: name, - targetName: targetName, - targetConfiguration: target, - sourceMap, - }; - return ( -
  • - -
  • - ); - } - )} -
-
- - ); -} - -export default ProjectDetails; diff --git a/graph/project-details/src/lib/target/target-configuration-details.tsx b/graph/project-details/src/lib/target/target-configuration-details.tsx deleted file mode 100644 index 0708cb6064bdd..0000000000000 --- a/graph/project-details/src/lib/target/target-configuration-details.tsx +++ /dev/null @@ -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; -} - -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 ( -
-
-
-
-

{targetName}

- {collapsed && ( -

- {singleCommand ? singleCommand : targetConfiguration.executor} -

- )} -
-
- { - e.stopPropagation(); - viewInTaskGraph(); - }} - /> - {targetConfiguration.cache && ( - ) as any} - > - - Cacheable - - - )} - {environment === 'nx-console' && ( - { - e.stopPropagation(); - runTarget(); - }} - /> - )} - {collapsed ? ( - - ) : ( - - )} -
-
- {!collapsed && ( -
- - - - - nx run {projectName}:{targetName} - - - - handleCopyClick(`nx run ${projectName}:${targetName}`) - } - /> - -
- )} -
- {/* body */} - {!collapsed && ( -
-
-

- {singleCommand ? ( - - Command - - - handleCopyClick(`"command": "${singleCommand}"`) - } - /> - - - ) : ( - ) as any} - > - - Executor - - - )} -

-

- {executorLink ? ( - - ) : singleCommand ? ( - singleCommand - ) : ( - targetConfiguration.executor - )} -

-
- - {targetConfiguration.inputs && ( -
-

- ) as any} - > - - Inputs - - - - - handleCopyClick( - `"inputs": ${JSON.stringify( - targetConfiguration.inputs - )}` - ) - } - /> - -

-
    - {targetConfiguration.inputs.map((input) => { - const sourceInfo = selectSourceInfo( - sourceMap, - `targets.${targetName}.inputs` - ); - return ( -
  • - - {sourceInfo && ( - - - - )} - -
  • - ); - })} -
-
- )} - {targetConfiguration.outputs && ( -
-

- ) as any} - > - - Outputs - - - - - handleCopyClick( - `"outputs": ${JSON.stringify( - targetConfiguration.outputs - )}` - ) - } - /> - -

-
    - {targetConfiguration.outputs?.map((output) => { - const sourceInfo = selectSourceInfo( - sourceMap, - `targets.${targetName}.outputs` - ); - return ( -
  • - - {sourceInfo && ( - - - - )} - -
  • - ); - }) ?? no outputs} -
-
- )} - {targetConfiguration.dependsOn && ( -
-

- ) as any} - > - - Depends On - - - - - handleCopyClick( - `"dependsOn": ${JSON.stringify( - targetConfiguration.dependsOn - )}` - ) - } - /> - -

-
    - {targetConfiguration.dependsOn.map((dep) => { - const sourceInfo = selectSourceInfo( - sourceMap, - `targets.${targetName}.dependsOn` - ); - - return ( -
  • - - - {sourceInfo && ( - - )} - - -
  • - ); - })} -
-
- )} - - {shouldRenderOptions ? ( - <> -

- ) as any} - > - - Options - - -

-
- - { - const sourceInfo = selectSourceInfo( - sourceMap, - `targets.${targetName}.options.${propertyName}` - ); - return sourceInfo ? ( - - - - ) : null; - }} - /> - -
- - ) : ( - '' - )} - - {shouldRenderConfigurations ? ( - <> -

- ) as any - } - > - - Configurations - - {' '} - {targetConfiguration.defaultConfiguration && ( - - {targetConfiguration.defaultConfiguration} - - )} -

- - { - const sourceInfo = selectSourceInfo( - sourceMap, - `targets.${targetName}.configurations.${propertyName}` - ); - return sourceInfo ? ( - - {' '} - - ) : null; - }} - /> - - - ) : ( - '' - )} -
- )} -
- ); -} - -export default TargetConfigurationDetails; diff --git a/graph/ui-project-details/.babelrc b/graph/ui-project-details/.babelrc new file mode 100644 index 0000000000000..1ea870ead410c --- /dev/null +++ b/graph/ui-project-details/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/graph/ui-project-details/.eslintrc.json b/graph/ui-project-details/.eslintrc.json new file mode 100644 index 0000000000000..a39ac5d057803 --- /dev/null +++ b/graph/ui-project-details/.eslintrc.json @@ -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": {} + } + ] +} diff --git a/graph/ui-project-details/.storybook/main.ts b/graph/ui-project-details/.storybook/main.ts new file mode 100644 index 0000000000000..1556e24dbedf6 --- /dev/null +++ b/graph/ui-project-details/.storybook/main.ts @@ -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 diff --git a/graph/ui-project-details/.storybook/preview.ts b/graph/ui-project-details/.storybook/preview.ts new file mode 100644 index 0000000000000..195b052493edc --- /dev/null +++ b/graph/ui-project-details/.storybook/preview.ts @@ -0,0 +1 @@ +import './tailwind.css'; diff --git a/graph/ui-project-details/.storybook/tailwind.css b/graph/ui-project-details/.storybook/tailwind.css new file mode 100644 index 0000000000000..23d597fe51b0b --- /dev/null +++ b/graph/ui-project-details/.storybook/tailwind.css @@ -0,0 +1,3 @@ +@tailwind components; +@tailwind base; +@tailwind utilities; diff --git a/graph/ui-project-details/README.md b/graph/ui-project-details/README.md new file mode 100644 index 0000000000000..9416b11259370 --- /dev/null +++ b/graph/ui-project-details/README.md @@ -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). diff --git a/graph/ui-project-details/postcss.config.js b/graph/ui-project-details/postcss.config.js new file mode 100644 index 0000000000000..cc1b086df2183 --- /dev/null +++ b/graph/ui-project-details/postcss.config.js @@ -0,0 +1,9 @@ +const { join } = require('path'); +module.exports = { + plugins: { + tailwindcss: { + config: join(__dirname, 'tailwind.config.js'), + }, + autoprefixer: {}, + }, +}; diff --git a/graph/ui-project-details/project.json b/graph/ui-project-details/project.json new file mode 100644 index 0000000000000..7ebc69483877e --- /dev/null +++ b/graph/ui-project-details/project.json @@ -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" + } + } + } + } +} diff --git a/graph/ui-project-details/src/index.ts b/graph/ui-project-details/src/index.ts new file mode 100644 index 0000000000000..c496e5eaaef2d --- /dev/null +++ b/graph/ui-project-details/src/index.ts @@ -0,0 +1 @@ +export * from './lib/project-details/project-details'; diff --git a/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx b/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx new file mode 100644 index 0000000000000..52583bded47c8 --- /dev/null +++ b/graph/ui-project-details/src/lib/project-details/project-details.stories.tsx @@ -0,0 +1,210 @@ +import type { Meta } from '@storybook/react'; +import { ProjectDetails } from './project-details'; + +const meta: Meta = { + 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', + ], + }, + }, +}; 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 new file mode 100644 index 0000000000000..297409121df80 --- /dev/null +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -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; + 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 + ) => { + const isCompact = variant === 'compact'; + const projectTargets = Object.keys(projectData.targets ?? {}); + const targetRefs = useRef( + projectTargets.reduce((acc, targetName) => { + acc[targetName] = createRef(); + return acc; + }, {} as Record>) + ); + + 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 ( + <> +
+

+ {name}{' '} + {onViewInProjectGraph ? ( + onViewInProjectGraph({ projectName: name })} + > + ) : null}{' '} +

+
+ {projectData.tags ? ( +

+ {projectData.tags?.map((tag) => ( + + {tag} + + ))} +

+ ) : null} +

+ Root: {root} +

+ {displayType ? ( +

+ Type: {displayType} +

+ ) : null} +
+
+
+

+ ) as any} + > + + Targets + + +

+
    + {projectTargets.map((targetName) => { + const target = projectData.targets?.[targetName]; + return target && targetRefs.current[targetName] ? ( +
  • + +
  • + ) : null; + })} +
+
+ + ); + } +); + +export default ProjectDetails; diff --git a/graph/project-details/src/lib/target/copy-to-clipboard.tsx b/graph/ui-project-details/src/lib/target-configuration-details/copy-to-clipboard.tsx similarity index 100% rename from graph/project-details/src/lib/target/copy-to-clipboard.tsx rename to graph/ui-project-details/src/lib/target-configuration-details/copy-to-clipboard.tsx diff --git a/graph/project-details/src/lib/target/fading-collapsible.tsx b/graph/ui-project-details/src/lib/target-configuration-details/fading-collapsible.tsx similarity index 100% rename from graph/project-details/src/lib/target/fading-collapsible.tsx rename to graph/ui-project-details/src/lib/target-configuration-details/fading-collapsible.tsx diff --git a/graph/project-details/src/lib/target/source-info.tsx b/graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx similarity index 85% rename from graph/project-details/src/lib/target/source-info.tsx rename to graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx index 53a4b43411af2..f6e983085fbb3 100644 --- a/graph/project-details/src/lib/target/source-info.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx @@ -19,8 +19,8 @@ export function SourceInfo(props: { ( ) as any } @@ -29,7 +29,8 @@ export function SourceInfo(props: { {/* */} {/**/} - {isTarget ? 'Created' : 'Set'} by {props.data[1]} from {props.data[0]} + {isTarget ? 'Created' : 'Set'} by {props.data?.[1]} from{' '} + {props.data?.[0]} diff --git a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx new file mode 100644 index 0000000000000..af52a4e54c481 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.tsx @@ -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; + 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 + ) => { + 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 ( +
+
+
+
+

{targetName}

+ {collapsed && ( +

+ {singleCommand ? singleCommand : targetConfiguration.executor} +

+ )} +
+
+ {onViewInTaskGraph && ( + { + e.stopPropagation(); + onViewInTaskGraph({ projectName, targetName }); + }} + /> + )} + {targetConfiguration.cache && ( + ) as any} + > + + Cacheable + + + )} + {onRunTarget && ( + { + e.stopPropagation(); + onRunTarget({ projectName, targetName }); + }} + /> + )} + {collapsed ? ( + + ) : ( + + )} +
+
+ {!collapsed && ( +
+ + + + + nx run {projectName}:{targetName} + + + + handleCopyClick(`nx run ${projectName}:${targetName}`) + } + /> + +
+ )} +
+ {/* body */} + {!collapsed && ( +
+
+

+ {singleCommand ? ( + + Command + + + handleCopyClick(`"command": "${singleCommand}"`) + } + /> + + + ) : ( + ) as any} + > + + Executor + + + )} +

+

+ {executorLink ? ( + + ) : singleCommand ? ( + singleCommand + ) : ( + targetConfiguration.executor + )} +

+
+ + {targetConfiguration.inputs && ( +
+

+ ) as any} + > + + Inputs + + + + + handleCopyClick( + `"inputs": ${JSON.stringify( + targetConfiguration.inputs + )}` + ) + } + /> + +

+
    + {targetConfiguration.inputs.map((input, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.inputs` + ); + return ( +
  • + + {sourceInfo && ( + + + + )} + +
  • + ); + })} +
+
+ )} + {targetConfiguration.outputs && ( +
+

+ ) as any} + > + + Outputs + + + + + handleCopyClick( + `"outputs": ${JSON.stringify( + targetConfiguration.outputs + )}` + ) + } + /> + +

+
    + {targetConfiguration.outputs?.map((output, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.outputs` + ); + return ( +
  • + + {sourceInfo && ( + + + + )} + +
  • + ); + }) ?? no outputs} +
+
+ )} + {targetConfiguration.dependsOn && ( +
+

+ ) as any} + > + + Depends On + + + + + handleCopyClick( + `"dependsOn": ${JSON.stringify( + targetConfiguration.dependsOn + )}` + ) + } + /> + +

+
    + {targetConfiguration.dependsOn.map((dep, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.dependsOn` + ); + + return ( +
  • + + + {sourceInfo && ( + + )} + + +
  • + ); + })} +
+
+ )} + + {shouldRenderOptions ? ( + <> +

+ ) as any} + > + + Options + + +

+
+ + { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.options.${propertyName}` + ); + return sourceInfo ? ( + + + + ) : null; + }} + /> + +
+ + ) : ( + '' + )} + + {shouldRenderConfigurations ? ( + <> +

+ ) as any + } + > + + Configurations + + {' '} + {targetConfiguration.defaultConfiguration && ( + + {targetConfiguration.defaultConfiguration} + + )} +

+ + { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.configurations.${propertyName}` + ); + return sourceInfo ? ( + + {' '} + + ) : null; + }} + /> + + + ) : ( + '' + )} +
+ )} +
+ ); + } +); + +export default TargetConfigurationDetails; diff --git a/graph/project-details/src/lib/target/target-configuration-details.util.spec.ts b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.util.spec.ts similarity index 100% rename from graph/project-details/src/lib/target/target-configuration-details.util.spec.ts rename to graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.util.spec.ts diff --git a/graph/project-details/src/lib/target/target-configuration-details.util.ts b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.util.ts similarity index 100% rename from graph/project-details/src/lib/target/target-configuration-details.util.ts rename to graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.util.ts diff --git a/graph/project-details/src/lib/target/target-configuration-property.tsx b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx similarity index 100% rename from graph/project-details/src/lib/target/target-configuration-property.tsx rename to graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx diff --git a/graph/project-details/src/lib/target/ui/tooltip-trigger-text.tsx b/graph/ui-project-details/src/lib/target-configuration-details/tooltip-trigger-text.tsx similarity index 100% rename from graph/project-details/src/lib/target/ui/tooltip-trigger-text.tsx rename to graph/ui-project-details/src/lib/target-configuration-details/tooltip-trigger-text.tsx diff --git a/graph/ui-project-details/tailwind.config.js b/graph/ui-project-details/tailwind.config.js new file mode 100644 index 0000000000000..18a8d985ec32a --- /dev/null +++ b/graph/ui-project-details/tailwind.config.js @@ -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', + }), + ], +}; diff --git a/graph/ui-project-details/tsconfig.json b/graph/ui-project-details/tsconfig.json new file mode 100644 index 0000000000000..6b4f8f64d222d --- /dev/null +++ b/graph/ui-project-details/tsconfig.json @@ -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" +} diff --git a/graph/ui-project-details/tsconfig.lib.json b/graph/ui-project-details/tsconfig.lib.json new file mode 100644 index 0000000000000..8c1bec17db74e --- /dev/null +++ b/graph/ui-project-details/tsconfig.lib.json @@ -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"] +} diff --git a/graph/ui-project-details/tsconfig.storybook.json b/graph/ui-project-details/tsconfig.storybook.json new file mode 100644 index 0000000000000..2da3caee121ed --- /dev/null +++ b/graph/ui-project-details/tsconfig.storybook.json @@ -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" + ] +} diff --git a/nx-dev/ui-markdoc/src/index.ts b/nx-dev/ui-markdoc/src/index.ts index a2a6662a81bf7..73e78b61feff9 100644 --- a/nx-dev/ui-markdoc/src/index.ts +++ b/nx-dev/ui-markdoc/src/index.ts @@ -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, diff --git a/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx index d88d4a13738c9..128bf6995e150 100644 --- a/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx +++ b/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx @@ -91,7 +91,7 @@ export function Graph({ } return ( -
+
{title}
diff --git a/nx-dev/ui-markdoc/src/lib/tags/project-details.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/project-details.component.tsx new file mode 100644 index 0000000000000..c8996387f3af6 --- /dev/null +++ b/nx-dev/ui-markdoc/src/lib/tags/project-details.component.tsx @@ -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 ( +
+
+ Loading... +
+
+ ); +} + +export function ProjectDetails({ + height, + title, + jsonFile, + children, +}: { + height: string; + title: string; + jsonFile?: string; + children: ReactElement; +}): JSX.Element { + const [theme] = useTheme(); + const [parsedProps, setParsedProps] = useState(); + 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 ( +
+

+ No JSON provided for graph, use JSON code fence to embed data for + the graph. +

+
+ ); + } + + try { + setParsedProps(JSON.parse(children?.props.children as any)); + } catch { + return ( +
+

Could not parse JSON for graph:

+
{children?.props.children as any}
+
+ ); + } + } + if (!parsedProps) { + return ; + } + + return ( +
+ {title && ( +
+ {title} +
+ )} +
+ +
+
+ ); +} diff --git a/nx-dev/ui-markdoc/src/lib/tags/project-details.schema.ts b/nx-dev/ui-markdoc/src/lib/tags/project-details.schema.ts new file mode 100644 index 0000000000000..0795771765da3 --- /dev/null +++ b/nx-dev/ui-markdoc/src/lib/tags/project-details.schema.ts @@ -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', + }, + }, +}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 7ea9e376d1483..0672131ea978a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"],