diff --git a/graph/client/src/app/app.tsx b/graph/client/src/app/app.tsx index 176498dc885158..d66004056ed971 100644 --- a/graph/client/src/app/app.tsx +++ b/graph/client/src/app/app.tsx @@ -1,4 +1,4 @@ -import { themeInit } from './theme-resolver'; +import { themeInit } from '@nx/ui-theme'; import { rankDirInit } from './rankdir-resolver'; import { RouterProvider } from 'react-router-dom'; import { getRouter } from './get-router'; diff --git a/graph/client/src/app/machines/graph.service.ts b/graph/client/src/app/machines/graph.service.ts index b067b5158efb5e..19476497d51fdf 100644 --- a/graph/client/src/app/machines/graph.service.ts +++ b/graph/client/src/app/machines/graph.service.ts @@ -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/ui-theme'; let graphService: GraphService; diff --git a/graph/client/src/app/shell.tsx b/graph/client/src/app/shell.tsx index 66aa63a6c15dbe..41a162184a304e 100644 --- a/graph/client/src/app/shell.tsx +++ b/graph/client/src/app/shell.tsx @@ -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/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,13 @@ export function Shell(): JSX.Element { const environment = useEnvironmentConfig(); const environmentConfig = useEnvironmentConfig(); + function onThemeChange(theme: Theme) { + if (theme !== 'system') { + graphService.theme = theme; + } else { + graphService.theme = getSystemTheme(); + } + } const navigate = useNavigate(); const currentPath = useCurrentPath(); @@ -71,7 +78,7 @@ export function Shell(): JSX.Element { } return ( - <> +
- +
@@ -202,6 +209,6 @@ export function Shell(): JSX.Element { - + ); } diff --git a/graph/client/src/index.html b/graph/client/src/index.html index 63a22e07a22ba5..e223588ff7b238 100644 --- a/graph/client/src/index.html +++ b/graph/client/src/index.html @@ -1,5 +1,5 @@ - + Nx Workspace Project Graph @@ -25,9 +25,7 @@ - -
+ +
diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx index c1e66eb8292a30..634034b66ff214 100644 --- a/graph/project-details/src/lib/project-details-page.tsx +++ b/graph/project-details/src/lib/project-details-page.tsx @@ -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/ui-theme'; export function ProjectDetailsPage() { const { project, sourceMap } = useRouteLoaderData( @@ -12,5 +14,49 @@ export function ProjectDetailsPage() { sourceMap: Record; }; - return ProjectDetails({ project, sourceMap }); + const environment = useEnvironmentConfig()?.environment; + + const routeConstructor = useRouteConstructor(); + + return ( +
+ {environment !== 'nx-console' ? ( +
+
+ + Nx + + +
    +
  • + + Graph + +
  • +
  • + +
  • +
+
+
+ ) : null} +
+ +
+
+ ); } diff --git a/graph/project-details/src/lib/project-details.tsx b/graph/project-details/src/lib/project-details.tsx index d11dd7c0b39e1f..42e8affe7c97da 100644 --- a/graph/project-details/src/lib/project-details.tsx +++ b/graph/project-details/src/lib/project-details.tsx @@ -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 ( -
-

- {name}{' '} - -

-

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

{tag}

- ))} -

+ <> +
+

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

+
+ {projectData.tags ? ( +

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

+ ) : null} +

+ Root: {root} +

+ {displayType ? ( +

+ Type: {displayType} +

+ ) : null} +
+
-
-

Targets

+

Targets

+
    {Object.entries(projectData.targets ?? {}).map( ([targetName, target]) => { const props = { @@ -68,30 +87,16 @@ export function ProjectDetails({ targetConfiguration: target, sourceMap, }; - return ; + return ( +
  • + +
  • + ); } )} -
- {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, - }); - })} +
-
+ ); } diff --git a/graph/project-details/src/lib/target.tsx b/graph/project-details/src/lib/target.tsx index 917e5437212a65..f52dfa269dd4b7 100644 --- a/graph/project-details/src/lib/target.tsx +++ b/graph/project-details/src/lib/target.tsx @@ -120,72 +120,49 @@ export function Target({ : true); return ( -
- {/* header */} -
-

- {targetName}{' '} -

- {targetConfiguration?.command ?? - targetConfiguration.options?.command ?? - targetConfiguration.executor} -

- - - { - e.stopPropagation(); - viewInTaskGraph(); - }} - > - - {environment === 'nx-console' && ( - - { - e.stopPropagation(); - runTarget(); - }} - /> - - )} +
+
+

{targetName}

+

+ {targetConfiguration?.command ?? + targetConfiguration.options?.command ?? + targetConfiguration.executor} +

+ {targetConfiguration.cache && ( + + Cacheable - {targetConfiguration.cache && ( - - Cacheable - + )} + + + {environment === 'nx-console' && ( + )} - -
{collapsed ? ( - - ) : ( + ) : ( + )} -
-
+
+ {/* body */} {!collapsed && ( -
+
{targetConfiguration.inputs && ( <>

Inputs

-
    +
      {targetConfiguration.inputs.map((input) => (
    • {input.toString()}
    • ))} @@ -194,8 +171,8 @@ export function Target({ )} {targetConfiguration.outputs && ( <> -

      Outputs

      -
        +

        Outputs

        +
          {targetConfiguration.outputs?.map((output) => (
        • {output.toString()}
        • )) ?? no outputs} @@ -204,8 +181,8 @@ export function Target({ )} {targetConfiguration.dependsOn && ( <> -

          Depends On

          -
            +

            Depends On

            +
              {targetConfiguration.dependsOn.map((dep) => (
            • {dep.toString()}
            • ))} @@ -214,20 +191,22 @@ export function Target({ )} {shouldRenderOptions ? ( <> -

              Options

              - - - {JSON.stringify(targetConfiguration.options, null, 2)} - - +

              Options

              +
              + + + {JSON.stringify(targetConfiguration.options, null, 2)} + + +
              ) : ( '' diff --git a/graph/ui-theme/.babelrc b/graph/ui-theme/.babelrc new file mode 100644 index 00000000000000..1ea870ead410c4 --- /dev/null +++ b/graph/ui-theme/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/graph/ui-theme/.eslintrc.json b/graph/ui-theme/.eslintrc.json new file mode 100644 index 00000000000000..a39ac5d0578035 --- /dev/null +++ b/graph/ui-theme/.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-theme/.storybook/main.ts b/graph/ui-theme/.storybook/main.ts new file mode 100644 index 00000000000000..8dcaad0fd463b3 --- /dev/null +++ b/graph/ui-theme/.storybook/main.ts @@ -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 diff --git a/graph/ui-theme/.storybook/preview.ts b/graph/ui-theme/.storybook/preview.ts new file mode 100644 index 00000000000000..8c8889d1a414d3 --- /dev/null +++ b/graph/ui-theme/.storybook/preview.ts @@ -0,0 +1 @@ +import './tailwind-imports.css'; diff --git a/graph/ui-theme/.storybook/tailwind-imports.css b/graph/ui-theme/.storybook/tailwind-imports.css new file mode 100644 index 00000000000000..b5c61c956711f9 --- /dev/null +++ b/graph/ui-theme/.storybook/tailwind-imports.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/graph/ui-theme/README.md b/graph/ui-theme/README.md new file mode 100644 index 00000000000000..65cd19c85bfebb --- /dev/null +++ b/graph/ui-theme/README.md @@ -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). diff --git a/graph/ui-theme/postcss.config.js b/graph/ui-theme/postcss.config.js new file mode 100644 index 00000000000000..72f701497a5940 --- /dev/null +++ b/graph/ui-theme/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + tailwindcss: { + config: './graph/ui-theme/tailwind.config.js', + }, + autoprefixer: {}, + }, +}; diff --git a/graph/ui-theme/project.json b/graph/ui-theme/project.json new file mode 100644 index 00000000000000..bb000d0dfca150 --- /dev/null +++ b/graph/ui-theme/project.json @@ -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 + } + } + } + } +} diff --git a/graph/ui-theme/src/index.ts b/graph/ui-theme/src/index.ts new file mode 100644 index 00000000000000..53ceced9b006b4 --- /dev/null +++ b/graph/ui-theme/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/theme-panel'; +export * from './lib/theme-resolver'; diff --git a/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx b/graph/ui-theme/src/lib/theme-panel.stories.tsx similarity index 50% rename from graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx rename to graph/ui-theme/src/lib/theme-panel.stories.tsx index a9aacfcbe4a452..782c2cc99fc289 100644 --- a/graph/client/src/app/feature-projects/panels/theme-panel.stories.tsx +++ b/graph/ui-theme/src/lib/theme-panel.stories.tsx @@ -3,7 +3,16 @@ import { ThemePanel } from './theme-panel'; const meta: Meta = { component: ThemePanel, - title: 'Project Graph/ThemePanel', + decorators: [ + (Story) => ( +
              +
              + +
              +
              + ), + ], + title: 'ThemePanel', }; export default meta; diff --git a/graph/client/src/app/feature-projects/panels/theme-panel.tsx b/graph/ui-theme/src/lib/theme-panel.tsx similarity index 94% rename from graph/client/src/app/feature-projects/panels/theme-panel.tsx rename to graph/ui-theme/src/lib/theme-panel.tsx index 588fadce2ace09..3e0ad6c84bed15 100644 --- a/graph/client/src/app/feature-projects/panels/theme-panel.tsx +++ b/graph/ui-theme/src/lib/theme-panel.tsx @@ -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 ( diff --git a/graph/client/src/app/theme-resolver.tsx b/graph/ui-theme/src/lib/theme-resolver.tsx similarity index 91% rename from graph/client/src/app/theme-resolver.tsx rename to graph/ui-theme/src/lib/theme-resolver.tsx index 1c71576fdc23fc..298870ef1fbf75 100644 --- a/graph/client/src/app/theme-resolver.tsx +++ b/graph/ui-theme/src/lib/theme-resolver.tsx @@ -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( diff --git a/graph/ui-theme/tailwind.config.js b/graph/ui-theme/tailwind.config.js new file mode 100644 index 00000000000000..18a8d985ec32a7 --- /dev/null +++ b/graph/ui-theme/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-theme/tsconfig.json b/graph/ui-theme/tsconfig.json new file mode 100644 index 00000000000000..0672cc05841f6d --- /dev/null +++ b/graph/ui-theme/tsconfig.json @@ -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" +} diff --git a/graph/ui-theme/tsconfig.lib.json b/graph/ui-theme/tsconfig.lib.json new file mode 100644 index 00000000000000..8c1bec17db74e0 --- /dev/null +++ b/graph/ui-theme/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-theme/tsconfig.storybook.json b/graph/ui-theme/tsconfig.storybook.json new file mode 100644 index 00000000000000..2da3caee121ed2 --- /dev/null +++ b/graph/ui-theme/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/graph/ui-tooltips/tsconfig.json b/graph/ui-tooltips/tsconfig.json index 79d9ca983d86d0..0672cc05841f6d 100644 --- a/graph/ui-tooltips/tsconfig.json +++ b/graph/ui-tooltips/tsconfig.json @@ -4,7 +4,8 @@ "allowJs": false, "esModuleInterop": false, "allowSyntheticDefaultImports": true, - "strict": true + "strict": true, + "lib": ["DOM", "es2022"] }, "files": [], "include": [], diff --git a/nx.json b/nx.json index d0bbe55b36d976..04b3fdaea3ddfc 100644 --- a/nx.json +++ b/nx.json @@ -200,5 +200,22 @@ "nxCloudUrl": "https://staging.nx.app", "parallel": 1, "cacheDirectory": "/tmp/nx-cache", - "bust": 5 + "bust": 5, + "generators": { + "@nx/react": { + "application": { + "babel": true + }, + "library": { + "unitTestRunner": "jest" + } + } + }, + "tasksRunnerOptions": { + "default": { + "options": { + "cacheableOperations": ["build-storybook"] + } + } + } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 61b1e150a0be2b..16726df9755b11 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -107,6 +107,7 @@ "@nx/storybook": ["packages/storybook"], "@nx/storybook/*": ["packages/storybook/*"], "@nx/typedoc-theme": ["typedoc-theme/src/index.ts"], + "@nx/ui-theme": ["graph/ui-theme/src/index.ts"], "@nx/vite": ["packages/vite"], "@nx/vite/*": ["packages/vite/*"], "@nx/vue": ["packages/vue"],