feat(graph): add nx console data loader (#20744)

This commit is contained in:
MaxKless 2024-01-11 21:42:57 +01:00 committed by GitHub
parent b60ae51100
commit b97c869279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1144 additions and 268 deletions

View 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;
}
}

View File

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

View File

@ -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[] = [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View 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>
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>
);
}

View 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 });
}

View File

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

View File

@ -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} />
) : (

View File

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

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

View File

@ -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
View File

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

View File

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

7
graph/shared/README.md Normal file
View 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).

View 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
View 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
View 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;
}
}

View 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';

View 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;
}

View 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);
}
}

View 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;
}

View File

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

View 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() : '',
};
}
};
};

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

View 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"]
}

View 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"
]
}

View File

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

View File

@ -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, {

View File

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

View File

@ -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"],