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' ? (
-
+
);
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 (
- <>
-
-
-
- ) 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 (
-
-
- {/* 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 (
+ <>
+
+
+
+ ) 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 (
+
+
+ {/* 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 (
+
+ );
+}
+
+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"],