feat(graph): add target groups and technology icon (#22839)

This commit is contained in:
Emily Xiong 2024-04-29 13:52:07 -04:00 committed by GitHub
parent addde70251
commit aa82f031c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 3128 additions and 785 deletions

View File

@ -10,7 +10,7 @@ export default {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 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 // 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 // for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
modulePathIgnorePatterns: [ modulePathIgnorePatterns: [

View File

@ -1,4 +1,6 @@
import { themeInit } from '@nx/graph/ui-theme'; 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 { rankDirInit } from './rankdir-resolver';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { getRouter } from './get-router'; import { getRouter } from './get-router';
@ -7,5 +9,9 @@ themeInit();
rankDirInit(); rankDirInit();
export function App() { export function App() {
return <RouterProvider router={getRouter()} />; return (
<StoreProvider store={rootStore}>
<RouterProvider router={getRouter()} />
</StoreProvider>
);
} }

View File

@ -6,7 +6,7 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import type { ProjectGraphNode } from '@nx/devkit'; import type { ProjectGraphProjectNode } from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */ /* eslint-enable @nx/enforce-module-boundaries */
import { useProjectGraphSelector } from './hooks/use-project-graph-selector'; import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
import { import {
@ -23,7 +23,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useRouteConstructor } from '@nx/graph/shared'; import { useRouteConstructor } from '@nx/graph/shared';
interface SidebarProject { interface SidebarProject {
projectGraphNode: ProjectGraphNode; projectGraphNode: ProjectGraphProjectNode;
isSelected: boolean; isSelected: boolean;
} }
@ -36,7 +36,7 @@ interface TracingInfo {
} }
function groupProjectsByDirectory( function groupProjectsByDirectory(
projects: ProjectGraphNode[], projects: ProjectGraphProjectNode[],
selectedProjects: string[], selectedProjects: string[],
workspaceLayout: { appsDir: string; libsDir: string } workspaceLayout: { appsDir: string; libsDir: string }
): DirectoryProjectRecord { ): DirectoryProjectRecord {

View File

@ -3,7 +3,7 @@ import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-deta
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
export function ErrorBoundary() { export function ErrorBoundary() {
let error = useRouteError()?.toString(); let error = useRouteError();
console.error(error); console.error(error);
const environment = useEnvironmentConfig()?.environment; const environment = useEnvironmentConfig()?.environment;
@ -20,7 +20,7 @@ export function ErrorBoundary() {
<h1 className="mb-4 text-4xl dark:text-slate-100">Error</h1> <h1 className="mb-4 text-4xl dark:text-slate-100">Error</h1>
<div> <div>
<p className="mb-4 text-lg dark:text-slate-200">{message}</p> <p className="mb-4 text-lg dark:text-slate-200">{message}</p>
<p className="text-sm">Error message: {error}</p> <p className="text-sm">Error message: {error?.toString()}</p>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,9 @@
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import { ProjectGraphDependency, ProjectGraphProjectNode } from '@nx/devkit'; import type {
import { getEnvironmentConfig } from '@nx/graph/shared'; ProjectGraphDependency,
ProjectGraphProjectNode,
} from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */ /* eslint-enable @nx/enforce-module-boundaries */
export function parseParentDirectoriesFromFilePath( export function parseParentDirectoriesFromFilePath(

View File

@ -1189,27 +1189,111 @@
} }
], ],
"targets": { "targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/cart-e2e/cypress.config.ts",
"devServerTarget": "cart:serve",
"testingType": "e2e"
},
"configurations": {
"production": {
"devServerTarget": "cart:serve:production"
}
},
"inputs": ["default", "^production"]
},
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "cache": true,
"options": { "options": {
"lintFilePatterns": ["apps/cart-e2e/**/*.{ts,tsx,js,jsx}"] "cwd": "apps/cart-e2e",
"command": "eslint ."
}, },
"outputs": ["{options.outputFile}"], "inputs": [
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"] "default",
"^default",
"{workspaceRoot}/.eslintrc.json",
"{projectRoot}/.eslintrc.json",
"{workspaceRoot}/tools/eslint-rules/**/*",
{
"externalDependencies": ["eslint"]
}
],
"executor": "nx:run-commands",
"configurations": {}
},
"e2e": {
"cache": true,
"inputs": ["default", "^production"],
"outputs": [
"{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
"{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
],
"metadata": {
"technologies": ["cypress"],
"description": "Runs Cypress Tests"
},
"executor": "nx:run-commands",
"options": {
"cwd": "apps/cart-e2e",
"command": "cypress run"
},
"configurations": {}
},
"e2e-ci--src/e2e/app.cy.ts": {
"outputs": [
"{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
"{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
],
"inputs": [
"default",
"^production",
{
"externalDependencies": ["cypress"]
}
],
"cache": true,
"options": {
"cwd": "apps/cart-e2e",
"command": "cypress run --env webServerCommand=\"nx run cart:serve\" --spec src/e2e/app.cy.ts"
},
"metadata": {
"technologies": ["cypress"],
"description": "Runs Cypress Tests in src/e2e/app.cy.ts in CI"
},
"executor": "nx:run-commands",
"configurations": {}
},
"e2e-ci": {
"executor": "nx:noop",
"cache": true,
"inputs": [
"default",
"^production",
{
"externalDependencies": ["cypress"]
}
],
"outputs": [
"{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
"{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
],
"dependsOn": [
{
"target": "e2e-ci--src/e2e/app.cy.ts",
"projects": "self",
"params": "forward"
}
],
"metadata": {
"technologies": ["cypress"],
"description": "Runs Cypress Tests in CI"
},
"options": {},
"configurations": {}
},
"open-cypress": {
"options": {
"cwd": "apps/cart-e2e",
"command": "cypress open"
},
"metadata": {
"technologies": ["cypress"],
"description": "Opens Cypress"
},
"executor": "nx:run-commands",
"configurations": {}
}
},
"metadata": {
"targetGroups": {
"E2E (CI)": ["e2e-ci--src/e2e/app.cy.ts", "e2e-ci"]
} }
} }
} }

View File

@ -44,3 +44,13 @@
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
/* Dark mode */
html.dark .adaptive-icon {
/* fill: white; */
filter: invert(1);
}
.adaptive-icon {
fill: black;
}

View File

@ -1,12 +1,12 @@
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit'; import type { ProjectGraphProjectNode } from '@nx/devkit';
import { import {
ScrollRestoration, ScrollRestoration,
useParams, useParams,
useRouteLoaderData, useRouteLoaderData,
} from 'react-router-dom'; } from 'react-router-dom';
import { ProjectDetailsWrapper } from './project-details-wrapper'; import ProjectDetailsWrapper from './project-details-wrapper';
import { import {
fetchProjectGraph, fetchProjectGraph,
getProjectGraphDataService, getProjectGraphDataService,

View File

@ -0,0 +1,33 @@
import {
AppDispatch,
RootState,
expandTargetActions,
getExpandedTargets,
} from '@nx/graph/state';
const mapStateToProps = (state: RootState) => {
return {
expandTargets: getExpandedTargets(state),
};
};
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
setExpandTargets(targets: string[]) {
dispatch(expandTargetActions.setExpandTargets(targets));
},
collapseAllTargets() {
dispatch(expandTargetActions.collapseAllTargets());
},
};
};
type mapStateToPropsType = ReturnType<typeof mapStateToProps>;
type mapDispatchToPropsType = ReturnType<typeof mapDispatchToProps>;
export {
mapStateToProps,
mapDispatchToProps,
mapStateToPropsType,
mapDispatchToPropsType,
};

View File

@ -1,10 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit'; import type { ProjectGraphProjectNode } from '@nx/devkit';
import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { import {
getExternalApiService, getExternalApiService,
useEnvironmentConfig, useEnvironmentConfig,
@ -12,19 +10,28 @@ import {
} from '@nx/graph/shared'; } from '@nx/graph/shared';
import { Spinner } from '@nx/graph/ui-components'; import { Spinner } from '@nx/graph/ui-components';
import { ProjectDetails } from '@nx/graph/ui-project-details';
import { useCallback, useEffect } from 'react';
import { import {
ProjectDetails, mapStateToProps,
ProjectDetailsImperativeHandle, mapDispatchToProps,
} from '@nx/graph/ui-project-details'; mapStateToPropsType,
import { useCallback, useLayoutEffect, useRef } from 'react'; mapDispatchToPropsType,
} from './project-details-wrapper.state';
export interface ProjectDetailsProps { type ProjectDetailsProps = mapStateToPropsType &
project: ProjectGraphProjectNode; mapDispatchToPropsType & {
sourceMap: Record<string, string[]>; project: ProjectGraphProjectNode;
} sourceMap: Record<string, string[]>;
};
export function ProjectDetailsWrapper(props: ProjectDetailsProps) { export function ProjectDetailsWrapperComponent({
const projectDetailsRef = useRef<ProjectDetailsImperativeHandle>(null); project,
sourceMap,
setExpandTargets,
expandTargets,
collapseAllTargets,
}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment; const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService(); const externalApiService = getExternalApiService();
const navigate = useNavigate(); const navigate = useNavigate();
@ -88,63 +95,56 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
[externalApiService] [externalApiService]
); );
const updateSearchParams = (params: URLSearchParams, sections: string[]) => { const updateSearchParams = (
if (sections.length === 0) { params: URLSearchParams,
targetNames: string[]
) => {
if (targetNames.length === 0) {
params.delete('expanded'); params.delete('expanded');
} else { } else {
params.set('expanded', sections.join(',')); params.set('expanded', targetNames.join(','));
} }
}; };
const handleTargetCollapse = useCallback( useEffect(() => {
(targetName: string) => { if (!project.data.targets) return;
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]
);
const handleTargetExpand = useCallback( const expandedTargetsParams = searchParams.get('expanded')?.split(',');
(targetName: string) => { if (expandedTargetsParams && expandedTargetsParams.length > 0) {
const expandedSections = searchParams.get('expanded')?.split(',') || []; setExpandTargets(expandedTargetsParams);
if (expandedSections.includes(targetName)) return;
expandedSections.push(targetName);
setSearchParams(
(currentSearchParams) => {
updateSearchParams(currentSearchParams, expandedSections);
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
},
[setSearchParams, searchParams]
);
useLayoutEffect(() => {
if (!props.project.data.targets) return;
const expandedSections = searchParams.get('expanded')?.split(',') || [];
for (const targetName of Object.keys(props.project.data.targets)) {
if (expandedSections.includes(targetName)) {
projectDetailsRef.current?.expandTarget(targetName);
} else {
projectDetailsRef.current?.collapseTarget(targetName);
}
} }
}, [searchParams, props.project.data.targets, projectDetailsRef]);
return () => {
collapseAllTargets();
searchParams.delete('expanded');
setSearchParams(searchParams, { replace: true });
};
}, []); // only run on mount
useEffect(() => {
if (!project.data.targets) return;
const expandedTargetsParams =
searchParams.get('expanded')?.split(',') || [];
if (expandedTargetsParams.join(',') === expandTargets.join(',')) {
return;
}
setSearchParams(
(currentSearchParams) => {
updateSearchParams(currentSearchParams, expandTargets);
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [
expandTargets,
project.data.targets,
setExpandTargets,
searchParams,
setSearchParams,
]);
if ( if (
navigationState === 'loading' && navigationState === 'loading' &&
@ -159,10 +159,8 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
return ( return (
<ProjectDetails <ProjectDetails
ref={projectDetailsRef} project={project}
{...props} sourceMap={sourceMap}
onTargetCollapse={handleTargetCollapse}
onTargetExpand={handleTargetExpand}
onViewInProjectGraph={handleViewInProjectGraph} onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph} onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined} onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
@ -170,4 +168,8 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
); );
} }
export const ProjectDetailsWrapper = connect(
mapStateToProps,
mapDispatchToProps
)(ProjectDetailsWrapperComponent);
export default ProjectDetailsWrapper; export default ProjectDetailsWrapper;

12
graph/state/.babelrc Normal file
View File

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

View File

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

11
graph/state/project.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "graph-state",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/state/src",
"projectType": "library",
"tags": [],
"// targets": "to see all targets run: nx show project ui-icons --web",
"targets": {
"lint": {}
}
}

6
graph/state/src/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './lib/expand-targets/expand-targets.slice';
export * from './lib/root/root-state.initial';
export * from './lib/root/root-state.interface';
export * from './lib/root/root.reducer';
export * from './lib/root/root.store';
export * from './lib/store.decorator';

View File

@ -0,0 +1,54 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
export const EXPAND_TARGETS_KEY = 'expandTargets';
export const initialExpandTargets: string[] = [];
export const expandTargetSlice = createSlice({
name: EXPAND_TARGETS_KEY,
initialState: initialExpandTargets,
reducers: {
expandTarget: (state: string[], action: PayloadAction<string>) => {
if (state.includes(action.payload)) {
return state;
}
state.push(action.payload);
return state;
},
collapseTarget: (state: string[], action: PayloadAction<string>) => {
if (state.includes(action.payload)) {
state = state.filter((target) => target !== action.payload);
}
return state;
},
toggleExpandTarget: (state: string[], action: PayloadAction<string>) => {
if (state.includes(action.payload)) {
state = state.filter((target) => target !== action.payload);
} else {
state.push(action.payload);
}
return state;
},
setExpandTargets: (state: string[], action: PayloadAction<string[]>) => {
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];

View File

@ -0,0 +1,9 @@
import {
EXPAND_TARGETS_KEY,
initialExpandTargets,
} from '../expand-targets/expand-targets.slice';
import { RootState } from './root-state.interface';
export const initialRootState: RootState = {
[EXPAND_TARGETS_KEY]: initialExpandTargets,
};

View File

@ -0,0 +1,5 @@
import { EXPAND_TARGETS_KEY } from '../expand-targets/expand-targets.slice';
export interface RootState {
[EXPAND_TARGETS_KEY]: string[];
}

View File

@ -0,0 +1,10 @@
import { combineReducers } from '@reduxjs/toolkit';
import {
EXPAND_TARGETS_KEY,
expandTargetReducer,
} from '../expand-targets/expand-targets.slice';
import { RootState } from './root-state.interface';
export const rootReducer = combineReducers<RootState>({
[EXPAND_TARGETS_KEY]: expandTargetReducer,
});

View File

@ -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;

View File

@ -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 <Provider store={rootStore}>{story()}</Provider>;
};

17
graph/state/tsconfig.json Normal file
View File

@ -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"
}

View File

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

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

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

View File

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

View File

@ -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;

View File

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

View File

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

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

@ -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).

View File

@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: path.join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -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"
}
}
}
}
}

View File

@ -0,0 +1,2 @@
export * from './lib/technology-icon';
export * from './lib/framework-icons';

View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Framework, frameworkIcons } from './framework-icons';
const meta: Meta<typeof frameworkIcons> = {
component: () => (
<>
{Object.keys(frameworkIcons).map((key) => (
<>
<div>{key}</div>
<div className="h-10 w-10">
{frameworkIcons[key as Framework].image}
</div>
</>
))}
</>
),
title: 'frameworkIcons',
};
export default meta;
type Story = StoryObj<typeof frameworkIcons>;
export const Primary: Story = {
args: {},
};

View File

@ -1,12 +1,74 @@
export type Framework =
| 'reactMono'
| 'tsMono'
| 'jsMono'
| 'nodeMono'
| 'angularMono'
| 'typescript'
| 'javascript'
| 'node'
| 'angular'
| 'youtube'
| 'nxagents'
| 'nxcloud'
| 'nx'
| 'nextjs'
| 'nestjs'
| 'rspack'
| 'express'
| 'jest'
| 'fastify'
| 'storybook'
| 'solid'
| 'lit'
| 'vite'
| 'trpc'
| 'remix'
| 'dotnet'
| 'qwik'
| 'gradle'
| 'go'
| 'vue'
| 'rust'
| 'nuxt'
| 'svelte'
| 'gatsby'
| 'astro'
| 'playwright'
| 'pnpm'
| 'monorepo'
| 'cra'
| 'cypress'
| 'expo'
| 'react'
| 'azure'
| 'bitbucket'
| 'circleci'
| 'github'
| 'gitlab'
| 'jenkins'
| 'apollo'
| 'prisma'
| 'redis'
| 'postgres'
| 'planetscale'
| 'mongodb'
| 'mfe'
| 'eslint';
export const frameworkIcons: Record< export const frameworkIcons: Record<
string, Framework,
{ {
image: JSX.Element; 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: { reactMono: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -22,6 +84,7 @@ export const frameworkIcons: Record<
tsMono: { tsMono: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -57,6 +120,7 @@ export const frameworkIcons: Record<
jsMono: { jsMono: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -83,6 +147,7 @@ export const frameworkIcons: Record<
nodeMono: { nodeMono: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -98,6 +163,7 @@ export const frameworkIcons: Record<
angularMono: { angularMono: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -136,6 +202,7 @@ export const frameworkIcons: Record<
typescript: { typescript: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="#3178C6" fill="#3178C6"
className="h-full w-full" className="h-full w-full"
@ -149,6 +216,7 @@ export const frameworkIcons: Record<
youtube: { youtube: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="red" fill="red"
className="h-full w-full" className="h-full w-full"
@ -178,6 +246,7 @@ export const frameworkIcons: Record<
nxcloud: { nxcloud: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke="currentColor"
fill="transparent" fill="transparent"
@ -191,6 +260,7 @@ export const frameworkIcons: Record<
nx: { nx: {
image: ( image: (
<svg <svg
role="img"
fill="currentColor" fill="currentColor"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -203,8 +273,8 @@ export const frameworkIcons: Record<
node: { node: {
image: ( image: (
<svg <svg
fill="#339933"
role="img" role="img"
fill="#339933"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -217,9 +287,8 @@ export const frameworkIcons: Record<
nextjs: { nextjs: {
image: ( image: (
<svg <svg
fill="#000000"
className="adaptive-icon h-full w-full"
role="img" role="img"
className="adaptive-icon h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
@ -227,12 +296,13 @@ export const frameworkIcons: Record<
<path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" /> <path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" />
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
nestjs: { nestjs: {
image: ( image: (
<svg <svg
fill="#E0234E"
role="img" role="img"
fill="#E0234E"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -245,6 +315,7 @@ export const frameworkIcons: Record<
rspack: { rspack: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="20 0 350 339.32" viewBox="20 0 350 339.32"
@ -275,6 +346,7 @@ export const frameworkIcons: Record<
jest: { jest: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="100 150 450 450" viewBox="100 150 450 450"
@ -328,7 +400,6 @@ export const frameworkIcons: Record<
fastify: { fastify: {
image: ( image: (
<svg <svg
fill="#000000"
role="img" role="img"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -338,13 +409,13 @@ export const frameworkIcons: Record<
<path d="M23.245 6.49L24 4.533l-.031-.121-7.473 1.967c.797-1.153.523-2.078.523-2.078s-2.387 1.524-4.193 1.485c-1.804-.04-2.387-.52-5.155.362-2.768.882-3.551 3.59-4.351 4.173-.804.583-3.32 2.477-3.32 2.477l.006.034 2.27-.724s-.622.585-1.945 2.37l-.062-.057.002.011s1.064 1.626 2.107 1.324a2.14 2.14 0 0 0 .353-.147c.419.234.967.463 1.572.525 0 0-.41-.475-.752-1.017l.238-.154.865.318-.096-.812c.003-.003.006-.003.008-.006l.849.311-.105-.738a5.65 5.65 0 0 1 .322-.158l.885-3.345 3.662-2.497-.291.733c-.741 1.826-2.135 2.256-2.135 2.256l-.582.22c-.433.512-.614.637-.764 2.353.348-.088.682-.107.984-.028 1.564.421 2.107 2.307 1.685 2.827-.104.13-.356.354-.673.617H7.77l-.008.514-.065.051h-.645l-.009.504-.17.127c-.607.011-1.373-.518-1.373-.518 0 .481.401 1.225.401 1.225l.07-.034-.061.045s1.625 1.083 2.646.681c.91-.356 3.263-2.213 5.296-3.093l6.15-1.62.811-2.1-4.688 1.235v-1.889l5.5-1.448.811-2.1-6.31 1.662V8.367zm-11.163 4l1.459-.384.02.074-.455 1.179-1.513.398zm.503 2.526l-1.512.398.489-1.266 1.459-.385.02.074zm1.971-.424l-1.513.398.49-1.266 1.459-.385.02.073Z" /> <path d="M23.245 6.49L24 4.533l-.031-.121-7.473 1.967c.797-1.153.523-2.078.523-2.078s-2.387 1.524-4.193 1.485c-1.804-.04-2.387-.52-5.155.362-2.768.882-3.551 3.59-4.351 4.173-.804.583-3.32 2.477-3.32 2.477l.006.034 2.27-.724s-.622.585-1.945 2.37l-.062-.057.002.011s1.064 1.626 2.107 1.324a2.14 2.14 0 0 0 .353-.147c.419.234.967.463 1.572.525 0 0-.41-.475-.752-1.017l.238-.154.865.318-.096-.812c.003-.003.006-.003.008-.006l.849.311-.105-.738a5.65 5.65 0 0 1 .322-.158l.885-3.345 3.662-2.497-.291.733c-.741 1.826-2.135 2.256-2.135 2.256l-.582.22c-.433.512-.614.637-.764 2.353.348-.088.682-.107.984-.028 1.564.421 2.107 2.307 1.685 2.827-.104.13-.356.354-.673.617H7.77l-.008.514-.065.051h-.645l-.009.504-.17.127c-.607.011-1.373-.518-1.373-.518 0 .481.401 1.225.401 1.225l.07-.034-.061.045s1.625 1.083 2.646.681c.91-.356 3.263-2.213 5.296-3.093l6.15-1.62.811-2.1-4.688 1.235v-1.889l5.5-1.448.811-2.1-6.31 1.662V8.367zm-11.163 4l1.459-.384.02.074-.455 1.179-1.513.398zm.503 2.526l-1.512.398.489-1.266 1.459-.385.02.074zm1.971-.424l-1.513.398.49-1.266 1.459-.385.02.073Z" />
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
express: { express: {
image: ( image: (
<svg <svg
fill="#000000"
className="adaptive-icon h-full w-full"
role="img" role="img"
className="adaptive-icon h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
@ -352,12 +423,13 @@ export const frameworkIcons: Record<
<path d="M24 18.588a1.529 1.529 0 01-1.895-.72l-3.45-4.771-.5-.667-4.003 5.444a1.466 1.466 0 01-1.802.708l5.158-6.92-4.798-6.251a1.595 1.595 0 011.9.666l3.576 4.83 3.596-4.81a1.435 1.435 0 011.788-.668L21.708 7.9l-2.522 3.283a.666.666 0 000 .994l4.804 6.412zM.002 11.576l.42-2.075c1.154-4.103 5.858-5.81 9.094-3.27 1.895 1.489 2.368 3.597 2.275 5.973H1.116C.943 16.447 4.005 19.009 7.92 17.7a4.078 4.078 0 002.582-2.876c.207-.666.548-.78 1.174-.588a5.417 5.417 0 01-2.589 3.957 6.272 6.272 0 01-7.306-.933 6.575 6.575 0 01-1.64-3.858c0-.235-.08-.455-.134-.666A88.33 88.33 0 010 11.577zm1.127-.286h9.654c-.06-3.076-2.001-5.258-4.59-5.278-2.882-.04-4.944 2.094-5.071 5.264z" /> <path d="M24 18.588a1.529 1.529 0 01-1.895-.72l-3.45-4.771-.5-.667-4.003 5.444a1.466 1.466 0 01-1.802.708l5.158-6.92-4.798-6.251a1.595 1.595 0 011.9.666l3.576 4.83 3.596-4.81a1.435 1.435 0 011.788-.668L21.708 7.9l-2.522 3.283a.666.666 0 000 .994l4.804 6.412zM.002 11.576l.42-2.075c1.154-4.103 5.858-5.81 9.094-3.27 1.895 1.489 2.368 3.597 2.275 5.973H1.116C.943 16.447 4.005 19.009 7.92 17.7a4.078 4.078 0 002.582-2.876c.207-.666.548-.78 1.174-.588a5.417 5.417 0 01-2.589 3.957 6.272 6.272 0 01-7.306-.933 6.575 6.575 0 01-1.64-3.858c0-.235-.08-.455-.134-.666A88.33 88.33 0 010 11.577zm1.127-.286h9.654c-.06-3.076-2.001-5.258-4.59-5.278-2.882-.04-4.944 2.094-5.071 5.264z" />
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
storybook: { storybook: {
image: ( image: (
<svg <svg
fill="#FF4785"
role="img" role="img"
fill="#FF4785"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -370,11 +442,12 @@ export const frameworkIcons: Record<
solid: { solid: {
image: ( image: (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
role="img" role="img"
xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 166 155.3" viewBox="0 0 166 155.3"
> >
<title>Solid</title>
<defs> <defs>
<linearGradient <linearGradient
id="a" id="a"
@ -467,8 +540,8 @@ export const frameworkIcons: Record<
lit: { lit: {
image: ( image: (
<svg <svg
fill="#324FFF"
role="img" role="img"
fill="#324FFF"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -481,8 +554,8 @@ export const frameworkIcons: Record<
vite: { vite: {
image: ( image: (
<svg <svg
viewBox="0 0 410 404"
role="img" role="img"
viewBox="0 0 410 404"
className="h-full w-full" className="h-full w-full"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -527,8 +600,8 @@ export const frameworkIcons: Record<
trpc: { trpc: {
image: ( image: (
<svg <svg
fill="#2596BE"
role="img" role="img"
fill="#2596BE"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -541,6 +614,7 @@ export const frameworkIcons: Record<
remix: { remix: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -605,6 +679,7 @@ export const frameworkIcons: Record<
dotnet: { dotnet: {
image: ( image: (
<svg <svg
role="img"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 456 456" viewBox="0 0 456 456"
fill="none" fill="none"
@ -633,6 +708,7 @@ export const frameworkIcons: Record<
qwik: { qwik: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
className="h-full w-full" className="h-full w-full"
@ -656,6 +732,7 @@ export const frameworkIcons: Record<
gradle: { gradle: {
image: ( image: (
<svg <svg
role="img"
id="Layer_1" id="Layer_1"
data-name="Layer 1" data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -686,6 +763,7 @@ export const frameworkIcons: Record<
go: { go: {
image: ( image: (
<svg <svg
role="img"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 32 32" viewBox="0 0 32 32"
fill="none" fill="none"
@ -715,6 +793,7 @@ export const frameworkIcons: Record<
vue: { vue: {
image: ( image: (
<svg <svg
role="img"
className="h-full w-full" className="h-full w-full"
viewBox="0 -17.5 256 256" viewBox="0 -17.5 256 256"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -738,6 +817,7 @@ export const frameworkIcons: Record<
rust: { rust: {
image: ( image: (
<svg <svg
role="img"
version="1.1" version="1.1"
viewBox="0 0 108 108" viewBox="0 0 108 108"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
@ -820,10 +900,12 @@ export const frameworkIcons: Record<
</g> </g>
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
nuxt: { nuxt: {
image: ( image: (
<svg <svg
role="img"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 900 900" viewBox="0 0 900 900"
fill="none" fill="none"
@ -839,6 +921,7 @@ export const frameworkIcons: Record<
svelte: { svelte: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 98.1 118" viewBox="0 0 98.1 118"
@ -866,6 +949,7 @@ export const frameworkIcons: Record<
gatsby: { gatsby: {
image: ( image: (
<svg <svg
role="img"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 32 32" viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -885,8 +969,8 @@ export const frameworkIcons: Record<
astro: { astro: {
image: ( image: (
<svg <svg
fill="#FF5D01"
role="img" role="img"
fill="#FF5D01"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -899,8 +983,8 @@ export const frameworkIcons: Record<
playwright: { playwright: {
image: ( image: (
<svg <svg
fill="#2EAD33"
role="img" role="img"
fill="#2EAD33"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -913,8 +997,8 @@ export const frameworkIcons: Record<
pnpm: { pnpm: {
image: ( image: (
<svg <svg
fill="#F69220"
role="img" role="img"
fill="#F69220"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -927,6 +1011,7 @@ export const frameworkIcons: Record<
monorepo: { monorepo: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" fill="currentColor"
className="h-full w-full" className="h-full w-full"
@ -949,6 +1034,7 @@ export const frameworkIcons: Record<
javascript: { javascript: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 630 630" viewBox="0 0 630 630"
@ -962,6 +1048,7 @@ export const frameworkIcons: Record<
cra: { cra: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="#09D3AC" fill="#09D3AC"
className="h-full w-full" className="h-full w-full"
@ -974,6 +1061,7 @@ export const frameworkIcons: Record<
angular: { angular: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full text-black dark:text-white" className="h-full w-full text-black dark:text-white"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -1009,6 +1097,7 @@ export const frameworkIcons: Record<
cypress: { cypress: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -1017,10 +1106,12 @@ export const frameworkIcons: Record<
<path d="M11.998 0C5.366 0 0 5.367 0 12a11.992 11.992 0 0012 12c6.633 0 12-5.367 12-12-.001-6.633-5.412-12-12.002-12zM6.37 14.575c.392.523.916.742 1.657.742.35 0 .699-.044 1.004-.175.306-.13.655-.306 1.09-.567l1.223 1.745c-1.003.83-2.138 1.222-3.447 1.222-1.048 0-1.92-.218-2.705-.654a4.393 4.393 0 01-1.746-1.92c-.392-.83-.611-1.79-.611-2.925 0-1.09.219-2.094.61-2.923a4.623 4.623 0 011.748-2.007c.741-.48 1.657-.698 2.661-.698.699 0 1.353.087 1.877.305a5.64 5.64 0 011.614.96l-1.222 1.658A4.786 4.786 0 009.12 8.77c-.305-.13-.698-.174-1.048-.174-1.483 0-2.225 1.134-2.225 3.446-.043 1.18.175 2.008.524 2.532H6.37zm12 2.705c-.436 1.353-1.091 2.357-2.008 3.098-.916.743-2.138 1.135-3.665 1.266l-.305-2.05c1.003-.132 1.745-.35 2.225-.7.174-.13.524-.523.524-.523L11.519 6.764h3.01l2.095 8.683 2.226-8.683h2.923L18.37 17.28z"></path> <path d="M11.998 0C5.366 0 0 5.367 0 12a11.992 11.992 0 0012 12c6.633 0 12-5.367 12-12-.001-6.633-5.412-12-12.002-12zM6.37 14.575c.392.523.916.742 1.657.742.35 0 .699-.044 1.004-.175.306-.13.655-.306 1.09-.567l1.223 1.745c-1.003.83-2.138 1.222-3.447 1.222-1.048 0-1.92-.218-2.705-.654a4.393 4.393 0 01-1.746-1.92c-.392-.83-.611-1.79-.611-2.925 0-1.09.219-2.094.61-2.923a4.623 4.623 0 011.748-2.007c.741-.48 1.657-.698 2.661-.698.699 0 1.353.087 1.877.305a5.64 5.64 0 011.614.96l-1.222 1.658A4.786 4.786 0 009.12 8.77c-.305-.13-.698-.174-1.048-.174-1.483 0-2.225 1.134-2.225 3.446-.043 1.18.175 2.008.524 2.532H6.37zm12 2.705c-.436 1.353-1.091 2.357-2.008 3.098-.916.743-2.138 1.135-3.665 1.266l-.305-2.05c1.003-.132 1.745-.35 2.225-.7.174-.13.524-.523.524-.523L11.519 6.764h3.01l2.095 8.683 2.226-8.683h2.923L18.37 17.28z"></path>
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
expo: { expo: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
fill="#000020" fill="#000020"
@ -1030,12 +1121,13 @@ export const frameworkIcons: Record<
<path d="M0 20.084c.043.53.23 1.063.718 1.778.58.849 1.576 1.315 2.303.567.49-.505 5.794-9.776 8.35-13.29a.761.761 0 011.248 0c2.556 3.514 7.86 12.785 8.35 13.29.727.748 1.723.282 2.303-.567.57-.835.728-1.42.728-2.046 0-.426-8.26-15.798-9.092-17.078-.8-1.23-1.044-1.498-2.397-1.542h-1.032c-1.353.044-1.597.311-2.398 1.542C8.267 3.991.33 18.758 0 19.77z"></path> <path d="M0 20.084c.043.53.23 1.063.718 1.778.58.849 1.576 1.315 2.303.567.49-.505 5.794-9.776 8.35-13.29a.761.761 0 011.248 0c2.556 3.514 7.86 12.785 8.35 13.29.727.748 1.723.282 2.303-.567.57-.835.728-1.42.728-2.046 0-.426-8.26-15.798-9.092-17.078-.8-1.23-1.044-1.498-2.397-1.542h-1.032c-1.353.044-1.597.311-2.398 1.542C8.267 3.991.33 18.758 0 19.77z"></path>
</svg> </svg>
), ),
isAdaptiveIcon: true,
}, },
react: { react: {
image: ( image: (
<svg <svg
fill="#61DAFB"
role="img" role="img"
fill="#61DAFB"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -1048,6 +1140,7 @@ export const frameworkIcons: Record<
azure: { azure: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="0 0 728.55 727.44" viewBox="0 0 728.55 727.44"
@ -1094,6 +1187,7 @@ export const frameworkIcons: Record<
bitbucket: { bitbucket: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
width="500" width="500"
@ -1132,6 +1226,7 @@ export const frameworkIcons: Record<
circleci: { circleci: {
image: ( image: (
<svg <svg
role="img"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 390 390" viewBox="0 0 390 390"
@ -1149,6 +1244,7 @@ export const frameworkIcons: Record<
github: { github: {
image: ( image: (
<svg <svg
role="img"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="-1 0 100 100" viewBox="-1 0 100 100"
@ -1166,6 +1262,7 @@ export const frameworkIcons: Record<
gitlab: { gitlab: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
width="463.16" width="463.16"
@ -1201,6 +1298,7 @@ export const frameworkIcons: Record<
jenkins: { jenkins: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-full w-full" className="h-full w-full"
viewBox="-2 0 448 620" viewBox="-2 0 448 620"
@ -1457,7 +1555,6 @@ export const frameworkIcons: Record<
apollo: { apollo: {
image: ( image: (
<svg <svg
fill="#000000"
role="img" role="img"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
height="256" height="256"
@ -1473,7 +1570,6 @@ export const frameworkIcons: Record<
prisma: { prisma: {
image: ( image: (
<svg <svg
fill="currentColor"
role="img" role="img"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
viewBox="0.34 -0.059977834648891726 33.11668247084116 39.96397783464889" viewBox="0.34 -0.059977834648891726 33.11668247084116 39.96397783464889"
@ -1492,6 +1588,7 @@ export const frameworkIcons: Record<
redis: { redis: {
image: ( image: (
<svg <svg
role="img"
width="32" width="32"
className="h-full w-full" className="h-full w-full"
height="32" height="32"
@ -1540,6 +1637,7 @@ export const frameworkIcons: Record<
postgres: { postgres: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="32" width="32"
height="32" height="32"
@ -1570,6 +1668,7 @@ export const frameworkIcons: Record<
planetscale: { planetscale: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@ -1591,10 +1690,10 @@ export const frameworkIcons: Record<
mongodb: { mongodb: {
image: ( image: (
<svg <svg
role="img"
width="15" width="15"
height="15" height="15"
viewBox="0 0 15 15" viewBox="0 0 15 15"
fill="none"
className="adaptive-icon h-full w-full" className="adaptive-icon h-full w-full"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
@ -1608,6 +1707,7 @@ export const frameworkIcons: Record<
mfe: { mfe: {
image: ( image: (
<svg <svg
role="img"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@ -1622,4 +1722,28 @@ export const frameworkIcons: Record<
</svg> </svg>
), ),
}, },
eslint: {
image: (
<svg
role="img"
width="256"
height="263"
viewBox="0 0 256 263"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
className="h-full w-full"
>
<g>
<path
d="M100.033785,262.105923 L0.598353254,172.570518 L28.4489774,41.6943426 L155.626242,0.353572377 L255.061673,89.8889774 L227.211049,220.765153 L100.033785,262.105923 L100.033785,262.105923 Z M51.9479416,155.925418 L111.239309,209.233254 L187.066985,184.537583 L203.712085,106.534077 L144.420717,53.1174502 L68.5930412,77.9219124 L51.9479416,155.925418 L51.9479416,155.925418 Z"
fill="#3A33D1"
></path>
<path
d="M181.301036,223.920106 L74.3589907,223.920106 L20.8335724,131.229748 L74.3589907,38.5393891 L181.301036,38.5393891 L234.826454,131.229748 L181.301036,223.920106 L181.301036,223.920106 Z M93.1799203,191.282656 L162.480106,191.282656 L197.184595,131.229748 L162.480106,71.1768393 L93.1799203,71.1768393 L58.5842231,131.229748 L93.1799203,191.282656 L93.1799203,191.282656 Z"
fill="#6464E2"
></path>
</g>
</svg>
),
},
}; };

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TechnologyIcon } from './technology-icon';
const meta: Meta<typeof TechnologyIcon> = {
component: TechnologyIcon,
title: 'TechnologyIcon',
};
export default meta;
type Story = StoryObj<typeof TechnologyIcon>;
export const Simple: Story = {
args: {
technology: 'react',
},
};
export const UnknownTechnology: Story = {
args: {
technology: 'unknown',
},
};

View File

@ -0,0 +1,33 @@
import { Framework, frameworkIcons } from './framework-icons';
export interface TechnologyIconProps {
technology?: string;
showTooltip?: boolean;
}
export function TechnologyIcon({
technology,
showTooltip,
}: TechnologyIconProps) {
if (!technology) {
return null;
}
const image = frameworkIcons[technology as Framework]?.image;
return (
<div
className={`h-4 w-4 ${
frameworkIcons[technology as Framework]?.isAdaptiveIcon
? 'adpative-icon'
: ''
} ${
!image
? 'flex items-center justify-center rounded bg-slate-800 text-sm text-slate-50 dark:bg-slate-50 dark:text-slate-800'
: ''
}`}
data-tooltip={showTooltip ? technology : null}
>
{image ?? technology[0]}
</div>
);
}

View File

@ -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')],
};

View File

@ -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"
}

View File

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

View File

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

View File

@ -1 +1,2 @@
export * from './lib/project-details/project-details'; export * from './lib/project-details/project-details';
export * from './lib/utils/group-targets';

View File

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CopyToClipboard } from './copy-to-clipboard';
const meta: Meta<typeof CopyToClipboard> = {
component: CopyToClipboard,
title: 'CopyToClipboard',
};
export default meta;
type Story = StoryObj<typeof CopyToClipboard>;
export const Simple: Story = {
args: {
onCopy: () => {},
tooltipAlignment: 'left',
},
};

View File

@ -1,9 +1,11 @@
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import { ProjectDetails } from './project-details'; import { ProjectDetails } from './project-details';
import { StoreDecorator } from '@nx/graph/state';
const meta: Meta<typeof ProjectDetails> = { const meta: Meta<typeof ProjectDetails> = {
component: ProjectDetails, component: ProjectDetails,
title: 'ProjectDetails', title: 'ProjectDetails',
decorators: [StoreDecorator],
}; };
export default meta; export default meta;
@ -208,3 +210,626 @@ export const Primary = {
}, },
}, },
}; };
export const Gradle = {
args: {
project: {
name: 'utilities',
type: 'lib',
data: {
root: 'utilities',
name: 'utilities',
metadata: {
targetGroups: {
Build: [
'assemble',
'build',
'buildDependents',
'buildKotlinToolingMetadata',
'buildNeeded',
'classes',
'clean',
'jar',
'kotlinSourcesJar',
'testClasses',
],
Documentation: ['javadoc'],
Help: [
'buildEnvironment',
'dependencies',
'dependencyInsight',
'help',
'javaToolchains',
'kotlinDslAccessorsReport',
'outgoingVariants',
'projects',
'properties',
'resolvableConfigurations',
'tasks',
],
Reporting: ['projectReport'],
Verification: [
'check',
'checkKotlinGradlePluginConfigurationErrors',
'test',
],
},
technologies: ['gradle'],
},
targets: {
assemble: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew assemble',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
build: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew build',
},
cache: true,
inputs: ['default', '^default'],
outputs: ['{workspaceRoot}/utilities/build'],
dependsOn: ['^build', 'classes'],
executor: 'nx:run-commands',
configurations: {},
},
buildDependents: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildDependents',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
buildKotlinToolingMetadata: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildKotlinToolingMetadata',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
buildNeeded: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildNeeded',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
classes: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew classes',
},
cache: true,
inputs: ['default', '^default'],
outputs: ['{workspaceRoot}/utilities/build/classes'],
dependsOn: ['^classes'],
executor: 'nx:run-commands',
configurations: {},
},
clean: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew clean',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
jar: {
options: {
cwd: 'utilities',
command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew jar',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
kotlinSourcesJar: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew kotlinSourcesJar',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
testClasses: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew testClasses',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
javadoc: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew javadoc',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
buildEnvironment: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew buildEnvironment',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
dependencies: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew dependencies',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
dependencyInsight: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew dependencyInsight',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
help: {
options: {
cwd: 'utilities',
command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew help',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
javaToolchains: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew javaToolchains',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
kotlinDslAccessorsReport: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew kotlinDslAccessorsReport',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
outgoingVariants: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew outgoingVariants',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
projects: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew projects',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
properties: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew properties',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
resolvableConfigurations: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew resolvableConfigurations',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
tasks: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew tasks',
},
cache: false,
executor: 'nx:run-commands',
configurations: {},
},
projectReport: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew projectReport',
},
cache: false,
outputs: ['{workspaceRoot}/utilities/build/reports/project'],
executor: 'nx:run-commands',
configurations: {},
},
check: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew check',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
checkKotlinGradlePluginConfigurationErrors: {
options: {
cwd: 'utilities',
command:
'/Users/emily/code/tmp/gradle-plugin-test3/gradlew checkKotlinGradlePluginConfigurationErrors',
},
cache: true,
executor: 'nx:run-commands',
configurations: {},
},
test: {
options: {
cwd: 'utilities',
command: '/Users/emily/code/tmp/gradle-plugin-test3/gradlew test',
},
cache: true,
inputs: ['default', '^default'],
dependsOn: ['classes'],
executor: 'nx:run-commands',
configurations: {},
},
},
implicitDependencies: [],
tags: [],
},
},
sourceMap: {
root: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
name: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
targets: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.nx-release-publish': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.dependsOn': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.executor': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.options': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
$schema: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
sourceRoot: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
projectType: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.test': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options.assets': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.outputs': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.lint': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
},
},
};
export const Cart = {
args: {
project: {
name: 'cart-e2e',
type: 'e2e',
data: {
root: 'apps/cart-e2e',
targets: {
lint: {
cache: true,
options: {
cwd: 'apps/cart-e2e',
command: 'eslint .',
},
inputs: [
'default',
'^default',
'{workspaceRoot}/.eslintrc.json',
'{projectRoot}/.eslintrc.json',
'{workspaceRoot}/tools/eslint-rules/**/*',
{
externalDependencies: ['eslint'],
},
],
executor: 'nx:run-commands',
configurations: {},
},
e2e: {
cache: true,
inputs: ['default', '^production'],
outputs: [
'{workspaceRoot}/dist/cypress/apps/cart-e2e/videos',
'{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots',
],
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Tests',
},
executor: 'nx:run-commands',
options: {
cwd: 'apps/cart-e2e',
command: 'cypress run',
},
configurations: {},
},
'e2e-ci--src/e2e/app.cy.ts': {
outputs: [
'{workspaceRoot}/dist/cypress/apps/cart-e2e/videos',
'{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots',
],
inputs: [
'default',
'^production',
{
externalDependencies: ['cypress'],
},
],
cache: true,
options: {
cwd: 'apps/cart-e2e',
command:
'cypress run --env webServerCommand="nx run cart:serve" --spec src/e2e/app.cy.ts',
},
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Tests in src/e2e/app.cy.ts in CI',
},
executor: 'nx:run-commands',
configurations: {},
},
'e2e-ci': {
executor: 'nx:noop',
cache: true,
inputs: [
'default',
'^production',
{
externalDependencies: ['cypress'],
},
],
outputs: [
'{workspaceRoot}/dist/cypress/apps/cart-e2e/videos',
'{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots',
],
dependsOn: [
{
target: 'e2e-ci--src/e2e/app.cy.ts',
projects: 'self',
params: 'forward',
},
],
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Tests in CI',
},
options: {},
configurations: {},
},
'open-cypress': {
options: {
cwd: 'apps/cart-e2e',
command: 'cypress open',
},
metadata: {
technologies: ['cypress'],
description: 'Opens Cypress',
},
executor: 'nx:run-commands',
configurations: {},
},
},
projectType: 'application',
metadata: {
targetGroups: {
'E2E (CI)': ['e2e-ci--src/e2e/app.cy.ts', 'e2e-ci'],
},
},
name: 'cart-e2e',
$schema: '../../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'apps/cart-e2e/src',
tags: ['scope:cart', 'type:e2e'],
implicitDependencies: ['cart'],
},
},
sourceMap: {
root: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
name: ['packages/jest/project.json', 'nx-core-build-project-json-nodes'],
targets: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.nx-release-publish': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.dependsOn': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.executor': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
'targets.nx-release-publish.options': [
'packages/jest/project.json',
'nx-core-build-package-json-nodes-next-to-project-json-nodes',
],
$schema: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
sourceRoot: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
projectType: [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.test': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build-base.options.assets': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.executor': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.outputs': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.build.options.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.add-extra-dependencies.command': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
'targets.lint': [
'packages/jest/project.json',
'nx-core-build-project-json-nodes',
],
},
},
};

View File

@ -2,32 +2,20 @@
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit'; import type { ProjectGraphProjectNode } from '@nx/devkit';
import { EyeIcon } from '@heroicons/react/24/outline'; import { EyeIcon } from '@heroicons/react/24/outline';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips'; 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 { TooltipTriggerText } from '../target-configuration-details/tooltip-trigger-text';
import {
createRef,
ForwardedRef,
forwardRef,
RefObject,
useImperativeHandle,
useRef,
} from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { Pill } from '../pill'; import { Pill } from '../pill';
import { TargetConfigurationDetailsList } from '../target-configuration-details-list/target-configuration-details-list';
import { TargetTechnologies } from '../target-technologies/target-technologies';
export interface ProjectDetailsProps { export interface ProjectDetailsProps {
project: ProjectGraphProjectNode; project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
onTargetCollapse?: (targetName: string) => void;
onTargetExpand?: (targetName: string) => void;
onViewInProjectGraph?: (data: { projectName: string }) => void; onViewInProjectGraph?: (data: { projectName: string }) => void;
onViewInTaskGraph?: (data: { onViewInTaskGraph?: (data: {
projectName: string; projectName: string;
@ -36,143 +24,121 @@ export interface ProjectDetailsProps {
onRunTarget?: (data: { projectName: string; targetName: string }) => void; onRunTarget?: (data: { projectName: string; targetName: string }) => void;
} }
export interface ProjectDetailsImperativeHandle { export const ProjectDetails = ({
collapseTarget: (targetName: string) => void; project,
expandTarget: (targetName: string) => void; sourceMap,
} variant,
onViewInProjectGraph,
onViewInTaskGraph,
onRunTarget,
}: ProjectDetailsProps) => {
const projectData = project.data;
const isCompact = variant === 'compact';
export const ProjectDetails = forwardRef( const displayType =
( projectData.projectType &&
{ projectData.projectType?.charAt(0)?.toUpperCase() +
project: { projectData.projectType?.slice(1);
name,
data: { root, ...projectData },
},
sourceMap,
variant,
onTargetCollapse,
onTargetExpand,
onViewInProjectGraph,
onViewInTaskGraph,
onRunTarget,
}: ProjectDetailsProps,
ref: ForwardedRef<ProjectDetailsImperativeHandle>
) => {
const isCompact = variant === 'compact';
const projectTargets = Object.keys(projectData.targets ?? {});
const targetRefs = useRef(
projectTargets.reduce((acc, targetName) => {
acc[targetName] = createRef<TargetConfigurationDetailsHandle>();
return acc;
}, {} as Record<string, RefObject<TargetConfigurationDetailsHandle>>)
);
const displayType = const technologies = [
projectData.projectType && ...new Set(
projectData.projectType?.charAt(0)?.toUpperCase() + [
projectData.projectType?.slice(1); ...(projectData.metadata?.technologies ?? []),
...Object.values(projectData.targets ?? {})
.map((target) => target?.metadata?.technologies)
.flat(),
].filter(Boolean)
),
] as string[];
useImperativeHandle(ref, () => ({ return (
collapseTarget: (targetName: string) => { <>
targetRefs.current[targetName]?.current?.collapse(); <header
}, className={twMerge(
expandTarget: (targetName: string) => { `border-b border-slate-900/10 dark:border-slate-300/10`,
targetRefs.current[targetName]?.current?.expand(); isCompact ? 'mb-2' : 'mb-4'
}, )}
})); >
<div
return (
<>
<header
className={twMerge( className={twMerge(
`border-b border-slate-900/10 dark:border-slate-300/10`, `flex items-center justify-between`,
isCompact ? 'mb-2' : 'mb-4' isCompact ? `gap-1` : `mb-4 gap-2`
)} )}
> >
<h1 <div className="flex items-center gap-2">
className={twMerge( <h1
`flex items-center justify-between dark:text-slate-100`, className={twMerge(
isCompact ? `text-2xl gap-1` : `text-4xl mb-4 gap-2` `dark:text-slate-100`,
)} isCompact ? `text-2xl` : `text-4xl`
> )}
<span>{name}</span>
<span>
{onViewInProjectGraph ? (
<button
className="text-base cursor-pointer items-center inline-flex gap-2 text-slate-600 dark:text-slate-300 ring-2 ring-inset ring-slate-400/40 dark:ring-slate-400/30 hover:bg-slate-50 dark:hover:bg-slate-800/60 rounded-md py-1 px-2"
onClick={() => onViewInProjectGraph({ projectName: name })}
>
<EyeIcon className="h-5 w-5 "></EyeIcon>
<span>View In Graph</span>
</button>
) : null}{' '}
</span>
</h1>
<div className="py-2 ">
{projectData.tags && projectData.tags.length ? (
<p>
<span className="font-medium inline-block w-10">Tags:</span>
{projectData.tags?.map((tag) => (
<span className="ml-2 font-mono">
<Pill text={tag} />
</span>
))}
</p>
) : null}
<p>
<span className="font-medium inline-block w-10">Root:</span>
<span className="font-mono"> {root}</span>
</p>
{displayType ? (
<p>
<span className="font-medium inline-block w-10">Type:</span>
<span className="font-mono"> {displayType}</span>
</p>
) : null}
</div>
</header>
<div>
<h2 className={isCompact ? `text-lg mb-3` : `text-xl mb-4`}>
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="targets" />) as any}
> >
<span className="text-slate-800 dark:text-slate-200"> {project.name}
<TooltipTriggerText>Targets</TooltipTriggerText> </h1>
</span> <TargetTechnologies
</Tooltip> technologies={technologies}
</h2> showTooltip={true}
<ul> />
{projectTargets.sort(sortNxReleasePublishLast).map((targetName) => { </div>
const target = projectData.targets?.[targetName]; <span>
return target && targetRefs.current[targetName] ? ( {onViewInProjectGraph ? (
<li className="mb-4 last:mb-0" key={`target-${targetName}`}> <button
<TargetConfigurationDetails className="inline-flex cursor-pointer items-center gap-2 rounded-md py-1 px-2 text-base text-slate-600 ring-2 ring-inset ring-slate-400/40 hover:bg-slate-50 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-800/60"
ref={targetRefs.current[targetName]} onClick={() =>
variant={variant} onViewInProjectGraph({ projectName: project.name })
projectName={name} }
targetName={targetName} >
targetConfiguration={target} <EyeIcon className="h-5 w-5 "></EyeIcon>
sourceMap={sourceMap} <span>View In Graph</span>
onRunTarget={onRunTarget} </button>
onViewInTaskGraph={onViewInTaskGraph} ) : null}{' '}
onCollapse={onTargetCollapse} </span>
onExpand={onTargetExpand}
/>
</li>
) : null;
})}
</ul>
</div> </div>
</> <div className="py-2 ">
); {projectData.tags && projectData.tags.length ? (
} <p>
); <span className="inline-block w-10 font-medium">Tags:</span>
{projectData.tags?.map((tag) => (
<span className="ml-2 font-mono">
<Pill text={tag} />
</span>
))}
</p>
) : null}
<p>
<span className="inline-block w-10 font-medium">Root:</span>
<span className="font-mono"> {projectData.root}</span>
</p>
{displayType ? (
<p>
<span className="inline-block w-10 font-medium">Type:</span>
<span className="font-mono"> {displayType}</span>
</p>
) : null}
</div>
</header>
<div>
<h2 className={isCompact ? `mb-3 text-lg` : `mb-4 text-xl`}>
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="targets" />) as any}
>
<span className="text-slate-800 dark:text-slate-200">
<TooltipTriggerText>Targets</TooltipTriggerText>
</span>
</Tooltip>
</h2>
function sortNxReleasePublishLast(a: string, b: string) { <TargetConfigurationDetailsList
if (a === 'nx-release-publish') return 1; className="w-full"
if (b === 'nx-release-publish') return -1; project={project}
return 1; sourceMap={sourceMap}
} variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
/>
</div>
</>
);
};
export default ProjectDetails; export default ProjectDetails;

View File

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SourceInfo } from './source-info';
const meta: Meta<typeof SourceInfo> = {
component: SourceInfo,
title: 'SourceInfo',
};
export default meta;
type Story = StoryObj<typeof SourceInfo>;
export const Simple: Story = {
args: {
data: ['data1', 'data2'],
propertyKey: 'test',
},
};

View File

@ -11,7 +11,7 @@ export function SourceInfo(props: {
// Every other property within in the target has the form `target.${targetName}.${propertyName} // Every other property within in the target has the form `target.${targetName}.${propertyName}
const isTarget = props.propertyKey.split('.').length === 2; const isTarget = props.propertyKey.split('.').length === 2;
return ( return (
<span className="inline-flex shrink-1 min-w-0 items-center gap-2"> <span className="inline-flex min-w-0 items-center gap-2">
<Tooltip <Tooltip
openAction="hover" openAction="hover"
strategy="fixed" strategy="fixed"
@ -36,7 +36,7 @@ export function SourceInfo(props: {
{/*</span>*/} {/*</span>*/}
<span <span
className={twMerge( className={twMerge(
'italic text-sm min-w-0 truncate', 'min-w-0 truncate text-sm italic',
props.color ?? 'text-gray-500' props.color ?? 'text-gray-500'
)} )}
> >

View File

@ -0,0 +1,33 @@
import { forwardRef } from 'react';
import { TargetConfigurationGroupHeader } from '../target-configuration-details-group-header/target-configuration-details-group-header';
export interface TargetConfigurationGroupContainerProps {
targetGroupName: string;
targetsNumber: number;
children: React.ReactNode;
}
export const TargetConfigurationGroupContainer = forwardRef(
(
{
targetGroupName,
targetsNumber,
children,
}: TargetConfigurationGroupContainerProps,
ref: React.Ref<any>
) => {
return (
<div>
<div ref={ref} className="mb-4 w-full">
<TargetConfigurationGroupHeader
targetGroupName={targetGroupName}
targetsNumber={targetsNumber}
/>
<div className="rounded-md border border-slate-200 p-2 dark:border-slate-700">
{children}
</div>
</div>
</div>
);
}
);

View File

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TargetConfigurationGroupHeader } from './target-configuration-details-group-header';
const meta: Meta<typeof TargetConfigurationGroupHeader> = {
component: TargetConfigurationGroupHeader,
title: 'TargetConfigurationGroupHeader',
};
export default meta;
type Story = StoryObj<typeof TargetConfigurationGroupHeader>;
export const Simple: Story = {
args: {
targetGroupName: 'Target Group Name',
targetsNumber: 5,
},
};

View File

@ -0,0 +1,25 @@
import { Pill } from '../pill';
export interface TargetConfigurationGroupHeaderProps {
targetGroupName: string;
targetsNumber: number;
className?: string;
}
export const TargetConfigurationGroupHeader = ({
targetGroupName,
targetsNumber,
className = '',
}: TargetConfigurationGroupHeaderProps) => {
return (
<header className={`px-4 py-2 text-lg ${className}`}>
{targetGroupName}{' '}
<Pill
text={
targetsNumber.toString() +
(targetsNumber === 1 ? ' target' : ' targets')
}
/>
</header>
);
};

View File

@ -0,0 +1,94 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
TargetConfigurationGroupList,
TargetConfigurationGroupListProps,
} from './target-configuration-details-group-list';
import { StoreDecorator } from '@nx/graph/state';
const meta: Meta<typeof TargetConfigurationGroupList> = {
component: TargetConfigurationGroupList,
title: 'TargetConfigurationGroupList',
decorators: [StoreDecorator],
};
export default meta;
type Story = StoryObj<typeof TargetConfigurationGroupList>;
export const OneTarget: Story = {
args: {
project: {
name: 'react',
type: 'lib',
data: {
root: 'libs/react',
targets: {
build: {
executor: 'nx',
options: {},
configurations: {
production: {
executor: 'nx',
options: {},
},
},
},
lint: {
executor: 'nx',
options: {},
},
},
},
},
sourceMap: {
react: ['react'],
},
variant: 'default',
onRunTarget: () => {},
onViewInTaskGraph: () => {},
selectedTargetGroup: 'build',
setExpandTargets: () => {},
collapseAllTargets: () => {},
} as TargetConfigurationGroupListProps,
};
export const TwoTargets: Story = {
args: {
project: {
name: 'react',
type: 'lib',
data: {
root: 'libs/react',
targets: {
build1: {
executor: 'nx',
options: {},
configurations: {
production: {
executor: 'nx',
options: {},
},
},
},
build2: {
executor: 'nx',
options: {},
},
},
metadata: {
targetGroups: {
build: ['build1', 'build2'],
},
},
},
},
sourceMap: {
react: ['react'],
},
variant: 'default',
onRunTarget: () => {},
onViewInTaskGraph: () => {},
selectedTargetGroup: 'build',
setExpandTargets: () => {},
collapseAllTargets: () => {},
} as TargetConfigurationGroupListProps,
};

View File

@ -0,0 +1,147 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
import { RefObject, createRef, useEffect, useRef, useState } from 'react';
import { Transition } from '@headlessui/react';
import { TargetConfigurationDetailsListItem } from '../target-configuration-details-list-item/target-configuration-details-list-item';
import { TargetConfigurationGroupContainer } from '../target-configuration-details-group-container/target-configuration-details-group-container';
import { TargetConfigurationGroupHeader } from '../target-configuration-details-group-header/target-configuration-details-group-header';
import { groupTargets } from '../utils/group-targets';
export interface TargetConfigurationGroupListProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
className?: string;
}
export function TargetConfigurationGroupList({
project,
variant,
sourceMap,
onRunTarget,
onViewInTaskGraph,
className = '',
}: TargetConfigurationGroupListProps) {
const [stickyHeaderContent, setStickHeaderContent] = useState('');
const targetsGroup = groupTargets(project);
const targetGroupRefs = useRef(
Object.keys(targetsGroup.groups).reduce((acc, targetGroupName) => {
acc[targetGroupName] = createRef();
return acc;
}, {} as Record<string, RefObject<any>>)
);
const targetNameRefs = useRef(
targetsGroup.targets.reduce((acc, targetName) => {
acc[targetName] = createRef();
return acc;
}, {} as Record<string, RefObject<any>>)
);
useEffect(() => {
window.addEventListener('scroll', isSticky);
return () => {
window.removeEventListener('scroll', isSticky);
};
}, []);
const isSticky = () => {
const scrollTop = window.scrollY + 30; // 30px for the header
const foundTargetGroup: string | undefined = Object.keys(
targetGroupRefs.current
).find((targetGroupName) => {
const targetGroup = targetGroupRefs.current[targetGroupName];
if (
targetGroup &&
targetGroup.current &&
scrollTop >= targetGroup.current.offsetTop &&
scrollTop <
targetGroup.current.offsetTop + targetGroup.current.offsetHeight
) {
return true;
}
return false;
});
if (foundTargetGroup) {
setStickHeaderContent(foundTargetGroup);
} else {
setStickHeaderContent('');
}
};
return (
<>
<Transition
show={!!stickyHeaderContent}
enter="transition-opacity ease-linear duration-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed top-0 left-0 right-0 z-10 mb-8 border-b-2 border-slate-900/10 bg-slate-50 dark:border-slate-300/10 dark:bg-slate-800 dark:text-slate-300">
<div className="mx-auto max-w-6xl px-8 pt-2">
<TargetConfigurationGroupHeader
targetGroupName={stickyHeaderContent}
targetsNumber={
project.data.metadata?.targetGroups?.[stickyHeaderContent]
?.length ?? 0
}
/>
</div>
</div>
</Transition>
{Object.entries(targetsGroup.groups).map(([targetGroupName, targets]) => {
return (
<TargetConfigurationGroupContainer
ref={targetGroupRefs.current[targetGroupName]}
targetGroupName={targetGroupName}
targetsNumber={targets.length}
key={targetGroupName}
>
<ul className={className}>
{targets.map((targetName) => (
<TargetConfigurationDetailsListItem
ref={targetNameRefs.current[targetName]}
project={project}
sourceMap={sourceMap}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
targetName={targetName}
collapsable={true}
key={targetName}
/>
))}
</ul>
</TargetConfigurationGroupContainer>
);
})}
<ul className={`mt-8 p-2 ${className}`}>
{targetsGroup.targets.map((targetName) => {
return (
<TargetConfigurationDetailsListItem
ref={targetNameRefs.current[targetName]}
project={project}
sourceMap={sourceMap}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
targetName={targetName}
collapsable={true}
key={targetName}
/>
);
})}
</ul>
</>
);
}

View File

@ -0,0 +1,73 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
TargetConfigurationDetailsHeader,
TargetConfigurationDetailsHeaderProps,
} from './target-configuration-details-header';
const meta: Meta<typeof TargetConfigurationDetailsHeader> = {
component: TargetConfigurationDetailsHeader,
title: 'TargetConfigurationDetailsHeader',
};
export default meta;
type Story = StoryObj<typeof TargetConfigurationDetailsHeader>;
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,
};

View File

@ -0,0 +1,176 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { TargetConfiguration } from '@nx/devkit';
import {
ChevronDownIcon,
ChevronUpIcon,
EyeIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
import { twMerge } from 'tailwind-merge';
import { Pill } from '../pill';
import { TargetTechnologies } from '../target-technologies/target-technologies';
import { SourceInfo } from '../source-info/source-info';
import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard';
export interface TargetConfigurationDetailsHeaderProps {
isCollasped: boolean;
toggleCollapse?: () => void;
collapsable: boolean;
isCompact?: boolean;
targetConfiguration: TargetConfiguration;
projectName: string;
targetName: string;
sourceMap: Record<string, string[]>;
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 (
<header
className={twMerge(
`group hover:bg-slate-50 dark:hover:bg-slate-800/60`,
collapsable ? 'cursor-pointer' : '',
isCompact ? 'px-2 py-1' : 'p-2',
!isCollasped || !collapsable
? 'border-b bg-slate-50 dark:border-slate-700/60 dark:border-slate-300/10 dark:bg-slate-800 '
: ''
)}
onClick={collapsable ? toggleCollapse : undefined}
>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
{collapsable &&
(isCollasped ? (
<ChevronDownIcon className="h-3 w-3" />
) : (
<ChevronUpIcon className="h-3 w-3" />
))}
<h3 className="font-medium dark:text-slate-300">{targetName}</h3>
<TargetTechnologies
technologies={targetConfiguration.metadata?.technologies}
showTooltip={!isCollasped}
/>
{isCollasped &&
targetConfiguration?.executor !== '@nx/js:release-publish' && (
<p className="min-w-0 flex-1 truncate text-sm text-slate-400">
{singleCommand ? singleCommand : targetConfiguration.executor}
</p>
)}
{targetName === 'nx-release-publish' && (
<Tooltip
openAction="hover"
strategy="fixed"
content={(<PropertyInfoTooltip type="release" />) as any}
>
<span className="inline-flex">
<Pill text="nx release" color="grey" />
</span>
</Tooltip>
)}
{targetConfiguration.cache && (
<Tooltip
openAction="hover"
strategy="fixed"
content={(<PropertyInfoTooltip type="cacheable" />) as any}
>
<span className="inline-flex">
<Pill text="Cacheable" color="green" />
</span>
</Tooltip>
)}
</div>
<div className="flex items-center gap-2">
{onViewInTaskGraph && (
<button
className="rounded-md bg-inherit p-1 text-sm text-slate-600 ring-1 ring-inset ring-slate-400/40 hover:bg-slate-200 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-700/60"
// TODO: fix tooltip overflow in collapsed state
data-tooltip={isCollasped ? false : 'View in Task Graph'}
data-tooltip-align-right
>
<EyeIcon
className={`h-5 w-5 !cursor-pointer`}
onClick={(e) => {
e.stopPropagation();
onViewInTaskGraph({ projectName, targetName });
}}
/>
</button>
)}
{onRunTarget && (
<span
className="rounded-md bg-inherit p-1 text-sm text-slate-600 ring-1 ring-inset ring-slate-400/40 hover:bg-slate-200 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-700/60"
// TODO: fix tooltip overflow in collapsed state
data-tooltip={isCollasped ? false : 'Run Target'}
data-tooltip-align-right
>
<PlayIcon
className="h-5 w-5 !cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onRunTarget({ projectName, targetName });
}}
/>
</span>
)}
</div>
</div>
{!isCollasped && (
<div className="mt-2 ml-5 text-sm">
<SourceInfo
data={sourceMap[`targets.${targetName}`]}
propertyKey={`targets.${targetName}`}
color="text-gray-500 dark:text-slate-400"
/>
{targetName !== 'nx-release-publish' && (
<div className="text-right">
<code className="ml-4 rounded bg-gray-100 px-2 py-1 font-mono text-gray-800 dark:bg-gray-700 dark:text-gray-300">
nx run {projectName}:{targetName}
</code>
<span>
<CopyToClipboard
onCopy={() =>
handleCopyClick(`nx run ${projectName}:${targetName}`)
}
tooltipAlignment="right"
/>
</span>
</div>
)}
</div>
)}
</header>
);
};

View File

@ -0,0 +1,140 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
TargetConfigurationDetailsListItem,
TargetConfigurationDetailsListItemProps,
} from './target-configuration-details-list-item';
import { StoreDecorator } from '@nx/graph/state';
const meta: Meta<typeof TargetConfigurationDetailsListItem> = {
component: TargetConfigurationDetailsListItem,
title: 'TargetConfigurationDetailsListItem',
decorators: [StoreDecorator],
};
export default meta;
type Story = StoryObj<typeof TargetConfigurationDetailsListItem>;
export const Simple: Story = {
args: {
isCollasped: true,
toggleCollapse: () => {},
collapsable: false,
isCompact: true,
targetConfiguration: {},
projectName: 'jest',
targetName: 'test',
sourceMap: {},
onRunTarget: () => {},
onViewInTaskGraph: () => {},
project: {
name: 'jest',
type: 'lib',
data: {
root: 'packages/jest',
name: 'jest',
targets: {
'nx-release-publish': {
dependsOn: ['^nx-release-publish'],
executor: '@nx/js:release-publish',
options: { packageRoot: 'build/packages/jest' },
configurations: {},
},
test: {
dependsOn: ['test-native', 'build-native', '^build-native'],
inputs: [
'default',
'^production',
'{workspaceRoot}/jest.preset.js',
],
executor: '@nx/jest:jest',
outputs: ['{workspaceRoot}/coverage/{projectRoot}'],
cache: true,
options: {
jestConfig: 'packages/jest/jest.config.ts',
passWithNoTests: true,
},
configurations: {},
},
'build-base': {
dependsOn: ['^build-base', 'build-native'],
inputs: ['production', '^production'],
executor: '@nx/js:tsc',
outputs: ['{options.outputPath}'],
cache: true,
options: {
outputPath: 'build/packages/jest',
tsConfig: 'packages/jest/tsconfig.lib.json',
main: 'packages/jest/index.ts',
assets: [
{
input: 'packages/jest',
glob: '**/@(files|files-angular)/**',
output: '/',
},
{
input: 'packages/jest',
glob: '**/files/**/.gitkeep',
output: '/',
},
{
input: 'packages/jest',
glob: '**/*.json',
ignore: [
'**/tsconfig*.json',
'project.json',
'.eslintrc.json',
],
output: '/',
},
{
input: 'packages/jest',
glob: '**/*.js',
ignore: ['**/jest.config.js'],
output: '/',
},
{ input: 'packages/jest', glob: '**/*.d.ts', output: '/' },
{ input: '', glob: 'LICENSE', output: '/' },
],
},
configurations: {},
},
build: {
dependsOn: ['build-base', 'build-native'],
inputs: ['production', '^production'],
cache: true,
executor: 'nx:run-commands',
outputs: ['{workspaceRoot}/build/packages/jest'],
options: { command: 'node ./scripts/copy-readme.js jest' },
configurations: {},
},
'add-extra-dependencies': {
executor: 'nx:run-commands',
options: {
command:
'node ./scripts/add-dependency-to-build.js jest @nrwl/jest',
},
configurations: {},
},
lint: {
dependsOn: ['build-native', '^build-native'],
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/tools/eslint-rules/**/*',
],
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
cache: true,
options: { lintFilePatterns: ['packages/jest'] },
configurations: {},
},
},
$schema: '../../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'packages/jest',
projectType: 'library',
implicitDependencies: [],
tags: [],
},
},
} as TargetConfigurationDetailsListItemProps,
};

View File

@ -0,0 +1,52 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
import { forwardRef, Ref } from 'react';
import TargetConfigurationDetails from '../target-configuration-details/target-configuration-details';
export interface TargetConfigurationDetailsListItemProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
targetName: string;
collapsable: boolean;
}
export const TargetConfigurationDetailsListItem = forwardRef(
(
{
project,
variant,
sourceMap,
onRunTarget,
onViewInTaskGraph,
targetName,
collapsable,
}: TargetConfigurationDetailsListItemProps,
ref: Ref<HTMLLIElement>
) => {
const target = project.data.targets?.[targetName];
if (!target) {
return null;
}
return (
<li className="mb-4 last:mb-0" key={`target-${targetName}`} ref={ref}>
<TargetConfigurationDetails
variant={variant}
projectName={project.name}
targetName={targetName}
targetConfiguration={target}
sourceMap={sourceMap}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
collapsable={collapsable}
/>
</li>
);
}
);

View File

@ -0,0 +1,28 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { AppDispatch, RootState, expandTargetActions } from '@nx/graph/state';
const mapStateToProps = (state: RootState) => {
return {};
};
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
setExpandTargets(targets: string[]) {
dispatch(expandTargetActions.setExpandTargets(targets));
},
collapseAllTargets() {
dispatch(expandTargetActions.collapseAllTargets());
},
};
};
type mapStateToPropsType = ReturnType<typeof mapStateToProps>;
type mapDispatchToPropsType = ReturnType<typeof mapDispatchToProps>;
export {
mapStateToProps,
mapDispatchToProps,
mapStateToPropsType,
mapDispatchToPropsType,
};

View File

@ -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<typeof TargetConfigurationDetailsListComponent> = {
component: TargetConfigurationDetailsListComponent,
title: 'TargetConfigurationDetailsListComponent',
decorators: [StoreDecorator],
};
export default meta;
type Story = StoryObj<typeof TargetConfigurationDetailsListComponent>;
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,
};

View File

@ -0,0 +1,50 @@
import { connect } from 'react-redux';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
import {
mapDispatchToProps,
mapDispatchToPropsType,
mapStateToProps,
mapStateToPropsType,
} from './target-configuration-details-list.state';
import { TargetConfigurationGroupList } from '../target-configuration-details-group-list/target-configuration-details-group-list';
export type TargetConfigurationDetailsListProps = mapStateToPropsType &
mapDispatchToPropsType & {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
className?: string;
};
export function TargetConfigurationDetailsListComponent({
project,
variant,
sourceMap,
onRunTarget,
onViewInTaskGraph,
className,
}: TargetConfigurationDetailsListProps) {
return (
<TargetConfigurationGroupList
project={project}
sourceMap={sourceMap}
variant={variant}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
className={className}
/>
);
}
export const TargetConfigurationDetailsList = connect(
mapStateToProps,
mapDispatchToProps
)(TargetConfigurationDetailsListComponent);
export default TargetConfigurationDetailsList;

View File

@ -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<typeof mapStateToProps>;
type mapDispatchToPropsType = ReturnType<typeof mapDispatchToProps>;
export {
mapStateToProps,
mapDispatchToProps,
mapStateToPropsType,
mapDispatchToPropsType,
};

View File

@ -1,547 +1,429 @@
/* eslint-disable @nx/enforce-module-boundaries */ /* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line // nx-ignore-next-line
import { import type { TargetConfiguration } from '@nx/devkit';
ChevronDownIcon,
ChevronUpIcon,
EyeIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
// nx-ignore-next-line
import { TargetConfiguration } from '@nx/devkit';
import { JsonCodeBlock } from '@nx/graph/ui-code-block'; import { JsonCodeBlock } from '@nx/graph/ui-code-block';
import { import { useCallback, useEffect, useMemo, useState } from 'react';
ForwardedRef, import { SourceInfo } from '../source-info/source-info';
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { SourceInfo } from './source-info';
import { FadingCollapsible } from './fading-collapsible'; import { FadingCollapsible } from './fading-collapsible';
import { TargetConfigurationProperty } from './target-configuration-property'; import { TargetConfigurationProperty } from './target-configuration-property';
import { selectSourceInfo } from './target-configuration-details.util'; import { selectSourceInfo } from './target-configuration-details.util';
import { CopyToClipboard } from './copy-to-clipboard'; import { CopyToClipboard } from '../copy-to-clipboard/copy-to-clipboard';
import { import {
ExternalLink, ExternalLink,
PropertyInfoTooltip, PropertyInfoTooltip,
Tooltip, Tooltip,
} from '@nx/graph/ui-tooltips'; } from '@nx/graph/ui-tooltips';
import { TooltipTriggerText } from './tooltip-trigger-text'; import { TooltipTriggerText } from './tooltip-trigger-text';
import { twMerge } from 'tailwind-merge';
import { Pill } from '../pill'; 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 */ type TargetConfigurationDetailsProps = mapStateToPropsType &
export interface TargetProps { mapDispatchToPropsType & {
projectName: string;
targetName: string;
targetConfiguration: TargetConfiguration;
sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onCollapse?: (targetName: string) => void;
onExpand?: (targetName: string) => void;
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string; projectName: string;
targetName: string; targetName: string;
}) => void; targetConfiguration: TargetConfiguration;
} sourceMap: Record<string, string[]>;
variant?: 'default' | 'compact';
onCollapse?: (targetName: string) => void;
onExpand?: (targetName: string) => void;
onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: {
projectName: string;
targetName: string;
}) => void;
collapsable: boolean;
};
export interface TargetConfigurationDetailsHandle { export const TargetConfigurationDetailsComponent = ({
collapse: () => void; variant,
expand: () => void; projectName,
} targetName,
targetConfiguration,
sourceMap,
onViewInTaskGraph,
onRunTarget,
expandedTargets,
toggleExpandTarget,
collapsable,
}: TargetConfigurationDetailsProps) => {
const isCompact = variant === 'compact';
const [collapsed, setCollapsed] = useState(true);
export const TargetConfigurationDetails = forwardRef( const handleCopyClick = async (copyText: string) => {
( await window.navigator.clipboard.writeText(copyText);
{ };
variant,
projectName,
targetName,
targetConfiguration,
sourceMap,
onExpand,
onCollapse,
onViewInTaskGraph,
onRunTarget,
}: TargetProps,
ref: ForwardedRef<TargetConfigurationDetailsHandle>
) => {
const isCompact = variant === 'compact';
const [collapsed, setCollapsed] = useState(true);
const handleCopyClick = async (copyText: string) => { const handleCollapseToggle = useCallback(() => {
await window.navigator.clipboard.writeText(copyText); toggleExpandTarget(targetName);
}; }, [toggleExpandTarget, targetName]);
const handleCollapseToggle = useCallback( useEffect(() => {
() => setCollapsed((collapsed) => !collapsed), if (!collapsable) {
[setCollapsed] setCollapsed(false);
); return;
useEffect(() => {
if (collapsed) {
onCollapse?.(targetName);
} else {
onExpand?.(targetName);
}
}, [collapsed, onCollapse, onExpand, projectName, targetName]);
useImperativeHandle(ref, () => ({
collapse: () => {
!collapsed && setCollapsed(true);
},
expand: () => {
collapsed && setCollapsed(false);
},
}));
let executorLink: string | null = null;
// TODO: Handle this better because this will not work with labs
if (targetConfiguration.executor?.startsWith('@nx/')) {
const packageName = targetConfiguration.executor
.split('/')[1]
.split(':')[0];
const executorName = targetConfiguration.executor
.split('/')[1]
.split(':')[1];
executorLink = `https://nx.dev/nx-api/${packageName}/executors/${executorName}`;
} else if (targetConfiguration.executor === 'nx:run-commands') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-commands`;
} else if (targetConfiguration.executor === 'nx:run-script') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-script`;
} }
if (expandedTargets.includes(targetName)) {
setCollapsed(false);
} else {
setCollapsed(true);
}
}, [expandedTargets, targetName, collapsable]);
const singleCommand = let executorLink: string | null = null;
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; // TODO: Handle this better because this will not work with labs
if (targetConfiguration.executor?.startsWith('@nx/')) {
const packageName = targetConfiguration.executor
.split('/')[1]
.split(':')[0];
const executorName = targetConfiguration.executor
.split('/')[1]
.split(':')[1];
executorLink = `https://nx.dev/nx-api/${packageName}/executors/${executorName}`;
} else if (targetConfiguration.executor === 'nx:run-commands') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-commands`;
} else if (targetConfiguration.executor === 'nx:run-script') {
executorLink = `https://nx.dev/nx-api/nx/executors/run-script`;
}
const shouldRenderOptions = const singleCommand =
options && targetConfiguration.executor === 'nx:run-commands'
(typeof options === 'object' ? Object.keys(options).length : true); ? targetConfiguration.command ?? targetConfiguration.options?.command
: null;
const options = useMemo(() => {
if (singleCommand) {
const { command, ...rest } = targetConfiguration.options;
return rest;
} else {
return targetConfiguration.options;
}
}, [targetConfiguration.options, singleCommand]);
const shouldRenderConfigurations = const configurations = targetConfiguration.configurations;
configurations &&
(typeof configurations === 'object'
? Object.keys(configurations).length
: true);
return ( const shouldRenderOptions =
<div className="relative overflow-hidden rounded-md border border-slate-200 dark:border-slate-700/60"> options &&
<header (typeof options === 'object' ? Object.keys(options).length : true);
className={twMerge(
`group cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/60`, const shouldRenderConfigurations =
isCompact ? 'px-2 py-1' : 'p-2', configurations &&
!collapsed (typeof configurations === 'object'
? 'border-b bg-slate-50 dark:border-slate-700/60 dark:border-slate-300/10 dark:bg-slate-800/60 ' ? Object.keys(configurations).length
: '' : true);
)}
onClick={handleCollapseToggle} return (
> <div className="relative overflow-hidden rounded-md border border-slate-200 dark:border-slate-700/60">
<div className="flex items-center justify-between gap-2"> <TargetConfigurationDetailsHeader
<div className="flex items-center gap-2"> isCollasped={collapsed}
{collapsed ? ( toggleCollapse={handleCollapseToggle}
<ChevronDownIcon className="h-3 w-3" /> collapsable={collapsable}
isCompact={isCompact}
targetConfiguration={targetConfiguration}
projectName={projectName}
targetName={targetName}
sourceMap={sourceMap}
onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph}
/>
{/* body */}
{!collapsed && (
<div className="p-4 text-base">
<div className="group mb-4">
<h4 className="mb-4">
{singleCommand ? (
<span className="font-medium">
Command
<span className="ml-2 mb-1 hidden group-hover:inline">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`"command": "${singleCommand}"`)
}
/>
</span>
</span>
) : ( ) : (
<ChevronUpIcon className="h-3 w-3" />
)}
<h3 className="font-medium dark:text-slate-300">{targetName}</h3>
{collapsed &&
targetConfiguration?.executor !== '@nx/js:release-publish' && (
<p className="text-sm text-slate-400">
{singleCommand
? singleCommand
: targetConfiguration.executor}
</p>
)}
{targetName === 'nx-release-publish' && (
<Tooltip <Tooltip
openAction="hover" openAction="hover"
strategy="fixed" content={(<PropertyInfoTooltip type="executors" />) as any}
content={(<PropertyInfoTooltip type="release" />) as any}
> >
<span className="inline-flex"> <span className="font-medium">
<Pill text="nx release" color="grey" /> <TooltipTriggerText>Executor</TooltipTriggerText>
</span> </span>
</Tooltip> </Tooltip>
)} )}
{targetConfiguration.cache && ( </h4>
<Tooltip <p className="pl-5 font-mono">
openAction="hover" {executorLink ? (
strategy="fixed" <span>
content={(<PropertyInfoTooltip type="cacheable" />) as any} <ExternalLink
> href={executorLink ?? 'https://nx.dev/nx-api'}
<span className="inline-flex"> text={
<Pill text="Cacheable" color="green" /> singleCommand
</span> ? singleCommand
</Tooltip> : targetConfiguration.executor
)} }
</div>
<div className="flex items-center gap-2">
{onViewInTaskGraph && (
<button
className="rounded-md bg-inherit p-1 text-sm text-slate-600 ring-1 ring-inset ring-slate-400/40 hover:bg-slate-200 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-700/60"
// TODO: fix tooltip overflow in collapsed state
data-tooltip={collapsed ? false : 'View in Task Graph'}
data-tooltip-align-right
>
<EyeIcon
className={`h-5 w-5 !cursor-pointer`}
onClick={(e) => {
e.stopPropagation();
onViewInTaskGraph({ projectName, targetName });
}}
/>
</button>
)}
{onRunTarget && (
<span
className="rounded-md bg-inherit p-1 text-sm text-slate-600 ring-1 ring-inset ring-slate-400/40 hover:bg-slate-200 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-700/60"
// TODO: fix tooltip overflow in collapsed state
data-tooltip={collapsed ? false : 'Run Target'}
data-tooltip-align-right
>
<PlayIcon
className="h-5 w-5 !cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onRunTarget({ projectName, targetName });
}}
/> />
</span> </span>
) : singleCommand ? (
singleCommand
) : (
targetConfiguration.executor
)} )}
</div> </p>
</div> </div>
{!collapsed && (
<div className="mt-2 ml-5 flex items-center text-sm"> {targetConfiguration.inputs && (
<span className="flex min-w-0 flex-1 items-center"> <div className="group">
<SourceInfo
data={sourceMap[`targets.${targetName}`]}
propertyKey={`targets.${targetName}`}
color="text-gray-500 dark:text-slate-400"
/>
</span>
{targetName !== 'nx-release-publish' && (
<div className="flex items-center gap-2">
<code className="ml-4 rounded bg-gray-100 px-2 py-1 font-mono text-gray-800 dark:bg-gray-700 dark:text-gray-300">
nx run {projectName}:{targetName}
</code>
<span>
<CopyToClipboard
onCopy={() =>
handleCopyClick(`nx run ${projectName}:${targetName}`)
}
tooltipAlignment="right"
/>
</span>
</div>
)}
</div>
)}
</header>
{/* body */}
{!collapsed && (
<div className="p-4 text-base">
<div className="group mb-4">
<h4 className="mb-4"> <h4 className="mb-4">
{singleCommand ? ( <Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="inputs" />) as any}
>
<span className="font-medium"> <span className="font-medium">
Command <TooltipTriggerText>Inputs</TooltipTriggerText>
<span className="ml-2 mb-1 hidden group-hover:inline">
<CopyToClipboard
onCopy={() =>
handleCopyClick(`"command": "${singleCommand}"`)
}
/>
</span>
</span> </span>
) : ( </Tooltip>
<Tooltip <span className="ml-2 mb-1 hidden group-hover:inline">
openAction="hover" <CopyToClipboard
content={(<PropertyInfoTooltip type="executors" />) as any} onCopy={() =>
> handleCopyClick(
<span className="font-medium"> `"inputs": ${JSON.stringify(
<TooltipTriggerText>Executor</TooltipTriggerText> targetConfiguration.inputs
</span> )}`
</Tooltip> )
)} }
/>
</span>
</h4> </h4>
<p className="pl-5 font-mono"> <ul className="mb-4 list-disc pl-5">
{executorLink ? ( {targetConfiguration.inputs.map((input, idx) => {
<span> const sourceInfo = selectSourceInfo(
<ExternalLink sourceMap,
href={executorLink ?? 'https://nx.dev/nx-api'} `targets.${targetName}.inputs`
text={ );
singleCommand return (
? singleCommand <li
: targetConfiguration.executor className="group/line overflow-hidden whitespace-nowrap"
} key={`input-${idx}`}
/> >
</span> <TargetConfigurationProperty data={input}>
) : singleCommand ? ( {sourceInfo && (
singleCommand <span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
) : (
targetConfiguration.executor
)}
</p>
</div>
{targetConfiguration.inputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="inputs" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Inputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="ml-2 mb-1 hidden group-hover:inline">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"inputs": ${JSON.stringify(
targetConfiguration.inputs
)}`
)
}
/>
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.inputs.map((input, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.inputs`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`input-${idx}`}
>
<TargetConfigurationProperty data={input}>
{sourceInfo && (
<span className="shrink-1 inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.inputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{targetConfiguration.outputs && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="outputs" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Outputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="ml-2 mb-1 hidden group-hover:inline">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"outputs": ${JSON.stringify(
targetConfiguration.outputs
)}`
)
}
/>
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.outputs?.map((output, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.outputs`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`output-${idx}`}
>
<TargetConfigurationProperty data={output}>
{sourceInfo && (
<span className="shrink-1 inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
}) ?? <span>no outputs</span>}
</ul>
</div>
)}
{targetConfiguration.dependsOn && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="dependsOn" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Depends On</TooltipTriggerText>
</span>
</Tooltip>
<span className="inline pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"dependsOn": ${JSON.stringify(
targetConfiguration.dependsOn
)}`
)
}
/>
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.dependsOn.map((dep, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.dependsOn`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`dependsOn-${idx}`}
>
<TargetConfigurationProperty data={dep}>
<span className="shrink-1 inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
{sourceInfo && (
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`}
/>
)}
</span>
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible>
<JsonCodeBlock
data={options}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.options.${propertyName}`
);
return sourceInfo ? (
<span className="shrink-1 flex min-w-0 pl-4">
<SourceInfo <SourceInfo
data={sourceInfo} data={sourceInfo}
propertyKey={`targets.${targetName}.options.${propertyName}`} propertyKey={`targets.${targetName}.inputs`}
/> />
</span> </span>
) : null; )}
}} </TargetConfigurationProperty>
/> </li>
</FadingCollapsible> );
</div> })}
</> </ul>
) : ( </div>
'' )}
)} {targetConfiguration.outputs && (
<div className="group">
{shouldRenderConfigurations ? ( <h4 className="mb-4">
<> <Tooltip
<h4 className="mb-4 py-2"> openAction="hover"
<Tooltip content={(<PropertyInfoTooltip type="outputs" />) as any}
openAction="hover" >
content={ <span className="font-medium">
(<PropertyInfoTooltip type="configurations" />) as any <TooltipTriggerText>Outputs</TooltipTriggerText>
</span>
</Tooltip>
<span className="ml-2 mb-1 hidden group-hover:inline">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"outputs": ${JSON.stringify(
targetConfiguration.outputs
)}`
)
} }
> />
<span className="font-medium"> </span>
<TooltipTriggerText>Configurations</TooltipTriggerText> </h4>
</span> <ul className="mb-4 list-disc pl-5">
</Tooltip>{' '} {targetConfiguration.outputs?.map((output, idx) => {
{targetConfiguration.defaultConfiguration && ( const sourceInfo = selectSourceInfo(
<span className="ml-3 cursor-help"> sourceMap,
<Pill `targets.${targetName}.outputs`
tooltip="Default Configuration" );
text={targetConfiguration.defaultConfiguration} return (
color="yellow" <li
/> className="group/line overflow-hidden whitespace-nowrap"
</span> key={`output-${idx}`}
)} >
</h4> <TargetConfigurationProperty data={output}>
{sourceInfo && (
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.outputs`}
/>
</span>
)}
</TargetConfigurationProperty>
</li>
);
}) ?? <span>no outputs</span>}
</ul>
</div>
)}
{targetConfiguration.dependsOn && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="dependsOn" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Depends On</TooltipTriggerText>
</span>
</Tooltip>
<span className="inline pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
<CopyToClipboard
onCopy={() =>
handleCopyClick(
`"dependsOn": ${JSON.stringify(
targetConfiguration.dependsOn
)}`
)
}
/>
</span>
</h4>
<ul className="mb-4 list-disc pl-5">
{targetConfiguration.dependsOn.map((dep, idx) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.dependsOn`
);
return (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`dependsOn-${idx}`}
>
<TargetConfigurationProperty data={dep}>
<span className="inline flex min-w-0 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
{sourceInfo && (
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.dependsOn`}
/>
)}
</span>
</TargetConfigurationProperty>
</li>
);
})}
</ul>
</div>
)}
{shouldRenderOptions ? (
<>
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={(<PropertyInfoTooltip type="options" />) as any}
>
<span className="font-medium">
<TooltipTriggerText>Options</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<div className="mb-4">
<FadingCollapsible> <FadingCollapsible>
<JsonCodeBlock <JsonCodeBlock
data={targetConfiguration.configurations} data={options}
renderSource={(propertyName: string) => { renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo( const sourceInfo = selectSourceInfo(
sourceMap, sourceMap,
`targets.${targetName}.configurations.${propertyName}` `targets.${targetName}.options.${propertyName}`
); );
return sourceInfo ? ( return sourceInfo ? (
<span className="shrink-1 flex min-w-0 pl-4"> <span className="flex min-w-0 pl-4">
<SourceInfo <SourceInfo
data={sourceInfo} data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`} propertyKey={`targets.${targetName}.options.${propertyName}`}
/>{' '} />
</span> </span>
) : null; ) : null;
}} }}
/> />
</FadingCollapsible> </FadingCollapsible>
</> </div>
) : ( </>
'' ) : (
)} ''
</div> )}
)}
</div>
);
}
);
{shouldRenderConfigurations ? (
<>
<h4 className="mb-4 py-2">
<Tooltip
openAction="hover"
content={
(<PropertyInfoTooltip type="configurations" />) as any
}
>
<span className="font-medium">
<TooltipTriggerText>Configurations</TooltipTriggerText>
</span>
</Tooltip>{' '}
{targetConfiguration.defaultConfiguration && (
<span className="ml-3 cursor-help">
<Pill
tooltip="Default Configuration"
text={targetConfiguration.defaultConfiguration}
color="yellow"
/>
</span>
)}
</h4>
<FadingCollapsible>
<JsonCodeBlock
data={targetConfiguration.configurations}
renderSource={(propertyName: string) => {
const sourceInfo = selectSourceInfo(
sourceMap,
`targets.${targetName}.configurations.${propertyName}`
);
return sourceInfo ? (
<span className="flex min-w-0 pl-4">
<SourceInfo
data={sourceInfo}
propertyKey={`targets.${targetName}.configurations.${propertyName}`}
/>{' '}
</span>
) : null;
}}
/>
</FadingCollapsible>
</>
) : (
''
)}
</div>
)}
</div>
);
};
export const TargetConfigurationDetails = connect(
mapStateToProps,
mapDispatchToProps
)(TargetConfigurationDetailsComponent);
export default TargetConfigurationDetails; export default TargetConfigurationDetails;

View File

@ -11,7 +11,7 @@ export function TargetConfigurationProperty({
}: RenderPropertyProps): JSX.Element | null { }: RenderPropertyProps): JSX.Element | null {
if (typeof data === 'string') { if (typeof data === 'string') {
return ( return (
<span className="font-mono flex shrink-1 text-sm"> <span className="flex font-mono text-sm">
{data} {data}
{children} {children}
</span> </span>
@ -20,7 +20,7 @@ export function TargetConfigurationProperty({
return ( return (
<ul> <ul>
{data.map((item, index) => ( {data.map((item, index) => (
<li key={index} className="font-mono flex shrink-1 text-sm"> <li key={index} className="flex font-mono text-sm">
{String(item)} {String(item)}
{children} {children}
</li> </li>
@ -31,7 +31,7 @@ export function TargetConfigurationProperty({
return ( return (
<ul> <ul>
{Object.entries(data).map(([key, value], index) => ( {Object.entries(data).map(([key, value], index) => (
<li key={index} className="font-mono flex shrink-1 text-sm"> <li key={index} className="flex font-mono text-sm">
<strong>{key}</strong>: {String(value)} <strong>{key}</strong>: {String(value)}
{children} {children}
</li> </li>

View File

@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TargetTechnologies } from './target-technologies';
const meta: Meta<typeof TargetTechnologies> = {
component: TargetTechnologies,
title: 'TargetTechnologies',
};
export default meta;
type Story = StoryObj<typeof TargetTechnologies>;
export const Simple: Story = {
args: {
technologies: ['react', 'angular'],
},
};

View File

@ -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 (
<div className="flex gap-2">
{technologies.map((technology, index) => (
<TechnologyIcon
key={index}
technology={technology}
showTooltip={showTooltip}
/>
))}
</div>
);
}

View File

@ -0,0 +1,35 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
/**
* This function groups targets based on the targetGroups metadata
* If there is no targetGroups metadata, it will create a group for each target
* @param project
* @returns
*/
export function groupTargets(project: ProjectGraphProjectNode): {
groups: Record<string, string[]>;
targets: string[];
} {
const targetGroups = project.data.metadata?.targetGroups ?? {};
Object.entries(targetGroups).forEach(([group, targets]) => {
targetGroups[group] = targets.sort(sortNxReleasePublishLast);
});
const allTargetsInTargetGroups: string[] = Object.values(targetGroups).flat();
const targets: string[] = Object.keys(project.data.targets ?? {})
.filter((target) => {
return !allTargetsInTargetGroups.includes(target);
})
.sort(sortNxReleasePublishLast);
return {
groups: targetGroups ?? {},
targets: targets ?? [],
};
}
function sortNxReleasePublishLast(a: string, b: string) {
if (a === 'nx-release-publish') return 1;
if (b === 'nx-release-publish') return -1;
return a.localeCompare(b);
}

View File

@ -1,7 +1,6 @@
import { import {
Attributes, Attributes,
cloneElement, cloneElement,
Fragment,
HTMLAttributes, HTMLAttributes,
ReactElement, ReactElement,
ReactNode, ReactNode,
@ -11,7 +10,6 @@ import {
} from 'react'; } from 'react';
import { import {
FloatingPortal,
useClick, useClick,
arrow, arrow,
autoUpdate, autoUpdate,

View File

@ -8,7 +8,7 @@ import {
import { NextSeo } from 'next-seo'; import { NextSeo } from 'next-seo';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; 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'; import { ReactNode, useEffect, useState } from 'react';
interface NewYearTip { interface NewYearTip {

View File

@ -54,7 +54,6 @@ import { VideoLink, videoLink } from './lib/tags/video-link.component';
// import { SvgAnimation, svgAnimation } from './lib/tags/svg-animation.component'; // import { SvgAnimation, svgAnimation } from './lib/tags/svg-animation.component';
import { Pill } from './lib/tags/pill.component'; import { Pill } from './lib/tags/pill.component';
import { pill } from './lib/tags/pill.schema'; import { pill } from './lib/tags/pill.schema';
import { frameworkIcons } from './lib/icons';
import { fence } from './lib/nodes/fence.schema'; import { fence } from './lib/nodes/fence.schema';
import { FenceWrapper } from './lib/nodes/fence-wrapper.component'; import { FenceWrapper } from './lib/nodes/fence-wrapper.component';
@ -139,8 +138,6 @@ export const parseMarkdown: (markdown: string) => Node = (markdown) => {
return parse(tokens); return parse(tokens);
}; };
export { frameworkIcons };
export const renderMarkdown: ( export const renderMarkdown: (
documentContent: string, documentContent: string,
options: { filePath: string } options: { filePath: string }

View File

@ -1,5 +1,5 @@
import { ChevronRightIcon } from '@heroicons/react/24/outline'; import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { frameworkIcons } from '../icons'; import { frameworkIcons } from '@nx/graph/ui-icons';
export function CallToAction({ export function CallToAction({
url, url,

View File

@ -4,7 +4,7 @@ import {
DocumentIcon, DocumentIcon,
PlayCircleIcon, PlayCircleIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { frameworkIcons } from '../icons'; import { Framework, frameworkIcons } from '@nx/graph/ui-icons';
import { cx } from '@nx/nx-dev/ui-primitives'; import { cx } from '@nx/nx-dev/ui-primitives';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
@ -127,7 +127,7 @@ export function LinkCard({
} }
)} )}
> >
{icon && frameworkIcons[icon]?.image} {icon && frameworkIcons[icon as Framework]?.image}
</div> </div>
)} )}
<div className={cx('pt-4', { 'pt-2': appearance === 'small' })}> <div className={cx('pt-4', { 'pt-2': appearance === 'small' })}>

View File

@ -1,5 +1,6 @@
import { useTheme } from '@nx/nx-dev/ui-theme';
import { JSX, ReactElement, useEffect, useState } from 'react'; 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'; import { ProjectDetails as ProjectDetailsUi } from '@nx/graph/ui-project-details';
export function Loading() { export function Loading() {
@ -26,7 +27,6 @@ export function ProjectDetails({
jsonFile?: string; jsonFile?: string;
children: ReactElement; children: ReactElement;
}): JSX.Element { }): JSX.Element {
const [theme] = useTheme();
const [parsedProps, setParsedProps] = useState<any>(); const [parsedProps, setParsedProps] = useState<any>();
const getData = async (path: string) => { const getData = async (path: string) => {
const response = await fetch('/documentation/' + path, { const response = await fetch('/documentation/' + path, {
@ -73,7 +73,7 @@ export function ProjectDetails({
return ( return (
<div className="w-full place-content-center overflow-hidden rounded-md ring-1 ring-slate-200 dark:ring-slate-700"> <div className="w-full place-content-center overflow-hidden rounded-md ring-1 ring-slate-200 dark:ring-slate-700">
{title && ( {title && (
<div className="relative flex justify-center p-2 border-b border-slate-200 bg-slate-100/50 dark:border-slate-700 dark:bg-slate-700/50 font-bold"> <div className="relative flex justify-center border-b border-slate-200 bg-slate-100/50 p-2 font-bold dark:border-slate-700 dark:bg-slate-700/50">
{title} {title}
</div> </div>
)} )}
@ -82,11 +82,13 @@ export function ProjectDetails({
height ? `p-4 h-[${height}] overflow-y-auto` : 'p-4' height ? `p-4 h-[${height}] overflow-y-auto` : 'p-4'
}`} }`}
> >
<ProjectDetailsUi <StoreProvider store={rootStore}>
project={parsedProps.project} <ProjectDetailsUi
sourceMap={parsedProps.sourceMap} project={parsedProps.project}
variant="compact" sourceMap={parsedProps.sourceMap}
/> variant="compact"
/>
</StoreProvider>
</div> </div>
</div> </div>
); );

View File

@ -69,6 +69,11 @@ describe('@nx/gradle/plugin', () => {
expect(nodes.projects.proj).toMatchInlineSnapshot(` expect(nodes.projects.proj).toMatchInlineSnapshot(`
{ {
"metadata": { "metadata": {
"targetGroups": {
"Test": [
"test",
],
},
"technologies": [ "technologies": [
"gradle", "gradle",
], ],
@ -85,6 +90,11 @@ describe('@nx/gradle/plugin', () => {
"default", "default",
"^production", "^production",
], ],
"metadata": {
"technologies": [
"gradle",
],
},
"options": { "options": {
"cwd": "proj", "cwd": "proj",
}, },
@ -124,6 +134,11 @@ describe('@nx/gradle/plugin', () => {
expect(nodes.projects['nested/nested/proj']).toMatchInlineSnapshot(` expect(nodes.projects['nested/nested/proj']).toMatchInlineSnapshot(`
{ {
"metadata": { "metadata": {
"targetGroups": {
"Test": [
"test",
],
},
"technologies": [ "technologies": [
"gradle", "gradle",
], ],
@ -140,6 +155,11 @@ describe('@nx/gradle/plugin', () => {
"default", "default",
"^production", "^production",
], ],
"metadata": {
"technologies": [
"gradle",
],
},
"options": { "options": {
"cwd": "nested/nested/proj", "cwd": "nested/nested/proj",
}, },

View File

@ -4,7 +4,6 @@ import {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration, TargetConfiguration,
readJsonFile, readJsonFile,
workspaceRoot,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
@ -42,7 +41,7 @@ export const calculatedTargets: Record<
{ {
name: string; name: string;
targets: Record<string, TargetConfiguration>; targets: Record<string, TargetConfiguration>;
targetGroups: Record<string, string[]>; metadata: ProjectConfiguration['metadata'];
} }
> = {}; > = {};
@ -51,7 +50,7 @@ function readTargetsCache(): Record<
{ {
name: string; name: string;
targets: Record<string, TargetConfiguration>; targets: Record<string, TargetConfiguration>;
targetGroups: Record<string, string[]>; metadata: ProjectConfiguration['metadata'];
} }
> { > {
return readJsonFile(cachePath); return readJsonFile(cachePath);
@ -63,7 +62,7 @@ export function writeTargetsToCache(
{ {
name: string; name: string;
targets: Record<string, TargetConfiguration>; targets: Record<string, TargetConfiguration>;
targetGroups: Record<string, string[]>; metadata: ProjectConfiguration['metadata'];
} }
> >
) { ) {
@ -88,12 +87,7 @@ export const createNodes: CreateNodes<GradlePluginOptions> = [
calculatedTargets[hash] = targetsCache[hash]; calculatedTargets[hash] = targetsCache[hash];
return { return {
projects: { projects: {
[projectRoot]: { [projectRoot]: targetsCache[hash],
...targetsCache[hash],
metadata: {
technologies: ['gradle'],
},
},
}, },
}; };
} }
@ -137,19 +131,15 @@ export const createNodes: CreateNodes<GradlePluginOptions> = [
context, context,
outputDirs outputDirs
); );
calculatedTargets[hash] = { const project = {
name: projectName,
targets,
targetGroups,
};
const project: Omit<ProjectConfiguration, 'root'> = {
name: projectName, name: projectName,
targets, targets,
metadata: { metadata: {
targetGroups,
technologies: ['gradle'], technologies: ['gradle'],
}, },
}; };
calculatedTargets[hash] = project;
return { return {
projects: { projects: {
@ -194,6 +184,9 @@ function createGradleTargets(
inputs: inputsMap[task.name], inputs: inputsMap[task.name],
outputs: outputs ? [outputs] : undefined, outputs: outputs ? [outputs] : undefined,
dependsOn: dependsOnMap[task.name], dependsOn: dependsOnMap[task.name],
metadata: {
technologies: ['gradle'],
},
}; };
if (!targetGroups[task.type]) { if (!targetGroups[task.type]) {
targetGroups[task.type] = []; targetGroups[task.type] = [];

View File

@ -39,9 +39,11 @@
"@nx/gradle/*": ["packages/gradle/*"], "@nx/gradle/*": ["packages/gradle/*"],
"@nx/graph/project-details": ["graph/project-details/src/index.ts"], "@nx/graph/project-details": ["graph/project-details/src/index.ts"],
"@nx/graph/shared": ["graph/shared/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-code-block": ["graph/ui-code-block/src/index.ts"],
"@nx/graph/ui-components": ["graph/ui-components/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-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-project-details": ["graph/ui-project-details/src/index.ts"],
"@nx/graph/ui-theme": ["graph/ui-theme/src/index.ts"], "@nx/graph/ui-theme": ["graph/ui-theme/src/index.ts"],
"@nx/graph/ui-tooltips": ["graph/ui-tooltips/src/index.ts"], "@nx/graph/ui-tooltips": ["graph/ui-tooltips/src/index.ts"],