diff --git a/graph/client/jest.config.ts b/graph/client/jest.config.ts
index 78336ace45..a246a340b4 100644
--- a/graph/client/jest.config.ts
+++ b/graph/client/jest.config.ts
@@ -10,7 +10,7 @@ export default {
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
- coverageDirectory: '../../coverage/nx-dev/nx-dev',
+ coverageDirectory: '../../coverage/graph/client',
// The mock for widnow.matchMedia has to be in a separete file and imported before the components to test
// for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
modulePathIgnorePatterns: [
diff --git a/graph/client/src/app/app.tsx b/graph/client/src/app/app.tsx
index 90dff08ff3..254b91f635 100644
--- a/graph/client/src/app/app.tsx
+++ b/graph/client/src/app/app.tsx
@@ -1,4 +1,6 @@
import { themeInit } from '@nx/graph/ui-theme';
+import { rootStore } from '@nx/graph/state';
+import { Provider as StoreProvider } from 'react-redux';
import { rankDirInit } from './rankdir-resolver';
import { RouterProvider } from 'react-router-dom';
import { getRouter } from './get-router';
@@ -7,5 +9,9 @@ themeInit();
rankDirInit();
export function App() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx
index f6c1f82f3f..fed918e0ed 100644
--- a/graph/client/src/app/feature-projects/project-list.tsx
+++ b/graph/client/src/app/feature-projects/project-list.tsx
@@ -6,7 +6,7 @@ import {
} from '@heroicons/react/24/outline';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
-import type { ProjectGraphNode } from '@nx/devkit';
+import type { ProjectGraphProjectNode } from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */
import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
import {
@@ -23,7 +23,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useRouteConstructor } from '@nx/graph/shared';
interface SidebarProject {
- projectGraphNode: ProjectGraphNode;
+ projectGraphNode: ProjectGraphProjectNode;
isSelected: boolean;
}
@@ -36,7 +36,7 @@ interface TracingInfo {
}
function groupProjectsByDirectory(
- projects: ProjectGraphNode[],
+ projects: ProjectGraphProjectNode[],
selectedProjects: string[],
workspaceLayout: { appsDir: string; libsDir: string }
): DirectoryProjectRecord {
diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx
index ef823476ea..92e366f3e7 100644
--- a/graph/client/src/app/ui-components/error-boundary.tsx
+++ b/graph/client/src/app/ui-components/error-boundary.tsx
@@ -3,7 +3,7 @@ import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-deta
import { useRouteError } from 'react-router-dom';
export function ErrorBoundary() {
- let error = useRouteError()?.toString();
+ let error = useRouteError();
console.error(error);
const environment = useEnvironmentConfig()?.environment;
@@ -20,7 +20,7 @@ export function ErrorBoundary() {
Error
{message}
-
Error message: {error}
+
Error message: {error?.toString()}
);
diff --git a/graph/client/src/app/util.ts b/graph/client/src/app/util.ts
index a3d71d40f9..e4e8cf503b 100644
--- a/graph/client/src/app/util.ts
+++ b/graph/client/src/app/util.ts
@@ -1,7 +1,9 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
-import { ProjectGraphDependency, ProjectGraphProjectNode } from '@nx/devkit';
-import { getEnvironmentConfig } from '@nx/graph/shared';
+import type {
+ ProjectGraphDependency,
+ ProjectGraphProjectNode,
+} from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */
export function parseParentDirectoriesFromFilePath(
diff --git a/graph/client/src/assets/project-graphs/e2e.json b/graph/client/src/assets/project-graphs/e2e.json
index 21a2f0f0b3..49f77dc01c 100644
--- a/graph/client/src/assets/project-graphs/e2e.json
+++ b/graph/client/src/assets/project-graphs/e2e.json
@@ -1189,27 +1189,111 @@
}
],
"targets": {
- "e2e": {
- "executor": "@nrwl/cypress:cypress",
- "options": {
- "cypressConfig": "apps/cart-e2e/cypress.config.ts",
- "devServerTarget": "cart:serve",
- "testingType": "e2e"
- },
- "configurations": {
- "production": {
- "devServerTarget": "cart:serve:production"
- }
- },
- "inputs": ["default", "^production"]
- },
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "cache": true,
"options": {
- "lintFilePatterns": ["apps/cart-e2e/**/*.{ts,tsx,js,jsx}"]
+ "cwd": "apps/cart-e2e",
+ "command": "eslint ."
},
- "outputs": ["{options.outputFile}"],
- "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
+ "inputs": [
+ "default",
+ "^default",
+ "{workspaceRoot}/.eslintrc.json",
+ "{projectRoot}/.eslintrc.json",
+ "{workspaceRoot}/tools/eslint-rules/**/*",
+ {
+ "externalDependencies": ["eslint"]
+ }
+ ],
+ "executor": "nx:run-commands",
+ "configurations": {}
+ },
+ "e2e": {
+ "cache": true,
+ "inputs": ["default", "^production"],
+ "outputs": [
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
+ ],
+ "metadata": {
+ "technologies": ["cypress"],
+ "description": "Runs Cypress Tests"
+ },
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "apps/cart-e2e",
+ "command": "cypress run"
+ },
+ "configurations": {}
+ },
+ "e2e-ci--src/e2e/app.cy.ts": {
+ "outputs": [
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
+ ],
+ "inputs": [
+ "default",
+ "^production",
+ {
+ "externalDependencies": ["cypress"]
+ }
+ ],
+ "cache": true,
+ "options": {
+ "cwd": "apps/cart-e2e",
+ "command": "cypress run --env webServerCommand=\"nx run cart:serve\" --spec src/e2e/app.cy.ts"
+ },
+ "metadata": {
+ "technologies": ["cypress"],
+ "description": "Runs Cypress Tests in src/e2e/app.cy.ts in CI"
+ },
+ "executor": "nx:run-commands",
+ "configurations": {}
+ },
+ "e2e-ci": {
+ "executor": "nx:noop",
+ "cache": true,
+ "inputs": [
+ "default",
+ "^production",
+ {
+ "externalDependencies": ["cypress"]
+ }
+ ],
+ "outputs": [
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/videos",
+ "{workspaceRoot}/dist/cypress/apps/cart-e2e/screenshots"
+ ],
+ "dependsOn": [
+ {
+ "target": "e2e-ci--src/e2e/app.cy.ts",
+ "projects": "self",
+ "params": "forward"
+ }
+ ],
+ "metadata": {
+ "technologies": ["cypress"],
+ "description": "Runs Cypress Tests in CI"
+ },
+ "options": {},
+ "configurations": {}
+ },
+ "open-cypress": {
+ "options": {
+ "cwd": "apps/cart-e2e",
+ "command": "cypress open"
+ },
+ "metadata": {
+ "technologies": ["cypress"],
+ "description": "Opens Cypress"
+ },
+ "executor": "nx:run-commands",
+ "configurations": {}
+ }
+ },
+ "metadata": {
+ "targetGroups": {
+ "E2E (CI)": ["e2e-ci--src/e2e/app.cy.ts", "e2e-ci"]
}
}
}
diff --git a/graph/client/src/styles.css b/graph/client/src/styles.css
index 0349580f6f..88e771941b 100644
--- a/graph/client/src/styles.css
+++ b/graph/client/src/styles.css
@@ -44,3 +44,13 @@
opacity: 1;
visibility: visible;
}
+
+/* Dark mode */
+html.dark .adaptive-icon {
+ /* fill: white; */
+ filter: invert(1);
+}
+
+.adaptive-icon {
+ fill: black;
+}
diff --git a/graph/project-details/src/lib/project-details-page.tsx b/graph/project-details/src/lib/project-details-page.tsx
index 34263a8ea2..1b325f8be1 100644
--- a/graph/project-details/src/lib/project-details-page.tsx
+++ b/graph/project-details/src/lib/project-details-page.tsx
@@ -1,12 +1,12 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
-import { ProjectGraphProjectNode } from '@nx/devkit';
+import type { ProjectGraphProjectNode } from '@nx/devkit';
import {
ScrollRestoration,
useParams,
useRouteLoaderData,
} from 'react-router-dom';
-import { ProjectDetailsWrapper } from './project-details-wrapper';
+import ProjectDetailsWrapper from './project-details-wrapper';
import {
fetchProjectGraph,
getProjectGraphDataService,
diff --git a/graph/project-details/src/lib/project-details-wrapper.state.ts b/graph/project-details/src/lib/project-details-wrapper.state.ts
new file mode 100644
index 0000000000..2ca6c92831
--- /dev/null
+++ b/graph/project-details/src/lib/project-details-wrapper.state.ts
@@ -0,0 +1,33 @@
+import {
+ AppDispatch,
+ RootState,
+ expandTargetActions,
+ getExpandedTargets,
+} from '@nx/graph/state';
+
+const mapStateToProps = (state: RootState) => {
+ return {
+ expandTargets: getExpandedTargets(state),
+ };
+};
+
+const mapDispatchToProps = (dispatch: AppDispatch) => {
+ return {
+ setExpandTargets(targets: string[]) {
+ dispatch(expandTargetActions.setExpandTargets(targets));
+ },
+ collapseAllTargets() {
+ dispatch(expandTargetActions.collapseAllTargets());
+ },
+ };
+};
+
+type mapStateToPropsType = ReturnType;
+type mapDispatchToPropsType = ReturnType;
+
+export {
+ mapStateToProps,
+ mapDispatchToProps,
+ mapStateToPropsType,
+ mapDispatchToPropsType,
+};
diff --git a/graph/project-details/src/lib/project-details-wrapper.tsx b/graph/project-details/src/lib/project-details-wrapper.tsx
index 55f80321dd..712abe4346 100644
--- a/graph/project-details/src/lib/project-details-wrapper.tsx
+++ b/graph/project-details/src/lib/project-details-wrapper.tsx
@@ -1,10 +1,8 @@
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-
-import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
-
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
-import { ProjectGraphProjectNode } from '@nx/devkit';
+import type { ProjectGraphProjectNode } from '@nx/devkit';
+import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
+import { connect } from 'react-redux';
import {
getExternalApiService,
useEnvironmentConfig,
@@ -12,19 +10,28 @@ import {
} from '@nx/graph/shared';
import { Spinner } from '@nx/graph/ui-components';
+import { ProjectDetails } from '@nx/graph/ui-project-details';
+import { useCallback, useEffect } from 'react';
import {
- ProjectDetails,
- ProjectDetailsImperativeHandle,
-} from '@nx/graph/ui-project-details';
-import { useCallback, useLayoutEffect, useRef } from 'react';
+ mapStateToProps,
+ mapDispatchToProps,
+ mapStateToPropsType,
+ mapDispatchToPropsType,
+} from './project-details-wrapper.state';
-export interface ProjectDetailsProps {
- project: ProjectGraphProjectNode;
- sourceMap: Record;
-}
+type ProjectDetailsProps = mapStateToPropsType &
+ mapDispatchToPropsType & {
+ project: ProjectGraphProjectNode;
+ sourceMap: Record;
+ };
-export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
- const projectDetailsRef = useRef(null);
+export function ProjectDetailsWrapperComponent({
+ project,
+ sourceMap,
+ setExpandTargets,
+ expandTargets,
+ collapseAllTargets,
+}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
@@ -88,63 +95,56 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
[externalApiService]
);
- const updateSearchParams = (params: URLSearchParams, sections: string[]) => {
- if (sections.length === 0) {
+ const updateSearchParams = (
+ params: URLSearchParams,
+ targetNames: string[]
+ ) => {
+ if (targetNames.length === 0) {
params.delete('expanded');
} else {
- params.set('expanded', sections.join(','));
+ params.set('expanded', targetNames.join(','));
}
};
- const handleTargetCollapse = useCallback(
- (targetName: string) => {
- const expandedSections = searchParams.get('expanded')?.split(',') || [];
- if (!expandedSections.includes(targetName)) return;
- const newExpandedSections = expandedSections.filter(
- (section) => section !== targetName
- );
- setSearchParams(
- (currentSearchParams) => {
- updateSearchParams(currentSearchParams, newExpandedSections);
- return currentSearchParams;
- },
- {
- replace: true,
- preventScrollReset: true,
- }
- );
- },
- [setSearchParams, searchParams]
- );
+ useEffect(() => {
+ if (!project.data.targets) return;
- const handleTargetExpand = useCallback(
- (targetName: string) => {
- const expandedSections = searchParams.get('expanded')?.split(',') || [];
- if (expandedSections.includes(targetName)) return;
- expandedSections.push(targetName);
- setSearchParams(
- (currentSearchParams) => {
- updateSearchParams(currentSearchParams, expandedSections);
- return currentSearchParams;
- },
- { replace: true, preventScrollReset: true }
- );
- },
- [setSearchParams, searchParams]
- );
-
- useLayoutEffect(() => {
- if (!props.project.data.targets) return;
-
- const expandedSections = searchParams.get('expanded')?.split(',') || [];
- for (const targetName of Object.keys(props.project.data.targets)) {
- if (expandedSections.includes(targetName)) {
- projectDetailsRef.current?.expandTarget(targetName);
- } else {
- projectDetailsRef.current?.collapseTarget(targetName);
- }
+ const expandedTargetsParams = searchParams.get('expanded')?.split(',');
+ if (expandedTargetsParams && expandedTargetsParams.length > 0) {
+ setExpandTargets(expandedTargetsParams);
}
- }, [searchParams, props.project.data.targets, projectDetailsRef]);
+
+ return () => {
+ collapseAllTargets();
+ searchParams.delete('expanded');
+ setSearchParams(searchParams, { replace: true });
+ };
+ }, []); // only run on mount
+
+ useEffect(() => {
+ if (!project.data.targets) return;
+
+ const expandedTargetsParams =
+ searchParams.get('expanded')?.split(',') || [];
+
+ if (expandedTargetsParams.join(',') === expandTargets.join(',')) {
+ return;
+ }
+
+ setSearchParams(
+ (currentSearchParams) => {
+ updateSearchParams(currentSearchParams, expandTargets);
+ return currentSearchParams;
+ },
+ { replace: true, preventScrollReset: true }
+ );
+ }, [
+ expandTargets,
+ project.data.targets,
+ setExpandTargets,
+ searchParams,
+ setSearchParams,
+ ]);
if (
navigationState === 'loading' &&
@@ -159,10 +159,8 @@ export function ProjectDetailsWrapper(props: ProjectDetailsProps) {
return (
) => {
+ if (state.includes(action.payload)) {
+ return state;
+ }
+ state.push(action.payload);
+ return state;
+ },
+ collapseTarget: (state: string[], action: PayloadAction) => {
+ if (state.includes(action.payload)) {
+ state = state.filter((target) => target !== action.payload);
+ }
+ return state;
+ },
+ toggleExpandTarget: (state: string[], action: PayloadAction) => {
+ if (state.includes(action.payload)) {
+ state = state.filter((target) => target !== action.payload);
+ } else {
+ state.push(action.payload);
+ }
+ return state;
+ },
+ setExpandTargets: (state: string[], action: PayloadAction) => {
+ state = action.payload;
+ return state;
+ },
+ collapseAllTargets: (state: string[]) => {
+ state = [];
+ return state;
+ },
+ },
+});
+
+/*
+ * Export reducer for store configuration.
+ */
+export const expandTargetReducer = expandTargetSlice.reducer;
+
+export const expandTargetActions = expandTargetSlice.actions;
+
+export const getExpandedTargets = <
+ ROOT extends { [EXPAND_TARGETS_KEY]: string[] }
+>(
+ rootState: ROOT
+): string[] => rootState[EXPAND_TARGETS_KEY];
diff --git a/graph/state/src/lib/root/root-state.initial.ts b/graph/state/src/lib/root/root-state.initial.ts
new file mode 100644
index 0000000000..b56a600101
--- /dev/null
+++ b/graph/state/src/lib/root/root-state.initial.ts
@@ -0,0 +1,9 @@
+import {
+ EXPAND_TARGETS_KEY,
+ initialExpandTargets,
+} from '../expand-targets/expand-targets.slice';
+import { RootState } from './root-state.interface';
+
+export const initialRootState: RootState = {
+ [EXPAND_TARGETS_KEY]: initialExpandTargets,
+};
diff --git a/graph/state/src/lib/root/root-state.interface.ts b/graph/state/src/lib/root/root-state.interface.ts
new file mode 100644
index 0000000000..ab5dc706d2
--- /dev/null
+++ b/graph/state/src/lib/root/root-state.interface.ts
@@ -0,0 +1,5 @@
+import { EXPAND_TARGETS_KEY } from '../expand-targets/expand-targets.slice';
+
+export interface RootState {
+ [EXPAND_TARGETS_KEY]: string[];
+}
diff --git a/graph/state/src/lib/root/root.reducer.ts b/graph/state/src/lib/root/root.reducer.ts
new file mode 100644
index 0000000000..48f5700ae9
--- /dev/null
+++ b/graph/state/src/lib/root/root.reducer.ts
@@ -0,0 +1,10 @@
+import { combineReducers } from '@reduxjs/toolkit';
+import {
+ EXPAND_TARGETS_KEY,
+ expandTargetReducer,
+} from '../expand-targets/expand-targets.slice';
+import { RootState } from './root-state.interface';
+
+export const rootReducer = combineReducers({
+ [EXPAND_TARGETS_KEY]: expandTargetReducer,
+});
diff --git a/graph/state/src/lib/root/root.store.ts b/graph/state/src/lib/root/root.store.ts
new file mode 100644
index 0000000000..1362159ede
--- /dev/null
+++ b/graph/state/src/lib/root/root.store.ts
@@ -0,0 +1,19 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { initialRootState } from './root-state.initial';
+import { rootReducer } from './root.reducer';
+
+declare const process: any;
+
+export const rootStore = configureStore({
+ reducer: rootReducer,
+ middleware: (getDefaultMiddleware) => {
+ const defaultMiddleware = getDefaultMiddleware({
+ serializableCheck: false,
+ });
+ return defaultMiddleware;
+ },
+ devTools: process.env.NODE_ENV === 'development',
+ preloadedState: initialRootState,
+});
+
+export type AppDispatch = typeof rootStore.dispatch;
diff --git a/graph/state/src/lib/store.decorator.tsx b/graph/state/src/lib/store.decorator.tsx
new file mode 100644
index 0000000000..4f2e9fb13f
--- /dev/null
+++ b/graph/state/src/lib/store.decorator.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { rootStore } from './root/root.store';
+
+export const StoreDecorator = (story: any) => {
+ return {story()};
+};
diff --git a/graph/state/tsconfig.json b/graph/state/tsconfig.json
new file mode 100644
index 0000000000..95cfeb243d
--- /dev/null
+++ b/graph/state/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowJs": false,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ }
+ ],
+ "extends": "../../tsconfig.base.json"
+}
diff --git a/graph/state/tsconfig.lib.json b/graph/state/tsconfig.lib.json
new file mode 100644
index 0000000000..8c1bec17db
--- /dev/null
+++ b/graph/state/tsconfig.lib.json
@@ -0,0 +1,27 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "exclude": [
+ "jest.config.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.js",
+ "src/**/*.test.js",
+ "src/**/*.spec.jsx",
+ "src/**/*.test.jsx",
+ "**/*.stories.ts",
+ "**/*.stories.js",
+ "**/*.stories.jsx",
+ "**/*.stories.tsx"
+ ],
+ "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
+}
diff --git a/graph/ui-icons/.babelrc b/graph/ui-icons/.babelrc
new file mode 100644
index 0000000000..1ea870ead4
--- /dev/null
+++ b/graph/ui-icons/.babelrc
@@ -0,0 +1,12 @@
+{
+ "presets": [
+ [
+ "@nx/react/babel",
+ {
+ "runtime": "automatic",
+ "useBuiltIns": "usage"
+ }
+ ]
+ ],
+ "plugins": []
+}
diff --git a/graph/ui-icons/.eslintrc.json b/graph/ui-icons/.eslintrc.json
new file mode 100644
index 0000000000..b96a5b888e
--- /dev/null
+++ b/graph/ui-icons/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "storybook-static"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/graph/ui-icons/.storybook/main.ts b/graph/ui-icons/.storybook/main.ts
new file mode 100644
index 0000000000..dfca182122
--- /dev/null
+++ b/graph/ui-icons/.storybook/main.ts
@@ -0,0 +1,21 @@
+/* eslint-disable @nx/enforce-module-boundaries */
+import type { StorybookConfig } from '@storybook/react-vite';
+import { mergeConfig } from 'vite';
+// nx-ignore-next-line
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+
+const config: StorybookConfig = {
+ stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
+ addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+
+ viteFinal: async (config) =>
+ mergeConfig(config, {
+ plugins: [nxViteTsPaths()],
+ }),
+};
+
+export default config;
diff --git a/graph/ui-icons/.storybook/preview.ts b/graph/ui-icons/.storybook/preview.ts
new file mode 100644
index 0000000000..195b052493
--- /dev/null
+++ b/graph/ui-icons/.storybook/preview.ts
@@ -0,0 +1 @@
+import './tailwind.css';
diff --git a/graph/ui-icons/.storybook/tailwind.css b/graph/ui-icons/.storybook/tailwind.css
new file mode 100644
index 0000000000..23d597fe51
--- /dev/null
+++ b/graph/ui-icons/.storybook/tailwind.css
@@ -0,0 +1,3 @@
+@tailwind components;
+@tailwind base;
+@tailwind utilities;
diff --git a/graph/ui-icons/README.md b/graph/ui-icons/README.md
new file mode 100644
index 0000000000..fda4b05bf5
--- /dev/null
+++ b/graph/ui-icons/README.md
@@ -0,0 +1,7 @@
+# ui-icons
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test ui-icons` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/graph/ui-icons/postcss.config.js b/graph/ui-icons/postcss.config.js
new file mode 100644
index 0000000000..8b2e63b9e6
--- /dev/null
+++ b/graph/ui-icons/postcss.config.js
@@ -0,0 +1,10 @@
+const path = require('path');
+
+module.exports = {
+ plugins: {
+ tailwindcss: {
+ config: path.join(__dirname, 'tailwind.config.js'),
+ },
+ autoprefixer: {},
+ },
+};
diff --git a/graph/ui-icons/project.json b/graph/ui-icons/project.json
new file mode 100644
index 0000000000..a126077853
--- /dev/null
+++ b/graph/ui-icons/project.json
@@ -0,0 +1,54 @@
+{
+ "name": "graph-ui-icons",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "graph/ui-icons/src",
+ "projectType": "library",
+ "tags": [],
+ "// targets": "to see all targets run: nx show project ui-icons --web",
+ "targets": {
+ "lint": {},
+ "storybook": {
+ "executor": "@nx/storybook:storybook",
+ "options": {
+ "port": 4400,
+ "configDir": "graph/ui-icons/.storybook"
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true
+ }
+ }
+ },
+ "build-storybook": {
+ "executor": "@nx/storybook:build",
+ "outputs": ["{options.outputDir}"],
+ "options": {
+ "outputDir": "dist/storybook/graph-ui-icons",
+ "configDir": "graph/ui-icons/.storybook"
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true
+ }
+ }
+ },
+ "test-storybook": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "test-storybook -c graph/ui-icons/.storybook --url=http://localhost:4400"
+ }
+ },
+ "static-storybook": {
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "graph-ui-icons:build-storybook",
+ "staticFilePath": "dist/storybook/graph-ui-icons"
+ },
+ "configurations": {
+ "ci": {
+ "buildTarget": "graph-ui-icons:build-storybook:ci"
+ }
+ }
+ }
+ }
+}
diff --git a/graph/ui-icons/src/index.ts b/graph/ui-icons/src/index.ts
new file mode 100644
index 0000000000..829dd6931b
--- /dev/null
+++ b/graph/ui-icons/src/index.ts
@@ -0,0 +1,2 @@
+export * from './lib/technology-icon';
+export * from './lib/framework-icons';
diff --git a/graph/ui-icons/src/lib/framework-icons.stories.tsx b/graph/ui-icons/src/lib/framework-icons.stories.tsx
new file mode 100644
index 0000000000..e57eaf595c
--- /dev/null
+++ b/graph/ui-icons/src/lib/framework-icons.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Framework, frameworkIcons } from './framework-icons';
+
+const meta: Meta = {
+ component: () => (
+ <>
+ {Object.keys(frameworkIcons).map((key) => (
+ <>
+ {key}
+
+ {frameworkIcons[key as Framework].image}
+
+ >
+ ))}
+ >
+ ),
+ title: 'frameworkIcons',
+};
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/nx-dev/ui-markdoc/src/lib/icons.tsx b/graph/ui-icons/src/lib/framework-icons.tsx
similarity index 98%
rename from nx-dev/ui-markdoc/src/lib/icons.tsx
rename to graph/ui-icons/src/lib/framework-icons.tsx
index f38a843658..697c66f0dc 100644
--- a/nx-dev/ui-markdoc/src/lib/icons.tsx
+++ b/graph/ui-icons/src/lib/framework-icons.tsx
@@ -1,12 +1,74 @@
+export type Framework =
+ | 'reactMono'
+ | 'tsMono'
+ | 'jsMono'
+ | 'nodeMono'
+ | 'angularMono'
+ | 'typescript'
+ | 'javascript'
+ | 'node'
+ | 'angular'
+ | 'youtube'
+ | 'nxagents'
+ | 'nxcloud'
+ | 'nx'
+ | 'nextjs'
+ | 'nestjs'
+ | 'rspack'
+ | 'express'
+ | 'jest'
+ | 'fastify'
+ | 'storybook'
+ | 'solid'
+ | 'lit'
+ | 'vite'
+ | 'trpc'
+ | 'remix'
+ | 'dotnet'
+ | 'qwik'
+ | 'gradle'
+ | 'go'
+ | 'vue'
+ | 'rust'
+ | 'nuxt'
+ | 'svelte'
+ | 'gatsby'
+ | 'astro'
+ | 'playwright'
+ | 'pnpm'
+ | 'monorepo'
+ | 'cra'
+ | 'cypress'
+ | 'expo'
+ | 'react'
+ | 'azure'
+ | 'bitbucket'
+ | 'circleci'
+ | 'github'
+ | 'gitlab'
+ | 'jenkins'
+ | 'apollo'
+ | 'prisma'
+ | 'redis'
+ | 'postgres'
+ | 'planetscale'
+ | 'mongodb'
+ | 'mfe'
+ | 'eslint';
+
export const frameworkIcons: Record<
- string,
+ Framework,
{
image: JSX.Element;
+ // this key determines whether the icon should be adaptive or not
+ // if true means icon is mostly monotone (black/white), its parent needs to specify the fill color
+ isAdaptiveIcon?: boolean;
}
> = {
reactMono: {
image: (
),
+ isAdaptiveIcon: true,
},
nestjs: {
image: (
),
+ isAdaptiveIcon: true,
},
express: {
image: (
),
+ isAdaptiveIcon: true,
},
storybook: {
image: (
),
+ isAdaptiveIcon: true,
},
nuxt: {
image: (
),
+ isAdaptiveIcon: true,
},
expo: {
image: (
),
+ isAdaptiveIcon: true,
},
react: {
image: (