feat(misc): add layout for project details view (#21172)

This commit is contained in:
Jason Jean 2024-01-17 13:02:45 -05:00 committed by GitHub
parent ea3c2426d3
commit 49cff89908
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 417 additions and 136 deletions

View File

@ -1,4 +1,4 @@
import { themeInit } from './theme-resolver';
import { themeInit } from '@nx/graph/ui-theme';
import { rankDirInit } from './rankdir-resolver';
import { RouterProvider } from 'react-router-dom';
import { getRouter } from './get-router';

View File

@ -1,7 +1,7 @@
import { GraphService } from '@nx/graph/ui-graph';
import { selectValueByThemeStatic } from '../theme-resolver';
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
import { getEnvironmentConfig } from '@nx/graph/shared';
import { selectValueByThemeStatic } from '@nx/graph/ui-theme';
let graphService: GraphService;

View File

@ -7,7 +7,7 @@ import classNames from 'classnames';
import { DebuggerPanel } from './ui-components/debugger-panel';
import { getGraphService } from './machines/graph.service';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { ThemePanel } from './feature-projects/panels/theme-panel';
import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme';
import { Dropdown } from '@nx/graph/ui-components';
import { useCurrentPath } from './hooks/use-current-path';
import { ExperimentalFeature } from './ui-components/experimental-feature';
@ -31,6 +31,9 @@ export function Shell(): JSX.Element {
const environment = useEnvironmentConfig();
const environmentConfig = useEnvironmentConfig();
function onThemeChange(theme: Theme) {
graphService.theme = theme === 'system' ? getSystemTheme() : theme;
}
const navigate = useNavigate();
const currentPath = useCurrentPath();
@ -71,7 +74,7 @@ export function Shell(): JSX.Element {
}
return (
<>
<div className="flex max-h-screen w-full">
<div
className={`${
environmentConfig.environment === 'nx-console'
@ -124,7 +127,7 @@ export function Shell(): JSX.Element {
<RankdirPanel />
</ExperimentalFeature>
<ThemePanel />
<ThemePanel onThemeChange={onThemeChange} />
</div>
</div>
@ -202,6 +205,6 @@ export function Shell(): JSX.Element {
</Tooltip>
</div>
</div>
</>
</div>
);
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="h-full w-full overflow-hidden">
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nx Workspace Project Graph</title>
@ -25,9 +25,7 @@
</script>
</head>
<body
class="h-full w-full overflow-hidden bg-white text-slate-500 dark:bg-slate-900 dark:text-slate-400"
>
<div class="flex h-full w-full overflow-hidden p-0" id="app"></div>
<body class="bg-white text-slate-500 dark:bg-slate-900 dark:text-slate-400">
<div class="flex p-0" id="app"></div>
</body>
</html>

View File

@ -1,8 +1,10 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
import { useRouteLoaderData } from 'react-router-dom';
import { Link, useRouteLoaderData } from 'react-router-dom';
import ProjectDetails from './project-details';
import { useEnvironmentConfig, useRouteConstructor } from '@nx/graph/shared';
import { ThemePanel } from '@nx/graph/ui-theme';
export function ProjectDetailsPage() {
const { project, sourceMap } = useRouteLoaderData(
@ -12,5 +14,49 @@ export function ProjectDetailsPage() {
sourceMap: Record<string, string[]>;
};
return ProjectDetails({ project, sourceMap });
const environment = useEnvironmentConfig()?.environment;
const routeConstructor = useRouteConstructor();
return (
<div className="flex flex-col justify-center w-full text-slate-700 dark:text-slate-400">
{environment !== 'nx-console' ? (
<header className="flex w-full justify-center items-center px-4 py-2 border-b-2 border-slate-900/10 mb-16 dark:border-slate-300/10">
<div className="flex flex-grow items-center justify-between max-w-6xl">
<svg
className="h-10 w-auto text-slate-900 dark:text-white"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>Nx</title>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
<ul className="flex items-center">
<li>
<Link
to={routeConstructor(
`/projects/${encodeURIComponent(project.name)}`,
true
)}
title="View in Graph"
>
Graph
</Link>
</li>
<li className="ml-4">
<ThemePanel />
</li>
</ul>
</div>
</header>
) : null}
<div className="flex-grow mx-auto w-full max-w-6xl px-8 mb-8">
<ProjectDetails
project={project}
sourceMap={sourceMap}
></ProjectDetails>
</div>
</div>
);
}

View File

@ -12,7 +12,6 @@ import {
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import PropertyRenderer from './property-renderer';
import Target from './target';
export interface ProjectDetailsProps {
@ -30,7 +29,12 @@ export function ProjectDetails({
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const routeContructor = useRouteConstructor();
const routeConstructor = useRouteConstructor();
const displayType =
projectData.projectType &&
projectData.projectType?.charAt(0)?.toUpperCase() +
projectData.projectType?.slice(1);
const viewInProjectGraph = () => {
if (environment === 'nx-console') {
@ -41,25 +45,40 @@ export function ProjectDetails({
},
});
} else {
navigate(routeContructor(`/projects/${encodeURIComponent(name)}`, true));
navigate(routeConstructor(`/projects/${encodeURIComponent(name)}`, true));
}
};
return (
<div className="m-4 overflow-auto w-full">
<h1 className="text-2xl flex items-center gap-2">
<>
<header className="border-b border-slate-900/10 mb-4 dark:border-slate-300/10">
<h1 className="text-6xl flex items-center mb-4 gap-2">
{name}{' '}
{environment === 'nx-console' ? (
<EyeIcon className="h-5 w-5" onClick={viewInProjectGraph}></EyeIcon>
) : null}{' '}
</h1>
<h2 className="text-lg pl-6 mb-3 flex flex-row gap-2">
{root}{' '}
<div className="p-4">
{projectData.tags ? (
<p>
{projectData.tags?.map((tag) => (
<p className="bg-slate-300">{tag}</p>
<span className="bg-slate-300 rounded-md p-1 mr-2">{tag}</span>
))}
</h2>
</p>
) : null}
<p>
<span className="font-bold">Root:</span> {root}
</p>
{displayType ? (
<p>
<span className="font-bold">Type:</span> {displayType}
</p>
) : null}
</div>
</header>
<div>
<div className="mb-2">
<h2 className="text-xl mb-2">Targets</h2>
<h2 className="text-3xl mb-2">Targets</h2>
<ul>
{Object.entries(projectData.targets ?? {}).map(
([targetName, target]) => {
const props = {
@ -68,30 +87,16 @@ export function ProjectDetails({
targetConfiguration: target,
sourceMap,
};
return <Target {...props} />;
return (
<li className="mb-4">
<Target {...props} />
</li>
);
}
)}
</ul>
</div>
{Object.entries(projectData).map(([key, value]) => {
if (
key === 'targets' ||
key === 'root' ||
key === 'name' ||
key === '$schema' ||
key === 'tags' ||
key === 'files' ||
key === 'sourceRoot'
)
return undefined;
return PropertyRenderer({
propertyKey: key,
propertyValue: value,
sourceMap,
});
})}
</div>
</div>
</>
);
}

View File

@ -120,72 +120,49 @@ export function Target({
: true);
return (
<div className="ml-3 mb-3 rounded-md border border-slate-500 relative overflow-hidden">
{/* header */}
<div className="group hover:bg-slate-800 px-2 cursor-pointer ">
<h3
className="text-lg font-bold flex items-center gap-2"
<div className="rounded-md border border-slate-500 relative overflow-hidden">
<header
className={`flex space-between group hover:bg-slate-200 dark:hover:bg-slate-800 p-2 cursor-pointer items-center ${
!collapsed
? 'bg-slate-200 dark:bg-slate-800 border-b-2 border-slate-900/10 dark:border-slate-300/10 '
: ''
}`}
onClick={toggleCollapsed}
>
{targetName}{' '}
<h4 className="text-sm text-slate-600">
<h3 className="font-bold mr-2">{targetName}</h3>
<p className="text-slate-600 mr-2">
{targetConfiguration?.command ??
targetConfiguration.options?.command ??
targetConfiguration.executor}
</h4>
<span
className={
collapsed ? 'hidden group-hover:inline-flex' : 'inline-flex'
}
>
<span
className={`inline-flex justify-center rounded-md p-1 hover:bg-slate-100 hover:dark:bg-slate-700
}`}
>
<EyeIcon
className="h-4 w-4"
onClick={(e) => {
e.stopPropagation();
viewInTaskGraph();
}}
></EyeIcon>
</span>
{environment === 'nx-console' && (
<span
className={`inline-flex justify-center rounded-md p-1 hover:bg-slate-100 hover:dark:bg-slate-700
}`}
>
<PlayIcon
className="h-4 w-4"
onClick={(e) => {
e.stopPropagation();
runTarget();
}}
/>
</span>
)}
</span>
</p>
{targetConfiguration.cache && (
<span className="rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50 ml-auto mr-6">
<span className="rounded-full inline-block text-xs bg-sky-500 px-2 text-slate-50">
Cacheable
</span>
)}
</h3>
<div className="absolute top-2 right-3" onClick={toggleCollapsed}>
{collapsed ? (
<ChevronUpIcon className="h-3 w-3" />
) : (
<ChevronDownIcon className="h-3 w-3" />
<span className="flex items-center ml-auto">
<EyeIcon
className="h-4 w-4 mr-2 hidden group-hover:inline-block"
title="View in Task Graph"
onClick={viewInTaskGraph}
></EyeIcon>
{environment === 'nx-console' && (
<PlayIcon className="h-5 w-5" onClick={runTarget} />
)}
</div>
</div>
{collapsed ? (
<ChevronDownIcon className="h-3 w-3" />
) : (
<ChevronUpIcon className="h-3 w-3" />
)}
</span>
</header>
{/* body */}
{!collapsed && (
<div className="pl-5 text-base pb-6 pt-2 ">
<div className="p-4 text-base">
{targetConfiguration.inputs && (
<>
<h4 className="font-bold">Inputs</h4>
<ul className="list-disc pl-5">
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.inputs.map((input) => (
<li> {input.toString()} </li>
))}
@ -194,8 +171,8 @@ export function Target({
)}
{targetConfiguration.outputs && (
<>
<h4 className="font-bold pt-2">Outputs</h4>
<ul className="list-disc pl-5">
<h4 className="font-bold">Outputs</h4>
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.outputs?.map((output) => (
<li> {output.toString()} </li>
)) ?? <span>no outputs</span>}
@ -204,8 +181,8 @@ export function Target({
)}
{targetConfiguration.dependsOn && (
<>
<h4 className="font-bold py-2">Depends On</h4>
<ul className="list-disc pl-5">
<h4 className="font-bold">Depends On</h4>
<ul className="list-disc pl-5 mb-4">
{targetConfiguration.dependsOn.map((dep) => (
<li> {dep.toString()} </li>
))}
@ -214,7 +191,8 @@ export function Target({
)}
{shouldRenderOptions ? (
<>
<h4 className="font-bold py-2">Options</h4>
<h4 className="font-bold mb-2">Options</h4>
<div className="mb-4">
<FadingCollapsible>
<Fence
language="json"
@ -228,6 +206,7 @@ export function Target({
{JSON.stringify(targetConfiguration.options, null, 2)}
</Fence>
</FadingCollapsible>
</div>
</>
) : (
''

12
graph/ui-theme/.babelrc Normal file
View File

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

View File

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

View File

@ -0,0 +1,19 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
const config: StorybookConfig = {
stories: ['../src/lib/**/*.stories.@(mdx|js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials', '@nx/react/plugins/storybook'],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: true,
},
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs

View File

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

View File

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

7
graph/ui-theme/README.md Normal file
View File

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

View File

@ -0,0 +1,8 @@
module.exports = {
plugins: {
tailwindcss: {
config: './graph/ui-theme/tailwind.config.js',
},
autoprefixer: {},
},
};

View File

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

View File

@ -0,0 +1,2 @@
export * from './lib/theme-panel';
export * from './lib/theme-resolver';

View File

@ -3,7 +3,16 @@ import { ThemePanel } from './theme-panel';
const meta: Meta<typeof ThemePanel> = {
component: ThemePanel,
title: 'Project Graph/ThemePanel',
decorators: [
(Story) => (
<div className="bg-white dark:bg-slate-800 block h-auto w-auto border-2 px-8 py-4">
<div className="flex justify-end">
<Story className="justify-items-end" />
</div>
</div>
),
],
title: 'ThemePanel',
};
export default meta;

View File

@ -6,19 +6,23 @@ import {
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
import { Fragment, useEffect, useState } from 'react';
import {
localStorageThemeKey,
Theme,
themeResolver,
} from '../../theme-resolver';
import { localStorageThemeKey, Theme, themeResolver } from './theme-resolver';
export function ThemePanel(): JSX.Element {
export function ThemePanel({
onThemeChange,
}: {
onThemeChange?: (theme: Theme) => void;
}): JSX.Element {
const [theme, setTheme] = useState(
(localStorage.getItem(localStorageThemeKey) as Theme) || 'system'
);
useEffect(() => {
themeResolver(theme);
if (onThemeChange) {
onThemeChange(theme);
}
}, [theme]);
return (

View File

@ -1,5 +1,3 @@
import { getGraphService } from './machines/graph.service';
const htmlEl = document.documentElement;
export const localStorageThemeKey = 'nx-dep-graph-theme';
export type Theme = 'light' | 'dark' | 'system';
@ -27,6 +25,11 @@ export function themeInit() {
themeResolver(theme);
}
export function getSystemTheme() {
const darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
return darkMedia.matches ? 'dark' : 'light';
}
export function themeResolver(theme: Theme) {
if (!('matchMedia' in window)) {
return;
@ -46,8 +49,6 @@ export function themeResolver(theme: Theme) {
}
localStorage.setItem(localStorageThemeKey, theme);
getGraphService().theme = currentTheme;
}
export function selectValueByThemeDynamic<T>(

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
"strict": true,
"lib": ["DOM", "es2022"]
},
"files": [],
"include": [],

View File

@ -39,6 +39,7 @@
"@nx/graph/shared": ["graph/shared/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-theme": ["graph/ui-theme/src/index.ts"],
"@nx/graph/ui-tooltips": ["graph/ui-tooltips/src/index.ts"],
"@nx/jest": ["packages/jest"],
"@nx/jest/*": ["packages/jest/*"],