feat(graph): add nx console data loader (#20744)
This commit is contained in:
parent
b60ae51100
commit
b97c869279
133
graph/client/src/app/external-api-impl.ts
Normal file
133
graph/client/src/app/external-api-impl.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { ExternalApi, getExternalApiService } from '@nx/graph/shared';
|
||||
import { getRouter } from './get-router';
|
||||
import { getProjectGraphService } from './machines/get-services';
|
||||
import { getGraphService } from './machines/graph.service';
|
||||
|
||||
export class ExternalApiImpl extends ExternalApi {
|
||||
_projectGraphService = getProjectGraphService();
|
||||
_graphIsReady = new Promise<void>((resolve) => {
|
||||
this._projectGraphService.subscribe((state) => {
|
||||
if (!state.matches('idle')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
_graphService = getGraphService();
|
||||
|
||||
router = getRouter();
|
||||
externalApiService = getExternalApiService();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.externalApiService.subscribe(
|
||||
({ type, payload }: { type: string; payload: any }) => {
|
||||
if (!this.graphInteractionEventListener) {
|
||||
console.log('graphInteractionEventListener not registered.');
|
||||
return;
|
||||
}
|
||||
if (type === 'file-click') {
|
||||
const url = `${payload.sourceRoot}/${payload.file}`;
|
||||
this.graphInteractionEventListener({
|
||||
type: 'file-click',
|
||||
payload: { url },
|
||||
});
|
||||
} else if (type === 'open-project-config') {
|
||||
this.graphInteractionEventListener({
|
||||
type: 'open-project-config',
|
||||
payload,
|
||||
});
|
||||
} else if (type === 'run-task') {
|
||||
this.graphInteractionEventListener({
|
||||
type: 'run-task',
|
||||
payload,
|
||||
});
|
||||
} else if (type === 'open-project-graph') {
|
||||
this.graphInteractionEventListener({
|
||||
type: 'open-project-graph',
|
||||
payload,
|
||||
});
|
||||
} else if (type === 'open-task-graph') {
|
||||
this.graphInteractionEventListener({
|
||||
type: 'open-task-graph',
|
||||
payload,
|
||||
});
|
||||
} else if (type === 'override-target') {
|
||||
this.graphInteractionEventListener({
|
||||
type: 'override-target',
|
||||
payload,
|
||||
});
|
||||
} else {
|
||||
console.log('unhandled event', type, payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// make sure properties set before are taken into account again
|
||||
if (window.externalApi?.loadProjectGraph) {
|
||||
this.loadProjectGraph = window.externalApi.loadProjectGraph;
|
||||
}
|
||||
if (window.externalApi?.loadTaskGraph) {
|
||||
this.loadTaskGraph = window.externalApi.loadTaskGraph;
|
||||
}
|
||||
if (window.externalApi?.loadExpandedTaskInputs) {
|
||||
this.loadExpandedTaskInputs = window.externalApi.loadExpandedTaskInputs;
|
||||
}
|
||||
if (window.externalApi?.loadSourceMaps) {
|
||||
this.loadSourceMaps = window.externalApi.loadSourceMaps;
|
||||
}
|
||||
if (window.externalApi?.graphInteractionEventListener) {
|
||||
this.graphInteractionEventListener =
|
||||
window.externalApi.graphInteractionEventListener;
|
||||
}
|
||||
}
|
||||
|
||||
focusProject(projectName: string) {
|
||||
this.router.navigate(`/projects/${encodeURIComponent(projectName)}`);
|
||||
}
|
||||
|
||||
toggleSelectProject(projectName: string) {
|
||||
this._graphIsReady.then(() => {
|
||||
const projectSelected = this._projectGraphService
|
||||
.getSnapshot()
|
||||
.context.selectedProjects.find((p) => p === projectName);
|
||||
if (!projectSelected) {
|
||||
this._projectGraphService.send({ type: 'selectProject', projectName });
|
||||
} else {
|
||||
this._projectGraphService.send({
|
||||
type: 'deselectProject',
|
||||
projectName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectAllProjects() {
|
||||
this.router.navigate(`/projects/all`);
|
||||
}
|
||||
|
||||
showAffectedProjects() {
|
||||
this.router.navigate(`/projects/affected`);
|
||||
}
|
||||
|
||||
focusTarget(projectName: string, targetName: string) {
|
||||
this.router.navigate(
|
||||
`/tasks/${encodeURIComponent(targetName)}?projects=${encodeURIComponent(
|
||||
projectName
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
selectAllTargetsByName(targetName: string) {
|
||||
this.router.navigate(`/tasks/${encodeURIComponent(targetName)}/all`);
|
||||
}
|
||||
|
||||
enableExperimentalFeatures() {
|
||||
localStorage.setItem('showExperimentalFeatures', 'true');
|
||||
window.appConfig.showExperimentalFeatures = true;
|
||||
}
|
||||
|
||||
disableExperimentalFeatures() {
|
||||
localStorage.setItem('showExperimentalFeatures', 'false');
|
||||
window.appConfig.showExperimentalFeatures = false;
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
import { getRouter } from './get-router';
|
||||
import { getProjectGraphService } from './machines/get-services';
|
||||
import { ProjectGraphMachineEvents } from './feature-projects/machines/interfaces';
|
||||
import { getGraphService } from './machines/graph.service';
|
||||
|
||||
export class ExternalApi {
|
||||
_projectGraphService = getProjectGraphService();
|
||||
_graphIsReady = new Promise<void>((resolve) => {
|
||||
this._projectGraphService.subscribe((state) => {
|
||||
if (!state.matches('idle')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router = getRouter();
|
||||
graphService = getGraphService();
|
||||
|
||||
projectGraphService = {
|
||||
send: (event: ProjectGraphMachineEvents) => {
|
||||
this.handleLegacyProjectGraphEvent(event);
|
||||
},
|
||||
};
|
||||
|
||||
private fileClickCallbackListeners: ((url: string) => void)[] = [];
|
||||
private openProjectConfigCallbackListeners: ((
|
||||
projectName: string
|
||||
) => void)[] = [];
|
||||
private runTaskCallbackListeners: ((taskId: string) => void)[] = [];
|
||||
|
||||
get depGraphService() {
|
||||
return this.projectGraphService;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.graphService.listen((event) => {
|
||||
if (event.type === 'FileLinkClick') {
|
||||
const url = `${event.sourceRoot}/${event.file}`;
|
||||
this.fileClickCallbackListeners.forEach((cb) => cb(url));
|
||||
}
|
||||
if (event.type === 'ProjectOpenConfigClick') {
|
||||
this.openProjectConfigCallbackListeners.forEach((cb) =>
|
||||
cb(event.projectName)
|
||||
);
|
||||
}
|
||||
if (event.type === 'RunTaskClick') {
|
||||
this.runTaskCallbackListeners.forEach((cb) => cb(event.taskId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focusProject(projectName: string) {
|
||||
this.router.navigate(`/projects/${encodeURIComponent(projectName)}`);
|
||||
}
|
||||
|
||||
selectAllProjects() {
|
||||
this.router.navigate(`/projects/all`);
|
||||
}
|
||||
|
||||
enableExperimentalFeatures() {
|
||||
localStorage.setItem('showExperimentalFeatures', 'true');
|
||||
window.appConfig.showExperimentalFeatures = true;
|
||||
}
|
||||
|
||||
disableExperimentalFeatures() {
|
||||
localStorage.setItem('showExperimentalFeatures', 'false');
|
||||
window.appConfig.showExperimentalFeatures = false;
|
||||
}
|
||||
|
||||
registerFileClickCallback(callback: (url: string) => void) {
|
||||
this.fileClickCallbackListeners.push(callback);
|
||||
}
|
||||
registerOpenProjectConfigCallback(callback: (projectName: string) => void) {
|
||||
this.openProjectConfigCallbackListeners.push(callback);
|
||||
}
|
||||
registerRunTaskCallback(callback: (taskId: string) => void) {
|
||||
this.runTaskCallbackListeners.push(callback);
|
||||
}
|
||||
|
||||
private handleLegacyProjectGraphEvent(event: ProjectGraphMachineEvents) {
|
||||
switch (event.type) {
|
||||
case 'focusProject':
|
||||
this.focusProject(event.projectName);
|
||||
break;
|
||||
case 'selectAll':
|
||||
this.selectAllProjects();
|
||||
break;
|
||||
default:
|
||||
this._graphIsReady.then(() => this._projectGraphService.send(event));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { interpret } from 'xstate';
|
||||
import { projectGraphMachine } from './project-graph.machine';
|
||||
import { AppConfig } from '../../interfaces';
|
||||
import { AppConfig } from '@nx/graph/shared';
|
||||
|
||||
export const mockProjects: ProjectGraphProjectNode[] = [
|
||||
{
|
||||
|
||||
@ -15,15 +15,12 @@ import {
|
||||
selectedProjectNamesSelector,
|
||||
workspaceLayoutSelector,
|
||||
} from './machines/selectors';
|
||||
import {
|
||||
getProjectsByType,
|
||||
parseParentDirectoriesFromFilePath,
|
||||
useRouteConstructor,
|
||||
} from '../util';
|
||||
import { getProjectsByType, parseParentDirectoriesFromFilePath } from '../util';
|
||||
import { ExperimentalFeature } from '../ui-components/experimental-feature';
|
||||
import { TracingAlgorithmType } from './machines/interfaces';
|
||||
import { getProjectGraphService } from '../machines/get-services';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useRouteConstructor } from '@nx/graph/shared';
|
||||
|
||||
interface SidebarProject {
|
||||
projectGraphNode: ProjectGraphNode;
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useIntervalWhen } from '../hooks/use-interval-when';
|
||||
import { getProjectGraphService } from '../machines/get-services';
|
||||
import { ExperimentalFeature } from '../ui-components/experimental-feature';
|
||||
import { FocusedPanel } from '../ui-components/focused-panel';
|
||||
import { ShowHideAll } from '../ui-components/show-hide-all';
|
||||
import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
|
||||
import { TracingAlgorithmType } from './machines/interfaces';
|
||||
import {
|
||||
collapseEdgesSelector,
|
||||
focusedProjectNameSelector,
|
||||
@ -12,21 +17,17 @@ import {
|
||||
textFilterSelector,
|
||||
} from './machines/selectors';
|
||||
import { CollapseEdgesPanel } from './panels/collapse-edges-panel';
|
||||
import { FocusedPanel } from '../ui-components/focused-panel';
|
||||
import { GroupByFolderPanel } from './panels/group-by-folder-panel';
|
||||
import { ProjectList } from './project-list';
|
||||
import { SearchDepth } from './panels/search-depth';
|
||||
import { ShowHideAll } from '../ui-components/show-hide-all';
|
||||
import { TextFilterPanel } from './panels/text-filter-panel';
|
||||
import { TracingPanel } from './panels/tracing-panel';
|
||||
import { useEnvironmentConfig } from '../hooks/use-environment-config';
|
||||
import { TracingAlgorithmType } from './machines/interfaces';
|
||||
import { getProjectGraphService } from '../machines/get-services';
|
||||
import { useIntervalWhen } from '../hooks/use-interval-when';
|
||||
import { ProjectList } from './project-list';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { useFloating } from '@floating-ui/react';
|
||||
import { useEnvironmentConfig, useRouteConstructor } from '@nx/graph/shared';
|
||||
import {
|
||||
useNavigate,
|
||||
useParams,
|
||||
@ -35,7 +36,7 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
|
||||
import { useCurrentPath } from '../hooks/use-current-path';
|
||||
import { useRouteConstructor } from '../util';
|
||||
import { ProjectDetailsModal } from '../ui-components/project-details-modal';
|
||||
|
||||
export function ProjectsSidebar(): JSX.Element {
|
||||
const environmentConfig = useEnvironmentConfig();
|
||||
@ -329,6 +330,8 @@ export function ProjectsSidebar(): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectDetailsModal />
|
||||
|
||||
{focusedProject ? (
|
||||
<FocusedPanel
|
||||
focusedLabel={focusedProject}
|
||||
|
||||
@ -16,12 +16,11 @@ import { useEffect, useMemo } from 'react';
|
||||
import { getGraphService } from '../machines/graph.service';
|
||||
import { CheckboxPanel } from '../ui-components/checkbox-panel';
|
||||
|
||||
import { useRouteConstructor } from '@nx/graph/shared';
|
||||
import { Dropdown } from '@nx/graph/ui-components';
|
||||
import { useCurrentPath } from '../hooks/use-current-path';
|
||||
import { ShowHideAll } from '../ui-components/show-hide-all';
|
||||
import { createTaskName, useRouteConstructor } from '../util';
|
||||
import { GraphInteractionEvents } from '@nx/graph/ui-graph';
|
||||
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
|
||||
import { createTaskName } from '../util';
|
||||
|
||||
export function TasksSidebar() {
|
||||
const graphService = getGraphService();
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { createBrowserRouter, createHashRouter } from 'react-router-dom';
|
||||
import { getRoutesForEnvironment } from './routes';
|
||||
import { getEnvironmentConfig } from './hooks/use-environment-config';
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
|
||||
let router;
|
||||
|
||||
export function getRouter() {
|
||||
if (!router) {
|
||||
const environmentConfig = getEnvironmentConfig();
|
||||
|
||||
let routerCreate = createBrowserRouter;
|
||||
if (environmentConfig.localMode === 'build') {
|
||||
if (
|
||||
environmentConfig.localMode === 'build' ||
|
||||
environmentConfig.environment === 'nx-console'
|
||||
) {
|
||||
routerCreate = createHashRouter;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { FetchProjectGraphService } from '../fetch-project-graph-service';
|
||||
import { ProjectGraphService } from '../interfaces';
|
||||
import { LocalProjectGraphService } from '../local-project-graph-service';
|
||||
import { MockProjectGraphService } from '../mock-project-graph-service';
|
||||
import { NxConsoleProjectGraphService } from '../nx-console-project-graph-service';
|
||||
|
||||
let projectGraphService: ProjectGraphService;
|
||||
|
||||
@ -11,10 +12,9 @@ export function getProjectGraphDataService() {
|
||||
projectGraphService = new FetchProjectGraphService();
|
||||
} else if (window.environment === 'watch') {
|
||||
projectGraphService = new MockProjectGraphService();
|
||||
} else if (
|
||||
window.environment === 'release' ||
|
||||
window.environment === 'nx-console'
|
||||
) {
|
||||
} else if (window.environment === 'nx-console') {
|
||||
projectGraphService = new NxConsoleProjectGraphService();
|
||||
} else if (window.environment === 'release') {
|
||||
if (window.localMode === 'build') {
|
||||
projectGraphService = new LocalProjectGraphService();
|
||||
} else {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { matchRoutes, useLocation } from 'react-router-dom';
|
||||
import { getRoutesForEnvironment } from '../routes';
|
||||
import { getEnvironmentConfig } from './use-environment-config';
|
||||
import { useState } from 'react';
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
|
||||
export const useCurrentPath = () => {
|
||||
const [lastLocation, setLastLocation] = useState<string>();
|
||||
|
||||
@ -6,15 +6,6 @@ import type {
|
||||
} from 'nx/src/command-line/graph/graph';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
|
||||
export interface WorkspaceData {
|
||||
id: string;
|
||||
label: string;
|
||||
projectGraphUrl: string;
|
||||
taskGraphUrl: string;
|
||||
taskInputsUrl: string;
|
||||
sourceMapsUrl: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceLayout {
|
||||
libsDir: string;
|
||||
appsDir: string;
|
||||
@ -35,13 +26,6 @@ export interface Environment {
|
||||
environment: 'dev' | 'watch' | 'release';
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
showDebugger: boolean;
|
||||
showExperimentalFeatures: boolean;
|
||||
workspaces: WorkspaceData[];
|
||||
defaultWorkspaceId: string;
|
||||
}
|
||||
|
||||
export interface GraphPerfReport {
|
||||
renderTime: number;
|
||||
numNodes: number;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { GraphService } from '@nx/graph/ui-graph';
|
||||
import { selectValueByThemeStatic } from '../theme-resolver';
|
||||
import { getEnvironmentConfig } from '../hooks/use-environment-config';
|
||||
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
|
||||
let graphService: GraphService;
|
||||
|
||||
|
||||
34
graph/client/src/app/nx-console-project-graph-service.ts
Normal file
34
graph/client/src/app/nx-console-project-graph-service.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import type {
|
||||
ProjectGraphClientResponse,
|
||||
TaskGraphClientResponse,
|
||||
} from 'nx/src/command-line/graph/graph';
|
||||
import { ProjectGraphService } from './interfaces';
|
||||
|
||||
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,15 +1,15 @@
|
||||
import { Shell } from './shell';
|
||||
import { redirect, RouteObject } from 'react-router-dom';
|
||||
import { ProjectsSidebar } from './feature-projects/projects-sidebar';
|
||||
import { TasksSidebar } from './feature-tasks/tasks-sidebar';
|
||||
import { getEnvironmentConfig } from './hooks/use-environment-config';
|
||||
import { Shell } from './shell';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { getProjectGraphDataService } from './hooks/get-project-graph-data-service';
|
||||
import { ProjectDetailsPage } from '@nx/graph/project-details';
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
import { TasksSidebarErrorBoundary } from './feature-tasks/tasks-sidebar-error-boundary';
|
||||
import { ProjectDetails } from '@nx/graph/project-details';
|
||||
import { getProjectGraphDataService } from './hooks/get-project-graph-data-service';
|
||||
|
||||
const { appConfig } = getEnvironmentConfig();
|
||||
const projectGraphDataService = getProjectGraphDataService();
|
||||
@ -44,7 +44,9 @@ const workspaceDataLoader = async (selectedWorkspaceId: string) => {
|
||||
|
||||
const targets = Array.from(targetsSet).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return { ...projectGraph, targets };
|
||||
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
|
||||
|
||||
return { ...projectGraph, targets, sourceMaps };
|
||||
};
|
||||
|
||||
const taskDataLoader = async (selectedWorkspaceId: string) => {
|
||||
@ -176,7 +178,7 @@ export const devRoutes: RouteObject[] = [
|
||||
{
|
||||
path: ':selectedWorkspaceId/project-details/:projectName',
|
||||
id: 'selectedProjectDetails',
|
||||
element: <ProjectDetails />,
|
||||
element: <ProjectDetailsPage />,
|
||||
loader: async ({ request, params }) => {
|
||||
const projectName = params.projectName;
|
||||
return projectDetailsLoader(params.selectedWorkspaceId, projectName);
|
||||
@ -213,7 +215,7 @@ export const releaseRoutes: RouteObject[] = [
|
||||
{
|
||||
path: 'project-details/:projectName',
|
||||
id: 'selectedProjectDetails',
|
||||
element: <ProjectDetails />,
|
||||
element: <ProjectDetailsPage />,
|
||||
loader: async ({ request, params }) => {
|
||||
const projectName = params.projectName;
|
||||
return projectDetailsLoader(appConfig.defaultWorkspaceId, projectName);
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import classNames from 'classnames';
|
||||
import { DebuggerPanel } from './ui-components/debugger-panel';
|
||||
import { useEnvironmentConfig } from './hooks/use-environment-config';
|
||||
import { getGraphService } from './machines/graph.service';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
import { ThemePanel } from './feature-projects/panels/theme-panel';
|
||||
@ -17,6 +16,7 @@ import { getProjectGraphService } from './machines/get-services';
|
||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||
import { Tooltip } from '@nx/graph/ui-tooltips';
|
||||
import { TooltipDisplay } from './ui-tooltips/graph-tooltip-display';
|
||||
import { useEnvironmentConfig } from '@nx/graph/shared';
|
||||
|
||||
export function Shell(): JSX.Element {
|
||||
const projectGraphService = getProjectGraphService();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { WorkspaceData, GraphPerfReport } from '../interfaces';
|
||||
import { GraphPerfReport } from '../interfaces';
|
||||
import { Dropdown } from '@nx/graph/ui-components';
|
||||
import type { WorkspaceData } from '@nx/graph/shared';
|
||||
|
||||
export interface DebuggerPanelProps {
|
||||
projects: WorkspaceData[];
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEnvironmentConfig } from '../hooks/use-environment-config';
|
||||
import { useEnvironmentConfig } from '@nx/graph/shared';
|
||||
import { Children, cloneElement } from 'react';
|
||||
|
||||
export function ExperimentalFeature(props) {
|
||||
|
||||
67
graph/client/src/app/ui-components/project-details-modal.tsx
Normal file
67
graph/client/src/app/ui-components/project-details-modal.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { useFloating } from '@floating-ui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { ProjectDetails } from '@nx/graph/project-details';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouteLoaderData, useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function ProjectDetailsModal() {
|
||||
const workspaceData = useRouteLoaderData(
|
||||
'selectedWorkspace'
|
||||
) as ProjectGraphClientResponse & { sourceMaps: string[] };
|
||||
const [project, setProject] = useState(null);
|
||||
const [sourceMap, setSourceMap] = useState(null);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { refs } = useFloating({
|
||||
open: isOpen,
|
||||
strategy: 'fixed',
|
||||
placement: 'right',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.has('projectDetails')) {
|
||||
const projectName = searchParams.get('projectDetails');
|
||||
const project = workspaceData.projects.find(
|
||||
(project) => project.name === projectName
|
||||
);
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
const sourceMap = workspaceData.sourceMaps[project.data.root];
|
||||
setProject(project);
|
||||
setSourceMap(sourceMap);
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [searchParams, workspaceData]);
|
||||
|
||||
function onClose() {
|
||||
searchParams.delete('projectDetails');
|
||||
setSearchParams(searchParams);
|
||||
setIsOpen(false);
|
||||
}
|
||||
return (
|
||||
isOpen && (
|
||||
<div
|
||||
className="top-24 z-20 right-4 opacity-100 bg-white dark:bg-slate-800 fixed h-max w-1/3"
|
||||
style={{
|
||||
height: 'calc(100vh - 6rem - 2rem)',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
<div className="rounded-md h-full border border-slate-500">
|
||||
<ProjectDetails project={project} sourceMap={sourceMap} />
|
||||
<div className="top-2 right-2 absolute" onClick={onClose}>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { ProjectNodeToolTipProps } from '@nx/graph/ui-tooltips';
|
||||
import { getProjectGraphService } from '../machines/get-services';
|
||||
import { useRouteConstructor } from '../util';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { TooltipButton, TooltipLinkButton } from '@nx/graph/ui-tooltips';
|
||||
import { FlagIcon, MapPinIcon } from '@heroicons/react/24/solid';
|
||||
import { useRouteConstructor } from '@nx/graph/shared';
|
||||
|
||||
export function ProjectNodeActions({ id }: ProjectNodeToolTipProps) {
|
||||
const projectGraphService = getProjectGraphService();
|
||||
@ -12,7 +12,11 @@ export function ProjectNodeActions({ id }: ProjectNodeToolTipProps) {
|
||||
const routeConstructor = useRouteConstructor();
|
||||
const navigate = useNavigate();
|
||||
const encodedId = encodeURIComponent(id);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
function onProjectDetails() {
|
||||
setSearchParams({ projectDetails: id });
|
||||
}
|
||||
function onExclude() {
|
||||
projectGraphService.send({
|
||||
type: 'deselectProject',
|
||||
@ -36,6 +40,7 @@ export function ProjectNodeActions({ id }: ProjectNodeToolTipProps) {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* <TooltipButton onClick={onProjectDetails}>Project Details</TooltipButton> */}
|
||||
<TooltipLinkButton to={routeConstructor(`/projects/${encodedId}`, true)}>
|
||||
Focus
|
||||
</TooltipLinkButton>
|
||||
|
||||
@ -1,49 +1,8 @@
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphDependency, ProjectGraphProjectNode } from '@nx/devkit';
|
||||
import { getEnvironmentConfig } from '@nx/graph/shared';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { getEnvironmentConfig } from './hooks/use-environment-config';
|
||||
import { To, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
export const useRouteConstructor = (): ((
|
||||
to: To,
|
||||
retainSearchParams: boolean
|
||||
) => To) => {
|
||||
const { environment } = getEnvironmentConfig();
|
||||
const { selectedWorkspaceId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (to: To, retainSearchParams: true) => {
|
||||
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() : '',
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export function parseParentDirectoriesFromFilePath(
|
||||
path: string,
|
||||
|
||||
12
graph/client/src/globals.d.ts
vendored
12
graph/client/src/globals.d.ts
vendored
@ -5,29 +5,23 @@ import type {
|
||||
ProjectGraphClientResponse,
|
||||
TaskGraphClientResponse,
|
||||
} from 'nx/src/command-line/graph/graph';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { AppConfig } from './app/interfaces';
|
||||
import { ExternalApi } from './app/external-api';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils';
|
||||
import { AppConfig, ExternalApi } from '@nx/graph/shared';
|
||||
|
||||
export declare global {
|
||||
export interface Window {
|
||||
interface Window {
|
||||
exclude: string[];
|
||||
watch: boolean;
|
||||
localMode: 'serve' | 'build';
|
||||
projectGraphResponse?: ProjectGraphClientResponse;
|
||||
taskGraphResponse?: TaskGraphClientResponse;
|
||||
expandedTaskInputsResponse?: ExpandedTaskInputsReponse;
|
||||
sourceMapsResponse?: ConfigurationSourceMaps;
|
||||
sourceMapsResponse?: Record<string, Record<string, string[]>>;
|
||||
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
||||
appConfig: AppConfig;
|
||||
useXstateInspect: boolean;
|
||||
externalApi?: ExternalApi;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cytoscape' {
|
||||
interface Core {
|
||||
anywherePanning: Function;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { inspect } from '@xstate/inspect';
|
||||
import { App } from './app/app';
|
||||
import { ExternalApi } from './app/external-api';
|
||||
import { ExternalApiImpl } from './app/external-api-impl';
|
||||
import { render } from 'preact';
|
||||
|
||||
if (window.useXstateInspect === true) {
|
||||
@ -11,7 +11,7 @@ if (window.useXstateInspect === true) {
|
||||
});
|
||||
}
|
||||
|
||||
window.externalApi = new ExternalApi();
|
||||
window.externalApi = new ExternalApiImpl();
|
||||
const container = document.getElementById('app');
|
||||
|
||||
if (!window.appConfig) {
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from './lib/project-details';
|
||||
export * from './lib/project-details-page';
|
||||
|
||||
288
graph/project-details/src/lib/json-line-renderer.tsx
Normal file
288
graph/project-details/src/lib/json-line-renderer.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
EyeIcon,
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { getSourceInformation } from './get-source-information';
|
||||
import useMapState from './use-map-state';
|
||||
import {
|
||||
getExternalApiService,
|
||||
useEnvironmentConfig,
|
||||
useRouteConstructor,
|
||||
} from '@nx/graph/shared';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { get } from 'http';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface JsonLineRendererProps {
|
||||
jsonData: any;
|
||||
sourceMap: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function JsonLineRenderer(props: JsonLineRendererProps) {
|
||||
let collapsibleSections = new Map<number, number>();
|
||||
let lines: [string, number][] = [];
|
||||
let currentLine = 0;
|
||||
let lineToPropertyPathMap = new Map<number, string>();
|
||||
let lineToInteractionMap = new Map<
|
||||
number,
|
||||
{ target: string; configuration?: string }
|
||||
>();
|
||||
|
||||
const [getCollapsed, setCollapsed] = useMapState<number, boolean>();
|
||||
const { environment } = useEnvironmentConfig();
|
||||
const externalApiService = getExternalApiService();
|
||||
const navigate = useNavigate();
|
||||
const routeContructor = useRouteConstructor();
|
||||
|
||||
function add(value: string, depth: number) {
|
||||
if (lines.length === currentLine) {
|
||||
lines.push(['', depth]);
|
||||
}
|
||||
lines[currentLine] = [lines[currentLine][0] + value, depth];
|
||||
}
|
||||
|
||||
function processJson(
|
||||
jsonData: any,
|
||||
depth = 0,
|
||||
propertyPath = '',
|
||||
isLast = false
|
||||
) {
|
||||
if (Array.isArray(jsonData)) {
|
||||
const sectionStart = currentLine;
|
||||
add('[', depth);
|
||||
currentLine++;
|
||||
|
||||
jsonData.forEach((value, index) => {
|
||||
const newPropertyPath = `${
|
||||
propertyPath ? propertyPath + '.' : ''
|
||||
}${value}`;
|
||||
lineToPropertyPathMap.set(currentLine, newPropertyPath);
|
||||
|
||||
processJson(
|
||||
value,
|
||||
depth + 1,
|
||||
newPropertyPath,
|
||||
index === jsonData.length - 1
|
||||
);
|
||||
});
|
||||
|
||||
add(']', depth);
|
||||
if (!isLast) {
|
||||
add(',', depth);
|
||||
}
|
||||
const sectionEnd = currentLine;
|
||||
collapsibleSections.set(sectionStart, sectionEnd);
|
||||
currentLine++;
|
||||
} else if (jsonData && typeof jsonData === 'object') {
|
||||
const sectionStart = currentLine;
|
||||
add('{', depth);
|
||||
currentLine++;
|
||||
|
||||
Object.entries(jsonData).forEach(([key, value], index, array) => {
|
||||
// skip empty objects
|
||||
if (
|
||||
Object.keys(value as any).length === 0 &&
|
||||
typeof value === 'object'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip certain root properties
|
||||
if (
|
||||
depth === 0 &&
|
||||
(key === 'sourceRoot' ||
|
||||
key === 'name' ||
|
||||
key === '$schema' ||
|
||||
key === 'tags')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
add(`"${key}": `, depth);
|
||||
|
||||
if (propertyPath === 'targets') {
|
||||
lineToInteractionMap.set(currentLine, { target: key });
|
||||
}
|
||||
if (propertyPath.match(/^targets\..*configurations$/)) {
|
||||
lineToInteractionMap.set(currentLine, {
|
||||
target: propertyPath.split('.')[1],
|
||||
configuration: key,
|
||||
});
|
||||
}
|
||||
|
||||
const newPropertyPath = `${
|
||||
propertyPath ? propertyPath + '.' : ''
|
||||
}${key}`;
|
||||
lineToPropertyPathMap.set(currentLine, newPropertyPath);
|
||||
|
||||
processJson(
|
||||
value,
|
||||
depth + 1,
|
||||
newPropertyPath,
|
||||
index === array.length - 1
|
||||
);
|
||||
});
|
||||
|
||||
add('}', depth);
|
||||
if (!isLast) {
|
||||
add(',', depth);
|
||||
}
|
||||
const sectionEnd = currentLine;
|
||||
collapsibleSections.set(sectionStart, sectionEnd);
|
||||
currentLine++;
|
||||
} else {
|
||||
add(`"${jsonData}"`, depth);
|
||||
if (!isLast) {
|
||||
add(',', depth);
|
||||
}
|
||||
currentLine++;
|
||||
}
|
||||
}
|
||||
|
||||
processJson(props.jsonData);
|
||||
|
||||
console.log(lineToInteractionMap);
|
||||
// start off with all targets & configurations collapsed~
|
||||
useEffect(() => {
|
||||
for (const line of lineToInteractionMap.keys()) {
|
||||
if (!getCollapsed(line)) {
|
||||
setCollapsed(line, true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
function toggleCollapsed(index: number) {
|
||||
setCollapsed(index, !getCollapsed(index));
|
||||
}
|
||||
|
||||
function lineIsCollapsed(index: number) {
|
||||
for (const [start, end] of collapsibleSections) {
|
||||
if (index > start && index < end) {
|
||||
if (getCollapsed(start)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function runTarget({
|
||||
target,
|
||||
configuration,
|
||||
}: {
|
||||
target: string;
|
||||
configuration?: string;
|
||||
}) {
|
||||
const projectName = props.jsonData.name;
|
||||
|
||||
externalApiService.postEvent({
|
||||
type: 'run-task',
|
||||
payload: { taskId: `${projectName}:${target}` },
|
||||
});
|
||||
}
|
||||
|
||||
function viewInTaskGraph({
|
||||
target,
|
||||
configuration,
|
||||
}: {
|
||||
target: string;
|
||||
configuration?: string;
|
||||
}) {
|
||||
const projectName = props.jsonData.name;
|
||||
if (environment === 'nx-console') {
|
||||
externalApiService.postEvent({
|
||||
type: 'open-task-graph',
|
||||
payload: {
|
||||
projectName: projectName,
|
||||
targetName: target,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate(
|
||||
routeContructor(
|
||||
{
|
||||
pathname: `/tasks/${encodeURIComponent(target)}`,
|
||||
search: `?projects=${encodeURIComponent(projectName)}`,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-auto w-full h-full flex">
|
||||
<div className="h-fit min-h-full w-12 shrink-0 pr-2 border-solid border-r-2 border-slate-700">
|
||||
{lines.map(([text, indentation], index) => {
|
||||
if (
|
||||
lineIsCollapsed(index) ||
|
||||
index === 0 ||
|
||||
index === lines.length - 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const canCollapse =
|
||||
collapsibleSections.has(index) &&
|
||||
collapsibleSections.get(index)! - index > 1;
|
||||
const interaction = lineToInteractionMap.get(index);
|
||||
return (
|
||||
<div className="flex justify-end items-center h-6">
|
||||
{interaction?.target && !interaction?.configuration && (
|
||||
<EyeIcon
|
||||
className="h-4 w-4"
|
||||
onClick={() => viewInTaskGraph(interaction!)}
|
||||
/>
|
||||
)}
|
||||
{environment === 'nx-console' && interaction?.target && (
|
||||
<PlayIcon
|
||||
className="h-4 w-4"
|
||||
onClick={() => runTarget(interaction!)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canCollapse && (
|
||||
<div onClick={() => toggleCollapsed(index)} className="h-4 w-4">
|
||||
{getCollapsed(index) ? (
|
||||
<ChevronRightIcon />
|
||||
) : (
|
||||
<ChevronDownIcon />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
{lines.map(([text, indentation], index) => {
|
||||
if (
|
||||
lineIsCollapsed(index) ||
|
||||
index === 0 ||
|
||||
index === lines.length - 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const propertyPathAtLine = lineToPropertyPathMap.get(index);
|
||||
const sourceInformation = propertyPathAtLine
|
||||
? getSourceInformation(props.sourceMap, propertyPathAtLine)
|
||||
: '';
|
||||
return (
|
||||
<pre
|
||||
style={{ paddingLeft: `${indentation}rem` }}
|
||||
className="group truncate hover:bg-slate-800 h-6"
|
||||
>
|
||||
{text}
|
||||
{getCollapsed(index) ? '...' : ''}
|
||||
|
||||
<span className="ml-16 hidden group-hover:inline-block text-sm text-slate-500">
|
||||
{sourceInformation}
|
||||
</span>
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
graph/project-details/src/lib/project-details-page.tsx
Normal file
16
graph/project-details/src/lib/project-details-page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphProjectNode } from '@nx/devkit';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import ProjectDetails from './project-details';
|
||||
|
||||
export function ProjectDetailsPage() {
|
||||
const { project, sourceMap } = useRouteLoaderData(
|
||||
'selectedProjectDetails'
|
||||
) as {
|
||||
project: ProjectGraphProjectNode;
|
||||
sourceMap: Record<string, string[]>;
|
||||
};
|
||||
|
||||
return ProjectDetails({ project, sourceMap });
|
||||
}
|
||||
@ -1,29 +1,86 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import styles from './app.module.css';
|
||||
import Target from './target';
|
||||
|
||||
import PropertyRenderer from './property-renderer';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import { ProjectGraphProjectNode } from '@nx/devkit';
|
||||
|
||||
export function ProjectDetails() {
|
||||
const {
|
||||
project: {
|
||||
name,
|
||||
data: { targets, root, ...projectData },
|
||||
},
|
||||
sourceMap,
|
||||
} = useRouteLoaderData('selectedProjectDetails') as {
|
||||
project: ProjectGraphProjectNode;
|
||||
sourceMap: Record<string, string[]>;
|
||||
import {
|
||||
getExternalApiService,
|
||||
useEnvironmentConfig,
|
||||
useRouteConstructor,
|
||||
} from '@nx/graph/shared';
|
||||
import { JsonLineRenderer } from './json-line-renderer';
|
||||
import { EyeIcon } from '@heroicons/react/24/outline';
|
||||
import PropertyRenderer from './property-renderer';
|
||||
import Target from './target';
|
||||
|
||||
export interface ProjectDetailsProps {
|
||||
project: ProjectGraphProjectNode;
|
||||
sourceMap: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function ProjectDetails({
|
||||
project: {
|
||||
name,
|
||||
data: { root, ...projectData },
|
||||
},
|
||||
sourceMap,
|
||||
}: ProjectDetailsProps) {
|
||||
const { environment } = useEnvironmentConfig();
|
||||
const externalApiService = getExternalApiService();
|
||||
const navigate = useNavigate();
|
||||
const routeContructor = useRouteConstructor();
|
||||
|
||||
const viewInProjectGraph = () => {
|
||||
if (environment === 'nx-console') {
|
||||
externalApiService.postEvent({
|
||||
type: 'open-project-graph',
|
||||
payload: {
|
||||
projectName: name,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate(routeContructor(`/projects/${encodeURIComponent(name)}`, true));
|
||||
}
|
||||
};
|
||||
|
||||
// const projectDataSorted = sortObjectWithTargetsFirst(projectData);
|
||||
// return (
|
||||
// <div className="flex flex-col w-full h-full">
|
||||
// <div className="flex">
|
||||
// <div className="w-12 pr-2 border-r-2 border-solid border-slate-700">
|
||||
// <EyeIcon
|
||||
// className="h-6 w-6 ml-3 mt-3"
|
||||
// onClick={viewInProjectGraph}
|
||||
// ></EyeIcon>
|
||||
// </div>
|
||||
// <div className="pl-6 pb-6">
|
||||
// <h1 className="text-4xl flex items-center">
|
||||
// <span>{name}</span>
|
||||
// </h1>
|
||||
// <div className="flex gap-2">
|
||||
// <span className="text-slate-500 text-xl"> {root}</span>
|
||||
|
||||
// {projectData.tags?.map((tag) => (
|
||||
// <div className="dark:bg-sky-500 text-white rounded px-1">
|
||||
// {tag}
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// {JsonLineRenderer({ jsonData: projectDataSorted, sourceMap })}
|
||||
// </div>
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="m-4 overflow-auto w-full">
|
||||
<h1 className="text-2xl">{name}</h1>
|
||||
<h1 className="text-2xl flex items-center gap-2">
|
||||
{name}{' '}
|
||||
<EyeIcon className="h-5 w-5" onClick={viewInProjectGraph}></EyeIcon>
|
||||
</h1>
|
||||
<h2 className="text-lg pl-6 mb-3 flex flex-row gap-2">
|
||||
{root}{' '}
|
||||
{projectData.tags?.map((tag) => (
|
||||
@ -33,12 +90,14 @@ export function ProjectDetails() {
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<h2 className="text-xl">Targets</h2>
|
||||
{Object.entries(targets ?? {}).map(([targetName, target]) =>
|
||||
Target({
|
||||
targetName: targetName,
|
||||
targetConfiguration: target,
|
||||
sourceMap,
|
||||
})
|
||||
{Object.entries(projectData.targets ?? {}).map(
|
||||
([targetName, target]) =>
|
||||
Target({
|
||||
projectName: name,
|
||||
targetName: targetName,
|
||||
targetConfiguration: target,
|
||||
sourceMap,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{Object.entries(projectData).map(([key, value]) => {
|
||||
@ -48,7 +107,8 @@ export function ProjectDetails() {
|
||||
key === 'name' ||
|
||||
key === '$schema' ||
|
||||
key === 'tags' ||
|
||||
key === 'files'
|
||||
key === 'files' ||
|
||||
key === 'sourceRoot'
|
||||
)
|
||||
return undefined;
|
||||
|
||||
@ -63,4 +123,22 @@ export function ProjectDetails() {
|
||||
);
|
||||
}
|
||||
|
||||
// function sortObjectWithTargetsFirst(obj: any) {
|
||||
// let sortedObj: any = {};
|
||||
|
||||
// // If 'targets' exists, set it as the first property
|
||||
// if (obj.hasOwnProperty('targets')) {
|
||||
// sortedObj.targets = obj.targets;
|
||||
// }
|
||||
|
||||
// // Copy the rest of the properties
|
||||
// for (let key in obj) {
|
||||
// if (key !== 'targets') {
|
||||
// sortedObj[key] = obj[key];
|
||||
// }
|
||||
// }
|
||||
|
||||
// return sortedObj;
|
||||
// }
|
||||
|
||||
export default ProjectDetails;
|
||||
|
||||
@ -20,14 +20,23 @@ export function PropertyRenderer(props: PropertyRendererProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div title={getSourceInformation(sourceMap, sourceMapKey)}>
|
||||
{isCollapsible && (
|
||||
<button className="text-xs" onClick={toggleCollapse}>
|
||||
{isCollapsed ? '\u25B6' : '\u25BC'}
|
||||
</button>
|
||||
)}
|
||||
<span className="font-medium">{propertyKey}</span>:{' '}
|
||||
{renderOpening(propertyValue)}
|
||||
<div
|
||||
title={getSourceInformation(sourceMap, sourceMapKey)}
|
||||
className={!isCollapsible ? 'pl-4 relative' : 'relative'}
|
||||
>
|
||||
<span>
|
||||
{isCollapsible && (
|
||||
<button className="text-xs w-4" onClick={toggleCollapse}>
|
||||
{isCollapsed ? '\u25B6' : '\u25BC'}
|
||||
</button>
|
||||
)}
|
||||
<span className="font-medium">
|
||||
{propertyKey}
|
||||
<div className="absolute top-0 left-0 w-full bg-grey-500 z-10"></div>
|
||||
</span>
|
||||
: {renderOpening(propertyValue)}
|
||||
</span>
|
||||
|
||||
{!isCollapsed || !isCollapsible ? (
|
||||
<PropertyValueRenderer {...props} />
|
||||
) : (
|
||||
|
||||
@ -1,22 +1,123 @@
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
// nx-ignore-next-line
|
||||
import {
|
||||
EyeIcon,
|
||||
PencilSquareIcon,
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
// nx-ignore-next-line
|
||||
import { TargetConfiguration } from '@nx/devkit';
|
||||
import {
|
||||
getExternalApiService,
|
||||
useEnvironmentConfig,
|
||||
useRouteConstructor,
|
||||
} from '@nx/graph/shared';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PropertyRenderer from './property-renderer';
|
||||
import { useState } from 'react';
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface TargetProps {
|
||||
projectName: string;
|
||||
targetName: string;
|
||||
targetConfiguration: TargetConfiguration;
|
||||
sourceMap: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function Target(props: TargetProps) {
|
||||
const { environment } = useEnvironmentConfig();
|
||||
const externalApiService = getExternalApiService();
|
||||
const navigate = useNavigate();
|
||||
const routeContructor = useRouteConstructor();
|
||||
|
||||
const runTarget = () => {
|
||||
externalApiService.postEvent({
|
||||
type: 'run-task',
|
||||
payload: { taskId: `${props.projectName}:${props.targetName}` },
|
||||
});
|
||||
};
|
||||
|
||||
const viewInTaskGraph = () => {
|
||||
if (environment === 'nx-console') {
|
||||
externalApiService.postEvent({
|
||||
type: 'open-task-graph',
|
||||
payload: {
|
||||
projectName: props.projectName,
|
||||
targetName: props.targetName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate(
|
||||
routeContructor(
|
||||
{
|
||||
pathname: `/tasks/${encodeURIComponent(props.targetName)}`,
|
||||
search: `?projects=${encodeURIComponent(props.projectName)}`,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const overrideTarget = () => {
|
||||
externalApiService.postEvent({
|
||||
type: 'override-target',
|
||||
payload: {
|
||||
projectName: props.projectName,
|
||||
targetName: props.targetName,
|
||||
targetConfigString: JSON.stringify(props.targetConfiguration),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const shouldDisplayOverrideTarget = () => {
|
||||
return (
|
||||
environment === 'nx-console' &&
|
||||
Object.entries(props.sourceMap ?? {})
|
||||
.filter(([key]) => key.startsWith(`targets.${props.targetName}`))
|
||||
.every(([, value]) => value[1] !== 'nx-core-build-project-json-nodes')
|
||||
);
|
||||
};
|
||||
|
||||
const targetConfigurationSortedAndFiltered = Object.entries(
|
||||
props.targetConfiguration
|
||||
)
|
||||
.filter(([, value]) => {
|
||||
return (
|
||||
value &&
|
||||
(Array.isArray(value) ? value.length : true) &&
|
||||
(typeof value === 'object' ? Object.keys(value).length : true)
|
||||
);
|
||||
})
|
||||
.sort(([a], [b]) => {
|
||||
const order = ['executor', 'inputs', 'outputs'];
|
||||
const indexA = order.indexOf(a);
|
||||
const indexB = order.indexOf(b);
|
||||
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB;
|
||||
} else if (indexA !== -1) {
|
||||
return -1;
|
||||
} else if (indexB !== -1) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="ml-3 mb-3">
|
||||
<h3 className="text-lg font-bold">{props.targetName}</h3>
|
||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||
{props.targetName}{' '}
|
||||
{environment === 'nx-console' && (
|
||||
<PlayIcon className="h-5 w-5" onClick={runTarget} />
|
||||
)}
|
||||
<EyeIcon className="h-5 w-5" onClick={viewInTaskGraph}></EyeIcon>
|
||||
{shouldDisplayOverrideTarget() && (
|
||||
<PencilSquareIcon className="h-5 w-5" onClick={overrideTarget} />
|
||||
)}
|
||||
</h3>
|
||||
<div className="ml-3">
|
||||
{Object.entries(props.targetConfiguration).map(([key, value]) =>
|
||||
{targetConfigurationSortedAndFiltered.map(([key, value]) =>
|
||||
PropertyRenderer({
|
||||
propertyKey: key,
|
||||
propertyValue: value,
|
||||
|
||||
21
graph/project-details/src/lib/use-map-state.ts
Normal file
21
graph/project-details/src/lib/use-map-state.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
function useMapState<K, V>(initialMap: Map<K, V> = new Map()) {
|
||||
const [map, setMap] = useState(new Map(initialMap));
|
||||
|
||||
// Function to set a key-value pair in the map
|
||||
const setKey = useCallback((key: K, value: V) => {
|
||||
setMap((prevMap) => {
|
||||
const newMap = new Map(prevMap);
|
||||
newMap.set(key, value);
|
||||
return newMap;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Function to get a value by key from the map
|
||||
const getKey = useCallback((key: K) => map.get(key), [map]);
|
||||
|
||||
return [getKey, setKey] as const;
|
||||
}
|
||||
|
||||
export default useMapState;
|
||||
@ -4,7 +4,6 @@
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"node",
|
||||
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts"
|
||||
]
|
||||
|
||||
12
graph/shared/.babelrc
Normal file
12
graph/shared/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
18
graph/shared/.eslintrc.json
Normal file
18
graph/shared/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
graph/shared/README.md
Normal file
7
graph/shared/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# graph-shared
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test graph-shared` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
9
graph/shared/jest.config.ts
Normal file
9
graph/shared/jest.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'graph-shared',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
};
|
||||
21
graph/shared/project.json
Normal file
21
graph/shared/project.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "graph-shared",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "graph/shared/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"]
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "graph/shared/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
graph/shared/src/globals.d.ts
vendored
Normal file
25
graph/shared/src/globals.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
5
graph/shared/src/index.ts
Normal file
5
graph/shared/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
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';
|
||||
15
graph/shared/src/lib/app-config.ts
Normal file
15
graph/shared/src/lib/app-config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
24
graph/shared/src/lib/external-api-service.ts
Normal file
24
graph/shared/src/lib/external-api-service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
40
graph/shared/src/lib/external-api.ts
Normal file
40
graph/shared/src/lib/external-api.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/* 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 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;
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
import type { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
||||
/* eslint-enable @nx/enforce-module-boundaries */
|
||||
import { useRef } from 'react';
|
||||
import { AppConfig } from '../interfaces';
|
||||
import { AppConfig } from './app-config';
|
||||
|
||||
export function useEnvironmentConfig(): {
|
||||
exclude: string[];
|
||||
42
graph/shared/src/lib/use-route-constructor.ts
Normal file
42
graph/shared/src/lib/use-route-constructor.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { To, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getEnvironmentConfig } from './use-environment-config';
|
||||
|
||||
export const useRouteConstructor = (): ((
|
||||
to: To,
|
||||
retainSearchParams: boolean
|
||||
) => To) => {
|
||||
const { environment } = getEnvironmentConfig();
|
||||
const { selectedWorkspaceId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (to: To, retainSearchParams: true) => {
|
||||
let pathname = '';
|
||||
|
||||
if (typeof to === 'object') {
|
||||
if (environment === 'dev') {
|
||||
pathname = `/${selectedWorkspaceId}${to.pathname}`;
|
||||
} else {
|
||||
pathname = to.pathname;
|
||||
}
|
||||
return {
|
||||
...to,
|
||||
pathname,
|
||||
search: to.search
|
||||
? to.search.toString()
|
||||
: retainSearchParams
|
||||
? searchParams.toString()
|
||||
: '',
|
||||
};
|
||||
} else if (typeof to === 'string') {
|
||||
if (environment === 'dev') {
|
||||
pathname = `/${selectedWorkspaceId}${to}`;
|
||||
} else {
|
||||
pathname = to;
|
||||
}
|
||||
return {
|
||||
pathname,
|
||||
search: retainSearchParams ? searchParams.toString() : '',
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
19
graph/shared/tsconfig.json
Normal file
19
graph/shared/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
24
graph/shared/tsconfig.lib.json
Normal file
24
graph/shared/tsconfig.lib.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"node",
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts"
|
||||
],
|
||||
"lib": ["dom"]
|
||||
},
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.test.jsx"
|
||||
],
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
20
graph/shared/tsconfig.spec.json
Normal file
20
graph/shared/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@ -32,28 +32,9 @@ interface BackgroundClickEvent {
|
||||
type: 'BackgroundClick';
|
||||
}
|
||||
|
||||
interface FileLinkClickEvent {
|
||||
type: 'FileLinkClick';
|
||||
sourceRoot: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface ProjectOpenConfigClickEvent {
|
||||
type: 'ProjectOpenConfigClick';
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface RunTaskClickEvent {
|
||||
type: 'RunTaskClick';
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export type GraphInteractionEvents =
|
||||
| ProjectNodeClickEvent
|
||||
| EdgeClickEvent
|
||||
| GraphRegeneratedEvent
|
||||
| TaskNodeClickEvent
|
||||
| BackgroundClickEvent
|
||||
| FileLinkClickEvent
|
||||
| ProjectOpenConfigClickEvent
|
||||
| RunTaskClickEvent;
|
||||
| BackgroundClickEvent;
|
||||
|
||||
@ -7,9 +7,11 @@ import {
|
||||
} from '@nx/graph/ui-tooltips';
|
||||
import { TooltipEvent } from './interfaces';
|
||||
import { GraphInteractionEvents } from './graph-interaction-events';
|
||||
import { getExternalApiService } from '@nx/graph/shared';
|
||||
|
||||
export class GraphTooltipService {
|
||||
private subscribers: Set<Function> = new Set();
|
||||
private externalApiService = getExternalApiService();
|
||||
|
||||
constructor(graph: GraphService) {
|
||||
graph.listen((event: GraphInteractionEvents) => {
|
||||
@ -24,9 +26,11 @@ export class GraphTooltipService {
|
||||
const openConfigCallback =
|
||||
graph.renderMode === 'nx-console'
|
||||
? () =>
|
||||
graph.broadcast({
|
||||
type: 'ProjectOpenConfigClick',
|
||||
projectName: event.data.id,
|
||||
this.externalApiService.postEvent({
|
||||
type: 'open-project-config',
|
||||
payload: {
|
||||
projectName: event.data.id,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
this.openProjectNodeToolTip(event.ref, {
|
||||
@ -41,9 +45,11 @@ export class GraphTooltipService {
|
||||
const runTaskCallback =
|
||||
graph.renderMode === 'nx-console'
|
||||
? () =>
|
||||
graph.broadcast({
|
||||
type: 'RunTaskClick',
|
||||
taskId: event.data.id,
|
||||
this.externalApiService.postEvent({
|
||||
type: 'run-task',
|
||||
payload: {
|
||||
taskId: event.data.id,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
this.openTaskNodeTooltip(event.ref, {
|
||||
@ -69,10 +75,12 @@ export class GraphTooltipService {
|
||||
const callback =
|
||||
graph.renderMode === 'nx-console'
|
||||
? (url) =>
|
||||
graph.broadcast({
|
||||
type: 'FileLinkClick',
|
||||
sourceRoot: event.data.sourceRoot,
|
||||
file: url,
|
||||
this.externalApiService.postEvent({
|
||||
type: 'file-click',
|
||||
payload: {
|
||||
sourceRoot: event.data.sourceRoot,
|
||||
file: url,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
this.openEdgeToolTip(event.ref, {
|
||||
|
||||
@ -523,6 +523,8 @@ async function startServer(
|
||||
currentProjectGraphClientResponse.groupByFolder = groupByFolder;
|
||||
currentProjectGraphClientResponse.exclude = exclude;
|
||||
|
||||
currentSourceMapsClientResponse = sourceMapResponse;
|
||||
|
||||
const app = http.createServer(async (req, res) => {
|
||||
// parse URL
|
||||
const parsedUrl = new URL(req.url, `http://${host}:${port}`);
|
||||
@ -531,6 +533,8 @@ async function startServer(
|
||||
// e.g curl --path-as-is http://localhost:9000/../fileInDanger.txt
|
||||
// by limiting the path to current directory only
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
const sanitizePath = basename(parsedUrl.pathname);
|
||||
if (sanitizePath === 'project-graph.json') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@ -660,7 +664,8 @@ function createFileWatcher() {
|
||||
|
||||
if (
|
||||
projectGraphClientResponse.hash !==
|
||||
currentProjectGraphClientResponse.hash
|
||||
currentProjectGraphClientResponse.hash &&
|
||||
sourceMapResponse
|
||||
) {
|
||||
output.note({ title: 'Graph changes updated.' });
|
||||
|
||||
@ -695,7 +700,7 @@ async function createProjectGraphAndSourceMapClientResponse(
|
||||
const dependencies = graph.dependencies;
|
||||
|
||||
const hasher = createHash('sha256');
|
||||
hasher.update(JSON.stringify({ layout, projects, dependencies }));
|
||||
hasher.update(JSON.stringify({ layout, projects, dependencies, sourceMaps }));
|
||||
|
||||
const hash = hasher.digest('hex');
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
"@nx/expo/*": ["packages/expo/*"],
|
||||
"@nx/express": ["packages/express"],
|
||||
"@nx/graph/project-details": ["graph/project-details/src/index.ts"],
|
||||
"@nx/graph/shared": ["graph/shared/src/index.ts"],
|
||||
"@nx/graph/ui-components": ["graph/ui-components/src/index.ts"],
|
||||
"@nx/graph/ui-graph": ["graph/ui-graph/src/index.ts"],
|
||||
"@nx/graph/ui-tooltips": ["graph/ui-tooltips/src/index.ts"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user