Compare commits

...

1 Commits

Author SHA1 Message Date
“JamesHenry” fb42a0dfac chore: use @nx/graph for graph implementation details 2024-07-27 22:56:21 +04:00
286 changed files with 67 additions and 13484 deletions
@@ -106,6 +106,8 @@ const mockAppConfig: AppConfig = {
label: 'local',
projectGraphUrl: 'assets/project-graphs/e2e.json',
taskGraphUrl: 'assets/task-graphs/e2e.json',
taskInputsUrl: '',
sourceMapsUrl: '',
},
],
defaultWorkspaceId: 'local',
@@ -1,9 +1,8 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
// nx-ignore-next-line
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
/* eslint-enable @nx/enforce-module-boundaries */
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
import { useFloating } from '@floating-ui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useEffect, useState } from 'react';
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { AppConfig, ExternalApi } from '@nx/graph/shared';
import type { AppConfig, ExternalApi } from '@nx/graph/shared';
export declare global {
interface Window {
+12 -1
View File
@@ -1,5 +1,10 @@
const path = require('path');
// @ts-check
const path = require('node:path');
// Ignore these nx related dependencies since they are the installed versions not the ones in the workspace
// nx-ignore-next-line
const { workspaceRoot } = require('@nx/devkit');
// nx-ignore-next-line
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
@@ -7,6 +12,12 @@ module.exports = {
content: [
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
...createGlobPatternsForDependencies(__dirname),
// Resolve the classes used in @nx/graph components
// TODO: make a decision on whether this is really the best approach, or if precompiling and deduplicating the classes would be better
path.join(
workspaceRoot,
'node_modules/@nx/graph/**/*.{js,ts,jsx,tsx,html}'
),
],
darkMode: 'class', // or 'media' or 'class'
theme: {
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-7
View File
@@ -1,7 +0,0 @@
# project-details
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test project-details` to execute the unit tests via [Jest](https://jestjs.io).
-11
View File
@@ -1,11 +0,0 @@
/* eslint-disable */
export default {
testEnvironment: 'jsdom',
displayName: 'graph-project-details',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/project-details',
};
-8
View File
@@ -1,8 +0,0 @@
{
"name": "graph-project-details",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/project-details/src",
"projectType": "library",
"tags": [],
"targets": {}
}
-3
View File
@@ -1,3 +0,0 @@
export * from './lib/project-details-wrapper';
export * from './lib/project-details-page';
export * from './lib/project-details-header';
@@ -1,25 +0,0 @@
import { Link } from 'react-router-dom';
import { useRouteConstructor } from '@nx/graph/shared';
import { ThemePanel } from '@nx/graph/ui-theme';
export function ProjectDetailsHeader() {
const routeConstructor = useRouteConstructor();
return (
<header className="mx-auto mb-8 flex w-full items-center justify-center border-b-2 border-slate-900/10 py-2 dark:border-slate-300/10">
<div className="flex max-w-6xl flex-grow items-center justify-between px-8 ">
<Link to={routeConstructor('/projects', false)}>
<svg
className="h-10 w-auto text-slate-900 dark:text-white"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>Nx</title>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
</Link>
<ThemePanel />
</div>
</header>
);
}
@@ -1,73 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphProjectNode } from '@nx/devkit';
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
import {
ScrollRestoration,
useParams,
useRouteLoaderData,
} from 'react-router-dom';
import { ProjectDetailsWrapper } from './project-details-wrapper';
import {
ExpandedTargetsProvider,
fetchProjectGraph,
getProjectGraphDataService,
useEnvironmentConfig,
usePoll,
} from '@nx/graph/shared';
import { ProjectDetailsHeader } from './project-details-header';
export function ProjectDetailsPage() {
const { project, sourceMap, hash, errors, connectedToCloud } =
useRouteLoaderData('selectedProjectDetails') as {
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
};
const { environment, watch, appConfig } = useEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();
const params = useParams();
usePoll(
async () => {
const data = await fetchProjectGraph(
projectGraphDataService,
params,
appConfig
);
if (data?.hash !== hash) {
window.location.reload();
}
},
1000,
watch
);
return (
<ExpandedTargetsProvider>
<div className="flex w-full flex-col justify-center text-slate-700 dark:text-slate-400">
<ScrollRestoration />
{environment !== 'nx-console' ? (
<ProjectDetailsHeader />
) : (
<div className="py-2"></div>
)}
<div className="mx-auto mb-8 w-full max-w-6xl flex-grow px-8">
<ProjectDetailsWrapper
project={project}
sourceMap={sourceMap}
errors={errors}
connectedToCloud={connectedToCloud}
></ProjectDetailsWrapper>
</div>
</div>
</ExpandedTargetsProvider>
);
}
@@ -1,181 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
import {
ErrorToast,
ExpandedTargetsContext,
getExternalApiService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph/shared';
import { Spinner } from '@nx/graph/ui-components';
import { ProjectDetails } from '@nx/graph/ui-project-details';
import { useCallback, useContext, useEffect } from 'react';
interface ProjectDetailsProps {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
}
export function ProjectDetailsWrapper({
project,
sourceMap,
errors,
connectedToCloud,
}: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService();
const navigate = useNavigate();
const { state: navigationState, location } = useNavigation();
const routeConstructor = useRouteConstructor();
const [searchParams, setSearchParams] = useSearchParams();
const { expandedTargets, setExpandedTargets, collapseAllTargets } =
useContext(ExpandedTargetsContext);
const handleViewInProjectGraph = useCallback(
(data: { projectName: string }) => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-project-graph',
payload: {
projectName: data.projectName,
},
});
} else {
navigate(
routeConstructor(
`/projects/${encodeURIComponent(data.projectName)}`,
true,
['expanded'] // omit expanded targets from search params
)
);
}
},
[externalApiService, routeConstructor, navigate, environment]
);
const handleViewInTaskGraph = useCallback(
(data: { projectName: string; targetName: string }) => {
if (environment === 'nx-console') {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: data.projectName,
targetName: data.targetName,
},
});
} else {
navigate(
routeConstructor(
{
pathname: `/tasks/${encodeURIComponent(data.targetName)}`,
search: `?projects=${encodeURIComponent(data.projectName)}`,
},
true,
['expanded'] // omit expanded targets from search params
)
);
}
},
[externalApiService, routeConstructor, navigate, environment]
);
const handleRunTarget = useCallback(
(data: { projectName: string; targetName: string }) => {
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${data.projectName}:${data.targetName}` },
});
},
[externalApiService]
);
const handleNxConnect = useCallback(
() =>
externalApiService.postEvent({
type: 'nx-connect',
}),
[externalApiService]
);
const updateSearchParams = (
params: URLSearchParams,
targetNames?: string[]
) => {
if (!targetNames || targetNames.length === 0) {
params.delete('expanded');
} else {
params.set('expanded', targetNames.join(','));
}
};
useEffect(() => {
const expandedTargetsParams = searchParams.get('expanded')?.split(',');
if (
expandedTargetsParams &&
expandedTargetsParams.length > 0 &&
setExpandedTargets
) {
setExpandedTargets(expandedTargetsParams);
}
return () => {
if (collapseAllTargets) {
collapseAllTargets();
}
};
}, []); // only run on mount
useEffect(() => {
const expandedTargetsParams =
searchParams.get('expanded')?.split(',') || [];
if (expandedTargetsParams.join(',') === expandedTargets?.join(',')) {
return;
}
setSearchParams(
(currentSearchParams) => {
updateSearchParams(currentSearchParams, expandedTargets);
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [expandedTargets, searchParams, setSearchParams]);
if (
navigationState === 'loading' &&
!location.pathname.includes('project-details') // do not show spinner when updating search params
) {
return (
<div className="flex justify-center">
<Spinner />
</div>
);
}
return (
<>
<ProjectDetails
project={project}
sourceMap={sourceMap}
onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
viewInProjectGraphPosition={
environment === 'nx-console' ? 'bottom' : 'top'
}
connectedToCloud={connectedToCloud}
onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined}
/>
<ErrorToast errors={errors} />
</>
);
}
-20
View File
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
-24
View File
@@ -1,24 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
],
"lib": ["DOM"]
},
"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"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-7
View File
@@ -1,7 +0,0 @@
# graph-shared
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test graph-shared` to execute the unit tests via [Jest](https://jestjs.io).
-9
View File
@@ -1,9 +0,0 @@
/* eslint-disable */
export default {
displayName: 'graph-shared',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
};
-8
View File
@@ -1,8 +0,0 @@
{
"name": "graph-shared",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/shared/src",
"projectType": "library",
"tags": [],
"targets": {}
}
-25
View File
@@ -1,25 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ExpandedTaskInputsReponse,
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { AppConfig } from './lib/app-config';
import { ExternalApi } from './lib/external-api';
export declare global {
interface Window {
exclude: string[];
watch: boolean;
localMode: 'serve' | 'build';
projectGraphResponse?: ProjectGraphClientResponse;
taskGraphResponse?: TaskGraphClientResponse;
expandedTaskInputsResponse?: ExpandedTaskInputsReponse;
sourceMapsResponse?: Record<string, Record<string, string[]>>;
environment: 'dev' | 'watch' | 'release' | 'nx-console';
appConfig: AppConfig;
useXstateInspect: boolean;
externalApi?: ExternalApi;
}
}
-10
View File
@@ -1,10 +0,0 @@
export * from './lib/external-api';
export * from './lib/external-api-service';
export * from './lib/use-environment-config';
export * from './lib/app-config';
export * from './lib/use-route-constructor';
export * from './lib/use-poll';
export * from './lib/project-graph-data-service/get-project-graph-data-service';
export * from './lib/fetch-project-graph';
export * from './lib/error-toast';
export * from './lib/expanded-targets-provider';
-15
View File
@@ -1,15 +0,0 @@
export interface AppConfig {
showDebugger: boolean;
showExperimentalFeatures: boolean;
workspaces: WorkspaceData[];
defaultWorkspaceId: string;
}
export interface WorkspaceData {
id: string;
label: string;
projectGraphUrl: string;
taskGraphUrl: string;
taskInputsUrl: string;
sourceMapsUrl: string;
}
-132
View File
@@ -1,132 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
import {
createRef,
ForwardedRef,
forwardRef,
useCallback,
useImperativeHandle,
useLayoutEffect,
} from 'react';
import { Transition } from '@headlessui/react';
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
import { SetURLSearchParams, useSearchParams } from 'react-router-dom';
import { ErrorRenderer, Modal, ModalHandle } from '@nx/graph/ui-components';
export interface ErrorToastImperativeHandle {
closeModal: () => void;
openModal: () => void;
}
interface ErrorToastProps {
errors?: GraphError[] | undefined;
}
export const ErrorToast = forwardRef(
(
{ errors }: ErrorToastProps,
ref: ForwardedRef<ErrorToastImperativeHandle>
) => {
const inputsModalRef = createRef<ModalHandle>();
const [searchParams, setSearchParams] = useSearchParams();
useImperativeHandle(ref, () => ({
openModal: () => {
inputsModalRef?.current?.openModal();
},
closeModal: () => {
inputsModalRef?.current?.closeModal();
},
}));
const handleModalOpen = useCallback(() => {
if (searchParams.get('show-error') === 'true') return;
setSearchParams(
(currentSearchParams) => {
currentSearchParams.set('show-error', 'true');
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [setSearchParams, searchParams]);
const handleModalClose = useCallback(() => {
if (!searchParams.get('show-error')) return;
setSearchParams(
(currentSearchParams) => {
currentSearchParams.delete('show-error');
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [setSearchParams, searchParams]);
useLayoutEffect(() => {
if (searchParams.get('show-error') === 'true') {
if (errors && errors.length > 0) {
inputsModalRef.current?.openModal();
} else {
setSearchParams(
(currentSearchParams) => {
currentSearchParams.delete('show-error');
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}
}
}, [searchParams, inputsModalRef, errors, setSearchParams]);
return (
<Transition
show={!!errors && errors.length > 0}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-x-0 bottom-0 px-4 py-2 text-center">
<div
onClick={() => inputsModalRef.current?.openModal()}
className="z-50 mx-auto flex w-fit max-w-[75%] cursor-pointer items-center rounded-md bg-red-600 p-4 text-slate-200 shadow-lg"
>
<ExclamationCircleIcon className="mr-2 inline-block h-6 w-6" />
Some project information might be missing. Click to see errors.
</div>
{errors?.length > 0 && (
<Modal
title={`Project Graph Errors`}
ref={inputsModalRef}
onOpen={handleModalOpen}
onClose={handleModalClose}
>
<ErrorRenderer errors={errors ?? []} />
</Modal>
)}
</div>
</Transition>
);
}
);
export const useRouterHandleModalOpen = (
searchParams: URLSearchParams,
setSearchParams: SetURLSearchParams
) =>
useCallback(() => {
if (searchParams.get('show-error') === 'true') return;
setSearchParams(
(currentSearchParams) => {
currentSearchParams.set('show-error', 'true');
return currentSearchParams;
},
{ replace: true, preventScrollReset: true }
);
}, [setSearchParams, searchParams]);
@@ -1,38 +0,0 @@
import { createContext, useState } from 'react';
export const ExpandedTargetsContext = createContext<{
expandedTargets?: string[];
setExpandedTargets?: (expandedTargets: string[]) => void;
toggleTarget?: (targetName: string) => void;
collapseAllTargets?: () => void;
}>({});
export const ExpandedTargetsProvider = ({ children }) => {
const [expandedTargets, setExpandedTargets] = useState<string[]>([]);
const toggleTarget = (targetName: string) => {
setExpandedTargets((prevExpandedTargets) => {
if (prevExpandedTargets.includes(targetName)) {
return prevExpandedTargets.filter((name) => name !== targetName);
}
return [...prevExpandedTargets, targetName];
});
};
const collapseAllTargets = () => {
setExpandedTargets([]);
};
return (
<ExpandedTargetsContext.Provider
value={{
expandedTargets,
setExpandedTargets,
toggleTarget,
collapseAllTargets,
}}
>
{children}
</ExpandedTargetsContext.Provider>
);
};
@@ -1,24 +0,0 @@
let externalApiService: ExternalApiService | null = null;
export function getExternalApiService() {
if (!externalApiService) {
externalApiService = new ExternalApiService();
}
return externalApiService;
}
export class ExternalApiService {
private subscribers: Set<(event: { type: string; payload?: any }) => void> =
new Set();
postEvent(event: { type: string; payload?: any }) {
this.subscribers.forEach((subscriber) => {
subscriber(event);
});
}
subscribe(callback: (event: { type: string; payload: any }) => void) {
this.subscribers.add(callback);
}
}
-42
View File
@@ -1,42 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
export abstract class ExternalApi {
abstract openProjectDetails(projectName: string, targetName?: string): void;
abstract focusProject(projectName: string): void;
abstract toggleSelectProject(projectName: string): void;
abstract selectAllProjects(): void;
abstract showAffectedProjects(): void;
abstract focusTarget(projectName: string, targetName: string): void;
abstract selectAllTargetsByName(targetName: string): void;
abstract enableExperimentalFeatures(): void;
abstract disableExperimentalFeatures(): void;
loadProjectGraph:
| ((url: string) => Promise<ProjectGraphClientResponse>)
| null = null;
loadTaskGraph: ((url: string) => Promise<TaskGraphClientResponse>) | null =
null;
loadExpandedTaskInputs:
| ((taskId: string) => Promise<Record<string, Record<string, string[]>>>)
| null = null;
loadSourceMaps:
| ((url: string) => Promise<Record<string, Record<string, string[]>>>)
| null = null;
graphInteractionEventListener:
| ((event: { type: string; payload: any }) => void | undefined)
| null = null;
}
@@ -1,18 +0,0 @@
import { Params } from 'react-router-dom';
import { ProjectGraphService } from './project-graph-data-service/get-project-graph-data-service';
import { AppConfig } from './app-config';
export async function fetchProjectGraph(
projectGraphService: ProjectGraphService,
params: Readonly<Params<string>>,
appConfig: AppConfig
) {
const selectedWorkspaceId =
params.selectedWorkspaceId ?? appConfig.defaultWorkspaceId;
const projectInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
return await projectGraphService.getProjectGraph(projectInfo.projectGraphUrl);
}
@@ -1,64 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { ProjectGraphService } from './get-project-graph-data-service';
/* eslint-enable @nx/enforce-module-boundaries */
export class FetchProjectGraphService implements ProjectGraphService {
private taskInputsUrl: string;
async getHash(): Promise<string> {
const request = new Request('currentHash', { mode: 'no-cors' });
const response = await fetch(request);
return response.json();
}
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
const request = new Request(url, { mode: 'no-cors' });
const response = await fetch(request);
return response.json();
}
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
const request = new Request(url, { mode: 'no-cors' });
const response = await fetch(request);
return response.json();
}
async getSourceMaps(
url: string
): Promise<Record<string, Record<string, string[]>>> {
const request = new Request(url, { mode: 'no-cors' });
const response = await fetch(request);
return response.json();
}
setTaskInputsUrl(url: string) {
this.taskInputsUrl = url;
}
async getExpandedTaskInputs(
taskId: string
): Promise<Record<string, string[]>> {
if (!this.taskInputsUrl) {
return {};
}
const request = new Request(`${this.taskInputsUrl}?taskId=${taskId}`, {
mode: 'no-cors',
});
const response = await fetch(request);
return (await response.json())[taskId];
}
}
@@ -1,44 +0,0 @@
import { FetchProjectGraphService } from './fetch-project-graph-service';
import { LocalProjectGraphService } from './local-project-graph-service';
import { MockProjectGraphService } from './mock-project-graph-service';
import { NxConsoleProjectGraphService } from './nx-console-project-graph-service';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
let projectGraphService: ProjectGraphService;
export interface ProjectGraphService {
getHash: () => Promise<string>;
getProjectGraph: (url: string) => Promise<ProjectGraphClientResponse>;
getTaskGraph: (url: string) => Promise<TaskGraphClientResponse>;
setTaskInputsUrl?: (url: string) => void;
getExpandedTaskInputs?: (taskId: string) => Promise<Record<string, string[]>>;
getSourceMaps?: (
url: string
) => Promise<Record<string, Record<string, string[]>>>;
}
export function getProjectGraphDataService() {
if (projectGraphService === undefined) {
if (window.environment === 'dev') {
projectGraphService = new FetchProjectGraphService();
} else if (window.environment === 'watch') {
projectGraphService = new MockProjectGraphService();
} else if (window.environment === 'nx-console') {
projectGraphService = new NxConsoleProjectGraphService();
} else if (window.environment === 'release') {
if (window.localMode === 'build') {
projectGraphService = new LocalProjectGraphService();
} else {
projectGraphService = new FetchProjectGraphService();
}
}
}
return projectGraphService;
}
@@ -1,36 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { ProjectGraphService } from './get-project-graph-data-service';
/* eslint-enable @nx/enforce-module-boundaries */
export class LocalProjectGraphService implements ProjectGraphService {
async getHash(): Promise<string> {
return new Promise((resolve) => resolve('some-hash'));
}
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
return new Promise((resolve) => resolve(window.projectGraphResponse));
}
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
return new Promise((resolve) => resolve(window.taskGraphResponse));
}
async getExpandedTaskInputs(
taskId: string
): Promise<Record<string, string[]>> {
return new Promise((resolve) =>
resolve(window.expandedTaskInputsResponse[taskId])
);
}
async getSourceMaps(
url: string
): Promise<Record<string, Record<string, string[]>>> {
return new Promise((resolve) => resolve(window.sourceMapsResponse));
}
}
@@ -1,133 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphDependency,
ProjectGraphProjectNode,
} from '@nx/devkit';
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { ProjectGraphService } from './get-project-graph-data-service';
/* eslint-enable @nx/enforce-module-boundaries */
export class MockProjectGraphService implements ProjectGraphService {
private projectGraphsResponse: ProjectGraphClientResponse = {
hash: '79054025255fb1a26e4bc422aef54eb4',
layout: {
appsDir: 'apps',
libsDir: 'libs',
},
projects: [
{
name: 'existing-app-1',
type: 'app',
data: {
root: 'apps/app1',
tags: [],
},
},
{
name: 'existing-lib-1',
type: 'lib',
data: {
root: 'libs/lib1',
tags: [],
},
},
],
dependencies: {
'existing-app-1': [
{
source: 'existing-app-1',
target: 'existing-lib-1',
type: 'static',
},
],
'existing-lib-1': [],
},
fileMap: {
'existing-app-1': [
{
file: 'some/file.ts',
hash: 'ecccd8481d2e5eae0e59928be1bc4c2d071729d7',
deps: ['existing-lib-1'],
},
],
'exiting-lib-1': [],
},
affected: [],
focus: null,
exclude: [],
groupByFolder: false,
isPartial: false,
};
private taskGraphsResponse: TaskGraphClientResponse = {
taskGraphs: {},
errors: {},
};
constructor(updateFrequency: number = 5000) {
setInterval(() => this.updateResponse(), updateFrequency);
}
async getHash(): Promise<string> {
return new Promise((resolve) => resolve(this.projectGraphsResponse.hash));
}
getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
return new Promise((resolve) => resolve(this.projectGraphsResponse));
}
getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
return new Promise((resolve) => resolve(this.taskGraphsResponse));
}
getSourceMaps(
url: string
): Promise<Record<string, Record<string, string[]>>> {
return new Promise((resolve) => resolve({}));
}
private createNewProject(): ProjectGraphProjectNode {
const type = Math.random() > 0.25 ? 'lib' : 'app';
const name = `${type}-${this.projectGraphsResponse.projects.length + 1}`;
return {
name,
type,
data: {
root: type === 'app' ? `apps/${name}` : `libs/${name}`,
tags: [],
},
};
}
private updateResponse() {
const newProject = this.createNewProject();
const libProjects = this.projectGraphsResponse.projects.filter(
(project) => project.type === 'lib'
);
const targetDependency =
libProjects[Math.floor(Math.random() * libProjects.length)];
const newDependency: ProjectGraphDependency[] = [
{
source: newProject.name,
target: targetDependency.name,
type: 'static',
},
];
this.projectGraphsResponse = {
...this.projectGraphsResponse,
projects: [...this.projectGraphsResponse.projects, newProject],
dependencies: {
...this.projectGraphsResponse.dependencies,
[newProject.name]: newDependency,
},
};
}
}
@@ -1,34 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import { ProjectGraphService } from './get-project-graph-data-service';
export class NxConsoleProjectGraphService implements ProjectGraphService {
async getHash(): Promise<string> {
return new Promise((resolve) => resolve('some-hash'));
}
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
return await window.externalApi.loadProjectGraph?.(url);
}
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
return await window.externalApi.loadTaskGraph?.(url);
}
async getExpandedTaskInputs(
taskId: string
): Promise<Record<string, string[]>> {
const res = await window.externalApi.loadExpandedTaskInputs?.(taskId);
return res ? res[taskId] : {};
}
async getSourceMaps(
url: string
): Promise<Record<string, Record<string, string[]>>> {
return await window.externalApi.loadSourceMaps?.(url);
}
}
@@ -1,39 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
import { useRef } from 'react';
import { AppConfig } from './app-config';
export function useEnvironmentConfig(): {
exclude: string[];
watch: boolean;
localMode: 'serve' | 'build';
projectGraphResponse?: ProjectGraphClientResponse;
environment: 'dev' | 'watch' | 'release' | 'nx-console' | 'docs';
appConfig: AppConfig;
useXstateInspect: boolean;
} {
const environmentConfig = useRef(getEnvironmentConfig());
return environmentConfig.current;
}
export function getEnvironmentConfig() {
return {
exclude: window.exclude,
watch: window.watch,
localMode: window.localMode,
projectGraphResponse: window.projectGraphResponse,
// If this was not built into JS or HTML, then it is rendered on docs (nx.dev).
environment: window.environment ?? ('docs' as const),
appConfig: {
...window.appConfig,
showExperimentalFeatures:
localStorage.getItem('showExperimentalFeatures') === 'true'
? true
: window.appConfig?.showExperimentalFeatures,
},
useXstateInspect: window.useXstateInspect,
};
}
-37
View File
@@ -1,37 +0,0 @@
import { useEffect, useRef } from 'react';
export const usePoll = (
callback: () => Promise<void>,
delay: number,
condition: boolean
) => {
const savedCallback = useRef(() => Promise.resolve());
useEffect(() => {
if (condition) {
savedCallback.current = callback;
}
}, [callback, condition]);
useEffect(() => {
if (!condition) {
return;
}
let timeoutId: NodeJS.Timeout;
async function callTickAfterDelay() {
await savedCallback.current();
if (delay !== null) {
timeoutId = setTimeout(callTickAfterDelay, delay);
}
}
callTickAfterDelay();
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [delay, condition]);
};
@@ -1,52 +0,0 @@
import { To, useParams, useSearchParams } from 'react-router-dom';
import { getEnvironmentConfig } from './use-environment-config';
export const useRouteConstructor = (): ((
to: To,
retainSearchParams: boolean,
searchParamsKeysToOmit?: string[]
) => To) => {
const { environment } = getEnvironmentConfig();
const { selectedWorkspaceId } = useParams();
const [searchParams] = useSearchParams();
return (
to: To,
retainSearchParams: boolean = true,
searchParamsKeysToOmit: string[] = []
) => {
if (searchParamsKeysToOmit?.length) {
searchParamsKeysToOmit.forEach((key) => {
searchParams.delete(key);
});
}
let pathname = '';
if (typeof to === 'object') {
if (environment === 'dev') {
pathname = `/${selectedWorkspaceId}${to.pathname}`;
} else {
pathname = to.pathname;
}
return {
...to,
pathname,
search: to.search
? to.search.toString()
: retainSearchParams
? searchParams.toString()
: '',
};
} else if (typeof to === 'string') {
if (environment === 'dev') {
pathname = `/${selectedWorkspaceId}${to}`;
} else {
pathname = to;
}
return {
pathname,
search: retainSearchParams ? searchParams.toString() : '',
};
}
};
};
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
-24
View File
@@ -1,24 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
],
"lib": ["dom"]
},
"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"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-21
View File
@@ -1,21 +0,0 @@
/* 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;
@@ -1 +0,0 @@
import './tailwind.css';
@@ -1,3 +0,0 @@
@tailwind components;
@tailwind base;
@tailwind utilities;
-7
View File
@@ -1,7 +0,0 @@
# graph-ui-code-block
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test graph-ui-code-block` to execute the unit tests via [Jest](https://jestjs.io).
-11
View File
@@ -1,11 +0,0 @@
/* eslint-disable */
export default {
testEnvironment: 'jsdom',
displayName: 'graph-ui-code-block',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/ui-graph',
};
-9
View File
@@ -1,9 +0,0 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};
-52
View File
@@ -1,52 +0,0 @@
{
"name": "graph-ui-code-block",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/ui-code-block/src",
"projectType": "library",
"tags": [],
"targets": {
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {
"port": 4400,
"configDir": "graph/ui-code-block/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nx/storybook:build",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/graph-ui-code-block",
"configDir": "graph/ui-code-block/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"test-storybook": {
"executor": "nx:run-commands",
"options": {
"command": "test-storybook -c graph/ui-code-block/.storybook --url=http://localhost:4400"
}
},
"static-storybook": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "graph-ui-code-block:build-storybook",
"staticFilePath": "dist/storybook/graph-ui-code-block"
},
"configurations": {
"ci": {
"buildTarget": "graph-ui-code-block:build-storybook:ci"
}
}
}
}
}
-1
View File
@@ -1 +0,0 @@
export { JsonCodeBlock } from './lib/json-code-block';
@@ -1,20 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { JsonCodeBlock } from './json-code-block';
const meta: Meta<typeof JsonCodeBlock> = {
component: JsonCodeBlock,
title: 'JsonCodeBlock',
};
export default meta;
type Story = StoryObj<typeof JsonCodeBlock>;
export const Simple: Story = {
args: {
data: {
commands: [{ command: 'remix build' }],
cwd: 'apps/demo',
},
renderSource: (propertyName: string) => <span>test</span>,
},
};
@@ -1,94 +0,0 @@
// @ts-ignore
import SyntaxHighlighter, { createElement } from 'react-syntax-highlighter';
import { JSX, ReactNode, useMemo } from 'react';
import { twMerge } from 'tailwind-merge';
import { CopyToClipboardButton } from '@nx/graph/ui-components';
export function JsonCodeBlockPreTag({
children,
}: {
children: ReactNode;
}): JSX.Element {
return (
<div
className={twMerge(
'hljs not-prose w-full overflow-hidden rounded-md',
'font-mono text-sm',
'border border-slate-200 bg-slate-50/50 dark:border-slate-700 dark:bg-slate-800/60'
)}
>
<div className="p-4">{children}</div>
</div>
);
}
export interface JsonCodeBlockProps {
data: any;
renderSource: (propertyName: string) => ReactNode;
copyTooltipText: string;
}
export function JsonCodeBlock(props: JsonCodeBlockProps): JSX.Element {
const jsonString = useMemo(
() => JSON.stringify(props.data, null, 2),
[props.data]
);
return (
<div className="code-block group relative w-full">
<div className="absolute right-0 top-0 z-10 flex">
<CopyToClipboardButton
text={jsonString}
tooltipAlignment="right"
tooltipText={props.copyTooltipText}
className={twMerge(
'not-prose flex',
'border border-slate-200 bg-slate-50/50 p-2 dark:border-slate-700 dark:bg-slate-800/60',
'opacity-0 transition-opacity group-hover:opacity-100'
)}
/>
</div>
<SyntaxHighlighter
language="json"
children={jsonString}
PreTag={JsonCodeBlockPreTag}
renderer={sourcesRenderer(props.renderSource)}
/>
</div>
);
}
export function sourcesRenderer(
renderSource: (propertyName: string) => ReactNode
) {
return ({ rows, stylesheet }: any) => {
return rows.map((node: any, idx: number) => {
const element = createElement({
node,
stylesheet,
useInlineStyles: false,
key: `code-line-${idx}`,
});
let sourceElement: ReactNode;
const attrNode = node.children.find(
(c: any) =>
c.type === 'element' && c.properties?.className?.includes('hljs-attr')
);
if (attrNode?.children?.length) {
for (const child of attrNode.children) {
sourceElement = renderSource(child.value); // e.g. command
if (sourceElement) break;
}
}
return (
<span className="group/line flex" key={`code-group${idx}`}>
<span>{element}</span>
{sourceElement && (
<span className="min-w-0 flex-1 pl-2 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100">
{sourceElement}
</span>
)}
</span>
);
});
};
}
-45
View File
@@ -1,45 +0,0 @@
const path = require('path');
// nx-ignore-next-line
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
module.exports = {
content: [
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: 'class', // or 'media' or 'class'
theme: {
extend: {
typography: {
DEFAULT: {
css: {
'code::before': {
content: '',
},
'code::after': {
content: '',
},
'blockquote p:first-of-type::before': {
content: '',
},
'blockquote p:last-of-type::after': {
content: '',
},
},
},
},
},
},
variants: {
extend: {
translate: ['group-hover'],
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')({
strategy: 'class',
}),
],
};
-23
View File
@@ -1,23 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}
],
"extends": "../../tsconfig.base.json"
}
-27
View File
@@ -1,27 +0,0 @@
{
"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"]
}
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
@@ -1,31 +0,0 @@
{
"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"
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
stories: ['../src/lib/**/*.stories.@(mdx|js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials', '@nx/react/plugins/storybook'],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: true,
},
};
@@ -1,3 +0,0 @@
import 'graph/client/.storybook/tailwind-imports.css';
export const parameters = {};
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-7
View File
@@ -1,7 +0,0 @@
# graph-ui-components
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test graph-ui-components` to execute the unit tests via [Jest](https://jestjs.io).
-10
View File
@@ -1,10 +0,0 @@
/* eslint-disable */
export default {
displayName: 'graph-ui-components',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/ui-graph',
};
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {
config: './graph/ui-components/tailwind.config.js',
},
autoprefixer: {},
},
};
-34
View File
@@ -1,34 +0,0 @@
{
"name": "graph-ui-components",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/ui-components/src",
"projectType": "library",
"tags": [],
"targets": {
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {
"port": 4400,
"configDir": "graph/ui-components/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nx/storybook:build",
"outputs": ["{options.outputDir}"],
"options": {
"configDir": "graph/ui-components/.storybook",
"outputDir": "dist/storybook/graph-ui-components"
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}
-7
View File
@@ -1,7 +0,0 @@
export * from './lib/copy-to-clipboard-button';
export * from './lib/debounced-text-input';
export * from './lib/tag';
export * from './lib/dropdown';
export * from './lib/spinner';
export * from './lib/error-renderer';
export * from './lib/modal';
@@ -1,20 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
CopyToClipboardButton,
CopyToClipboardButtonProps,
} from './copy-to-clipboard-button';
const meta: Meta<typeof CopyToClipboardButton> = {
component: CopyToClipboardButton,
title: 'CopyToClipboardButton',
};
export default meta;
type Story = StoryObj<typeof CopyToClipboardButton>;
export const Simple: Story = {
args: {
text: 'Hello, world!',
tooltipAlignment: 'left',
} as CopyToClipboardButtonProps,
};
@@ -1,61 +0,0 @@
// @ts-ignore
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { JSX, ReactNode, useEffect, useState } from 'react';
import {
ClipboardDocumentCheckIcon,
ClipboardDocumentIcon,
} from '@heroicons/react/24/outline';
export interface CopyToClipboardButtonProps {
text: string;
tooltipText?: string;
tooltipAlignment?: 'left' | 'right';
className?: string;
children?: ReactNode;
}
export function CopyToClipboardButton({
text,
tooltipAlignment,
tooltipText,
className,
children,
}: CopyToClipboardButtonProps) {
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!copied) return;
const t = setTimeout(() => {
setCopied(false);
}, 3000);
return () => clearTimeout(t);
}, [copied]);
return (
<CopyToClipboard
text={text}
onCopy={() => {
setCopied(true);
}}
>
<button
type="button"
data-tooltip={tooltipText ? tooltipText : false}
data-tooltip-align-right={tooltipAlignment === 'right'}
data-tooltip-align-left={tooltipAlignment === 'left'}
className={className}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{copied ? (
<ClipboardDocumentCheckIcon className="inline h-5 w-5 text-blue-500 dark:text-sky-500" />
) : (
<ClipboardDocumentIcon className="inline h-5 w-5" />
)}
{children}
</button>
</CopyToClipboard>
);
}
@@ -1,25 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DebouncedTextInput } from './debounced-text-input';
const meta: Meta<typeof DebouncedTextInput> = {
component: DebouncedTextInput,
title: 'Shared/DebouncedTextInput',
argTypes: {
resetTextFilter: {
action: 'resetTextFilter',
},
updateTextFilter: {
action: 'updateTextFilter',
},
},
};
export default meta;
type Story = StoryObj<typeof DebouncedTextInput>;
export const Primary: Story = {
args: {
initialText: '',
placeholderText: '',
},
};
@@ -1,85 +0,0 @@
import { KeyboardEvent, useEffect, useState } from 'react';
import { useDebounce } from './use-debounce';
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
export interface DebouncedTextInputProps {
initialText: string;
placeholderText: string;
resetTextFilter: () => void;
updateTextFilter: (textFilter: string) => void;
}
export function DebouncedTextInput({
initialText,
placeholderText,
resetTextFilter,
updateTextFilter,
}: DebouncedTextInputProps) {
const [currentTextFilter, setCurrentTextFilter] = useState(initialText ?? '');
const [debouncedValue, setDebouncedValue] = useDebounce(
currentTextFilter,
500
);
function onTextFilterKeyUp(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
onTextInputChange(event.currentTarget.value);
}
}
function onTextInputChange(change: string) {
if (change === '') {
setCurrentTextFilter('');
setDebouncedValue('');
resetTextFilter();
} else {
setCurrentTextFilter(change);
}
}
function resetClicked() {
setCurrentTextFilter('');
setDebouncedValue('');
resetTextFilter();
}
useEffect(() => {
if (debouncedValue !== '') {
updateTextFilter(debouncedValue);
}
}, [debouncedValue, updateTextFilter]);
return (
<form
className="group relative flex rounded-md shadow-sm"
onSubmit={(event) => event.preventDefault()}
>
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-50 p-2 dark:border-slate-900 dark:bg-slate-800">
<FunnelIcon className="h-4 w-4"></FunnelIcon>
</span>
<input
type="text"
className={`block w-full flex-1 rounded-none rounded-r-md border border-slate-300 bg-white p-1.5 font-light text-slate-400 placeholder:font-light placeholder:text-slate-400 dark:border-slate-900 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700`}
placeholder={placeholderText}
data-cy="textFilterInput"
name="filter"
value={currentTextFilter}
onKeyUp={onTextFilterKeyUp}
onChange={(event) => onTextInputChange(event.currentTarget.value)}
></input>
{currentTextFilter.length > 0 ? (
<button
data-cy="textFilterReset"
type="reset"
onClick={resetClicked}
className="absolute right-1 top-1 inline-block rounded-md bg-slate-50 p-1 text-slate-400 dark:bg-slate-800"
>
<BackspaceIcon className="h-5 w-5"></BackspaceIcon>
</button>
) : null}
</form>
);
}
@@ -1,22 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Dropdown } from './dropdown';
const meta: Meta<typeof Dropdown> = {
component: Dropdown,
title: 'Shared/Dropdown',
argTypes: {
onChange: { action: 'onChange' },
},
};
export default meta;
type Story = StoryObj<typeof Dropdown>;
export const Primary: Story = {
render: () => (
<Dropdown {...{}}>
<option value="Option 1">Option 1</option>
<option value="Option 2">Option 2</option>
</Dropdown>
),
};
-18
View File
@@ -1,18 +0,0 @@
/* eslint-disable-next-line */
import React, { ReactNode } from 'react';
export type DropdownProps = {
children: ReactNode[];
} & React.HTMLAttributes<HTMLSelectElement>;
export function Dropdown(props: DropdownProps) {
const { className, children, ...rest } = props;
return (
<select
className={`form-select flex items-center rounded-md rounded-md border border-slate-300 bg-white py-2 pl-4 pr-8 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700 ${className}`}
{...rest}
>
{children}
</select>
);
}
@@ -1,65 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */
export function ErrorRenderer({ errors }: { errors: GraphError[] }) {
return (
<div>
{errors.map((error, index) => {
const errorHeading =
error.pluginName && error.name
? `${error.name} - ${error.pluginName}`
: error.name ?? error.message;
const fileSpecifier =
isCauseWithLocation(error.cause) && error.cause.errors.length === 1
? `${error.fileName}:${error.cause.errors[0].location.line}:${error.cause.errors[0].location.column}`
: error.fileName;
return (
<div className="overflow-hidden pb-4">
<span className="inline-flex max-w-full flex-col break-words font-bold font-normal text-gray-900 md:inline dark:text-slate-200">
<span>{errorHeading}</span>
{fileSpecifier && (
<span className="hidden px-1 md:inline">-</span>
)}
<span>{fileSpecifier}</span>
</span>
<pre className="overflow-x-scroll pl-4 pt-3">
{isCauseWithErrors(error.cause) &&
error.cause.errors.length === 1 ? (
<div>
{error.message} <br />
{error.cause.errors[0].text}{' '}
</div>
) : (
<div>{error.stack}</div>
)}
</pre>
</div>
);
})}
</div>
);
}
function isCauseWithLocation(cause: unknown): cause is {
errors: {
location: {
column: number;
line: number;
};
text: string;
}[];
} {
return (
isCauseWithErrors(cause) &&
(cause as any).errors[0].location &&
(cause as any).errors[0].location.column &&
(cause as any).errors[0].location.line
);
}
function isCauseWithErrors(
cause: unknown
): cause is { errors: { text: string }[] } {
return cause && (cause as any).errors && (cause as any).errors[0].text;
}
-108
View File
@@ -1,108 +0,0 @@
// component from https://tailwindui.com/components/application-ui/overlays/dialogs
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import {
ForwardedRef,
Fragment,
ReactNode,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
export interface ModalProps {
children: ReactNode;
title: string;
onOpen?: () => void;
onClose?: () => void;
}
export interface ModalHandle {
openModal: () => void;
closeModal: () => void;
}
export const Modal = forwardRef(
(
{ children, title, onOpen, onClose }: ModalProps,
ref: ForwardedRef<ModalHandle>
) => {
const [open, setOpen] = useState(false);
useEffect(() => {
if (open) {
onOpen?.();
} else {
onClose?.();
}
}, [open, onOpen, onClose]);
useImperativeHandle(ref, () => ({
closeModal: () => {
setOpen(false);
},
openModal: () => {
setOpen(true);
},
}));
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={`relative mx-4 my-8 w-full transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all md:max-w-5xl xl:max-w-7xl dark:bg-slate-900
`}
>
<div className="flex items-center justify-between rounded-t border-b bg-white p-2 pb-1 md:p-4 md:pb-2 dark:border-gray-600 dark:bg-slate-900">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 dark:text-slate-200"
>
{title}
</Dialog.Title>
<button
type="button"
className="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
data-modal-hide="default-modal"
onClick={() => setOpen(false)}
>
<XMarkIcon />
<span className="sr-only">Close modal</span>
</button>
</div>
<div className="max-h-[90vh] overflow-y-auto bg-white p-2 md:p-4 dark:bg-slate-900">
{children}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
);
@@ -1,16 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Spinner } from './spinner';
const meta: Meta<typeof Spinner> = {
component: Spinner,
title: 'Shared/Spinner',
};
export default meta;
type Story = StoryObj<typeof Spinner>;
export const Primary: Story = {
args: {
className: '',
},
};
-31
View File
@@ -1,31 +0,0 @@
/**
* Spinner component from https://tailwindcss.com/docs/animation#spin
*/
import React from 'react';
export type SpinnerProps = React.SVGProps<SVGSVGElement>;
export function Spinner({ className, ...rest }: SpinnerProps) {
return (
<svg
className={`${className} h-8 w-8 animate-spin`}
viewBox="0 0 24 24"
{...rest}
>
<circle
className="opacity-10"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
className="opacity-90"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
}
@@ -1,16 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Tag } from './tag';
const meta: Meta<typeof Tag> = {
component: Tag,
title: 'Shared/Tag',
};
export default meta;
type Story = StoryObj<typeof Tag>;
export const Primary: Story = {
args: {
content: 'tag',
},
};
-19
View File
@@ -1,19 +0,0 @@
/* eslint-disable-next-line */
import React, { ReactNode } from 'react';
export type TagProps = Partial<{
className: string;
children: ReactNode | ReactNode[];
}> &
React.HTMLAttributes<HTMLSpanElement>;
export function Tag({ className, children, ...rest }: TagProps) {
return (
<span
className={`${className} inline-block rounded-md bg-slate-300 p-2 font-sans text-xs font-semibold uppercase leading-4 tracking-wide text-slate-700`}
{...rest}
>
{children}
</span>
);
}
@@ -1,26 +0,0 @@
import type { Dispatch, SetStateAction } from 'react';
import { useEffect, useState } from 'react';
export function useDebounce(
value: string,
delay: number
): [string, Dispatch<SetStateAction<string>>] {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return [debouncedValue, setDebouncedValue];
}
-40
View File
@@ -1,40 +0,0 @@
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')],
};
-23
View File
@@ -1,23 +0,0 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.storybook.json"
}
],
"extends": "../../tsconfig.base.json"
}
-28
View File
@@ -1,28 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"],
"lib": ["dom"]
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@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"]
}
-20
View File
@@ -1,20 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
@@ -1,26 +0,0 @@
{
"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/**/*.spec.js",
"src/**/*.spec.tsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.stories.ts",
"src/**/*.stories.js",
"src/**/*.stories.jsx",
"src/**/*.stories.tsx",
"src/**/*.stories.mdx",
".storybook/*.js"
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
-18
View File
@@ -1,18 +0,0 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
stories: ['../src/lib/**/*.stories.@(mdx|js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials', '@nx/react/plugins/storybook'],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {
autodocs: true,
},
};
@@ -1,26 +0,0 @@
<script>
window.exclude = [];
window.watch = false;
window.environment = 'dev';
window.useXstateInspect = false;
window.appConfig = {
showDebugger: true,
showExperimentalFeatures: true,
workspaces: [
{
id: 'e2e',
label: 'e2e',
projectGraphUrl: 'assets/project-graphs/e2e.json',
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
{
id: 'affected',
label: 'affected',
projectGraphUrl: 'assets/project-graphs/affected.json',
taskGraphUrl: 'assets/task-graphs/affected.json',
},
],
defaultWorkspaceId: 'e2e',
};
</script>
-3
View File
@@ -1,3 +0,0 @@
import './tailwind-imports.css';
export const parameters = {};
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-7
View File
@@ -1,7 +0,0 @@
# graph-ui-graph
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test graph-ui-graph` to execute the unit tests via [Jest](https://jestjs.io).
-10
View File
@@ -1,10 +0,0 @@
/* eslint-disable */
export default {
displayName: 'graph-ui-graph',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/ui-graph',
};
-10
View File
@@ -1,10 +0,0 @@
const path = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: path.join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};
-34
View File
@@ -1,34 +0,0 @@
{
"name": "graph-ui-graph",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/ui-graph/src",
"projectType": "library",
"tags": [],
"targets": {
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {
"port": 4400,
"configDir": "graph/ui-graph/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nx/storybook:build",
"outputs": ["{options.outputDir}"],
"options": {
"configDir": "graph/ui-graph/.storybook",
"outputDir": "dist/storybook/graph-ui-graph"
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}
-5
View File
@@ -1,5 +0,0 @@
export * from './lib/nx-project-graph-viz';
export * from './lib/nx-task-graph-viz';
export * from './lib/graph';
export * from './lib/tooltip-service';
export * from './lib/graph-interaction-events';
@@ -1,40 +0,0 @@
import { VirtualElement } from '@floating-ui/react';
import { ProjectNodeDataDefinition } from './util-cytoscape/project-node';
import { TaskNodeDataDefinition } from './util-cytoscape/task-node';
import { ProjectEdgeDataDefinition } from './util-cytoscape';
interface ProjectNodeClickEvent {
type: 'ProjectNodeClick';
ref: VirtualElement;
id: string;
data: ProjectNodeDataDefinition;
}
interface TaskNodeClickEvent {
type: 'TaskNodeClick';
ref: VirtualElement;
id: string;
data: TaskNodeDataDefinition;
}
interface EdgeClickEvent {
type: 'EdgeClick';
ref: VirtualElement;
id: string;
data: ProjectEdgeDataDefinition;
}
interface GraphRegeneratedEvent {
type: 'GraphRegenerated';
}
interface BackgroundClickEvent {
type: 'BackgroundClick';
}
export type GraphInteractionEvents =
| ProjectNodeClickEvent
| EdgeClickEvent
| GraphRegeneratedEvent
| TaskNodeClickEvent
| BackgroundClickEvent;
-294
View File
@@ -1,294 +0,0 @@
// nx-ignore-next-line
import { CollectionReturnValue, use } from 'cytoscape';
import cytoscapeDagre from 'cytoscape-dagre';
import popper from 'cytoscape-popper';
import {
GraphPerfReport,
ProjectGraphRenderEvents,
TaskGraphRenderEvents,
} from './interfaces';
import { GraphInteractionEvents } from './graph-interaction-events';
import { RenderGraph } from './util-cytoscape/render-graph';
import { ProjectTraversalGraph } from './util-cytoscape/project-traversal-graph';
import { TaskTraversalGraph } from './util-cytoscape/task-traversal.graph';
export class GraphService {
private projectTraversalGraph: ProjectTraversalGraph;
private taskTraversalGraph: TaskTraversalGraph;
private renderGraph: RenderGraph;
lastPerformanceReport: GraphPerfReport = {
numEdges: 0,
numNodes: 0,
renderTime: 0,
};
private listeners = new Map<
number,
(event: GraphInteractionEvents) => void
>();
constructor(
container: string | HTMLElement,
theme: 'light' | 'dark',
public renderMode?: 'nx-console' | 'nx-docs',
rankDir: 'TB' | 'LR' = 'TB',
public getTaskInputs: (
taskId: string
) => Promise<Record<string, string[]>> = undefined
) {
use(cytoscapeDagre);
use(popper);
this.renderGraph = new RenderGraph(container, theme, renderMode, rankDir);
this.renderGraph.listen((event) => this.broadcast(event));
this.projectTraversalGraph = new ProjectTraversalGraph();
this.taskTraversalGraph = new TaskTraversalGraph();
}
set theme(theme: 'light' | 'dark') {
this.renderGraph.theme = theme;
}
set rankDir(rankDir: 'TB' | 'LR') {
this.renderGraph.rankDir = rankDir;
}
listen(callback: (event: GraphInteractionEvents) => void) {
const listenerId = this.listeners.size + 1;
this.listeners.set(listenerId, callback);
return () => {
this.listeners.delete(listenerId);
};
}
broadcast(event: GraphInteractionEvents) {
this.listeners.forEach((callback) => callback(event));
}
handleProjectEvent(event: ProjectGraphRenderEvents): {
selectedProjectNames: string[];
perfReport: GraphPerfReport;
} {
const time = Date.now();
if (event.type !== 'notifyGraphUpdateGraph') {
this.renderGraph.clearFocussedElement();
}
let elementsToSendToRender: CollectionReturnValue;
switch (event.type) {
case 'notifyGraphInitGraph':
this.renderGraph.collapseEdges = event.collapseEdges;
this.broadcast({ type: 'GraphRegenerated' });
this.projectTraversalGraph.initGraph(
event.fileMap,
event.projects,
event.groupByFolder,
event.workspaceLayout,
event.dependencies,
event.affectedProjects,
event.collapseEdges
);
break;
case 'notifyGraphUpdateGraph':
this.renderGraph.collapseEdges = event.collapseEdges;
this.broadcast({ type: 'GraphRegenerated' });
this.projectTraversalGraph.initGraph(
event.fileMap,
event.projects,
event.groupByFolder,
event.workspaceLayout,
event.dependencies,
event.affectedProjects,
event.collapseEdges
);
elementsToSendToRender = this.projectTraversalGraph.setShownProjects(
event.selectedProjects.length > 0
? event.selectedProjects
: this.renderGraph.getCurrentlyShownProjectIds()
);
break;
case 'notifyGraphFocusProject':
elementsToSendToRender = this.projectTraversalGraph.focusProject(
event.projectName,
event.searchDepth
);
break;
case 'notifyGraphFilterProjectsByText':
elementsToSendToRender =
this.projectTraversalGraph.filterProjectsByText(
event.search,
event.includeProjectsByPath,
event.searchDepth
);
break;
case 'notifyGraphShowProjects':
elementsToSendToRender = this.projectTraversalGraph.showProjects(
event.projectNames,
this.renderGraph.getCurrentlyShownProjectIds()
);
break;
case 'notifyGraphHideProjects':
elementsToSendToRender = this.projectTraversalGraph.hideProjects(
event.projectNames,
this.renderGraph.getCurrentlyShownProjectIds()
);
break;
case 'notifyGraphShowAllProjects':
elementsToSendToRender = this.projectTraversalGraph.showAllProjects();
break;
case 'notifyGraphHideAllProjects':
elementsToSendToRender = this.projectTraversalGraph.hideAllProjects();
break;
case 'notifyGraphShowAffectedProjects':
elementsToSendToRender =
this.projectTraversalGraph.showAffectedProjects();
break;
case 'notifyGraphTracing':
if (event.start && event.end) {
if (event.algorithm === 'shortest') {
elementsToSendToRender = this.projectTraversalGraph.traceProjects(
event.start,
event.end
);
} else {
elementsToSendToRender =
this.projectTraversalGraph.traceAllProjects(
event.start,
event.end
);
}
}
break;
}
let selectedProjectNames: string[] = [];
let perfReport: GraphPerfReport = {
numEdges: 0,
numNodes: 0,
renderTime: 0,
};
if (this.renderGraph) {
if (elementsToSendToRender) {
this.renderGraph.setElements(elementsToSendToRender);
if (event.type === 'notifyGraphFocusProject') {
this.renderGraph.setFocussedElement(event.projectName);
}
const { numEdges, numNodes } = this.renderGraph.render();
selectedProjectNames = (
elementsToSendToRender.nodes('[type!="dir"]') ?? []
).map((node) => node.id());
const renderTime = Date.now() - time;
perfReport = {
renderTime,
numNodes,
numEdges,
};
} else {
const { numEdges, numNodes } = this.renderGraph.render();
this.renderGraph.getCurrentlyShownProjectIds();
const renderTime = Date.now() - time;
perfReport = {
renderTime,
numNodes,
numEdges,
};
}
}
this.lastPerformanceReport = perfReport;
this.broadcast({ type: 'GraphRegenerated' });
return { selectedProjectNames, perfReport };
}
handleTaskEvent(event: TaskGraphRenderEvents) {
const time = Date.now();
this.broadcast({ type: 'GraphRegenerated' });
let elementsToSendToRender: CollectionReturnValue;
switch (event.type) {
case 'notifyTaskGraphSetProjects':
this.taskTraversalGraph.setProjects(event.projects, event.taskGraphs);
break;
case 'notifyTaskGraphSetTasks':
elementsToSendToRender = this.taskTraversalGraph.setTasks(
event.taskIds
);
break;
case 'notifyTaskGraphTasksSelected':
elementsToSendToRender = this.taskTraversalGraph.selectTask(
event.taskIds
);
break;
case 'notifyTaskGraphTasksDeselected':
elementsToSendToRender = this.taskTraversalGraph.deselectTask(
event.taskIds
);
break;
case 'setGroupByProject':
elementsToSendToRender = this.taskTraversalGraph.setGroupByProject(
event.groupByProject
);
break;
}
let selectedProjectNames: string[] = [];
let perfReport: GraphPerfReport = {
numEdges: 0,
numNodes: 0,
renderTime: 0,
};
if (this.renderGraph && elementsToSendToRender) {
this.renderGraph.setElements(elementsToSendToRender);
const { numEdges, numNodes } = this.renderGraph.render();
selectedProjectNames = (
elementsToSendToRender.nodes('[type!="dir"]') ?? []
).map((node) => node.id());
const renderTime = Date.now() - time;
perfReport = {
renderTime,
numNodes,
numEdges,
};
}
this.lastPerformanceReport = perfReport;
this.broadcast({ type: 'GraphRegenerated' });
return { selectedProjectNames, perfReport };
}
getImage() {
return this.renderGraph.getImage();
}
}
-122
View File
@@ -1,122 +0,0 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type {
ProjectFileMap,
ProjectGraphDependency,
ProjectGraphProjectNode,
TaskGraph,
} from '@nx/devkit';
/* eslint-enable @nx/enforce-module-boundaries */
import { VirtualElement } from '@floating-ui/react';
import {
ProjectEdgeNodeTooltipProps,
ProjectNodeToolTipProps,
TaskNodeTooltipProps,
} from '@nx/graph/ui-tooltips';
export interface GraphPerfReport {
renderTime: number;
numNodes: number;
numEdges: number;
}
export type TracingAlgorithmType = 'shortest' | 'all';
// The events that the graph actor handles
export type ProjectGraphRenderEvents =
| {
type: 'notifyGraphInitGraph';
projects: ProjectGraphProjectNode[];
fileMap: ProjectFileMap;
dependencies: Record<string, ProjectGraphDependency[]>;
affectedProjects: string[];
workspaceLayout: {
libsDir: string;
appsDir: string;
};
groupByFolder: boolean;
collapseEdges: boolean;
}
| {
type: 'notifyGraphUpdateGraph';
projects: ProjectGraphProjectNode[];
fileMap: ProjectFileMap;
dependencies: Record<string, ProjectGraphDependency[]>;
affectedProjects: string[];
workspaceLayout: {
libsDir: string;
appsDir: string;
};
groupByFolder: boolean;
collapseEdges: boolean;
selectedProjects: string[];
}
| {
type: 'notifyGraphFocusProject';
projectName: string;
searchDepth: number;
}
| {
type: 'notifyGraphShowProjects';
projectNames: string[];
}
| {
type: 'notifyGraphHideProjects';
projectNames: string[];
}
| {
type: 'notifyGraphShowAllProjects';
}
| {
type: 'notifyGraphHideAllProjects';
}
| {
type: 'notifyGraphShowAffectedProjects';
}
| {
type: 'notifyGraphFilterProjectsByText';
search: string;
includeProjectsByPath: boolean;
searchDepth: number;
}
| {
type: 'notifyGraphTracing';
start: string;
end: string;
algorithm: TracingAlgorithmType;
};
export type TaskGraphRecord = Record<string, TaskGraph>;
export type TaskGraphRenderEvents =
| {
type: 'notifyTaskGraphSetProjects';
projects: ProjectGraphProjectNode[];
taskGraphs: TaskGraphRecord;
}
| {
type: 'notifyTaskGraphTasksSelected';
taskIds: string[];
}
| {
type: 'notifyTaskGraphTasksDeselected';
taskIds: string[];
}
| {
type: 'setGroupByProject';
groupByProject: boolean;
}
| { type: 'notifyTaskGraphSetTasks'; taskIds: string[] };
export type TooltipEvent =
| {
ref: VirtualElement;
type: 'projectNode';
props: ProjectNodeToolTipProps;
}
| { ref: VirtualElement; type: 'taskNode'; props: TaskNodeTooltipProps }
| {
ref: VirtualElement;
type: 'projectEdge';
props: ProjectEdgeNodeTooltipProps;
};
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { NxProjectGraphViz } from './nx-project-graph-viz';
const meta: Meta<typeof NxProjectGraphViz> = {
component: NxProjectGraphViz,
title: 'NxProjectGraphViz',
};
export default meta;
type Story = StoryObj<typeof NxProjectGraphViz>;
export const Primary: Story = {
args: {
projects: [
{
type: 'app',
name: 'app',
data: {
tags: ['scope:cart'],
description: 'This is your top-level app',
files: [
{
file: 'whatever.ts',
deps: ['lib'],
},
],
},
} as any,
{
type: 'lib',
name: 'lib',
data: {
tags: ['scope:cart'],
description: 'This lib implements some type of feature for your app.',
},
},
{
type: 'lib',
name: 'lib2',
data: {
root: 'libs/nested-scope/lib2',
tags: ['scope:cart'],
},
},
{
type: 'lib',
name: 'lib3',
data: {
root: 'libs/nested-scope/lib3',
tags: ['scope:cart'],
},
},
],
groupByFolder: true,
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
dependencies: {
app: [{ target: 'lib', source: 'app', type: 'direct' }],
lib: [
{ target: 'lib2', source: 'lib', type: 'implicit' },
{ target: 'lib3', source: 'lib', type: 'direct' },
],
lib2: [],
lib3: [],
},
affectedProjectIds: [],
theme: 'light',
height: '450px',
enableTooltips: true,
},
};

Some files were not shown because too many files have changed in this diff Show More