From cc0d56ca4a500229e2b91bb0dd68c7fc52da713b Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Thu, 11 Apr 2024 13:47:48 -0400 Subject: [PATCH] feat(graph): add target groups --- graph/client/jest.config.ts | 2 +- graph/client/src/app/app.tsx | 8 +- graph/client/src/app/external-api-impl.ts | 2 +- .../src/app/ui-components/error-boundary.tsx | 4 +- graph/client/src/styles.css | 10 + .../src/lib/project-details-page.tsx | 4 +- .../src/lib/project-details-wrapper.state.ts | 42 + .../src/lib/project-details-wrapper.tsx | 176 ++-- graph/state/.babelrc | 12 + graph/state/.eslintrc.json | 18 + graph/state/project.json | 11 + graph/state/src/index.ts | 6 + .../expand-targets/expand-targets.slice.ts | 54 ++ .../state/src/lib/root/root-state.initial.ts | 15 + graph/state/src/lib/root/root.reducer.ts | 19 + graph/state/src/lib/root/root.store.ts | 19 + .../select-target-group.slice.ts | 31 + graph/state/src/lib/store.decorator.tsx | 7 + graph/state/tsconfig.json | 17 + graph/state/tsconfig.lib.json | 27 + graph/ui-icons/.babelrc | 12 + graph/ui-icons/.eslintrc.json | 18 + graph/ui-icons/.storybook/main.ts | 21 + graph/ui-icons/.storybook/preview.ts | 1 + graph/ui-icons/.storybook/tailwind.css | 3 + graph/ui-icons/README.md | 7 + graph/ui-icons/postcss.config.js | 10 + graph/ui-icons/project.json | 54 ++ graph/ui-icons/src/index.ts | 2 + .../src/lib/framework-icons.stories.tsx | 28 + .../ui-icons/src/lib/framework-icons.tsx | 92 +- .../src/lib/technology-icon.stories.tsx | 16 + graph/ui-icons/src/lib/technology-icon.tsx | 27 + graph/ui-icons/tailwind.config.js | 40 + graph/ui-icons/tsconfig.json | 20 + graph/ui-icons/tsconfig.lib.json | 27 + graph/ui-icons/tsconfig.storybook.json | 31 + graph/ui-project-details/src/index.ts | 1 + .../copy-to-clipboard.stories.tsx | 17 + .../copy-to-clipboard.tsx | 0 .../project-details.stories.tsx | 625 +++++++++++++ .../lib/project-details/project-details.tsx | 237 ++--- .../lib/source-info/source-info.stories.tsx | 17 + .../source-info.tsx | 0 ...t-configuration-details-header.stories.tsx | 73 ++ .../target-configuration-details-header.tsx | 178 ++++ ...target-configuration-details-list.state.ts | 35 + ...get-configuration-details-list.stories.tsx | 94 ++ .../target-configuration-details-list.tsx | 87 ++ .../target-configuration-details.state.ts | 36 + .../target-configuration-details.tsx | 828 ++++++++---------- .../lib/target-group/target-group.stories.tsx | 28 + .../src/lib/target-group/target-group.tsx | 41 + .../lib/target-groups/target-groups.state.ts | 30 + .../target-groups/target-groups.stories.tsx | 142 +++ .../src/lib/target-groups/target-groups.tsx | 141 +++ .../target-technologies.stories.tsx | 16 + .../target-technologies.tsx | 26 + .../src/lib/utils/group-targets.ts | 53 ++ graph/ui-tooltips/src/lib/tooltip.tsx | 2 - nx-dev/nx-dev/pages/tips.tsx | 2 +- nx-dev/ui-markdoc/src/index.ts | 3 - .../src/lib/tags/call-to-action.component.tsx | 2 +- .../src/lib/tags/cards.component.tsx | 2 +- .../lib/tags/project-details.component.tsx | 16 +- nx.json | 43 +- packages/gradle/src/plugin/nodes.ts | 1 + tsconfig.base.json | 2 + 68 files changed, 2957 insertions(+), 714 deletions(-) create mode 100644 graph/project-details/src/lib/project-details-wrapper.state.ts create mode 100644 graph/state/.babelrc create mode 100644 graph/state/.eslintrc.json create mode 100644 graph/state/project.json create mode 100644 graph/state/src/index.ts create mode 100644 graph/state/src/lib/expand-targets/expand-targets.slice.ts create mode 100644 graph/state/src/lib/root/root-state.initial.ts create mode 100644 graph/state/src/lib/root/root.reducer.ts create mode 100644 graph/state/src/lib/root/root.store.ts create mode 100644 graph/state/src/lib/select-target-group/select-target-group.slice.ts create mode 100644 graph/state/src/lib/store.decorator.tsx create mode 100644 graph/state/tsconfig.json create mode 100644 graph/state/tsconfig.lib.json create mode 100644 graph/ui-icons/.babelrc create mode 100644 graph/ui-icons/.eslintrc.json create mode 100644 graph/ui-icons/.storybook/main.ts create mode 100644 graph/ui-icons/.storybook/preview.ts create mode 100644 graph/ui-icons/.storybook/tailwind.css create mode 100644 graph/ui-icons/README.md create mode 100644 graph/ui-icons/postcss.config.js create mode 100644 graph/ui-icons/project.json create mode 100644 graph/ui-icons/src/index.ts create mode 100644 graph/ui-icons/src/lib/framework-icons.stories.tsx rename nx-dev/ui-markdoc/src/lib/icons.tsx => graph/ui-icons/src/lib/framework-icons.tsx (98%) create mode 100644 graph/ui-icons/src/lib/technology-icon.stories.tsx create mode 100644 graph/ui-icons/src/lib/technology-icon.tsx create mode 100644 graph/ui-icons/tailwind.config.js create mode 100644 graph/ui-icons/tsconfig.json create mode 100644 graph/ui-icons/tsconfig.lib.json create mode 100644 graph/ui-icons/tsconfig.storybook.json create mode 100644 graph/ui-project-details/src/lib/copy-to-clipboard/copy-to-clipboard.stories.tsx rename graph/ui-project-details/src/lib/{target-configuration-details => copy-to-clipboard}/copy-to-clipboard.tsx (100%) create mode 100644 graph/ui-project-details/src/lib/source-info/source-info.stories.tsx rename graph/ui-project-details/src/lib/{target-configuration-details => source-info}/source-info.tsx (100%) create mode 100644 graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.stories.tsx create mode 100644 graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx create mode 100644 graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.state.ts create mode 100644 graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.stories.tsx create mode 100644 graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.tsx create mode 100644 graph/ui-project-details/src/lib/target-configuration-details/target-configuration-details.state.ts create mode 100644 graph/ui-project-details/src/lib/target-group/target-group.stories.tsx create mode 100644 graph/ui-project-details/src/lib/target-group/target-group.tsx create mode 100644 graph/ui-project-details/src/lib/target-groups/target-groups.state.ts create mode 100644 graph/ui-project-details/src/lib/target-groups/target-groups.stories.tsx create mode 100644 graph/ui-project-details/src/lib/target-groups/target-groups.tsx create mode 100644 graph/ui-project-details/src/lib/target-technologies/target-technologies.stories.tsx create mode 100644 graph/ui-project-details/src/lib/target-technologies/target-technologies.tsx create mode 100644 graph/ui-project-details/src/lib/utils/group-targets.ts diff --git a/graph/client/jest.config.ts b/graph/client/jest.config.ts index 78336ace45f6bf..a246a340b46bf4 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 90dff08ff3b002..254b91f635bfc5 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/external-api-impl.ts b/graph/client/src/app/external-api-impl.ts index 724c9ebf878171..839103bac26c4c 100644 --- a/graph/client/src/app/external-api-impl.ts +++ b/graph/client/src/app/external-api-impl.ts @@ -94,7 +94,7 @@ export class ExternalApiImpl extends ExternalApi { const currentLocation = this.router.state.location; const searchParams = new URLSearchParams(currentLocation.search); - searchParams.set('expanded', targetName); + searchParams.set('targetName', targetName); const newUrl = `${currentLocation.pathname}?${searchParams.toString()}`; this.router.navigate(newUrl); diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx index 561a95a691be5b..e98efaa07285e8 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/styles.css b/graph/client/src/styles.css index 0349580f6f685c..88e771941b23d7 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 c6e85a8992d86d..fc4e6a01f2b4c4 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 00000000000000..117bd937f40fba --- /dev/null +++ b/graph/project-details/src/lib/project-details-wrapper.state.ts @@ -0,0 +1,42 @@ +import { + AppDispatch, + RootState, + expandTargetActions, + getExpandedTargets, + getSelectedTargetGroup, + selectTargetGroupActions, +} from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return { + expandTargets: getExpandedTargets(state), + selectedTargetGroup: getSelectedTargetGroup(state), + }; +}; + +const mapDispatchToProps = (dispatch: AppDispatch) => { + return { + setExpandTargets(targets: string[]) { + dispatch(expandTargetActions.setExpandTargets(targets)); + }, + selectTargetGroup(targetGroup: string) { + dispatch(selectTargetGroupActions.selectTargetGroup(targetGroup)); + }, + collapseAllTargets() { + dispatch(expandTargetActions.collapseAllTargets()); + }, + clearTargetGroup() { + dispatch(selectTargetGroupActions.clearTargetGroup()); + }, + }; +}; + +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 e9c56339741b8f..0035815a94b236 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, 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, useSearchParams } from 'react-router-dom'; +import { connect } from 'react-redux'; import { getExternalApiService, useEnvironmentConfig, @@ -12,17 +10,33 @@ import { } from '@nx/graph/shared'; import { ProjectDetails, - ProjectDetailsImperativeHandle, + defaultSelectTargetGroup, + getTargetGroupForTarget, } from '@nx/graph/ui-project-details'; -import { useCallback, useLayoutEffect, useRef } from 'react'; +import { useCallback, useEffect } from 'react'; +import { + 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, + selectedTargetGroup, + selectTargetGroup, + clearTargetGroup, + collapseAllTargets, +}: ProjectDetailsProps) { const environment = useEnvironmentConfig()?.environment; const externalApiService = getExternalApiService(); const navigate = useNavigate(); @@ -85,70 +99,96 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) { [externalApiService] ); - const updateSearchParams = (params: URLSearchParams, sections: string[]) => { - if (sections.length === 0) { - params.delete('expanded'); + const updateSearchParams = ( + params: URLSearchParams, + targetGroup: string | null, + targetNames: string[] + ) => { + if (targetGroup) { + params.set('targetGroup', targetGroup); + } else { + params.delete('targetGroup'); + } + if (targetNames.length === 0) { + params.delete('targetName'); } else { - params.set('expanded', sections.join(',')); + params.set('targetName', 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] - ); + const selectedTargetGroupParams = searchParams.get('targetGroup'); + if ( + selectedTargetGroupParams && + selectedTargetGroup !== selectedTargetGroupParams + ) { + selectTargetGroup(selectedTargetGroupParams); + } else if (!selectedTargetGroupParams) { + selectTargetGroup(defaultSelectTargetGroup(project)); // set first target group as default + } - useLayoutEffect(() => { - if (!props.project.data.targets) return; + const expandedTargetsParams = + searchParams.get('targetName')?.split(',') || []; + if (expandedTargetsParams.length > 0) { + setExpandTargets(expandedTargetsParams); + } - 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 targetName = searchParams.get('targetName'); + if (targetName) { + const targetGroup = getTargetGroupForTarget(targetName, project); + selectTargetGroup(targetGroup); + setExpandTargets([targetName]); + } + + return () => { + clearTargetGroup(); + collapseAllTargets(); + searchParams.delete('targetGroup'); + searchParams.delete('targetName'); + setSearchParams(searchParams, { replace: true }); + }; + }, []); // only run on mount + + useEffect(() => { + if (!project.data.targets) return; + + const selectedTargetGroupParams = searchParams.get('targetGroup'); + const expandedTargetsParams = + searchParams.get('targetName')?.split(',') || []; + + if ( + selectedTargetGroup === selectedTargetGroupParams && + expandedTargetsParams.join(',') === expandTargets.join(',') + ) { + return; } - }, [searchParams, props.project.data.targets, projectDetailsRef]); + + setSearchParams( + (currentSearchParams) => { + updateSearchParams( + currentSearchParams, + selectedTargetGroup, + expandTargets + ); + return currentSearchParams; + }, + { replace: true, preventScrollReset: true } + ); + }, [ + expandTargets, + selectedTargetGroup, + project.data.targets, + setExpandTargets, + searchParams, + setSearchParams, + ]); 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 00000000000000..1ba293691481cc --- /dev/null +++ b/graph/state/src/lib/root/root-state.initial.ts @@ -0,0 +1,15 @@ +import { + EXPAND_TARGETS_KEY, + initialExpandTargets, +} from '../expand-targets/expand-targets.slice'; +import { + SELECT_TARGET_GROUP_KEY, + initialSelectTargetGroup, +} from '../select-target-group/select-target-group.slice'; + +import { RootState } from './root.reducer'; + +export const initialRootState: RootState = { + [EXPAND_TARGETS_KEY]: initialExpandTargets, + [SELECT_TARGET_GROUP_KEY]: initialSelectTargetGroup, +}; 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 00000000000000..dd5dcd58a73a50 --- /dev/null +++ b/graph/state/src/lib/root/root.reducer.ts @@ -0,0 +1,19 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import { + EXPAND_TARGETS_KEY, + expandTargetReducer, +} from '../expand-targets/expand-targets.slice'; +import { + SELECT_TARGET_GROUP_KEY, + selectTargetGroupReducer, +} from '../select-target-group/select-target-group.slice'; + +export const rootReducer = combineReducers<{ + [EXPAND_TARGETS_KEY]: string[]; + [SELECT_TARGET_GROUP_KEY]: string; +}>({ + [EXPAND_TARGETS_KEY]: expandTargetReducer, + [SELECT_TARGET_GROUP_KEY]: selectTargetGroupReducer as any, +}); + +export type RootState = ReturnType; 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 00000000000000..1362159ede6322 --- /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/select-target-group/select-target-group.slice.ts b/graph/state/src/lib/select-target-group/select-target-group.slice.ts new file mode 100644 index 00000000000000..e3598f34a4dc99 --- /dev/null +++ b/graph/state/src/lib/select-target-group/select-target-group.slice.ts @@ -0,0 +1,31 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +export const SELECT_TARGET_GROUP_KEY = 'selectTargetGroup'; + +export const initialSelectTargetGroup: string = ''; + +export const selectTargetSlice = createSlice({ + name: SELECT_TARGET_GROUP_KEY, + initialState: initialSelectTargetGroup, + reducers: { + selectTargetGroup: (_: string, action: PayloadAction): string => { + return action.payload; + }, + clearTargetGroup: (): string => { + return ''; + }, + }, +}); + +/* + * Export reducer for store configuration. + */ +export const selectTargetGroupReducer = selectTargetSlice.reducer; + +export const selectTargetGroupActions = selectTargetSlice.actions; + +export const getSelectedTargetGroup = < + ROOT extends { [SELECT_TARGET_GROUP_KEY]: string } +>( + rootState: ROOT +): string => rootState[SELECT_TARGET_GROUP_KEY]; diff --git a/graph/state/src/lib/store.decorator.tsx b/graph/state/src/lib/store.decorator.tsx new file mode 100644 index 00000000000000..4f2e9fb13fe72e --- /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 00000000000000..95cfeb243dd57d --- /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 00000000000000..8c1bec17db74e0 --- /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 00000000000000..1ea870ead410c4 --- /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 00000000000000..b96a5b888e9c0f --- /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 00000000000000..dfca18212214db --- /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 00000000000000..195b052493edcf --- /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 00000000000000..23d597fe51b0bc --- /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 00000000000000..fda4b05bf551d2 --- /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 00000000000000..8b2e63b9e6bea2 --- /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 00000000000000..a12607785331a5 --- /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 00000000000000..829dd6931b3168 --- /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 00000000000000..b02eeb30528d55 --- /dev/null +++ b/graph/ui-icons/src/lib/framework-icons.stories.tsx @@ -0,0 +1,28 @@ +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 = { + args: {}, +}; + +export const Heading: 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 c2688ebbbf8f03..b9624aae3112c2 100644 --- a/nx-dev/ui-markdoc/src/lib/icons.tsx +++ b/graph/ui-icons/src/lib/framework-icons.tsx @@ -1,7 +1,68 @@ +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: { @@ -227,6 +288,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, nestjs: { image: ( @@ -338,6 +400,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, express: { image: ( @@ -352,6 +415,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, storybook: { image: ( @@ -820,6 +884,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, nuxt: { image: ( @@ -1017,6 +1082,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, expo: { image: ( @@ -1030,6 +1096,7 @@ export const frameworkIcons: Record< ), + isAdaptiveIcon: true, }, react: { image: ( @@ -1622,4 +1689,27 @@ export const frameworkIcons: Record< ), }, + 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 00000000000000..d75b572cc6535d --- /dev/null +++ b/graph/ui-icons/src/lib/technology-icon.stories.tsx @@ -0,0 +1,16 @@ +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', + }, +}; 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 00000000000000..f8696f44d1427f --- /dev/null +++ b/graph/ui-icons/src/lib/technology-icon.tsx @@ -0,0 +1,27 @@ +import { Framework, frameworkIcons } from './framework-icons'; + +export interface TechnologyIconProps { + technology?: string; + showTooltip?: boolean; +} + +export function TechnologyIcon({ + technology, + showTooltip, +}: TechnologyIconProps) { + if (!technology || !frameworkIcons[technology as Framework]) { + return null; + } + return ( +
+ {frameworkIcons[technology as Framework].image} +
+ ); +} diff --git a/graph/ui-icons/tailwind.config.js b/graph/ui-icons/tailwind.config.js new file mode 100644 index 00000000000000..a130bfceccd483 --- /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 00000000000000..53e12dc04e104f --- /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 00000000000000..8c1bec17db74e0 --- /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 00000000000000..2da3caee121ed2 --- /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 c496e5eaaef2d3..fa5bc208e12594 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 00000000000000..8101be70cfbea3 --- /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 52583bded47c85..9fffd027df1058 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 CartE2e = { + 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 27b0c4d75eb261..ab16d2d644cc47 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 { TargetGroups } from '../target-groups/target-groups'; +import { TargetConfigurationDetailsList } from '../target-configuration-details-list/target-configuration-details-list'; 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,104 @@ export interface ProjectDetailsProps { onRunTarget?: (data: { projectName: string; targetName: string }) => void; } -export interface ProjectDetailsImperativeHandle { - collapseTarget: (targetName: string) => void; - expandTarget: (targetName: string) => void; -} - -export const ProjectDetails = forwardRef( - ( - { - project: { - name, - data: { root, ...projectData }, - }, - sourceMap, - variant, - onTargetCollapse, - onTargetExpand, - onViewInProjectGraph, - onViewInTaskGraph, - onRunTarget, - }: ProjectDetailsProps, - ref: ForwardedRef - ) => { - const isCompact = variant === 'compact'; - const projectTargets = Object.keys(projectData.targets ?? {}); - const targetRefs = useRef( - projectTargets.reduce((acc, targetName) => { - acc[targetName] = createRef(); - return acc; - }, {} as Record>) - ); - - const displayType = - projectData.projectType && - projectData.projectType?.charAt(0)?.toUpperCase() + - projectData.projectType?.slice(1); +export const ProjectDetails = ({ + project, + sourceMap, + variant, + onViewInProjectGraph, + onViewInTaskGraph, + onRunTarget, +}: ProjectDetailsProps) => { + const projectData = project.data; + const isCompact = variant === 'compact'; - useImperativeHandle(ref, () => ({ - collapseTarget: (targetName: string) => { - targetRefs.current[targetName]?.current?.collapse(); - }, - expandTarget: (targetName: string) => { - targetRefs.current[targetName]?.current?.expand(); - }, - })); + const displayType = + projectData.projectType && + projectData.projectType?.charAt(0)?.toUpperCase() + + projectData.projectType?.slice(1); - return ( - <> -
+
+

-

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

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

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

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

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

- {displayType ? ( -

- Type: - {displayType} -

- ) : null} -
-
-
-

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

-
    - {projectTargets.sort(sortNxReleasePublishLast).map((targetName) => { - const target = projectData.targets?.[targetName]; - return target && targetRefs.current[targetName] ? ( -
  • - -
  • - ) : null; - })} -
+ ) : 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 00000000000000..7b5cebe7daf077 --- /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 100% 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 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 00000000000000..eabcb7989e713d --- /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 00000000000000..a22d99f7fd8faf --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-header/target-configuration-details-header.tsx @@ -0,0 +1,178 @@ +/* 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/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 00000000000000..4dc91ff3ae6845 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.state.ts @@ -0,0 +1,35 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import { + AppDispatch, + RootState, + getSelectedTargetGroup, + expandTargetActions, +} from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return { + selectedTargetGroup: getSelectedTargetGroup(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/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 00000000000000..aec737eeadebe2 --- /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 00000000000000..768d0bdba888b6 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-configuration-details-list/target-configuration-details-list.tsx @@ -0,0 +1,87 @@ +import { connect } from 'react-redux'; +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import TargetConfigurationDetails from '../target-configuration-details/target-configuration-details'; +import { + mapDispatchToProps, + mapDispatchToPropsType, + mapStateToProps, + mapStateToPropsType, +} from './target-configuration-details-list.state'; +import { useEffect, useState } from 'react'; + +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, + selectedTargetGroup, +}: TargetConfigurationDetailsListProps) { + const [targetsUnderTargetGroup, setTargetsUnderTargetGroup] = useState< + string[] + >([]); + + useEffect(() => { + if (selectedTargetGroup) { + let targets: string[] = []; + if (project.data.metadata?.targetGroups) { + targets = project.data.metadata.targetGroups[selectedTargetGroup] ?? []; + } + if (targets.length === 0 && project.data.targets?.[selectedTargetGroup]) { + targets = [selectedTargetGroup]; + } + setTargetsUnderTargetGroup(targets); + } + }, [ + selectedTargetGroup, + project.data.metadata?.targetGroups, + project.data.targets, + ]); + + return ( +
    + {targetsUnderTargetGroup.map((targetName) => { + const target = project.data.targets?.[targetName]; + if (!target) { + return null; + } + return ( +
  • + 1} + /> +
  • + ); + })} +
+ ); +} + +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 00000000000000..4c235d622cf950 --- /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 6d987cd30f2feb..adabf65b9c1229 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; -} - -export interface TargetConfigurationDetailsHandle { - collapse: () => void; - expand: () => 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 const TargetConfigurationDetails = forwardRef( - ( - { - variant, - projectName, - targetName, - targetConfiguration, - sourceMap, - onExpand, - onCollapse, - onViewInTaskGraph, - onRunTarget, - }: TargetProps, - ref: ForwardedRef - ) => { - const isCompact = variant === 'compact'; - const [collapsed, setCollapsed] = useState(true); +export const TargetConfigurationDetailsComponent = ({ + variant, + projectName, + targetName, + targetConfiguration, + sourceMap, + onViewInTaskGraph, + onRunTarget, + expandedTargets, + toggleExpandTarget, + collapsable, +}: TargetConfigurationDetailsProps) => { + 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( - () => setCollapsed((collapsed) => !collapsed), - [setCollapsed] - ); + const handleCollapseToggle = useCallback(() => { + toggleExpandTarget(targetName); + }, [toggleExpandTarget, targetName]); - useEffect(() => { - if (collapsed) { - onCollapse?.(targetName); - } else { - onExpand?.(targetName); - } - }, [collapsed, onCollapse, onExpand, projectName, targetName]); + useEffect(() => { + if (!collapsable) { + setCollapsed(false); + return; + } + if (expandedTargets.includes(targetName)) { + setCollapsed(false); + } else { + setCollapsed(true); + } + }, [expandedTargets, targetName, collapsable]); - useImperativeHandle(ref, () => ({ - collapse: () => { - !collapsed && setCollapsed(true); - }, - expand: () => { - collapsed && setCollapsed(false); - }, - })); + let executorLink: string | null = null; - 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`; + } - // TODO: Handle this better because this will not work with labs - if (targetConfiguration.executor?.startsWith('@nx/')) { - const packageName = targetConfiguration.executor - .split('/')[1] - .split(':')[0]; - const executorName = targetConfiguration.executor - .split('/')[1] - .split(':')[1]; - executorLink = `https://nx.dev/nx-api/${packageName}/executors/${executorName}`; - } else if (targetConfiguration.executor === 'nx:run-commands') { - executorLink = `https://nx.dev/nx-api/nx/executors/run-commands`; - } else if (targetConfiguration.executor === 'nx:run-script') { - executorLink = `https://nx.dev/nx-api/nx/executors/run-script`; + const singleCommand = + targetConfiguration.executor === 'nx:run-commands' + ? targetConfiguration.command ?? targetConfiguration.options?.command + : null; + const options = useMemo(() => { + if (singleCommand) { + const { command, ...rest } = targetConfiguration.options; + return rest; + } else { + return targetConfiguration.options; } + }, [targetConfiguration.options, singleCommand]); - const singleCommand = - targetConfiguration.executor === 'nx:run-commands' - ? targetConfiguration.command ?? targetConfiguration.options?.command - : null; - const options = useMemo(() => { - if (singleCommand) { - const { command, ...rest } = targetConfiguration.options; - return rest; - } else { - return targetConfiguration.options; - } - }, [targetConfiguration.options, singleCommand]); + const configurations = targetConfiguration.configurations; - const configurations = targetConfiguration.configurations; + const shouldRenderOptions = + options && + (typeof options === 'object' ? Object.keys(options).length : true); - const shouldRenderOptions = - options && - (typeof options === 'object' ? Object.keys(options).length : true); + const shouldRenderConfigurations = + configurations && + (typeof configurations === 'object' + ? Object.keys(configurations).length + : true); - const shouldRenderConfigurations = - configurations && - (typeof configurations === 'object' - ? Object.keys(configurations).length - : true); - - return ( -
-
-
-
- {collapsed ? ( - + 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 && ( +

+

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

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

) as any} + content={() as any} > - - + + Inputs - )} -

-
- {onViewInTaskGraph && ( - - )} - - {onRunTarget && ( - - { - e.stopPropagation(); - onRunTarget({ projectName, targetName }); - }} + + + handleCopyClick( + `"inputs": ${JSON.stringify( + targetConfiguration.inputs + )}` + ) + } /> - )} + +
    + {targetConfiguration.inputs.map((input, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.inputs` + ); + return ( +
  • + + {sourceInfo && ( + + + + )} + +
  • + ); + })} +
-
- {!collapsed && ( -
- - - - {targetName !== 'nx-release-publish' && ( -
- - nx run {projectName}:{targetName} - - - - handleCopyClick(`nx run ${projectName}:${targetName}`) - } - tooltipAlignment="right" - /> + )} + {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} +
)} -
- {/* body */} - {!collapsed && ( -
-
+ {targetConfiguration.dependsOn && ( +

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

-

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

-
+
    + {targetConfiguration.dependsOn.map((dep, idx) => { + const sourceInfo = selectSourceInfo( + sourceMap, + `targets.${targetName}.dependsOn` + ); - {targetConfiguration.inputs && ( -
    -

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

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

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

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

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

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

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

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

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

+ {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-group/target-group.stories.tsx b/graph/ui-project-details/src/lib/target-group/target-group.stories.tsx new file mode 100644 index 00000000000000..95e03ed56af0f6 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-group/target-group.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TargetGroup, TargetGroupProps } from './target-group'; + +const meta: Meta = { + component: TargetGroup, + title: 'TargetGroup', +}; +export default meta; + +type Story = StoryObj; + +export const Simple: Story = { + args: { + name: 'react', + selected: false, + isCompact: false, + onClick: () => {}, + }, +}; + +export const Compact: Story = { + args: { + name: 'react', + selected: false, + isCompact: true, + onClick: () => {}, + } as TargetGroupProps, +}; diff --git a/graph/ui-project-details/src/lib/target-group/target-group.tsx b/graph/ui-project-details/src/lib/target-group/target-group.tsx new file mode 100644 index 00000000000000..173557c350e548 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-group/target-group.tsx @@ -0,0 +1,41 @@ +import { ChevronRightIcon } from '@heroicons/react/24/outline'; +import { twMerge } from 'tailwind-merge'; +import { TargetTechnologies } from '../target-technologies/target-technologies'; + +export interface TargetGroupProps { + name: string; + selected: boolean; + isCompact: boolean; + onClick: (name: string) => void; + technologies?: string[]; +} + +export function TargetGroup({ + selected, + name, + onClick, + isCompact, + technologies, +}: TargetGroupProps) { + return ( +
  • onClick(name)} + > +
    +

    + + {name} +

    + +
    +
  • + ); +} diff --git a/graph/ui-project-details/src/lib/target-groups/target-groups.state.ts b/graph/ui-project-details/src/lib/target-groups/target-groups.state.ts new file mode 100644 index 00000000000000..8b4fccc1f8c812 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-groups/target-groups.state.ts @@ -0,0 +1,30 @@ +import { + AppDispatch, + RootState, + selectTargetGroupActions, + getSelectedTargetGroup, +} from '@nx/graph/state'; + +const mapStateToProps = (state: RootState) => { + return { + selectedTargetGroup: getSelectedTargetGroup(state), + }; +}; + +const mapDispatchToProps = (dispatch: AppDispatch) => { + return { + selectTargetGroup(targetGroup: string) { + dispatch(selectTargetGroupActions.selectTargetGroup(targetGroup)); + }, + }; +}; + +type mapStateToPropsType = ReturnType; +type mapDispatchToPropsType = ReturnType; + +export { + mapStateToProps, + mapDispatchToProps, + mapStateToPropsType, + mapDispatchToPropsType, +}; diff --git a/graph/ui-project-details/src/lib/target-groups/target-groups.stories.tsx b/graph/ui-project-details/src/lib/target-groups/target-groups.stories.tsx new file mode 100644 index 00000000000000..7d3d45ee62e35b --- /dev/null +++ b/graph/ui-project-details/src/lib/target-groups/target-groups.stories.tsx @@ -0,0 +1,142 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { TargetGroupsComponent, TargetGroupsProps } from './target-groups'; + +const meta: Meta = { + component: TargetGroupsComponent, + title: 'TargetGroupsComponent', +}; +export default meta; + +type Story = StoryObj; + +export const BuildBaseSelected: Story = { + args: { + 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: '/', + }, + ], + }, + configurations: {}, + }, + }, + }, + }, + selectedTargetGroup: 'build-base', + selectTargetGroup(targetGroup) { + console.log(targetGroup); + }, + } as TargetGroupsProps, +}; + +export const TestSelected: Story = { + args: { + 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: '/', + }, + ], + }, + configurations: {}, + }, + }, + }, + }, + selectedTargetGroup: 'test', + selectTargetGroup(targetGroup) { + console.log(targetGroup); + }, + } as TargetGroupsProps, +}; diff --git a/graph/ui-project-details/src/lib/target-groups/target-groups.tsx b/graph/ui-project-details/src/lib/target-groups/target-groups.tsx new file mode 100644 index 00000000000000..afb51eb0a56124 --- /dev/null +++ b/graph/ui-project-details/src/lib/target-groups/target-groups.tsx @@ -0,0 +1,141 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +// nx-ignore-next-line +import type { ProjectGraphProjectNode } from '@nx/devkit'; +import { Fragment, useEffect, useState } from 'react'; +import { Listbox, Transition } from '@headlessui/react'; +import { TargetGroup } from '../target-group/target-group'; +import { groupTargets } from '../utils/group-targets'; +import { + mapDispatchToProps, + mapDispatchToPropsType, + mapStateToProps, + mapStateToPropsType, +} from './target-groups.state'; +import { connect } from 'react-redux'; +import { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/24/outline'; + +export type TargetGroupsProps = mapStateToPropsType & + mapDispatchToPropsType & { + className?: string; + project: ProjectGraphProjectNode; + variant?: 'default' | 'compact'; + }; + +export function TargetGroupsComponent({ + className, + project, + variant, + selectedTargetGroup, + selectTargetGroup, +}: TargetGroupsProps) { + const [targetGroups, setTargetGroups] = useState>( + {} + ); + const isCompact = variant === 'compact'; + + useEffect(() => { + const groups = groupTargets(project); + setTargetGroups(groups); + }, [project]); + + if (!targetGroups || !Object.keys(targetGroups).length) { + return null; + } + + return ( + <> +
    + +
    +
      + {Object.keys(targetGroups).map((targetGroup) => { + return ( + selectTargetGroup(targetGroup)} + technologies={project.data.metadata?.technologies} + /> + ); + })} +
    + + ); +} + +// example from https://headlessui.com/react/listbox +export function TargetGroupsInSelectBox({ + targetGroupNames, + selectedTargetGroup, + selectTargetGroup, +}: { + targetGroupNames: string[]; + selectedTargetGroup: string; + selectTargetGroup: (name: string) => void; +}) { + return ( + +
    + + {selectedTargetGroup} + + + + + + {targetGroupNames.map((targetGroupName, index) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? 'bg-slate-50 dark:bg-slate-800/60 dark:border-slate-700/60 dark:border-slate-300/10' + : 'text-slate-500 dark:text-slate-400 ' + }` + } + value={targetGroupName} + > + {({ selected }) => ( + <> + + {targetGroupName} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
    +
    + ); +} + +export const TargetGroups = connect( + mapStateToProps, + mapDispatchToProps +)(TargetGroupsComponent); +export default TargetGroups; 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 00000000000000..bf29d539ddf316 --- /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 00000000000000..c4409e048d2c71 --- /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 00000000000000..3866836a9fdb75 --- /dev/null +++ b/graph/ui-project-details/src/lib/utils/group-targets.ts @@ -0,0 +1,53 @@ +/* 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 +): Record { + let targetGroups = project.data.metadata?.targetGroups ?? {}; + const allTargetsInTargetGroups: string[] = Object.values(targetGroups).flat(); + const allTargets: string[] = Object.keys(project.data.targets ?? {}).sort( + sortNxReleasePublishLast + ); + allTargets.forEach((target) => { + if (!allTargetsInTargetGroups.includes(target)) { + targetGroups[target] = [target]; + } + }); + return targetGroups; +} + +export function defaultSelectTargetGroup(project: ProjectGraphProjectNode) { + return Object.keys(groupTargets(project))[0]; +} + +function sortNxReleasePublishLast(a: string, b: string) { + if (a === 'nx-release-publish') return 1; + if (b === 'nx-release-publish') return -1; + return 1; +} + +/** + * This funciton returns the target group for a given target + * If the target is not in a group, it will return the target name + * @param targetName + * @param project + * @returns + */ +export function getTargetGroupForTarget( + targetName: string, + project: ProjectGraphProjectNode +): string { + let targetGroups = project.data.metadata?.targetGroups ?? {}; + const foundTargetGroup = Object.keys(targetGroups).find((group) => + targetGroups[group].includes(targetName) + ); + return foundTargetGroup ?? targetName; +} diff --git a/graph/ui-tooltips/src/lib/tooltip.tsx b/graph/ui-tooltips/src/lib/tooltip.tsx index 916865b0f49e57..d428d63c7655ca 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 3a78941cf88713..34a59329b3d46c 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 73e78b61feff90..8bbe75fba39fa7 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 bbc171aee7fd60..052789adb339ce 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 2cc1930dfa2ea9..b3220203dd0a3e 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 { frameworkIcons } from '@nx/graph/ui-icons'; import { cx } from '@nx/nx-dev/ui-primitives'; import { ReactNode } from 'react'; 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 c8996387f3af6e..6b993fe74917cd 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, { @@ -82,11 +82,13 @@ export function ProjectDetails({ height ? `p-4 h-[${height}] overflow-y-auto` : 'p-4' }`} > - + + + ); diff --git a/nx.json b/nx.json index cbdae48da3798d..3be901557fd4ba 100644 --- a/nx.json +++ b/nx.json @@ -70,7 +70,10 @@ }, "build-native": { "inputs": ["native"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["rust"] + } }, "build-base": { "dependsOn": ["^build-base", "build-native"], @@ -82,13 +85,19 @@ "main": "{projectRoot}/index.ts" }, "outputs": ["{options.outputPath}"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["typescript"] + } }, "test-native": { "inputs": ["native"], "executor": "@monodon/rust:test", "options": {}, - "cache": true + "cache": true, + "metadata": { + "technologies": ["rust"] + } }, "test": { "dependsOn": ["test-native", "build-native", "^build-native"], @@ -99,7 +108,10 @@ "passWithNoTests": true }, "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["jest"] + } }, "lint": { "dependsOn": ["build-native", "^build-native"], @@ -113,7 +125,10 @@ "lintFilePatterns": ["{projectRoot}"] }, "outputs": ["{options.outputFile}"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["eslint"] + } }, "e2e": { "inputs": [ @@ -141,7 +156,10 @@ "runInBand": true }, "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["cypress"] + } }, "e2e-macos": { "inputs": [ @@ -166,7 +184,10 @@ "runInBand": true }, "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "cache": true + "cache": true, + "metadata": { + "technologies": ["cypress"] + } }, "e2e-base": { "inputs": ["default", "^production"] @@ -179,7 +200,10 @@ "{projectRoot}/.storybook/**/*", "{projectRoot}/tsconfig.storybook.json" ], - "cache": true + "cache": true, + "metadata": { + "technologies": ["storybook"] + } }, "build-ng": { "cache": true @@ -204,6 +228,9 @@ "ci": true, "codeCoverage": true } + }, + "metadata": { + "technologies": ["jest"] } } }, diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index e54fae0e1a92db..113ab6829a315b 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -146,6 +146,7 @@ export const createNodes: CreateNodes = [ name: projectName, targets, metadata: { + targetGroups, technologies: ['gradle'], }, }; diff --git a/tsconfig.base.json b/tsconfig.base.json index de34ac5a19fd4b..5bb03a0d55ea5b 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"],