diff --git a/graph/client/jest.config.ts b/graph/client/jest.config.ts index 78336ace45..a246a340b4 100644 --- a/graph/client/jest.config.ts +++ b/graph/client/jest.config.ts @@ -10,7 +10,7 @@ export default { '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - coverageDirectory: '../../coverage/nx-dev/nx-dev', + coverageDirectory: '../../coverage/graph/client', // The mock for widnow.matchMedia has to be in a separete file and imported before the components to test // for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom modulePathIgnorePatterns: [ diff --git a/graph/client/src/app/app.tsx b/graph/client/src/app/app.tsx index 90dff08ff3..254b91f635 100644 --- a/graph/client/src/app/app.tsx +++ b/graph/client/src/app/app.tsx @@ -1,4 +1,6 @@ import { themeInit } from '@nx/graph/ui-theme'; +import { rootStore } from '@nx/graph/state'; +import { Provider as StoreProvider } from 'react-redux'; import { rankDirInit } from './rankdir-resolver'; import { RouterProvider } from 'react-router-dom'; import { getRouter } from './get-router'; @@ -7,5 +9,9 @@ themeInit(); rankDirInit(); export function App() { - return ; + return ( + + + + ); } diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx index f6c1f82f3f..fed918e0ed 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -6,7 +6,7 @@ import { } from '@heroicons/react/24/outline'; /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import type { ProjectGraphNode } from '@nx/devkit'; +import type { ProjectGraphProjectNode } from '@nx/devkit'; /* eslint-enable @nx/enforce-module-boundaries */ import { useProjectGraphSelector } from './hooks/use-project-graph-selector'; import { @@ -23,7 +23,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { useRouteConstructor } from '@nx/graph/shared'; interface SidebarProject { - projectGraphNode: ProjectGraphNode; + projectGraphNode: ProjectGraphProjectNode; isSelected: boolean; } @@ -36,7 +36,7 @@ interface TracingInfo { } function groupProjectsByDirectory( - projects: ProjectGraphNode[], + projects: ProjectGraphProjectNode[], selectedProjects: string[], workspaceLayout: { appsDir: string; libsDir: string } ): DirectoryProjectRecord { diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx index ef823476ea..92e366f3e7 100644 --- a/graph/client/src/app/ui-components/error-boundary.tsx +++ b/graph/client/src/app/ui-components/error-boundary.tsx @@ -3,7 +3,7 @@ import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-deta import { useRouteError } from 'react-router-dom'; export function ErrorBoundary() { - let error = useRouteError()?.toString(); + let error = useRouteError(); console.error(error); const environment = useEnvironmentConfig()?.environment; @@ -20,7 +20,7 @@ export function ErrorBoundary() {

Error

{message}

-

Error message: {error}

+

Error message: {error?.toString()}

); diff --git a/graph/client/src/app/util.ts b/graph/client/src/app/util.ts index a3d71d40f9..e4e8cf503b 100644 --- a/graph/client/src/app/util.ts +++ b/graph/client/src/app/util.ts @@ -1,7 +1,9 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { ProjectGraphDependency, ProjectGraphProjectNode } from '@nx/devkit'; -import { getEnvironmentConfig } from '@nx/graph/shared'; +import type { + ProjectGraphDependency, + ProjectGraphProjectNode, +} from '@nx/devkit'; /* eslint-enable @nx/enforce-module-boundaries */ export function parseParentDirectoriesFromFilePath( diff --git a/graph/client/src/assets/project-graphs/e2e.json b/graph/client/src/assets/project-graphs/e2e.json index 21a2f0f0b3..49f77dc01c 100644 --- a/graph/client/src/assets/project-graphs/e2e.json +++ b/graph/client/src/assets/project-graphs/e2e.json @@ -1189,27 +1189,111 @@ } ], "targets": { - "e2e": { - "executor": "@nrwl/cypress:cypress", - "options": { - "cypressConfig": "apps/cart-e2e/cypress.config.ts", - "devServerTarget": "cart:serve", - "testingType": "e2e" - }, - "configurations": { - "production": { - "devServerTarget": "cart:serve:production" - } - }, - "inputs": ["default", "^production"] - }, "lint": { - "executor": "@nrwl/linter:eslint", + "cache": true, "options": { - "lintFilePatterns": ["apps/cart-e2e/**/*.{ts,tsx,js,jsx}"] + "cwd": "apps/cart-e2e", + "command": "eslint ." }, - "outputs": ["{options.outputFile}"], - "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] + "inputs": [ + "default", + "^default", + "{workspaceRoot}/.eslintrc.json", + "{projectRoot}/.eslintrc.json", + "{workspaceRoot}/tools/eslint-rules/**/*", + { + "externalDependencies": ["eslint"] + } + ], + "executor": "nx:run-commands", + "configurations": {} + }, + "e2e": { + "cache": true, + "inputs": ["default", "^production"], + "outputs": [ + "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos", + "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots" + ], + "metadata": { + "technologies": ["cypress"], + "description": "Runs Cypress Tests" + }, + "executor": "nx:run-commands", + "options": { + "cwd": "apps/cart-e2e", + "command": "cypress run" + }, + "configurations": {} + }, + "e2e-ci--src/e2e/app.cy.ts": { + "outputs": [ + "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos", + "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots" + ], + "inputs": [ + "default", + "^production", + { + "externalDependencies": ["cypress"] + } + ], + "cache": true, + "options": { + "cwd": "apps/cart-e2e", + "command": "cypress run --env webServerCommand=\"nx run cart:serve\" --spec src/e2e/app.cy.ts" + }, + "metadata": { + "technologies": ["cypress"], + "description": "Runs Cypress Tests in src/e2e/app.cy.ts in CI" + }, + "executor": "nx:run-commands", + "configurations": {} + }, + "e2e-ci": { + "executor": "nx:noop", + "cache": true, + "inputs": [ + "default", + "^production", + { + "externalDependencies": ["cypress"] + } + ], + "outputs": [ + "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos", + "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots" + ], + "dependsOn": [ + { + "target": "e2e-ci--src/e2e/app.cy.ts", + "projects": "self", + "params": "forward" + } + ], + "metadata": { + "technologies": ["cypress"], + "description": "Runs Cypress Tests in CI" + }, + "options": {}, + "configurations": {} + }, + "open-cypress": { + "options": { + "cwd": "apps/cart-e2e", + "command": "cypress open" + }, + "metadata": { + "technologies": ["cypress"], + "description": "Opens Cypress" + }, + "executor": "nx:run-commands", + "configurations": {} + } + }, + "metadata": { + "targetGroups": { + "E2E (CI)": ["e2e-ci--src/e2e/app.cy.ts", "e2e-ci"] } } } diff --git a/graph/client/src/styles.css b/graph/client/src/styles.css index 0349580f6f..88e771941b 100644 --- a/graph/client/src/styles.css +++ b/graph/client/src/styles.css @@ -44,3 +44,13 @@ opacity: 1; visibility: visible; } + +/* Dark mode */ +html.dark .adaptive-icon { + /* fill: white; */ + filter: invert(1); +} + +.adaptive-icon { + fill: black; +} diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx index 34263a8ea2..1b325f8be1 100644 --- a/graph/project-details/src/lib/project-details-page.tsx +++ b/graph/project-details/src/lib/project-details-page.tsx @@ -1,12 +1,12 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { ProjectGraphProjectNode } from '@nx/devkit'; +import type { ProjectGraphProjectNode } from '@nx/devkit'; import { ScrollRestoration, useParams, useRouteLoaderData, } from 'react-router-dom'; -import { ProjectDetailsWrapper } from './project-details-wrapper'; +import ProjectDetailsWrapper from './project-details-wrapper'; import { fetchProjectGraph, getProjectGraphDataService, diff --git a/graph/project-details/src/lib/project-details-wrapper.state.ts b/graph/project-details/src/lib/project-details-wrapper.state.ts new file mode 100644 index 0000000000..2ca6c92831 --- /dev/null +++ b/graph/project-details/src/lib/project-details-wrapper.state.ts @@ -0,0 +1,33 @@ +import { + AppDispatch, + RootState, + expandTargetActions, + getExpandedTargets, +} from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return { + expandTargets: getExpandedTargets(state), + }; +}; + +const mapDispatchToProps = (dispatch: AppDispatch) => { + return { + setExpandTargets(targets: string[]) { + dispatch(expandTargetActions.setExpandTargets(targets)); + }, + collapseAllTargets() { + dispatch(expandTargetActions.collapseAllTargets()); + }, + }; +}; + +type mapStateToPropsType = ReturnType; +type mapDispatchToPropsType = ReturnType; + +export { + mapStateToProps, + mapDispatchToProps, + mapStateToPropsType, + mapDispatchToPropsType, +}; diff --git a/graph/project-details/src/lib/project-details-wrapper.tsx b/graph/project-details/src/lib/project-details-wrapper.tsx index 55f80321dd..712abe4346 100644 --- a/graph/project-details/src/lib/project-details-wrapper.tsx +++ b/graph/project-details/src/lib/project-details-wrapper.tsx @@ -1,10 +1,8 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars - -import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom'; - /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { ProjectGraphProjectNode } from '@nx/devkit'; +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom'; +import { connect } from 'react-redux'; import { getExternalApiService, useEnvironmentConfig, @@ -12,19 +10,28 @@ import { } from '@nx/graph/shared'; import { Spinner } from '@nx/graph/ui-components'; +import { ProjectDetails } from '@nx/graph/ui-project-details'; +import { useCallback, useEffect } from 'react'; import { - ProjectDetails, - ProjectDetailsImperativeHandle, -} from '@nx/graph/ui-project-details'; -import { useCallback, useLayoutEffect, useRef } from 'react'; + mapStateToProps, + mapDispatchToProps, + mapStateToPropsType, + mapDispatchToPropsType, +} from './project-details-wrapper.state'; -export interface ProjectDetailsProps { - project: ProjectGraphProjectNode; - sourceMap: Record; -} +type ProjectDetailsProps = mapStateToPropsType & + mapDispatchToPropsType & { + project: ProjectGraphProjectNode; + sourceMap: Record; + }; -export function ProjectDetailsWrapper(props: ProjectDetailsProps) { - const projectDetailsRef = useRef(null); +export function ProjectDetailsWrapperComponent({ + project, + sourceMap, + setExpandTargets, + expandTargets, + collapseAllTargets, +}: ProjectDetailsProps) { const environment = useEnvironmentConfig()?.environment; const externalApiService = getExternalApiService(); const navigate = useNavigate(); @@ -88,63 +95,56 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) { [externalApiService] ); - const updateSearchParams = (params: URLSearchParams, sections: string[]) => { - if (sections.length === 0) { + const updateSearchParams = ( + params: URLSearchParams, + targetNames: string[] + ) => { + if (targetNames.length === 0) { params.delete('expanded'); } else { - params.set('expanded', sections.join(',')); + params.set('expanded', targetNames.join(',')); } }; - const handleTargetCollapse = useCallback( - (targetName: string) => { - const expandedSections = searchParams.get('expanded')?.split(',') || []; - if (!expandedSections.includes(targetName)) return; - const newExpandedSections = expandedSections.filter( - (section) => section !== targetName - ); - setSearchParams( - (currentSearchParams) => { - updateSearchParams(currentSearchParams, newExpandedSections); - return currentSearchParams; - }, - { - replace: true, - preventScrollReset: true, - } - ); - }, - [setSearchParams, searchParams] - ); + useEffect(() => { + if (!project.data.targets) return; - const handleTargetExpand = useCallback( - (targetName: string) => { - const expandedSections = searchParams.get('expanded')?.split(',') || []; - if (expandedSections.includes(targetName)) return; - expandedSections.push(targetName); - setSearchParams( - (currentSearchParams) => { - updateSearchParams(currentSearchParams, expandedSections); - return currentSearchParams; - }, - { replace: true, preventScrollReset: true } - ); - }, - [setSearchParams, searchParams] - ); - - useLayoutEffect(() => { - if (!props.project.data.targets) return; - - const expandedSections = searchParams.get('expanded')?.split(',') || []; - for (const targetName of Object.keys(props.project.data.targets)) { - if (expandedSections.includes(targetName)) { - projectDetailsRef.current?.expandTarget(targetName); - } else { - projectDetailsRef.current?.collapseTarget(targetName); - } + const expandedTargetsParams = searchParams.get('expanded')?.split(','); + if (expandedTargetsParams && expandedTargetsParams.length > 0) { + setExpandTargets(expandedTargetsParams); } - }, [searchParams, props.project.data.targets, projectDetailsRef]); + + return () => { + collapseAllTargets(); + searchParams.delete('expanded'); + setSearchParams(searchParams, { replace: true }); + }; + }, []); // only run on mount + + useEffect(() => { + if (!project.data.targets) return; + + const expandedTargetsParams = + searchParams.get('expanded')?.split(',') || []; + + if (expandedTargetsParams.join(',') === expandTargets.join(',')) { + return; + } + + setSearchParams( + (currentSearchParams) => { + updateSearchParams(currentSearchParams, expandTargets); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + }, [ + expandTargets, + project.data.targets, + setExpandTargets, + searchParams, + setSearchParams, + ]); if ( navigationState === 'loading' && @@ -159,10 +159,8 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) { return ( ) => { + if (state.includes(action.payload)) { + return state; + } + state.push(action.payload); + return state; + }, + collapseTarget: (state: string[], action: PayloadAction) => { + if (state.includes(action.payload)) { + state = state.filter((target) => target !== action.payload); + } + return state; + }, + toggleExpandTarget: (state: string[], action: PayloadAction) => { + if (state.includes(action.payload)) { + state = state.filter((target) => target !== action.payload); + } else { + state.push(action.payload); + } + return state; + }, + setExpandTargets: (state: string[], action: PayloadAction) => { + state = action.payload; + return state; + }, + collapseAllTargets: (state: string[]) => { + state = []; + return state; + }, + }, +}); + +/* + * Export reducer for store configuration. + */ +export const expandTargetReducer = expandTargetSlice.reducer; + +export const expandTargetActions = expandTargetSlice.actions; + +export const getExpandedTargets = < + ROOT extends { [EXPAND_TARGETS_KEY]: string[] } +>( + rootState: ROOT +): string[] => rootState[EXPAND_TARGETS_KEY]; diff --git a/graph/state/src/lib/root/root-state.initial.ts b/graph/state/src/lib/root/root-state.initial.ts new file mode 100644 index 0000000000..b56a600101 --- /dev/null +++ b/graph/state/src/lib/root/root-state.initial.ts @@ -0,0 +1,9 @@ +import { + EXPAND_TARGETS_KEY, + initialExpandTargets, +} from '../expand-targets/expand-targets.slice'; +import { RootState } from './root-state.interface'; + +export const initialRootState: RootState = { + [EXPAND_TARGETS_KEY]: initialExpandTargets, +}; diff --git a/graph/state/src/lib/root/root-state.interface.ts b/graph/state/src/lib/root/root-state.interface.ts new file mode 100644 index 0000000000..ab5dc706d2 --- /dev/null +++ b/graph/state/src/lib/root/root-state.interface.ts @@ -0,0 +1,5 @@ +import { EXPAND_TARGETS_KEY } from '../expand-targets/expand-targets.slice'; + +export interface RootState { + [EXPAND_TARGETS_KEY]: string[]; +} diff --git a/graph/state/src/lib/root/root.reducer.ts b/graph/state/src/lib/root/root.reducer.ts new file mode 100644 index 0000000000..48f5700ae9 --- /dev/null +++ b/graph/state/src/lib/root/root.reducer.ts @@ -0,0 +1,10 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import { + EXPAND_TARGETS_KEY, + expandTargetReducer, +} from '../expand-targets/expand-targets.slice'; +import { RootState } from './root-state.interface'; + +export const rootReducer = combineReducers({ + [EXPAND_TARGETS_KEY]: expandTargetReducer, +}); diff --git a/graph/state/src/lib/root/root.store.ts b/graph/state/src/lib/root/root.store.ts new file mode 100644 index 0000000000..1362159ede --- /dev/null +++ b/graph/state/src/lib/root/root.store.ts @@ -0,0 +1,19 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { initialRootState } from './root-state.initial'; +import { rootReducer } from './root.reducer'; + +declare const process: any; + +export const rootStore = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => { + const defaultMiddleware = getDefaultMiddleware({ + serializableCheck: false, + }); + return defaultMiddleware; + }, + devTools: process.env.NODE_ENV === 'development', + preloadedState: initialRootState, +}); + +export type AppDispatch = typeof rootStore.dispatch; diff --git a/graph/state/src/lib/store.decorator.tsx b/graph/state/src/lib/store.decorator.tsx new file mode 100644 index 0000000000..4f2e9fb13f --- /dev/null +++ b/graph/state/src/lib/store.decorator.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { rootStore } from './root/root.store'; + +export const StoreDecorator = (story: any) => { + return {story()}; +}; diff --git a/graph/state/tsconfig.json b/graph/state/tsconfig.json new file mode 100644 index 0000000000..95cfeb243d --- /dev/null +++ b/graph/state/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/graph/state/tsconfig.lib.json b/graph/state/tsconfig.lib.json new file mode 100644 index 0000000000..8c1bec17db --- /dev/null +++ b/graph/state/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-icons/.babelrc b/graph/ui-icons/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/graph/ui-icons/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/graph/ui-icons/.eslintrc.json b/graph/ui-icons/.eslintrc.json new file mode 100644 index 0000000000..b96a5b888e --- /dev/null +++ b/graph/ui-icons/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "storybook-static"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/graph/ui-icons/.storybook/main.ts b/graph/ui-icons/.storybook/main.ts new file mode 100644 index 0000000000..dfca182122 --- /dev/null +++ b/graph/ui-icons/.storybook/main.ts @@ -0,0 +1,21 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +import type { StorybookConfig } from '@storybook/react-vite'; +import { mergeConfig } from 'vite'; +// nx-ignore-next-line +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +const config: StorybookConfig = { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + + viteFinal: async (config) => + mergeConfig(config, { + plugins: [nxViteTsPaths()], + }), +}; + +export default config; diff --git a/graph/ui-icons/.storybook/preview.ts b/graph/ui-icons/.storybook/preview.ts new file mode 100644 index 0000000000..195b052493 --- /dev/null +++ b/graph/ui-icons/.storybook/preview.ts @@ -0,0 +1 @@ +import './tailwind.css'; diff --git a/graph/ui-icons/.storybook/tailwind.css b/graph/ui-icons/.storybook/tailwind.css new file mode 100644 index 0000000000..23d597fe51 --- /dev/null +++ b/graph/ui-icons/.storybook/tailwind.css @@ -0,0 +1,3 @@ +@tailwind components; +@tailwind base; +@tailwind utilities; diff --git a/graph/ui-icons/README.md b/graph/ui-icons/README.md new file mode 100644 index 0000000000..fda4b05bf5 --- /dev/null +++ b/graph/ui-icons/README.md @@ -0,0 +1,7 @@ +# ui-icons + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-icons` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/graph/ui-icons/postcss.config.js b/graph/ui-icons/postcss.config.js new file mode 100644 index 0000000000..8b2e63b9e6 --- /dev/null +++ b/graph/ui-icons/postcss.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + plugins: { + tailwindcss: { + config: path.join(__dirname, 'tailwind.config.js'), + }, + autoprefixer: {}, + }, +}; diff --git a/graph/ui-icons/project.json b/graph/ui-icons/project.json new file mode 100644 index 0000000000..a126077853 --- /dev/null +++ b/graph/ui-icons/project.json @@ -0,0 +1,54 @@ +{ + "name": "graph-ui-icons", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "graph/ui-icons/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-icons --web", + "targets": { + "lint": {}, + "storybook": { + "executor": "@nx/storybook:storybook", + "options": { + "port": 4400, + "configDir": "graph/ui-icons/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nx/storybook:build", + "outputs": ["{options.outputDir}"], + "options": { + "outputDir": "dist/storybook/graph-ui-icons", + "configDir": "graph/ui-icons/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "test-storybook": { + "executor": "nx:run-commands", + "options": { + "command": "test-storybook -c graph/ui-icons/.storybook --url=http://localhost:4400" + } + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "graph-ui-icons:build-storybook", + "staticFilePath": "dist/storybook/graph-ui-icons" + }, + "configurations": { + "ci": { + "buildTarget": "graph-ui-icons:build-storybook:ci" + } + } + } + } +} diff --git a/graph/ui-icons/src/index.ts b/graph/ui-icons/src/index.ts new file mode 100644 index 0000000000..829dd6931b --- /dev/null +++ b/graph/ui-icons/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/technology-icon'; +export * from './lib/framework-icons'; diff --git a/graph/ui-icons/src/lib/framework-icons.stories.tsx b/graph/ui-icons/src/lib/framework-icons.stories.tsx new file mode 100644 index 0000000000..e57eaf595c --- /dev/null +++ b/graph/ui-icons/src/lib/framework-icons.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Framework, frameworkIcons } from './framework-icons'; + +const meta: Meta = { + component: () => ( + <> + {Object.keys(frameworkIcons).map((key) => ( + <> +
{key}
+
+ {frameworkIcons[key as Framework].image} +
+ + ))} + + ), + title: 'frameworkIcons', +}; +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; diff --git a/nx-dev/ui-markdoc/src/lib/icons.tsx b/graph/ui-icons/src/lib/framework-icons.tsx similarity index 98% rename from nx-dev/ui-markdoc/src/lib/icons.tsx rename to graph/ui-icons/src/lib/framework-icons.tsx index f38a843658..697c66f0dc 100644 --- a/nx-dev/ui-markdoc/src/lib/icons.tsx +++ b/graph/ui-icons/src/lib/framework-icons.tsx @@ -1,12 +1,74 @@ +export type Framework = + | 'reactMono' + | 'tsMono' + | 'jsMono' + | 'nodeMono' + | 'angularMono' + | 'typescript' + | 'javascript' + | 'node' + | 'angular' + | 'youtube' + | 'nxagents' + | 'nxcloud' + | 'nx' + | 'nextjs' + | 'nestjs' + | 'rspack' + | 'express' + | 'jest' + | 'fastify' + | 'storybook' + | 'solid' + | 'lit' + | 'vite' + | 'trpc' + | 'remix' + | 'dotnet' + | 'qwik' + | 'gradle' + | 'go' + | 'vue' + | 'rust' + | 'nuxt' + | 'svelte' + | 'gatsby' + | 'astro' + | 'playwright' + | 'pnpm' + | 'monorepo' + | 'cra' + | 'cypress' + | 'expo' + | 'react' + | 'azure' + | 'bitbucket' + | 'circleci' + | 'github' + | 'gitlab' + | 'jenkins' + | 'apollo' + | 'prisma' + | 'redis' + | 'postgres' + | 'planetscale' + | 'mongodb' + | 'mfe' + | 'eslint'; + export const frameworkIcons: Record< - string, + Framework, { image: JSX.Element; + // this key determines whether the icon should be adaptive or not + // if true means icon is mostly monotone (black/white), its parent needs to specify the fill color + isAdaptiveIcon?: boolean; } > = { reactMono: { image: ( @@ -227,12 +296,13 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, nestjs: { image: ( ), + isAdaptiveIcon: true, }, express: { image: ( @@ -352,12 +423,13 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, storybook: { image: ( + Solid ), + isAdaptiveIcon: true, }, nuxt: { image: ( ), + isAdaptiveIcon: true, }, expo: { image: ( ), + isAdaptiveIcon: true, }, react: { image: ( @@ -1608,6 +1707,7 @@ export const frameworkIcons: Record< mfe: { image: ( ), }, + eslint: { + image: ( + + + + + + + ), + }, }; diff --git a/graph/ui-icons/src/lib/technology-icon.stories.tsx b/graph/ui-icons/src/lib/technology-icon.stories.tsx new file mode 100644 index 0000000000..594b87b8d9 --- /dev/null +++ b/graph/ui-icons/src/lib/technology-icon.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TechnologyIcon } from './technology-icon'; + +const meta: Meta = { + component: TechnologyIcon, + title: 'TechnologyIcon', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + technology: 'react', + }, +}; + +export const UnknownTechnology: Story = { + args: { + technology: 'unknown', + }, +}; diff --git a/graph/ui-icons/src/lib/technology-icon.tsx b/graph/ui-icons/src/lib/technology-icon.tsx new file mode 100644 index 0000000000..857a963c9b --- /dev/null +++ b/graph/ui-icons/src/lib/technology-icon.tsx @@ -0,0 +1,33 @@ +import { Framework, frameworkIcons } from './framework-icons'; + +export interface TechnologyIconProps { + technology?: string; + showTooltip?: boolean; +} + +export function TechnologyIcon({ + technology, + showTooltip, +}: TechnologyIconProps) { + if (!technology) { + return null; + } + const image = frameworkIcons[technology as Framework]?.image; + + return ( +
+ {image ?? technology[0]} +
+ ); +} diff --git a/graph/ui-icons/tailwind.config.js b/graph/ui-icons/tailwind.config.js new file mode 100644 index 0000000000..a130bfcecc --- /dev/null +++ b/graph/ui-icons/tailwind.config.js @@ -0,0 +1,40 @@ +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')], +}; diff --git a/graph/ui-icons/tsconfig.json b/graph/ui-icons/tsconfig.json new file mode 100644 index 0000000000..53e12dc04e --- /dev/null +++ b/graph/ui-icons/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.storybook.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/graph/ui-icons/tsconfig.lib.json b/graph/ui-icons/tsconfig.lib.json new file mode 100644 index 0000000000..8c1bec17db --- /dev/null +++ b/graph/ui-icons/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-icons/tsconfig.storybook.json b/graph/ui-icons/tsconfig.storybook.json new file mode 100644 index 0000000000..2da3caee12 --- /dev/null +++ b/graph/ui-icons/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-project-details/src/index.ts b/graph/ui-project-details/src/index.ts index c496e5eaae..fa5bc208e1 100644 --- a/graph/ui-project-details/src/index.ts +++ b/graph/ui-project-details/src/index.ts @@ -1 +1,2 @@ export * from './lib/project-details/project-details'; +export * from './lib/utils/group-targets'; diff --git a/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx b/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx new file mode 100644 index 0000000000..8101be70cf --- /dev/null +++ b/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CopyToClipboard } from './copy-to-clipboard'; + +const meta: Meta = { + component: CopyToClipboard, + title: 'CopyToClipboard', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + onCopy: () => {}, + tooltipAlignment: 'left', + }, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details/copy-to-clipboard.tsx b/graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx similarity index 100% rename from graph/ui-project-details/src/lib/target-configuration-details/copy-to-clipboard.tsx rename to graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.tsx 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 index 52583bded4..fb98d5e58d 100644 --- 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 @@ -1,9 +1,11 @@ import type { Meta } from '@storybook/react'; import { ProjectDetails } from './project-details'; +import { StoreDecorator } from '@nx/graph/state'; const meta: Meta = { component: ProjectDetails, title: 'ProjectDetails', + decorators: [StoreDecorator], }; export default meta; @@ -208,3 +210,626 @@ export const Primary = { }, }, }; + +export const Gradle = { + args: { + project: { + name: 'utilities', + type: 'lib', + data: { + root: 'utilities', + name: 'utilities', + metadata: { + targetGroups: { + Build: [ + 'assemble', + 'build', + 'buildDependents', + 'buildKotlinToolingMetadata', + 'buildNeeded', + 'classes', + 'clean', + 'jar', + 'kotlinSourcesJar', + 'testClasses', + ], + Documentation: ['javadoc'], + Help: [ + 'buildEnvironment', + 'dependencies', + 'dependencyInsight', + 'help', + 'javaToolchains', + 'kotlinDslAccessorsReport', + 'outgoingVariants', + 'projects', + 'properties', + 'resolvableConfigurations', + 'tasks', + ], + Reporting: ['projectReport'], + Verification: [ + 'check', + 'checkKotlinGradlePluginConfigurationErrors', + 'test', + ], + }, + technologies: ['gradle'], + }, + targets: { + assemble: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew assemble', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + build: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew build', + }, + cache: true, + inputs: ['default', '^default'], + outputs: ['{workspaceRoot}/utilities/build'], + dependsOn: ['^build', 'classes'], + executor: 'nx:run-commands', + configurations: {}, + }, + buildDependents: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildDependents', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + buildKotlinToolingMetadata: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildKotlinToolingMetadata', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + buildNeeded: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildNeeded', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + classes: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew classes', + }, + cache: true, + inputs: ['default', '^default'], + outputs: ['{workspaceRoot}/utilities/build/classes'], + dependsOn: ['^classes'], + executor: 'nx:run-commands', + configurations: {}, + }, + clean: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew clean', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + jar: { + options: { + cwd: 'utilities', + command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew jar', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + kotlinSourcesJar: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew kotlinSourcesJar', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + testClasses: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew testClasses', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + javadoc: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew javadoc', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + buildEnvironment: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildEnvironment', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + dependencies: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew dependencies', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + dependencyInsight: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew dependencyInsight', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + help: { + options: { + cwd: 'utilities', + command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew help', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + javaToolchains: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew javaToolchains', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + kotlinDslAccessorsReport: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew kotlinDslAccessorsReport', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + outgoingVariants: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew outgoingVariants', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + projects: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew projects', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + properties: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew properties', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + resolvableConfigurations: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew resolvableConfigurations', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + tasks: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew tasks', + }, + cache: false, + executor: 'nx:run-commands', + configurations: {}, + }, + projectReport: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew projectReport', + }, + cache: false, + outputs: ['{workspaceRoot}/utilities/build/reports/project'], + executor: 'nx:run-commands', + configurations: {}, + }, + check: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew check', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + checkKotlinGradlePluginConfigurationErrors: { + options: { + cwd: 'utilities', + command: + '/Users/emily/code/tmp/gradle-plugin-test3/gradlew checkKotlinGradlePluginConfigurationErrors', + }, + cache: true, + executor: 'nx:run-commands', + configurations: {}, + }, + test: { + options: { + cwd: 'utilities', + command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew test', + }, + cache: true, + inputs: ['default', '^default'], + dependsOn: ['classes'], + executor: 'nx:run-commands', + configurations: {}, + }, + }, + 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', + ], + }, + }, +}; + +export const Cart = { + args: { + project: { + name: 'cart-e2e', + type: 'e2e', + data: { + root: 'apps/cart-e2e', + targets: { + lint: { + cache: true, + options: { + cwd: 'apps/cart-e2e', + command: 'eslint .', + }, + inputs: [ + 'default', + '^default', + '{workspaceRoot}/.eslintrc.json', + '{projectRoot}/.eslintrc.json', + '{workspaceRoot}/tools/eslint-rules/**/*', + { + externalDependencies: ['eslint'], + }, + ], + executor: 'nx:run-commands', + configurations: {}, + }, + e2e: { + cache: true, + inputs: ['default', '^production'], + outputs: [ + '{workspaceRoot}/dist/cypress/apps/cart-e2e/videos', + '{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots', + ], + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Tests', + }, + executor: 'nx:run-commands', + options: { + cwd: 'apps/cart-e2e', + command: 'cypress run', + }, + configurations: {}, + }, + 'e2e-ci--src/e2e/app.cy.ts': { + outputs: [ + '{workspaceRoot}/dist/cypress/apps/cart-e2e/videos', + '{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots', + ], + inputs: [ + 'default', + '^production', + { + externalDependencies: ['cypress'], + }, + ], + cache: true, + options: { + cwd: 'apps/cart-e2e', + command: + 'cypress run --env webServerCommand="nx run cart:serve" --spec src/e2e/app.cy.ts', + }, + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Tests in src/e2e/app.cy.ts in CI', + }, + executor: 'nx:run-commands', + configurations: {}, + }, + 'e2e-ci': { + executor: 'nx:noop', + cache: true, + inputs: [ + 'default', + '^production', + { + externalDependencies: ['cypress'], + }, + ], + outputs: [ + '{workspaceRoot}/dist/cypress/apps/cart-e2e/videos', + '{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots', + ], + dependsOn: [ + { + target: 'e2e-ci--src/e2e/app.cy.ts', + projects: 'self', + params: 'forward', + }, + ], + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Tests in CI', + }, + options: {}, + configurations: {}, + }, + 'open-cypress': { + options: { + cwd: 'apps/cart-e2e', + command: 'cypress open', + }, + metadata: { + technologies: ['cypress'], + description: 'Opens Cypress', + }, + executor: 'nx:run-commands', + configurations: {}, + }, + }, + projectType: 'application', + metadata: { + targetGroups: { + 'E2E (CI)': ['e2e-ci--src/e2e/app.cy.ts', 'e2e-ci'], + }, + }, + name: 'cart-e2e', + $schema: '../../node_modules/nx/schemas/project-schema.json', + sourceRoot: 'apps/cart-e2e/src', + tags: ['scope:cart', 'type:e2e'], + implicitDependencies: ['cart'], + }, + }, + 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 index 27b0c4d75e..0e47fff8f9 100644 --- a/graph/ui-project-details/src/lib/project-details/project-details.tsx +++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx @@ -2,32 +2,20 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { ProjectGraphProjectNode } from '@nx/devkit'; +import type { 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'; import { Pill } from '../pill'; +import { TargetConfigurationDetailsList } from '../target-configuration-details-list/target-configuration-details-list'; +import { TargetTechnologies } from '../target-technologies/target-technologies'; 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; @@ -36,143 +24,121 @@ export interface ProjectDetailsProps { onRunTarget?: (data: { projectName: string; targetName: string }) => void; } -export interface ProjectDetailsImperativeHandle { - collapseTarget: (targetName: string) => void; - expandTarget: (targetName: string) => void; -} +export const ProjectDetails = ({ + project, + sourceMap, + variant, + onViewInProjectGraph, + onViewInTaskGraph, + onRunTarget, +}: ProjectDetailsProps) => { + const projectData = project.data; + const isCompact = variant === 'compact'; -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); - const displayType = - projectData.projectType && - projectData.projectType?.charAt(0)?.toUpperCase() + - projectData.projectType?.slice(1); + const technologies = [ + ...new Set( + [ + ...(projectData.metadata?.technologies ?? []), + ...Object.values(projectData.targets ?? {}) + .map((target) => target?.metadata?.technologies) + .flat(), + ].filter(Boolean) + ), + ] as string[]; - useImperativeHandle(ref, () => ({ - collapseTarget: (targetName: string) => { - targetRefs.current[targetName]?.current?.collapse(); - }, - expandTarget: (targetName: string) => { - targetRefs.current[targetName]?.current?.expand(); - }, - })); - - return ( - <> -
+
+
-

- {name} - - {onViewInProjectGraph ? ( - - ) : null}{' '} - -

-
- {projectData.tags && projectData.tags.length ? ( -

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

- ) : null} -

- Root: - {root} -

- {displayType ? ( -

- Type: - {displayType} -

- ) : null} -
-
-
-

- ) as any} +
+

- - Targets - - -

-
    - {projectTargets.sort(sortNxReleasePublishLast).map((targetName) => { - const target = projectData.targets?.[targetName]; - return target && targetRefs.current[targetName] ? ( -
  • - -
  • - ) : null; - })} -
+ {project.name} +

+ +
+ + {onViewInProjectGraph ? ( + + ) : null}{' '} + - - ); - } -); +
+ {projectData.tags && projectData.tags.length ? ( +

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

+ ) : null} +

+ Root: + {projectData.root} +

+ {displayType ? ( +

+ Type: + {displayType} +

+ ) : null} +
+
+
+

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

-function sortNxReleasePublishLast(a: string, b: string) { - if (a === 'nx-release-publish') return 1; - if (b === 'nx-release-publish') return -1; - return 1; -} + +
+ + ); +}; export default ProjectDetails; diff --git a/graph/ui-project-details/src/lib/source-info/source-info.stories.tsx b/graph/ui-project-details/src/lib/source-info/source-info.stories.tsx new file mode 100644 index 0000000000..7b5cebe7da --- /dev/null +++ b/graph/ui-project-details/src/lib/source-info/source-info.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { SourceInfo } from './source-info'; + +const meta: Meta = { + component: SourceInfo, + title: 'SourceInfo', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + data: ['data1', 'data2'], + propertyKey: 'test', + }, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx b/graph/ui-project-details/src/lib/source-info/source-info.tsx similarity index 92% rename from graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx rename to graph/ui-project-details/src/lib/source-info/source-info.tsx index 659ecba1bb..5b185dae3f 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details/source-info.tsx +++ b/graph/ui-project-details/src/lib/source-info/source-info.tsx @@ -11,7 +11,7 @@ export function SourceInfo(props: { // Every other property within in the target has the form `target.${targetName}.${propertyName} const isTarget = props.propertyKey.split('.').length === 2; return ( - + */} diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-container/target-configuration-details-group-container.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-container/target-configuration-details-group-container.tsx new file mode 100644 index 0000000000..0b34d20955 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-container/target-configuration-details-group-container.tsx @@ -0,0 +1,33 @@ +import { forwardRef } from 'react'; +import { TargetConfigurationGroupHeader } from '../target-configuration-details-group-header/target-configuration-details-group-header'; + +export interface TargetConfigurationGroupContainerProps { + targetGroupName: string; + targetsNumber: number; + children: React.ReactNode; +} + +export const TargetConfigurationGroupContainer = forwardRef( + ( + { + targetGroupName, + targetsNumber, + children, + }: TargetConfigurationGroupContainerProps, + ref: React.Ref + ) => { + return ( +
+
+ +
+ {children} +
+
+
+ ); + } +); diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.stories.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.stories.tsx new file mode 100644 index 0000000000..b9051371c3 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TargetConfigurationGroupHeader } from './target-configuration-details-group-header'; + +const meta: Meta = { + component: TargetConfigurationGroupHeader, + title: 'TargetConfigurationGroupHeader', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + targetGroupName: 'Target Group Name', + targetsNumber: 5, + }, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.tsx new file mode 100644 index 0000000000..06b7a72c08 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-header/target-configuration-details-group-header.tsx @@ -0,0 +1,25 @@ +import { Pill } from '../pill'; + +export interface TargetConfigurationGroupHeaderProps { + targetGroupName: string; + targetsNumber: number; + className?: string; +} + +export const TargetConfigurationGroupHeader = ({ + targetGroupName, + targetsNumber, + className = '', +}: TargetConfigurationGroupHeaderProps) => { + return ( +
+ {targetGroupName}{' '} + +
+ ); +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.stories.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.stories.tsx new file mode 100644 index 0000000000..05c5a40af3 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + TargetConfigurationGroupList, + TargetConfigurationGroupListProps, +} from './target-configuration-details-group-list'; +import { StoreDecorator } from '@nx/graph/state'; + +const meta: Meta = { + component: TargetConfigurationGroupList, + title: 'TargetConfigurationGroupList', + decorators: [StoreDecorator], +}; +export default meta; + +type Story = StoryObj; + +export const OneTarget: Story = { + args: { + project: { + name: 'react', + type: 'lib', + data: { + root: 'libs/react', + targets: { + build: { + executor: 'nx', + options: {}, + configurations: { + production: { + executor: 'nx', + options: {}, + }, + }, + }, + lint: { + executor: 'nx', + options: {}, + }, + }, + }, + }, + sourceMap: { + react: ['react'], + }, + variant: 'default', + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + selectedTargetGroup: 'build', + setExpandTargets: () => {}, + collapseAllTargets: () => {}, + } as TargetConfigurationGroupListProps, +}; + +export const TwoTargets: Story = { + args: { + project: { + name: 'react', + type: 'lib', + data: { + root: 'libs/react', + targets: { + build1: { + executor: 'nx', + options: {}, + configurations: { + production: { + executor: 'nx', + options: {}, + }, + }, + }, + build2: { + executor: 'nx', + options: {}, + }, + }, + metadata: { + targetGroups: { + build: ['build1', 'build2'], + }, + }, + }, + }, + sourceMap: { + react: ['react'], + }, + variant: 'default', + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + selectedTargetGroup: 'build', + setExpandTargets: () => {}, + collapseAllTargets: () => {}, + } as TargetConfigurationGroupListProps, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx new file mode 100644 index 0000000000..a561781739 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx @@ -0,0 +1,147 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { RefObject, createRef, useEffect, useRef, useState } from 'react'; +import { Transition } from '@headlessui/react'; + +import { TargetConfigurationDetailsListItem } from '../target-configuration-details-list-item/target-configuration-details-list-item'; +import { TargetConfigurationGroupContainer } from '../target-configuration-details-group-container/target-configuration-details-group-container'; +import { TargetConfigurationGroupHeader } from '../target-configuration-details-group-header/target-configuration-details-group-header'; +import { groupTargets } from '../utils/group-targets'; + +export interface TargetConfigurationGroupListProps { + project: ProjectGraphProjectNode; + sourceMap: Record; + variant?: 'default' | 'compact'; + onRunTarget?: (data: { projectName: string; targetName: string }) => void; + onViewInTaskGraph?: (data: { + projectName: string; + targetName: string; + }) => void; + className?: string; +} + +export function TargetConfigurationGroupList({ + project, + variant, + sourceMap, + onRunTarget, + onViewInTaskGraph, + className = '', +}: TargetConfigurationGroupListProps) { + const [stickyHeaderContent, setStickHeaderContent] = useState(''); + const targetsGroup = groupTargets(project); + const targetGroupRefs = useRef( + Object.keys(targetsGroup.groups).reduce((acc, targetGroupName) => { + acc[targetGroupName] = createRef(); + return acc; + }, {} as Record>) + ); + const targetNameRefs = useRef( + targetsGroup.targets.reduce((acc, targetName) => { + acc[targetName] = createRef(); + return acc; + }, {} as Record>) + ); + + useEffect(() => { + window.addEventListener('scroll', isSticky); + return () => { + window.removeEventListener('scroll', isSticky); + }; + }, []); + + const isSticky = () => { + const scrollTop = window.scrollY + 30; // 30px for the header + const foundTargetGroup: string | undefined = Object.keys( + targetGroupRefs.current + ).find((targetGroupName) => { + const targetGroup = targetGroupRefs.current[targetGroupName]; + if ( + targetGroup && + targetGroup.current && + scrollTop >= targetGroup.current.offsetTop && + scrollTop < + targetGroup.current.offsetTop + targetGroup.current.offsetHeight + ) { + return true; + } + return false; + }); + if (foundTargetGroup) { + setStickHeaderContent(foundTargetGroup); + } else { + setStickHeaderContent(''); + } + }; + + return ( + <> + +
+
+ +
+
+
+ + {Object.entries(targetsGroup.groups).map(([targetGroupName, targets]) => { + return ( + +
    + {targets.map((targetName) => ( + + ))} +
+
+ ); + })} +
    + {targetsGroup.targets.map((targetName) => { + return ( + + ); + })} +
+ + ); +} diff --git a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.stories.tsx b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.stories.tsx new file mode 100644 index 0000000000..eabcb7989e --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + TargetConfigurationDetailsHeader, + TargetConfigurationDetailsHeaderProps, +} from './target-configuration-details-header'; + +const meta: Meta = { + component: TargetConfigurationDetailsHeader, + title: 'TargetConfigurationDetailsHeader', +}; +export default meta; + +type Story = StoryObj; + +export const Compact: Story = { + args: { + isCollasped: true, + toggleCollapse: () => {}, + collapsable: false, + isCompact: true, + targetConfiguration: {}, + projectName: 'jest', + targetName: 'test', + sourceMap: {}, + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + } as TargetConfigurationDetailsHeaderProps, +}; + +export const NotCompact: Story = { + args: { + isCollasped: true, + toggleCollapse: () => {}, + collapsable: false, + isCompact: false, + targetConfiguration: {}, + projectName: 'jest', + targetName: 'test', + sourceMap: {}, + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + } as TargetConfigurationDetailsHeaderProps, +}; + +export const Expanded: Story = { + args: { + isCollasped: false, + toggleCollapse: () => {}, + collapsable: true, + isCompact: false, + targetConfiguration: {}, + projectName: 'jest', + targetName: 'test', + sourceMap: {}, + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + } as TargetConfigurationDetailsHeaderProps, +}; + +export const Collapsed: Story = { + args: { + isCollasped: true, + toggleCollapse: () => {}, + collapsable: true, + isCompact: false, + targetConfiguration: {}, + projectName: 'jest', + targetName: 'test', + sourceMap: {}, + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + } as TargetConfigurationDetailsHeaderProps, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx new file mode 100644 index 0000000000..53f47960d2 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx @@ -0,0 +1,176 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { TargetConfiguration } from '@nx/devkit'; +import { + ChevronDownIcon, + ChevronUpIcon, + EyeIcon, + PlayIcon, +} from '@heroicons/react/24/outline'; + +import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; +import { twMerge } from 'tailwind-merge'; +import { Pill } from '../pill'; +import { TargetTechnologies } from '../target-technologies/target-technologies'; +import { SourceInfo } from '../source-info/source-info'; +import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard'; + +export interface TargetConfigurationDetailsHeaderProps { + isCollasped: boolean; + toggleCollapse?: () => void; + collapsable: boolean; + isCompact?: boolean; + targetConfiguration: TargetConfiguration; + projectName: string; + targetName: string; + sourceMap: Record; + onRunTarget?: (data: { projectName: string; targetName: string }) => void; + onViewInTaskGraph?: (data: { + projectName: string; + targetName: string; + }) => void; +} + +export const TargetConfigurationDetailsHeader = ({ + isCollasped, + toggleCollapse, + collapsable, + isCompact, + targetConfiguration, + projectName, + targetName, + sourceMap, + onRunTarget, + onViewInTaskGraph, +}: TargetConfigurationDetailsHeaderProps) => { + const handleCopyClick = async (copyText: string) => { + await window.navigator.clipboard.writeText(copyText); + }; + + if (!collapsable) { + // when collapsable is false, isCollasped should be false + isCollasped = false; + } + + const singleCommand = + targetConfiguration.executor === 'nx:run-commands' + ? targetConfiguration.command ?? targetConfiguration.options?.command + : null; + + return ( +
+
+
+ {collapsable && + (isCollasped ? ( + + ) : ( + + ))} +

{targetName}

+ + {isCollasped && + targetConfiguration?.executor !== '@nx/js:release-publish' && ( +

+ {singleCommand ? singleCommand : targetConfiguration.executor} +

+ )} + {targetName === 'nx-release-publish' && ( + ) as any} + > + + + + + )} + {targetConfiguration.cache && ( + ) as any} + > + + + + + )} +
+
+ {onViewInTaskGraph && ( + + )} + + {onRunTarget && ( + + { + e.stopPropagation(); + onRunTarget({ projectName, targetName }); + }} + /> + + )} +
+
+ {!isCollasped && ( +
+ + {targetName !== 'nx-release-publish' && ( +
+ + nx run {projectName}:{targetName} + + + + handleCopyClick(`nx run ${projectName}:${targetName}`) + } + tooltipAlignment="right" + /> + +
+ )} +
+ )} +
+ ); +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.stories.tsx b/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.stories.tsx new file mode 100644 index 0000000000..4df1ce5bfe --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.stories.tsx @@ -0,0 +1,140 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + TargetConfigurationDetailsListItem, + TargetConfigurationDetailsListItemProps, +} from './target-configuration-details-list-item'; +import { StoreDecorator } from '@nx/graph/state'; + +const meta: Meta = { + component: TargetConfigurationDetailsListItem, + title: 'TargetConfigurationDetailsListItem', + decorators: [StoreDecorator], +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + isCollasped: true, + toggleCollapse: () => {}, + collapsable: false, + isCompact: true, + targetConfiguration: {}, + projectName: 'jest', + targetName: 'test', + sourceMap: {}, + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + project: { + name: 'jest', + type: 'lib', + 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: [], + }, + }, + } as TargetConfigurationDetailsListItemProps, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.tsx b/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.tsx new file mode 100644 index 0000000000..1c657de755 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list-item/target-configuration-details-list-item.tsx @@ -0,0 +1,52 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { forwardRef, Ref } from 'react'; +import TargetConfigurationDetails from '../target-configuration-details/target-configuration-details'; + +export interface TargetConfigurationDetailsListItemProps { + project: ProjectGraphProjectNode; + sourceMap: Record; + variant?: 'default' | 'compact'; + onRunTarget?: (data: { projectName: string; targetName: string }) => void; + onViewInTaskGraph?: (data: { + projectName: string; + targetName: string; + }) => void; + targetName: string; + collapsable: boolean; +} + +export const TargetConfigurationDetailsListItem = forwardRef( + ( + { + project, + variant, + sourceMap, + onRunTarget, + onViewInTaskGraph, + targetName, + collapsable, + }: TargetConfigurationDetailsListItemProps, + ref: Ref + ) => { + const target = project.data.targets?.[targetName]; + if (!target) { + return null; + } + return ( +
  • + +
  • + ); + } +); diff --git a/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.state.ts b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.state.ts new file mode 100644 index 0000000000..2e4ed592be --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.state.ts @@ -0,0 +1,28 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import { AppDispatch, RootState, expandTargetActions } from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return {}; +}; + +const mapDispatchToProps = (dispatch: AppDispatch) => { + return { + setExpandTargets(targets: string[]) { + dispatch(expandTargetActions.setExpandTargets(targets)); + }, + collapseAllTargets() { + dispatch(expandTargetActions.collapseAllTargets()); + }, + }; +}; + +type mapStateToPropsType = ReturnType; +type mapDispatchToPropsType = ReturnType; + +export { + mapStateToProps, + mapDispatchToProps, + mapStateToPropsType, + mapDispatchToPropsType, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.stories.tsx b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.stories.tsx new file mode 100644 index 0000000000..aec737eead --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + TargetConfigurationDetailsListComponent, + TargetConfigurationDetailsListProps, +} from './target-configuration-details-list'; +import { StoreDecorator } from '@nx/graph/state'; + +const meta: Meta = { + component: TargetConfigurationDetailsListComponent, + title: 'TargetConfigurationDetailsListComponent', + decorators: [StoreDecorator], +}; +export default meta; + +type Story = StoryObj; + +export const OneTarget: Story = { + args: { + project: { + name: 'react', + type: 'lib', + data: { + root: 'libs/react', + targets: { + build: { + executor: 'nx', + options: {}, + configurations: { + production: { + executor: 'nx', + options: {}, + }, + }, + }, + lint: { + executor: 'nx', + options: {}, + }, + }, + }, + }, + sourceMap: { + react: ['react'], + }, + variant: 'default', + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + selectedTargetGroup: 'build', + setExpandTargets: () => {}, + collapseAllTargets: () => {}, + } as TargetConfigurationDetailsListProps, +}; + +export const TwoTargets: Story = { + args: { + project: { + name: 'react', + type: 'lib', + data: { + root: 'libs/react', + targets: { + build1: { + executor: 'nx', + options: {}, + configurations: { + production: { + executor: 'nx', + options: {}, + }, + }, + }, + build2: { + executor: 'nx', + options: {}, + }, + }, + metadata: { + targetGroups: { + build: ['build1', 'build2'], + }, + }, + }, + }, + sourceMap: { + react: ['react'], + }, + variant: 'default', + onRunTarget: () => {}, + onViewInTaskGraph: () => {}, + selectedTargetGroup: 'build', + setExpandTargets: () => {}, + collapseAllTargets: () => {}, + } as TargetConfigurationDetailsListProps, +}; diff --git a/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.tsx b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.tsx new file mode 100644 index 0000000000..456bb3218a --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.tsx @@ -0,0 +1,50 @@ +import { connect } from 'react-redux'; +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { + mapDispatchToProps, + mapDispatchToPropsType, + mapStateToProps, + mapStateToPropsType, +} from './target-configuration-details-list.state'; +import { TargetConfigurationGroupList } from '../target-configuration-details-group-list/target-configuration-details-group-list'; + +export type TargetConfigurationDetailsListProps = mapStateToPropsType & + mapDispatchToPropsType & { + project: ProjectGraphProjectNode; + sourceMap: Record; + variant?: 'default' | 'compact'; + onRunTarget?: (data: { projectName: string; targetName: string }) => void; + onViewInTaskGraph?: (data: { + projectName: string; + targetName: string; + }) => void; + className?: string; + }; + +export function TargetConfigurationDetailsListComponent({ + project, + variant, + sourceMap, + onRunTarget, + onViewInTaskGraph, + className, +}: TargetConfigurationDetailsListProps) { + return ( + + ); +} + +export const TargetConfigurationDetailsList = connect( + mapStateToProps, + mapDispatchToProps +)(TargetConfigurationDetailsListComponent); +export default TargetConfigurationDetailsList; diff --git a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.state.ts b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.state.ts new file mode 100644 index 0000000000..4c235d622c --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.state.ts @@ -0,0 +1,36 @@ +import { + AppDispatch, + RootState, + expandTargetActions, + getExpandedTargets, +} from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return { + expandedTargets: getExpandedTargets(state), + }; +}; + +const mapDispatchToProps = (dispatch: AppDispatch) => { + return { + expandTarget(target: string) { + dispatch(expandTargetActions.expandTarget(target)); + }, + collapseTarget(target: string) { + dispatch(expandTargetActions.collapseTarget(target)); + }, + toggleExpandTarget(target: string) { + dispatch(expandTargetActions.toggleExpandTarget(target)); + }, + }; +}; + +type mapStateToPropsType = ReturnType; +type mapDispatchToPropsType = ReturnType; + +export { + mapStateToProps, + mapDispatchToProps, + mapStateToPropsType, + mapDispatchToPropsType, +}; 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 index 5b9d662e1a..1e240c7b3c 100644 --- 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 @@ -1,547 +1,429 @@ /* eslint-disable @nx/enforce-module-boundaries */ // nx-ignore-next-line -import { - ChevronDownIcon, - ChevronUpIcon, - EyeIcon, - PlayIcon, -} from '@heroicons/react/24/outline'; +import type { TargetConfiguration } from '@nx/devkit'; -// 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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { SourceInfo } from '../source-info/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 { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard'; import { ExternalLink, PropertyInfoTooltip, Tooltip, } from '@nx/graph/ui-tooltips'; import { TooltipTriggerText } from './tooltip-trigger-text'; -import { twMerge } from 'tailwind-merge'; import { Pill } from '../pill'; +import { + mapDispatchToProps, + mapStateToProps, + mapDispatchToPropsType, + mapStateToPropsType, +} from './target-configuration-details.state'; +import { connect } from 'react-redux'; +import { TargetConfigurationDetailsHeader } from '../target-configuration-details-header/target-configuration-details-header'; -/* 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: { +type TargetConfigurationDetailsProps = mapStateToPropsType & + mapDispatchToPropsType & { projectName: string; targetName: string; - }) => void; -} + 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; + collapsable: boolean; + }; -export interface TargetConfigurationDetailsHandle { - collapse: () => void; - expand: () => void; -} +export const TargetConfigurationDetailsComponent = ({ + variant, + projectName, + targetName, + targetConfiguration, + sourceMap, + onViewInTaskGraph, + onRunTarget, + expandedTargets, + toggleExpandTarget, + collapsable, +}: TargetConfigurationDetailsProps) => { + const isCompact = variant === 'compact'; + const [collapsed, setCollapsed] = useState(true); -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 handleCopyClick = async (copyText: string) => { - await window.navigator.clipboard.writeText(copyText); - }; + const handleCollapseToggle = useCallback(() => { + toggleExpandTarget(targetName); + }, [toggleExpandTarget, targetName]); - 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`; + useEffect(() => { + if (!collapsable) { + setCollapsed(false); + return; } + if (expandedTargets.includes(targetName)) { + setCollapsed(false); + } else { + setCollapsed(true); + } + }, [expandedTargets, targetName, collapsable]); - 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]); + let executorLink: string | null = null; - const configurations = targetConfiguration.configurations; + // 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 shouldRenderOptions = - options && - (typeof options === 'object' ? Object.keys(options).length : 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 shouldRenderConfigurations = - configurations && - (typeof configurations === 'object' - ? Object.keys(configurations).length - : true); + const configurations = targetConfiguration.configurations; - return ( -
    -
    -
    -
    - {collapsed ? ( - + 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}"`) + } + /> + + ) : ( - - )} -

    {targetName}

    - {collapsed && - targetConfiguration?.executor !== '@nx/js:release-publish' && ( -

    - {singleCommand - ? singleCommand - : targetConfiguration.executor} -

    - )} - {targetName === 'nx-release-publish' && ( ) as any} + content={() as any} > - - + + Executor )} - {targetConfiguration.cache && ( - ) as any} - > - - - - - )} -

    -
    - {onViewInTaskGraph && ( - - )} - - {onRunTarget && ( - - { - e.stopPropagation(); - onRunTarget({ projectName, targetName }); - }} + +

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

    +

    - {!collapsed && ( -
    - - - - {targetName !== 'nx-release-publish' && ( -
    - - nx run {projectName}:{targetName} - - - - handleCopyClick(`nx run ${projectName}:${targetName}`) - } - tooltipAlignment="right" - /> - -
    - )} -
    - )} -
    - {/* body */} - {!collapsed && ( -
    -
    + + {targetConfiguration.inputs && ( +

    - {singleCommand ? ( + ) as any} + > - Command - - - handleCopyClick(`"command": "${singleCommand}"`) - } - /> - + Inputs - ) : ( - ) as any} - > - - Executor - - - )} + + + + handleCopyClick( + `"inputs": ${JSON.stringify( + targetConfiguration.inputs + )}` + ) + } + /> +

    -

    - {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 ? ( - +
      + {targetConfiguration.inputs.map((input, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.inputs` + ); + return ( +
    • + + {sourceInfo && ( + - ) : null; - }} - /> - -
    - - ) : ( - '' - )} - - {shouldRenderConfigurations ? ( - <> -

    - ) as any + )} + + + ); + })} + +

    + )} + {targetConfiguration.outputs && ( +
    +

    + ) as any} + > + + Outputs + + + + + handleCopyClick( + `"outputs": ${JSON.stringify( + targetConfiguration.outputs + )}` + ) } - > - - Configurations - - {' '} - {targetConfiguration.defaultConfiguration && ( - - - - )} -

    + /> + + +
      + {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}.configurations.${propertyName}` + `targets.${targetName}.options.${propertyName}` ); return sourceInfo ? ( - + {' '} + propertyKey={`targets.${targetName}.options.${propertyName}`} + /> ) : null; }} /> - - ) : ( - '' - )} -
    - )} -
    - ); - } -); +
    + + ) : ( + '' + )} + {shouldRenderConfigurations ? ( + <> +

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

    + + { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.configurations.${propertyName}` + ); + return sourceInfo ? ( + + {' '} + + ) : null; + }} + /> + + + ) : ( + '' + )} + + )} + + ); +}; + +export const TargetConfigurationDetails = connect( + mapStateToProps, + mapDispatchToProps +)(TargetConfigurationDetailsComponent); export default TargetConfigurationDetails; diff --git a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx index 01afc5be9e..ce3b3e5ae5 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details/target-configuration-property.tsx @@ -11,7 +11,7 @@ export function TargetConfigurationProperty({ }: RenderPropertyProps): JSX.Element | null { if (typeof data === 'string') { return ( - + {data} {children} @@ -20,7 +20,7 @@ export function TargetConfigurationProperty({ return (
      {data.map((item, index) => ( -
    • +
    • {String(item)} {children}
    • @@ -31,7 +31,7 @@ export function TargetConfigurationProperty({ return (
        {Object.entries(data).map(([key, value], index) => ( -
      • +
      • {key}: {String(value)} {children}
      • diff --git a/graph/ui-project-details/src/lib/target-technologies/target-technologies.stories.tsx b/graph/ui-project-details/src/lib/target-technologies/target-technologies.stories.tsx new file mode 100644 index 0000000000..bf29d539dd --- /dev/null +++ b/graph/ui-project-details/src/lib/target-technologies/target-technologies.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TargetTechnologies } from './target-technologies'; + +const meta: Meta = { + component: TargetTechnologies, + title: 'TargetTechnologies', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + technologies: ['react', 'angular'], + }, +}; diff --git a/graph/ui-project-details/src/lib/target-technologies/target-technologies.tsx b/graph/ui-project-details/src/lib/target-technologies/target-technologies.tsx new file mode 100644 index 0000000000..c4409e048d --- /dev/null +++ b/graph/ui-project-details/src/lib/target-technologies/target-technologies.tsx @@ -0,0 +1,26 @@ +import { TechnologyIcon } from '@nx/graph/ui-icons'; + +export interface TargetTechnologiesProps { + technologies?: string[]; + showTooltip?: boolean; +} + +export function TargetTechnologies({ + technologies, + showTooltip, +}: TargetTechnologiesProps) { + if (!technologies || technologies.length === 0) { + return null; + } + return ( +
        + {technologies.map((technology, index) => ( + + ))} +
        + ); +} diff --git a/graph/ui-project-details/src/lib/utils/group-targets.ts b/graph/ui-project-details/src/lib/utils/group-targets.ts new file mode 100644 index 0000000000..0e7c4b4cd6 --- /dev/null +++ b/graph/ui-project-details/src/lib/utils/group-targets.ts @@ -0,0 +1,35 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; + +/** + * This function groups targets based on the targetGroups metadata + * If there is no targetGroups metadata, it will create a group for each target + * @param project + * @returns + */ +export function groupTargets(project: ProjectGraphProjectNode): { + groups: Record; + targets: string[]; +} { + const targetGroups = project.data.metadata?.targetGroups ?? {}; + Object.entries(targetGroups).forEach(([group, targets]) => { + targetGroups[group] = targets.sort(sortNxReleasePublishLast); + }); + const allTargetsInTargetGroups: string[] = Object.values(targetGroups).flat(); + const targets: string[] = Object.keys(project.data.targets ?? {}) + .filter((target) => { + return !allTargetsInTargetGroups.includes(target); + }) + .sort(sortNxReleasePublishLast); + return { + groups: targetGroups ?? {}, + targets: targets ?? [], + }; +} + +function sortNxReleasePublishLast(a: string, b: string) { + if (a === 'nx-release-publish') return 1; + if (b === 'nx-release-publish') return -1; + return a.localeCompare(b); +} diff --git a/graph/ui-tooltips/src/lib/tooltip.tsx b/graph/ui-tooltips/src/lib/tooltip.tsx index 916865b0f4..d428d63c76 100644 --- a/graph/ui-tooltips/src/lib/tooltip.tsx +++ b/graph/ui-tooltips/src/lib/tooltip.tsx @@ -1,7 +1,6 @@ import { Attributes, cloneElement, - Fragment, HTMLAttributes, ReactElement, ReactNode, @@ -11,7 +10,6 @@ import { } from 'react'; import { - FloatingPortal, useClick, arrow, autoUpdate, diff --git a/nx-dev/nx-dev/pages/tips.tsx b/nx-dev/nx-dev/pages/tips.tsx index b9ea497cd0..fabe98ef54 100644 --- a/nx-dev/nx-dev/pages/tips.tsx +++ b/nx-dev/nx-dev/pages/tips.tsx @@ -8,7 +8,7 @@ import { import { NextSeo } from 'next-seo'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { frameworkIcons } from '@nx/nx-dev/ui-markdoc'; +import { frameworkIcons } from '@nx/graph/ui-icons'; import { ReactNode, useEffect, useState } from 'react'; interface NewYearTip { diff --git a/nx-dev/ui-markdoc/src/index.ts b/nx-dev/ui-markdoc/src/index.ts index 73e78b61fe..8bbe75fba3 100644 --- a/nx-dev/ui-markdoc/src/index.ts +++ b/nx-dev/ui-markdoc/src/index.ts @@ -54,7 +54,6 @@ import { VideoLink, videoLink } from './lib/tags/video-link.component'; // import { SvgAnimation, svgAnimation } from './lib/tags/svg-animation.component'; import { Pill } from './lib/tags/pill.component'; import { pill } from './lib/tags/pill.schema'; -import { frameworkIcons } from './lib/icons'; import { fence } from './lib/nodes/fence.schema'; import { FenceWrapper } from './lib/nodes/fence-wrapper.component'; @@ -139,8 +138,6 @@ export const parseMarkdown: (markdown: string) => Node = (markdown) => { return parse(tokens); }; -export { frameworkIcons }; - export const renderMarkdown: ( documentContent: string, options: { filePath: string } diff --git a/nx-dev/ui-markdoc/src/lib/tags/call-to-action.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/call-to-action.component.tsx index bbc171aee7..052789adb3 100644 --- a/nx-dev/ui-markdoc/src/lib/tags/call-to-action.component.tsx +++ b/nx-dev/ui-markdoc/src/lib/tags/call-to-action.component.tsx @@ -1,5 +1,5 @@ import { ChevronRightIcon } from '@heroicons/react/24/outline'; -import { frameworkIcons } from '../icons'; +import { frameworkIcons } from '@nx/graph/ui-icons'; export function CallToAction({ url, diff --git a/nx-dev/ui-markdoc/src/lib/tags/cards.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/cards.component.tsx index a64114bfa7..db6f9d2fe8 100644 --- a/nx-dev/ui-markdoc/src/lib/tags/cards.component.tsx +++ b/nx-dev/ui-markdoc/src/lib/tags/cards.component.tsx @@ -4,7 +4,7 @@ import { DocumentIcon, PlayCircleIcon, } from '@heroicons/react/24/outline'; -import { frameworkIcons } from '../icons'; +import { Framework, frameworkIcons } from '@nx/graph/ui-icons'; import { cx } from '@nx/nx-dev/ui-primitives'; import { ReactNode } from 'react'; @@ -127,7 +127,7 @@ export function LinkCard({ } )} > - {icon && frameworkIcons[icon]?.image} + {icon && frameworkIcons[icon as Framework]?.image} )}
        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 index c8996387f3..f3ac978aef 100644 --- a/nx-dev/ui-markdoc/src/lib/tags/project-details.component.tsx +++ b/nx-dev/ui-markdoc/src/lib/tags/project-details.component.tsx @@ -1,5 +1,6 @@ -import { useTheme } from '@nx/nx-dev/ui-theme'; import { JSX, ReactElement, useEffect, useState } from 'react'; +import { Provider as StoreProvider } from 'react-redux'; +import { rootStore } from '@nx/graph/state'; import { ProjectDetails as ProjectDetailsUi } from '@nx/graph/ui-project-details'; export function Loading() { @@ -26,7 +27,6 @@ export function ProjectDetails({ jsonFile?: string; children: ReactElement; }): JSX.Element { - const [theme] = useTheme(); const [parsedProps, setParsedProps] = useState(); const getData = async (path: string) => { const response = await fetch('/documentation/' + path, { @@ -73,7 +73,7 @@ export function ProjectDetails({ return (
        {title && ( -
        +
        {title}
        )} @@ -82,11 +82,13 @@ export function ProjectDetails({ height ? `p-4 h-[${height}] overflow-y-auto` : 'p-4' }`} > - + + +
        ); diff --git a/packages/gradle/src/plugin/nodes.spec.ts b/packages/gradle/src/plugin/nodes.spec.ts index e56ce0b858..304a07d94a 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -69,6 +69,11 @@ describe('@nx/gradle/plugin', () => { expect(nodes.projects.proj).toMatchInlineSnapshot(` { "metadata": { + "targetGroups": { + "Test": [ + "test", + ], + }, "technologies": [ "gradle", ], @@ -85,6 +90,11 @@ describe('@nx/gradle/plugin', () => { "default", "^production", ], + "metadata": { + "technologies": [ + "gradle", + ], + }, "options": { "cwd": "proj", }, @@ -124,6 +134,11 @@ describe('@nx/gradle/plugin', () => { expect(nodes.projects['nested/nested/proj']).toMatchInlineSnapshot(` { "metadata": { + "targetGroups": { + "Test": [ + "test", + ], + }, "technologies": [ "gradle", ], @@ -140,6 +155,11 @@ describe('@nx/gradle/plugin', () => { "default", "^production", ], + "metadata": { + "technologies": [ + "gradle", + ], + }, "options": { "cwd": "nested/nested/proj", }, diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index 4da844fc0e..cdc8dbe773 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -4,7 +4,6 @@ import { ProjectConfiguration, TargetConfiguration, readJsonFile, - workspaceRoot, writeJsonFile, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; @@ -42,7 +41,7 @@ export const calculatedTargets: Record< { name: string; targets: Record; - targetGroups: Record; + metadata: ProjectConfiguration['metadata']; } > = {}; @@ -51,7 +50,7 @@ function readTargetsCache(): Record< { name: string; targets: Record; - targetGroups: Record; + metadata: ProjectConfiguration['metadata']; } > { return readJsonFile(cachePath); @@ -63,7 +62,7 @@ export function writeTargetsToCache( { name: string; targets: Record; - targetGroups: Record; + metadata: ProjectConfiguration['metadata']; } > ) { @@ -88,12 +87,7 @@ export const createNodes: CreateNodes = [ calculatedTargets[hash] = targetsCache[hash]; return { projects: { - [projectRoot]: { - ...targetsCache[hash], - metadata: { - technologies: ['gradle'], - }, - }, + [projectRoot]: targetsCache[hash], }, }; } @@ -137,19 +131,15 @@ export const createNodes: CreateNodes = [ context, outputDirs ); - calculatedTargets[hash] = { - name: projectName, - targets, - targetGroups, - }; - - const project: Omit = { + const project = { name: projectName, targets, metadata: { + targetGroups, technologies: ['gradle'], }, }; + calculatedTargets[hash] = project; return { projects: { @@ -194,6 +184,9 @@ function createGradleTargets( inputs: inputsMap[task.name], outputs: outputs ? [outputs] : undefined, dependsOn: dependsOnMap[task.name], + metadata: { + technologies: ['gradle'], + }, }; if (!targetGroups[task.type]) { targetGroups[task.type] = []; diff --git a/tsconfig.base.json b/tsconfig.base.json index de34ac5a19..5bb03a0d55 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,9 +39,11 @@ "@nx/gradle/*": ["packages/gradle/*"], "@nx/graph/project-details": ["graph/project-details/src/index.ts"], "@nx/graph/shared": ["graph/shared/src/index.ts"], + "@nx/graph/state": ["graph/state/src/index.ts"], "@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-icons": ["graph/ui-icons/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"],