Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb42a0dfac |
@@ -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';
|
||||
|
||||
Vendored
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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'],
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "graph-shared",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "graph/shared/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {}
|
||||
}
|
||||
Vendored
-25
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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() : '',
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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).
|
||||
@@ -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',
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const { join } = require('path');
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -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 +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>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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).
|
||||
@@ -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',
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: './graph/ui-components/tailwind.config.js',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: '',
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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')],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import './tailwind-imports.css';
|
||||
|
||||
export const parameters = {};
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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).
|
||||
@@ -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',
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: path.join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user