feat(graph): display expanded task inputs (#19597)
This commit is contained in:
parent
92e05003cc
commit
c727a22530
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ jest.debug.config.js
|
|||||||
/graph/client/src/assets/dev/environment.js
|
/graph/client/src/assets/dev/environment.js
|
||||||
/graph/client/src/assets/generated-project-graphs
|
/graph/client/src/assets/generated-project-graphs
|
||||||
/graph/client/src/assets/generated-task-graphs
|
/graph/client/src/assets/generated-task-graphs
|
||||||
|
/graph/client/src/assets/generated-task-inputs
|
||||||
/nx-dev/nx-dev/public/documentation
|
/nx-dev/nx-dev/public/documentation
|
||||||
/nx-dev/nx-dev/public/images/open-graph
|
/nx-dev/nx-dev/public/images/open-graph
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { parseJson } from '@nx/devkit';
|
||||||
import {
|
import {
|
||||||
checkFilesExist,
|
checkFilesExist,
|
||||||
cleanupProject,
|
cleanupProject,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
setMaxWorkers,
|
setMaxWorkers,
|
||||||
uniq,
|
uniq,
|
||||||
updateFile,
|
updateFile,
|
||||||
|
readFile,
|
||||||
updateJson,
|
updateJson,
|
||||||
} from '@nx/e2e/utils';
|
} from '@nx/e2e/utils';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@ -321,4 +323,108 @@ describe('Extra Nx Misc Tests', () => {
|
|||||||
expect(unitTestsOutput).toContain('Successfully ran target test');
|
expect(unitTestsOutput).toContain('Successfully ran target test');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('task graph inputs', () => {
|
||||||
|
const readExpandedTaskInputResponse = (): Record<
|
||||||
|
string,
|
||||||
|
Record<string, string[]>
|
||||||
|
> =>
|
||||||
|
parseJson(
|
||||||
|
readFile('static/environment.js').match(
|
||||||
|
/window\.expandedTaskInputsResponse\s*=\s*(.*?);/
|
||||||
|
)[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseLib = 'lib-base-123';
|
||||||
|
beforeAll(() => {
|
||||||
|
runCLI(`generate @nx/js:lib ${baseLib}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly expand default task inputs', () => {
|
||||||
|
runCLI('graph --file=graph.html');
|
||||||
|
|
||||||
|
expect(readExpandedTaskInputResponse()[`${baseLib}:build`])
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"external": [
|
||||||
|
"npm:@nx/js",
|
||||||
|
"npm:tslib",
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
".gitignore",
|
||||||
|
"nx.json",
|
||||||
|
],
|
||||||
|
"lib-base-123": [
|
||||||
|
"libs/lib-base-123/README.md",
|
||||||
|
"libs/lib-base-123/package.json",
|
||||||
|
"libs/lib-base-123/project.json",
|
||||||
|
"libs/lib-base-123/src/index.ts",
|
||||||
|
"libs/lib-base-123/src/lib/lib-base-123.ts",
|
||||||
|
"libs/lib-base-123/tsconfig.json",
|
||||||
|
"libs/lib-base-123/tsconfig.lib.json",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly expand dependent task inputs', () => {
|
||||||
|
const dependentLib = 'lib-dependent-123';
|
||||||
|
runCLI(`generate @nx/js:lib ${dependentLib}`);
|
||||||
|
|
||||||
|
updateJson(join('libs', baseLib, 'project.json'), (config) => {
|
||||||
|
config.targets['build'].inputs = ['default', '^default'];
|
||||||
|
config.implicitDependencies = [dependentLib];
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateJson('nx.json', (json) => {
|
||||||
|
json.namedInputs = {
|
||||||
|
...json.namedInputs,
|
||||||
|
default: ['{projectRoot}/**/*'],
|
||||||
|
};
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
runCLI('graph --file=graph.html');
|
||||||
|
|
||||||
|
expect(readExpandedTaskInputResponse()[`${baseLib}:build`])
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"external": [
|
||||||
|
"npm:@nx/js",
|
||||||
|
"npm:tslib",
|
||||||
|
],
|
||||||
|
"general": [
|
||||||
|
".gitignore",
|
||||||
|
"nx.json",
|
||||||
|
],
|
||||||
|
"lib-base-123": [
|
||||||
|
"libs/lib-base-123/.eslintrc.json",
|
||||||
|
"libs/lib-base-123/README.md",
|
||||||
|
"libs/lib-base-123/jest.config.ts",
|
||||||
|
"libs/lib-base-123/package.json",
|
||||||
|
"libs/lib-base-123/project.json",
|
||||||
|
"libs/lib-base-123/src/index.ts",
|
||||||
|
"libs/lib-base-123/src/lib/lib-base-123.spec.ts",
|
||||||
|
"libs/lib-base-123/src/lib/lib-base-123.ts",
|
||||||
|
"libs/lib-base-123/tsconfig.json",
|
||||||
|
"libs/lib-base-123/tsconfig.lib.json",
|
||||||
|
"libs/lib-base-123/tsconfig.spec.json",
|
||||||
|
],
|
||||||
|
"lib-dependent-123": [
|
||||||
|
"libs/lib-dependent-123/.eslintrc.json",
|
||||||
|
"libs/lib-dependent-123/README.md",
|
||||||
|
"libs/lib-dependent-123/jest.config.ts",
|
||||||
|
"libs/lib-dependent-123/package.json",
|
||||||
|
"libs/lib-dependent-123/project.json",
|
||||||
|
"libs/lib-dependent-123/src/index.ts",
|
||||||
|
"libs/lib-dependent-123/src/lib/lib-dependent-123.spec.ts",
|
||||||
|
"libs/lib-dependent-123/src/lib/lib-dependent-123.ts",
|
||||||
|
"libs/lib-dependent-123/tsconfig.json",
|
||||||
|
"libs/lib-dependent-123/tsconfig.lib.json",
|
||||||
|
"libs/lib-dependent-123/tsconfig.spec.json",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1
graph/client-e2e/cypress/downloads/downloads.html
Normal file
1
graph/client-e2e/cypress/downloads/downloads.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Cr24
|
||||||
@ -18,11 +18,12 @@ import {
|
|||||||
getToggleAllButtonForFolder,
|
getToggleAllButtonForFolder,
|
||||||
getUncheckedProjectItems,
|
getUncheckedProjectItems,
|
||||||
getUnfocusProjectButton,
|
getUnfocusProjectButton,
|
||||||
|
openTooltipForNode,
|
||||||
} from '../support/app.po';
|
} from '../support/app.po';
|
||||||
|
|
||||||
import * as affectedJson from '../fixtures/affected.json';
|
import * as affectedJson from '../fixtures/affected.json';
|
||||||
import { testProjectsRoutes, testTaskRoutes } from '../support/routing-tests';
|
import { testProjectsRoutes, testTaskRoutes } from '../support/routing-tests';
|
||||||
import * as nxExamplesJson from '../fixtures/nx-examples-project-graph.json';
|
import * as nxExamplesTaskInputs from '../fixtures/nx-examples-task-inputs.json';
|
||||||
|
|
||||||
describe('dev mode - task graph', () => {
|
describe('dev mode - task graph', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
@ -183,4 +184,54 @@ describe('dev mode - task graph', () => {
|
|||||||
// and also new /projects route
|
// and also new /projects route
|
||||||
testTaskRoutes('browser', ['/e2e/tasks']);
|
testTaskRoutes('browser', ['/e2e/tasks']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('file inputs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept(
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/task-inputs.json*',
|
||||||
|
},
|
||||||
|
async (req) => {
|
||||||
|
// Extract the desired query parameter
|
||||||
|
const taskId = req.url.split('taskId=')[1];
|
||||||
|
// Load the fixture data and find the property based on the query parameter
|
||||||
|
|
||||||
|
const expandedInputs = nxExamplesTaskInputs[taskId];
|
||||||
|
|
||||||
|
// Reply with the selected property
|
||||||
|
req.reply({
|
||||||
|
body: expandedInputs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
).as('getTaskInputs');
|
||||||
|
});
|
||||||
|
it('should display input files', () => {
|
||||||
|
getSelectTargetDropdown().select('build', { force: true });
|
||||||
|
cy.get('[data-project="cart"]').click({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
openTooltipForNode('cart:build');
|
||||||
|
cy.get('[data-cy="inputs-accordion"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="input-list-entry"]').should('have.length', 18);
|
||||||
|
const expectedSections = [
|
||||||
|
'cart-cart-page',
|
||||||
|
'shared-assets',
|
||||||
|
'shared-header',
|
||||||
|
'shared-styles',
|
||||||
|
'External Inputs',
|
||||||
|
];
|
||||||
|
cy.get('[data-cy="input-section-entry"]').each((el, idx) => {
|
||||||
|
expect(el.text()).to.equal(expectedSections[idx]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedHeaderSelector =
|
||||||
|
'[data-cy="input-section-entry"]:contains(shared-header)';
|
||||||
|
cy.get(sharedHeaderSelector).click();
|
||||||
|
cy.get(sharedHeaderSelector)
|
||||||
|
.nextAll('[data-cy="input-list-entry"]')
|
||||||
|
.should('have.length', 9);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
4369
graph/client-e2e/src/fixtures/nx-examples-task-inputs.json
Normal file
4369
graph/client-e2e/src/fixtures/nx-examples-task-inputs.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -45,3 +45,12 @@ export const getToggleAllButtonForFolder = (folderName: string) =>
|
|||||||
|
|
||||||
export const getSelectTargetDropdown = () =>
|
export const getSelectTargetDropdown = () =>
|
||||||
cy.get('[data-cy=selected-target-dropdown]');
|
cy.get('[data-cy=selected-target-dropdown]');
|
||||||
|
|
||||||
|
export const openTooltipForNode = (nodeId: string) =>
|
||||||
|
cy.window().then((window) => {
|
||||||
|
// @ts-ignore - we will access private methods only in this e2e test
|
||||||
|
const pos = window.externalApi.graphService.renderGraph.cy
|
||||||
|
.$(nodeId)
|
||||||
|
.renderedPosition();
|
||||||
|
cy.get('#cytoscape-graph').click(pos.x, pos.y);
|
||||||
|
});
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
"graph/client/src/assets/task-graphs/",
|
"graph/client/src/assets/task-graphs/",
|
||||||
"graph/client/src/assets/generated-project-graphs/",
|
"graph/client/src/assets/generated-project-graphs/",
|
||||||
"graph/client/src/assets/generated-task-graphs/",
|
"graph/client/src/assets/generated-task-graphs/",
|
||||||
|
"graph/client/src/assets/generated-task-inputs/",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/dev",
|
"input": "graph/client/src/assets/dev",
|
||||||
"output": "/",
|
"output": "/",
|
||||||
@ -79,6 +80,7 @@
|
|||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
"graph/client/src/assets/project-graphs/",
|
"graph/client/src/assets/project-graphs/",
|
||||||
"graph/client/src/assets/task-graphs/",
|
"graph/client/src/assets/task-graphs/",
|
||||||
|
"graph/client/src/assets/task-inputs/",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/dev-e2e",
|
"input": "graph/client/src/assets/dev-e2e",
|
||||||
"output": "/",
|
"output": "/",
|
||||||
|
|||||||
@ -250,7 +250,6 @@ export const projectGraphMachine = createMachine<
|
|||||||
setGraph: assign((ctx, event) => {
|
setGraph: assign((ctx, event) => {
|
||||||
if (event.type !== 'setProjects' && event.type !== 'updateGraph')
|
if (event.type !== 'setProjects' && event.type !== 'updateGraph')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ctx.projects = event.projects;
|
ctx.projects = event.projects;
|
||||||
ctx.dependencies = event.dependencies;
|
ctx.dependencies = event.dependencies;
|
||||||
ctx.fileMap = event.fileMap;
|
ctx.fileMap = event.fileMap;
|
||||||
|
|||||||
@ -114,6 +114,7 @@ const mockAppConfig: AppConfig = {
|
|||||||
describe('dep-graph machine', () => {
|
describe('dep-graph machine', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.appConfig = mockAppConfig;
|
window.appConfig = mockAppConfig;
|
||||||
|
window.environment = 'release';
|
||||||
});
|
});
|
||||||
describe('initGraph', () => {
|
describe('initGraph', () => {
|
||||||
it('should set projects, dependencies, and workspaceLayout', () => {
|
it('should set projects, dependencies, and workspaceLayout', () => {
|
||||||
|
|||||||
@ -305,7 +305,6 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
await projectGraphDataService.getProjectGraph(
|
await projectGraphDataService.getProjectGraph(
|
||||||
projectInfo.projectGraphUrl
|
projectInfo.projectGraphUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
projectGraphService.send({
|
projectGraphService.send({
|
||||||
type: 'updateGraph',
|
type: 'updateGraph',
|
||||||
projects: response.projects,
|
projects: response.projects,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { TaskList } from './task-list';
|
|
||||||
import {
|
import {
|
||||||
useNavigate,
|
useNavigate,
|
||||||
useParams,
|
useParams,
|
||||||
useRouteLoaderData,
|
useRouteLoaderData,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
import { TaskList } from './task-list';
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type {
|
import type {
|
||||||
@ -12,14 +12,16 @@ import type {
|
|||||||
TaskGraphClientResponse,
|
TaskGraphClientResponse,
|
||||||
} from 'nx/src/command-line/graph/graph';
|
} from 'nx/src/command-line/graph/graph';
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
import { getGraphService } from '../machines/graph.service';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { getGraphService } from '../machines/graph.service';
|
||||||
import { CheckboxPanel } from '../ui-components/checkbox-panel';
|
import { CheckboxPanel } from '../ui-components/checkbox-panel';
|
||||||
|
|
||||||
import { Dropdown } from '@nx/graph/ui-components';
|
import { Dropdown } from '@nx/graph/ui-components';
|
||||||
import { ShowHideAll } from '../ui-components/show-hide-all';
|
|
||||||
import { useCurrentPath } from '../hooks/use-current-path';
|
import { useCurrentPath } from '../hooks/use-current-path';
|
||||||
|
import { ShowHideAll } from '../ui-components/show-hide-all';
|
||||||
import { createTaskName, useRouteConstructor } from '../util';
|
import { createTaskName, useRouteConstructor } from '../util';
|
||||||
|
import { GraphInteractionEvents } from '@nx/graph/ui-graph';
|
||||||
|
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
|
||||||
|
|
||||||
export function TasksSidebar() {
|
export function TasksSidebar() {
|
||||||
const graphService = getGraphService();
|
const graphService = getGraphService();
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import type {
|
|||||||
import { ProjectGraphService } from './interfaces';
|
import { ProjectGraphService } from './interfaces';
|
||||||
|
|
||||||
export class FetchProjectGraphService implements ProjectGraphService {
|
export class FetchProjectGraphService implements ProjectGraphService {
|
||||||
|
private taskInputsUrl: string;
|
||||||
|
|
||||||
async getHash(): Promise<string> {
|
async getHash(): Promise<string> {
|
||||||
const request = new Request('currentHash', { mode: 'no-cors' });
|
const request = new Request('currentHash', { mode: 'no-cors' });
|
||||||
|
|
||||||
@ -31,4 +33,22 @@ export class FetchProjectGraphService implements ProjectGraphService {
|
|||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTaskInputsUrl(url: string) {
|
||||||
|
this.taskInputsUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExpandedTaskInputs(
|
||||||
|
taskId: string
|
||||||
|
): Promise<Record<string, string[]>> {
|
||||||
|
if (!this.taskInputsUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const request = new Request(`${this.taskInputsUrl}?taskId=${taskId}`, {
|
||||||
|
mode: 'no-cors',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(request);
|
||||||
|
return (await response.json())[taskId];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface WorkspaceData {
|
|||||||
label: string;
|
label: string;
|
||||||
projectGraphUrl: string;
|
projectGraphUrl: string;
|
||||||
taskGraphUrl: string;
|
taskGraphUrl: string;
|
||||||
|
taskInputsUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceLayout {
|
export interface WorkspaceLayout {
|
||||||
@ -22,6 +23,8 @@ export interface ProjectGraphService {
|
|||||||
getHash: () => Promise<string>;
|
getHash: () => Promise<string>;
|
||||||
getProjectGraph: (url: string) => Promise<ProjectGraphClientResponse>;
|
getProjectGraph: (url: string) => Promise<ProjectGraphClientResponse>;
|
||||||
getTaskGraph: (url: string) => Promise<TaskGraphClientResponse>;
|
getTaskGraph: (url: string) => Promise<TaskGraphClientResponse>;
|
||||||
|
setTaskInputsUrl?: (url: string) => void;
|
||||||
|
getExpandedTaskInputs?: (taskId: string) => Promise<Record<string, string[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Environment {
|
export interface Environment {
|
||||||
|
|||||||
@ -19,4 +19,12 @@ export class LocalProjectGraphService implements ProjectGraphService {
|
|||||||
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
||||||
return new Promise((resolve) => resolve(window.taskGraphResponse));
|
return new Promise((resolve) => resolve(window.taskGraphResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getExpandedTaskInputs(
|
||||||
|
taskId: string
|
||||||
|
): Promise<Record<string, string[]>> {
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
resolve(window.expandedTaskInputsResponse[taskId])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import { GraphService } from '@nx/graph/ui-graph';
|
import { GraphService } from '@nx/graph/ui-graph';
|
||||||
import { selectValueByThemeStatic } from '../theme-resolver';
|
import { selectValueByThemeStatic } from '../theme-resolver';
|
||||||
import { getEnvironmentConfig } from '../hooks/use-environment-config';
|
import { getEnvironmentConfig } from '../hooks/use-environment-config';
|
||||||
|
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
|
||||||
|
|
||||||
let graphService: GraphService;
|
let graphService: GraphService;
|
||||||
|
|
||||||
export function getGraphService(): GraphService {
|
export function getGraphService(): GraphService {
|
||||||
const environment = getEnvironmentConfig();
|
const environment = getEnvironmentConfig();
|
||||||
|
|
||||||
if (!graphService) {
|
if (!graphService) {
|
||||||
|
const projectDataService = getProjectGraphDataService();
|
||||||
graphService = new GraphService(
|
graphService = new GraphService(
|
||||||
'cytoscape-graph',
|
'cytoscape-graph',
|
||||||
selectValueByThemeStatic('dark', 'light'),
|
selectValueByThemeStatic('dark', 'light'),
|
||||||
environment.environment === 'nx-console' ? 'nx-console' : undefined
|
environment.environment === 'nx-console' ? 'nx-console' : undefined,
|
||||||
|
'TB',
|
||||||
|
(taskId: string) => projectDataService.getExpandedTaskInputs(taskId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,8 @@ const workspaceDataLoader = async (selectedWorkspaceId: string) => {
|
|||||||
(graph) => graph.id === selectedWorkspaceId
|
(graph) => graph.id === selectedWorkspaceId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
projectGraphDataService.setTaskInputsUrl?.(workspaceInfo.taskInputsUrl);
|
||||||
|
|
||||||
const projectGraph: ProjectGraphClientResponse =
|
const projectGraph: ProjectGraphClientResponse =
|
||||||
await projectGraphDataService.getProjectGraph(
|
await projectGraphDataService.getProjectGraph(
|
||||||
workspaceInfo.projectGraphUrl
|
workspaceInfo.projectGraphUrl
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@nx/graph/ui-tooltips';
|
} from '@nx/graph/ui-tooltips';
|
||||||
import { ProjectNodeActions } from './project-node-actions';
|
import { ProjectNodeActions } from './project-node-actions';
|
||||||
|
import { TaskNodeActions } from './task-node-actions';
|
||||||
|
|
||||||
const tooltipService = getTooltipService();
|
const tooltipService = getTooltipService();
|
||||||
|
|
||||||
@ -29,7 +30,11 @@ export function TooltipDisplay() {
|
|||||||
tooltipToRender = <ProjectEdgeNodeTooltip {...currentTooltip.props} />;
|
tooltipToRender = <ProjectEdgeNodeTooltip {...currentTooltip.props} />;
|
||||||
break;
|
break;
|
||||||
case 'taskNode':
|
case 'taskNode':
|
||||||
tooltipToRender = <TaskNodeTooltip {...currentTooltip.props} />;
|
tooltipToRender = (
|
||||||
|
<TaskNodeTooltip {...currentTooltip.props}>
|
||||||
|
<TaskNodeActions {...currentTooltip.props} />
|
||||||
|
</TaskNodeTooltip>
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
graph/client/src/app/ui-tooltips/task-node-actions.tsx
Normal file
126
graph/client/src/app/ui-tooltips/task-node-actions.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { TaskNodeTooltipProps } from '@nx/graph/ui-tooltips';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function TaskNodeActions(props: TaskNodeTooltipProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [props.id]);
|
||||||
|
const project = props.id.split(':')[0];
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto w-full min-w-[350px] max-w-full rounded-md border border-slate-200 dark:border-slate-800 w-full">
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center w-full bg-slate-50 px-4 py-2 text-xs font-medium uppercase text-slate-500 dark:bg-slate-800 dark:text-slate-400"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
data-cy="inputs-accordion"
|
||||||
|
>
|
||||||
|
<span>Inputs</span>
|
||||||
|
<span>
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronUpIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className={`max-h-[300px] divide-y divide-slate-200 overflow-auto dark:divide-slate-800 ${
|
||||||
|
!isOpen && 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Object.entries(props.inputs ?? {})
|
||||||
|
.sort(compareInputSectionKeys(project))
|
||||||
|
.map(([key, inputs]) => {
|
||||||
|
if (!inputs.length) return undefined;
|
||||||
|
if (key === 'general' || key === project) {
|
||||||
|
return renderInputs(inputs);
|
||||||
|
}
|
||||||
|
if (key === 'external') {
|
||||||
|
return InputAccordion({ section: 'External Inputs', inputs });
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputAccordion({ section: key, inputs });
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputAccordion({ section, inputs }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return [
|
||||||
|
<li
|
||||||
|
key={section}
|
||||||
|
className="flex justify-between items-center whitespace-nowrap px-4 py-2 text-sm font-medium text-slate-800 dark:text-slate-300"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
data-cy="input-section-entry"
|
||||||
|
>
|
||||||
|
<span className="block truncate font-normal font-bold">{section}</span>
|
||||||
|
<span>
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronUpIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</li>,
|
||||||
|
isOpen ? renderInputs(inputs) : undefined,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInputs(inputs: string[]) {
|
||||||
|
return inputs.map((input) => (
|
||||||
|
<li
|
||||||
|
key={input}
|
||||||
|
className="whitespace-nowrap px-4 py-2 text-sm font-medium text-slate-800 dark:text-slate-300"
|
||||||
|
title={input}
|
||||||
|
data-cy="input-list-entry"
|
||||||
|
>
|
||||||
|
<span className="block truncate font-normal">{input}</span>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareInputSectionKeys(project: string) {
|
||||||
|
return ([keya]: [string, string[]], [keyb]: [string, string[]]) => {
|
||||||
|
const first = 'general';
|
||||||
|
const second = project;
|
||||||
|
const last = 'external';
|
||||||
|
|
||||||
|
// Check if 'keya' and/or 'keyb' are one of the special strings
|
||||||
|
if (
|
||||||
|
keya === first ||
|
||||||
|
keya === second ||
|
||||||
|
keya === last ||
|
||||||
|
keyb === first ||
|
||||||
|
keyb === second ||
|
||||||
|
keyb === last
|
||||||
|
) {
|
||||||
|
// If 'keya' is 'general', 'keya' should always be first
|
||||||
|
if (keya === first) return -1;
|
||||||
|
// If 'keyb' is 'general', 'keyb' should always be first
|
||||||
|
if (keyb === first) return 1;
|
||||||
|
// At this point, we know neither 'keya' nor 'keyb' are 'general'
|
||||||
|
// If 'keya' is project, 'keya' should be second (i.e., before 'keyb' unless 'keyb' is 'general')
|
||||||
|
if (keya === second) return -1;
|
||||||
|
// If 'keyb' is project, 'keyb' should be second (i.e., before 'keya')
|
||||||
|
if (keyb === second) return 1;
|
||||||
|
// At this point, we know neither 'keya' nor 'keyb' are 'general' or project
|
||||||
|
// If 'keya' is 'external', 'keya' should be last (i.e., after 'keyb')
|
||||||
|
if (keya === last) return 1;
|
||||||
|
// If 'keyb' is 'external', 'keyb' should be last (i.e., after 'keya')
|
||||||
|
if (keyb === last) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither 'keya' nor 'b' are one of the special strings, sort alphabetically
|
||||||
|
if (keya < keyb) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (keya > keyb) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -12,12 +12,14 @@ window.appConfig = {
|
|||||||
label: 'e2e',
|
label: 'e2e',
|
||||||
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
|
taskInputsUrl: 'assets/task-inputs/e2e.json',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'affected',
|
id: 'affected',
|
||||||
label: 'affected',
|
label: 'affected',
|
||||||
projectGraphUrl: 'assets/project-graphs/affected.json',
|
projectGraphUrl: 'assets/project-graphs/affected.json',
|
||||||
taskGraphUrl: 'assets/task-graphs/affected.json',
|
taskGraphUrl: 'assets/task-graphs/affected.json',
|
||||||
|
taskInputsUrl: 'assets/task-inputs/affected.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultWorkspaceId: 'e2e',
|
defaultWorkspaceId: 'e2e',
|
||||||
|
|||||||
@ -12,6 +12,7 @@ window.appConfig = {
|
|||||||
label: 'local',
|
label: 'local',
|
||||||
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
|
taskInputsUrl: 'assets/task-inputs/e2e.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultWorkspaceId: 'local',
|
defaultWorkspaceId: 'local',
|
||||||
|
|||||||
9786
graph/client/src/assets/task-inputs/e2e-affected.json
Normal file
9786
graph/client/src/assets/task-inputs/e2e-affected.json
Normal file
File diff suppressed because it is too large
Load Diff
9786
graph/client/src/assets/task-inputs/e2e.json
Normal file
9786
graph/client/src/assets/task-inputs/e2e.json
Normal file
File diff suppressed because it is too large
Load Diff
2
graph/client/src/globals.d.ts
vendored
2
graph/client/src/globals.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type {
|
import type {
|
||||||
|
ExpandedTaskInputsReponse,
|
||||||
ProjectGraphClientResponse,
|
ProjectGraphClientResponse,
|
||||||
TaskGraphClientResponse,
|
TaskGraphClientResponse,
|
||||||
} from 'nx/src/command-line/graph/graph';
|
} from 'nx/src/command-line/graph/graph';
|
||||||
@ -15,6 +16,7 @@ export declare global {
|
|||||||
localMode: 'serve' | 'build';
|
localMode: 'serve' | 'build';
|
||||||
projectGraphResponse?: ProjectGraphClientResponse;
|
projectGraphResponse?: ProjectGraphClientResponse;
|
||||||
taskGraphResponse?: TaskGraphClientResponse;
|
taskGraphResponse?: TaskGraphClientResponse;
|
||||||
|
expandedTaskInputsResponse?: ExpandedTaskInputsReponse;
|
||||||
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
useXstateInspect: boolean;
|
useXstateInspect: boolean;
|
||||||
|
|||||||
@ -32,7 +32,10 @@ export class GraphService {
|
|||||||
container: string | HTMLElement,
|
container: string | HTMLElement,
|
||||||
theme: 'light' | 'dark',
|
theme: 'light' | 'dark',
|
||||||
public renderMode?: 'nx-console' | 'nx-docs',
|
public renderMode?: 'nx-console' | 'nx-docs',
|
||||||
rankDir: 'TB' | 'LR' = 'TB'
|
rankDir: 'TB' | 'LR' = 'TB',
|
||||||
|
public getTaskInputs: (
|
||||||
|
taskId: string
|
||||||
|
) => Promise<Record<string, string[]>> = undefined
|
||||||
) {
|
) {
|
||||||
use(cytoscapeDagre);
|
use(cytoscapeDagre);
|
||||||
use(popper);
|
use(popper);
|
||||||
|
|||||||
@ -6,12 +6,13 @@ import {
|
|||||||
ProjectEdgeNodeTooltipProps,
|
ProjectEdgeNodeTooltipProps,
|
||||||
} from '@nx/graph/ui-tooltips';
|
} from '@nx/graph/ui-tooltips';
|
||||||
import { TooltipEvent } from './interfaces';
|
import { TooltipEvent } from './interfaces';
|
||||||
|
import { GraphInteractionEvents } from './graph-interaction-events';
|
||||||
|
|
||||||
export class GraphTooltipService {
|
export class GraphTooltipService {
|
||||||
private subscribers: Set<Function> = new Set();
|
private subscribers: Set<Function> = new Set();
|
||||||
|
|
||||||
constructor(graph: GraphService) {
|
constructor(graph: GraphService) {
|
||||||
graph.listen((event) => {
|
graph.listen((event: GraphInteractionEvents) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'GraphRegenerated':
|
case 'GraphRegenerated':
|
||||||
this.hideAll();
|
this.hideAll();
|
||||||
@ -49,6 +50,20 @@ export class GraphTooltipService {
|
|||||||
...event.data,
|
...event.data,
|
||||||
runTaskCallback,
|
runTaskCallback,
|
||||||
});
|
});
|
||||||
|
if (graph.getTaskInputs) {
|
||||||
|
graph.getTaskInputs(event.data.id).then((inputs) => {
|
||||||
|
if (
|
||||||
|
this.currentTooltip.type === 'taskNode' &&
|
||||||
|
this.currentTooltip.props.id === event.data.id
|
||||||
|
) {
|
||||||
|
this.openTaskNodeTooltip(event.ref, {
|
||||||
|
...event.data,
|
||||||
|
runTaskCallback,
|
||||||
|
inputs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'EdgeClick':
|
case 'EdgeClick':
|
||||||
const callback =
|
const callback =
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { PlayIcon } from '@heroicons/react/24/outline';
|
import { PlayIcon } from '@heroicons/react/24/outline';
|
||||||
import { Tag } from '@nx/graph/ui-components';
|
import { Tag } from '@nx/graph/ui-components';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface TaskNodeTooltipProps {
|
export interface TaskNodeTooltipProps {
|
||||||
id: string;
|
id: string;
|
||||||
executor: string;
|
executor: string;
|
||||||
runTaskCallback?: () => void;
|
runTaskCallback?: () => void;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
inputs?: Record<string, string[]>;
|
||||||
|
|
||||||
|
children?: ReactNode | ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskNodeTooltip({
|
export function TaskNodeTooltip({
|
||||||
@ -13,10 +17,11 @@ export function TaskNodeTooltip({
|
|||||||
executor,
|
executor,
|
||||||
description,
|
description,
|
||||||
runTaskCallback: runTargetCallback,
|
runTaskCallback: runTargetCallback,
|
||||||
|
children,
|
||||||
}: TaskNodeTooltipProps) {
|
}: TaskNodeTooltipProps) {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-slate-700 dark:text-slate-400">
|
<div className="text-sm text-slate-700 dark:text-slate-400">
|
||||||
<h4 className="flex justify-between items-center gap-4">
|
<h4 className="flex justify-between items-center gap-4 mb-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Tag className="mr-3">{executor}</Tag>
|
<Tag className="mr-3">{executor}</Tag>
|
||||||
<span className="font-mono">{id}</span>
|
<span className="font-mono">{id}</span>
|
||||||
@ -31,8 +36,8 @@ export function TaskNodeTooltip({
|
|||||||
</button>
|
</button>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</h4>
|
</h4>
|
||||||
<h4></h4>
|
|
||||||
{description ? <p className="mt-4">{description}</p> : null}
|
{description ? <p className="mt-4">{description}</p> : null}
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,21 +87,15 @@ export function PackageSchemaSubList({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{vm.type === 'document' ? (
|
{vm.type === 'document' ? (
|
||||||
<>
|
<DocumentList documents={vm.package.documents} />
|
||||||
<DocumentList documents={vm.package.documents} />
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{vm.type === 'executor' ? (
|
{vm.type === 'executor' ? (
|
||||||
<>
|
<SchemaList files={vm.package.executors} type={'executor'} />
|
||||||
<SchemaList files={vm.package.executors} type={'executor'} />
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{vm.type === 'generator' ? (
|
{vm.type === 'generator' ? (
|
||||||
<>
|
<SchemaList files={vm.package.generators} type={'generator'} />
|
||||||
<SchemaList files={vm.package.generators} type={'generator'} />
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,17 +16,15 @@ export function DocumentList({
|
|||||||
documents: DocumentMetadata[];
|
documents: DocumentMetadata[];
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
{!!documents.length ? (
|
||||||
{!!documents.length ? (
|
documents.map((guide) => (
|
||||||
documents.map((guide) => (
|
<DocumentListItem key={guide.id} document={guide} />
|
||||||
<DocumentListItem key={guide.id} document={guide} />
|
))
|
||||||
))
|
) : (
|
||||||
) : (
|
<EmptyList type="document" />
|
||||||
<EmptyList type="document" />
|
)}
|
||||||
)}
|
</ul>
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,17 +61,15 @@ export function SchemaList({
|
|||||||
type: 'executor' | 'generator';
|
type: 'executor' | 'generator';
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
{!!files.length ? (
|
||||||
{!!files.length ? (
|
files.map((schema) => (
|
||||||
files.map((schema) => (
|
<SchemaListItem key={schema.name} file={schema} />
|
||||||
<SchemaListItem key={schema.name} file={schema} />
|
))
|
||||||
))
|
) : (
|
||||||
) : (
|
<EmptyList type={type} />
|
||||||
<EmptyList type={type} />
|
)}
|
||||||
)}
|
</ul>
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,6 @@ import { useLayoutEffect as ReactUseLayoutEffect } from 'react';
|
|||||||
*
|
*
|
||||||
* See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect
|
* See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect
|
||||||
*/
|
*/
|
||||||
export const useLayoutEffect = (<any>globalThis)?.document
|
export const useLayoutEffect = (globalThis as any)?.document
|
||||||
? ReactUseLayoutEffect
|
? ReactUseLayoutEffect
|
||||||
: () => void 0;
|
: () => void 0;
|
||||||
|
|||||||
@ -1,36 +1,51 @@
|
|||||||
import { workspaceRoot } from '../../utils/workspace-root';
|
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
||||||
import { copySync, ensureDirSync } from 'fs-extra';
|
import { copySync, ensureDirSync } from 'fs-extra';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as open from 'open';
|
import * as minimatch from 'minimatch';
|
||||||
import { basename, dirname, extname, isAbsolute, join, parse } from 'path';
|
|
||||||
import { performance } from 'perf_hooks';
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
import * as open from 'open';
|
||||||
|
import {
|
||||||
|
basename,
|
||||||
|
dirname,
|
||||||
|
extname,
|
||||||
|
isAbsolute,
|
||||||
|
join,
|
||||||
|
parse,
|
||||||
|
relative,
|
||||||
|
} from 'path';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
import { readNxJson, workspaceLayout } from '../../config/configuration';
|
import { readNxJson, workspaceLayout } from '../../config/configuration';
|
||||||
import { output } from '../../utils/output';
|
|
||||||
import { writeJsonFile } from '../../utils/fileutils';
|
|
||||||
import {
|
import {
|
||||||
ProjectFileMap,
|
ProjectFileMap,
|
||||||
ProjectGraph,
|
ProjectGraph,
|
||||||
ProjectGraphDependency,
|
ProjectGraphDependency,
|
||||||
ProjectGraphProjectNode,
|
ProjectGraphProjectNode,
|
||||||
} from '../../config/project-graph';
|
} from '../../config/project-graph';
|
||||||
|
import { writeJsonFile } from '../../utils/fileutils';
|
||||||
|
import { output } from '../../utils/output';
|
||||||
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
|
|
||||||
|
import { Server } from 'net';
|
||||||
|
|
||||||
|
import { FileData } from '../../config/project-graph';
|
||||||
|
import { TaskGraph } from '../../config/task-graph';
|
||||||
|
import { daemonClient } from '../../daemon/client/client';
|
||||||
|
import { filterUsingGlobPatterns } from '../../hasher/task-hasher';
|
||||||
|
import { getRootTsConfigPath } from '../../plugins/js/utils/typescript';
|
||||||
import { pruneExternalNodes } from '../../project-graph/operators';
|
import { pruneExternalNodes } from '../../project-graph/operators';
|
||||||
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
import {
|
import {
|
||||||
createTaskGraph,
|
createTaskGraph,
|
||||||
mapTargetDefaultsToDependencies,
|
mapTargetDefaultsToDependencies,
|
||||||
} from '../../tasks-runner/create-task-graph';
|
} from '../../tasks-runner/create-task-graph';
|
||||||
import { TaskGraph } from '../../config/task-graph';
|
import { allFileData } from '../../utils/all-file-data';
|
||||||
import { daemonClient } from '../../daemon/client/client';
|
|
||||||
import { Server } from 'net';
|
|
||||||
import { readFileMapCache } from '../../project-graph/nx-deps-cache';
|
|
||||||
import { getAffectedGraphNodes } from '../affected/affected';
|
|
||||||
import { splitArgsIntoNxArgsAndOverrides } from '../../utils/command-line-utils';
|
import { splitArgsIntoNxArgsAndOverrides } from '../../utils/command-line-utils';
|
||||||
import { NxJsonConfiguration } from '../../config/nx-json';
|
import { NxJsonConfiguration } from '../../config/nx-json';
|
||||||
import { HashPlanner } from '../../native';
|
import { HashPlanner } from '../../native';
|
||||||
import { transformProjectGraphForRust } from '../../native/transform-objects';
|
import { transformProjectGraphForRust } from '../../native/transform-objects';
|
||||||
|
import { getAffectedGraphNodes } from '../affected/affected';
|
||||||
|
import { readFileMapCache } from '../../project-graph/nx-deps-cache';
|
||||||
|
|
||||||
export interface ProjectGraphClientResponse {
|
export interface ProjectGraphClientResponse {
|
||||||
hash: string;
|
hash: string;
|
||||||
@ -50,6 +65,10 @@ export interface TaskGraphClientResponse {
|
|||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpandedTaskInputsReponse {
|
||||||
|
[taskId: string]: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
// maps file extention to MIME types
|
// maps file extention to MIME types
|
||||||
const mimeType = {
|
const mimeType = {
|
||||||
'.ico': 'image/x-icon',
|
'.ico': 'image/x-icon',
|
||||||
@ -73,7 +92,8 @@ function buildEnvironmentJs(
|
|||||||
watchMode: boolean,
|
watchMode: boolean,
|
||||||
localMode: 'build' | 'serve',
|
localMode: 'build' | 'serve',
|
||||||
depGraphClientResponse?: ProjectGraphClientResponse,
|
depGraphClientResponse?: ProjectGraphClientResponse,
|
||||||
taskGraphClientResponse?: TaskGraphClientResponse
|
taskGraphClientResponse?: TaskGraphClientResponse,
|
||||||
|
expandedTaskInputsReponse?: ExpandedTaskInputsReponse
|
||||||
) {
|
) {
|
||||||
let environmentJs = `window.exclude = ${JSON.stringify(exclude)};
|
let environmentJs = `window.exclude = ${JSON.stringify(exclude)};
|
||||||
window.watch = ${!!watchMode};
|
window.watch = ${!!watchMode};
|
||||||
@ -88,7 +108,8 @@ function buildEnvironmentJs(
|
|||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
projectGraphUrl: 'project-graph.json',
|
projectGraphUrl: 'project-graph.json',
|
||||||
taskGraphUrl: 'task-graph.json'
|
taskGraphUrl: 'task-graph.json',
|
||||||
|
taskInputsUrl: 'task-inputs.json',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultWorkspaceId: 'local',
|
defaultWorkspaceId: 'local',
|
||||||
@ -105,9 +126,13 @@ function buildEnvironmentJs(
|
|||||||
taskGraphClientResponse
|
taskGraphClientResponse
|
||||||
)};
|
)};
|
||||||
`;
|
`;
|
||||||
|
environmentJs += `window.expandedTaskInputsResponse = ${JSON.stringify(
|
||||||
|
expandedTaskInputsReponse
|
||||||
|
)};`;
|
||||||
} else {
|
} else {
|
||||||
environmentJs += `window.projectGraphResponse = null;`;
|
environmentJs += `window.projectGraphResponse = null;`;
|
||||||
environmentJs += `window.taskGraphResponse = null;`;
|
environmentJs += `window.taskGraphResponse = null;`;
|
||||||
|
environmentJs += `window.expandedTaskInputsResponse = null;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return environmentJs;
|
return environmentJs;
|
||||||
@ -318,13 +343,18 @@ export async function generateGraph(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const taskGraphClientResponse = await createTaskGraphClientResponse();
|
const taskGraphClientResponse = await createTaskGraphClientResponse();
|
||||||
|
const taskInputsReponse = await createExpandedTaskInputResponse(
|
||||||
|
taskGraphClientResponse,
|
||||||
|
depGraphClientResponse
|
||||||
|
);
|
||||||
|
|
||||||
const environmentJs = buildEnvironmentJs(
|
const environmentJs = buildEnvironmentJs(
|
||||||
args.exclude || [],
|
args.exclude || [],
|
||||||
args.watch,
|
args.watch,
|
||||||
!!args.file && args.file.endsWith('html') ? 'build' : 'serve',
|
!!args.file && args.file.endsWith('html') ? 'build' : 'serve',
|
||||||
depGraphClientResponse,
|
depGraphClientResponse,
|
||||||
taskGraphClientResponse
|
taskGraphClientResponse,
|
||||||
|
taskInputsReponse
|
||||||
);
|
);
|
||||||
html = html.replace(/src="/g, 'src="static/');
|
html = html.replace(/src="/g, 'src="static/');
|
||||||
html = html.replace(/href="styles/g, 'href="static/styles');
|
html = html.replace(/href="styles/g, 'href="static/styles');
|
||||||
@ -455,7 +485,6 @@ async function startServer(
|
|||||||
// by limiting the path to current directory only
|
// by limiting the path to current directory only
|
||||||
|
|
||||||
const sanitizePath = basename(parsedUrl.pathname);
|
const sanitizePath = basename(parsedUrl.pathname);
|
||||||
|
|
||||||
if (sanitizePath === 'project-graph.json') {
|
if (sanitizePath === 'project-graph.json') {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify(currentDepGraphClientResponse));
|
res.end(JSON.stringify(currentDepGraphClientResponse));
|
||||||
@ -468,6 +497,23 @@ async function startServer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sanitizePath === 'task-inputs.json') {
|
||||||
|
performance.mark('task input generation:start');
|
||||||
|
|
||||||
|
const taskId = parsedUrl.searchParams.get('taskId');
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
const inputs = await getExpandedTaskInputs(taskId);
|
||||||
|
performance.mark('task input generation:end');
|
||||||
|
|
||||||
|
res.end(JSON.stringify({ [taskId]: inputs }));
|
||||||
|
performance.measure(
|
||||||
|
'task input generation',
|
||||||
|
'task input generation:start',
|
||||||
|
'task input generation:end'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sanitizePath === 'currentHash') {
|
if (sanitizePath === 'currentHash') {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ hash: currentDepGraphClientResponse.hash }));
|
res.end(JSON.stringify({ hash: currentDepGraphClientResponse.hash }));
|
||||||
@ -481,7 +527,6 @@ async function startServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pathname = join(__dirname, '../../core/graph/', sanitizePath);
|
let pathname = join(__dirname, '../../core/graph/', sanitizePath);
|
||||||
|
|
||||||
// if the file is not found or is a directory, return index.html
|
// if the file is not found or is a directory, return index.html
|
||||||
if (!existsSync(pathname) || statSync(pathname).isDirectory()) {
|
if (!existsSync(pathname) || statSync(pathname).isDirectory()) {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
@ -618,10 +663,17 @@ async function createDepGraphClientResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTaskGraphClientResponse(): Promise<TaskGraphClientResponse> {
|
async function createTaskGraphClientResponse(
|
||||||
let graph = pruneExternalNodes(
|
pruneExternal: boolean = false
|
||||||
await createProjectGraphAsync({ exitOnError: true })
|
): Promise<TaskGraphClientResponse> {
|
||||||
);
|
let graph: ProjectGraph;
|
||||||
|
if (pruneExternal) {
|
||||||
|
graph = pruneExternalNodes(
|
||||||
|
await createProjectGraphAsync({ exitOnError: true })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
graph = await createProjectGraphAsync({ exitOnError: true });
|
||||||
|
}
|
||||||
|
|
||||||
const nxJson = readNxJson();
|
const nxJson = readNxJson();
|
||||||
|
|
||||||
@ -667,6 +719,36 @@ async function createTaskGraphClientResponse(): Promise<TaskGraphClientResponse>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createExpandedTaskInputResponse(
|
||||||
|
taskGraphClientResponse: TaskGraphClientResponse,
|
||||||
|
depGraphClientResponse: ProjectGraphClientResponse
|
||||||
|
): Promise<ExpandedTaskInputsReponse> {
|
||||||
|
performance.mark('task input static generation:start');
|
||||||
|
|
||||||
|
const allWorkspaceFiles = await allFileData();
|
||||||
|
const response: Record<string, Record<string, string[]>> = {};
|
||||||
|
|
||||||
|
Object.entries(taskGraphClientResponse.plans).forEach(([key, inputs]) => {
|
||||||
|
const [project] = key.split(':');
|
||||||
|
|
||||||
|
const expandedInputs = expandInputs(
|
||||||
|
inputs,
|
||||||
|
depGraphClientResponse.projects.find((p) => p.name === project),
|
||||||
|
allWorkspaceFiles,
|
||||||
|
depGraphClientResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
response[key] = expandedInputs;
|
||||||
|
});
|
||||||
|
performance.mark('task input static generation:end');
|
||||||
|
performance.measure(
|
||||||
|
'task input static generation',
|
||||||
|
'task input static generation:start',
|
||||||
|
'task input static generation:end'
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
function getAllTaskGraphsForWorkspace(
|
function getAllTaskGraphsForWorkspace(
|
||||||
nxJson: NxJsonConfiguration,
|
nxJson: NxJsonConfiguration,
|
||||||
projectGraph: ProjectGraph
|
projectGraph: ProjectGraph
|
||||||
@ -684,7 +766,7 @@ function getAllTaskGraphsForWorkspace(
|
|||||||
// TODO(cammisuli): improve performance here. Cache results or something.
|
// TODO(cammisuli): improve performance here. Cache results or something.
|
||||||
for (const projectName in projectGraph.nodes) {
|
for (const projectName in projectGraph.nodes) {
|
||||||
const project = projectGraph.nodes[projectName];
|
const project = projectGraph.nodes[projectName];
|
||||||
const targets = Object.keys(project.data.targets);
|
const targets = Object.keys(project.data.targets ?? {});
|
||||||
|
|
||||||
targets.forEach((target) => {
|
targets.forEach((target) => {
|
||||||
const taskId = createTaskId(projectName, target);
|
const taskId = createTaskId(projectName, target);
|
||||||
@ -752,6 +834,130 @@ function createTaskId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExpandedTaskInputs(
|
||||||
|
taskId: string
|
||||||
|
): Promise<Record<string, string[]>> {
|
||||||
|
const [project] = taskId.split(':');
|
||||||
|
const taskGraphResponse = await createTaskGraphClientResponse(false);
|
||||||
|
|
||||||
|
const allWorkspaceFiles = await allFileData();
|
||||||
|
|
||||||
|
const inputs = taskGraphResponse.plans[taskId];
|
||||||
|
if (inputs) {
|
||||||
|
return expandInputs(
|
||||||
|
inputs,
|
||||||
|
currentDepGraphClientResponse.projects.find((p) => p.name === project),
|
||||||
|
allWorkspaceFiles,
|
||||||
|
currentDepGraphClientResponse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandInputs(
|
||||||
|
inputs: string[],
|
||||||
|
project: ProjectGraphProjectNode,
|
||||||
|
allWorkspaceFiles: FileData[],
|
||||||
|
depGraphClientResponse: ProjectGraphClientResponse
|
||||||
|
): Record<string, string[]> {
|
||||||
|
const projectNames = depGraphClientResponse.projects.map((p) => p.name);
|
||||||
|
|
||||||
|
const workspaceRootInputs: string[] = [];
|
||||||
|
const projectRootInputs: string[] = [];
|
||||||
|
const externalInputs: string[] = [];
|
||||||
|
const otherInputs: string[] = [];
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
if (input.startsWith('{workspaceRoot}')) {
|
||||||
|
workspaceRootInputs.push(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maybeProjectName = input.split(':')[0];
|
||||||
|
if (projectNames.includes(maybeProjectName)) {
|
||||||
|
projectRootInputs.push(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input === 'ProjectConfiguration' ||
|
||||||
|
input === 'TsConfig' ||
|
||||||
|
input === 'AllExternalDependencies'
|
||||||
|
) {
|
||||||
|
otherInputs.push(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// there shouldn't be any other imports in here, but external ones are always going to have a modifier in front
|
||||||
|
if (input.includes(':')) {
|
||||||
|
externalInputs.push(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceRootsExpanded: string[] = workspaceRootInputs.flatMap(
|
||||||
|
(input) => {
|
||||||
|
const matches = [];
|
||||||
|
const withoutWorkspaceRoot = input.substring(16);
|
||||||
|
const matchingFile = allWorkspaceFiles.find(
|
||||||
|
(t) => t.file === withoutWorkspaceRoot
|
||||||
|
);
|
||||||
|
if (matchingFile) {
|
||||||
|
matches.push(matchingFile.file);
|
||||||
|
} else {
|
||||||
|
allWorkspaceFiles
|
||||||
|
.filter((f) => minimatch(f.file, withoutWorkspaceRoot))
|
||||||
|
.forEach((f) => {
|
||||||
|
matches.push(f.file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const otherInputsExpanded = otherInputs.map((input) => {
|
||||||
|
if (input === 'TsConfig') {
|
||||||
|
return relative(workspaceRoot, getRootTsConfigPath());
|
||||||
|
}
|
||||||
|
if (input === 'ProjectConfiguration') {
|
||||||
|
return depGraphClientResponse.fileMap[project.name].find(
|
||||||
|
(file) =>
|
||||||
|
file.file === `${project.data.root}/project.json` ||
|
||||||
|
file.file === `${project.data.root}/package.json`
|
||||||
|
).file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectRootsExpanded = projectRootInputs
|
||||||
|
.map((input) => {
|
||||||
|
const fileSetProjectName = input.split(':')[0];
|
||||||
|
const fileSetProject = depGraphClientResponse.projects.find(
|
||||||
|
(p) => p.name === fileSetProjectName
|
||||||
|
);
|
||||||
|
const fileSets = input.replace(`${fileSetProjectName}:`, '').split(',');
|
||||||
|
|
||||||
|
const projectInputExpanded = {
|
||||||
|
[fileSetProject.name]: filterUsingGlobPatterns(
|
||||||
|
fileSetProject.data.root,
|
||||||
|
depGraphClientResponse.fileMap[fileSetProject.name],
|
||||||
|
fileSets
|
||||||
|
).map((f) => f.file),
|
||||||
|
};
|
||||||
|
|
||||||
|
return projectInputExpanded;
|
||||||
|
})
|
||||||
|
.reduce((curr, acc) => {
|
||||||
|
for (let key in curr) {
|
||||||
|
acc[key] = curr[key];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
general: [...workspaceRootsExpanded, ...otherInputsExpanded],
|
||||||
|
...projectRootsExpanded,
|
||||||
|
external: externalInputs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GraphJsonResponse {
|
interface GraphJsonResponse {
|
||||||
tasks?: TaskGraph;
|
tasks?: TaskGraph;
|
||||||
graph: ProjectGraph;
|
graph: ProjectGraph;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function transformProjectGraphForRust(
|
|||||||
for (const [projectName, projectNode] of Object.entries(graph.nodes)) {
|
for (const [projectName, projectNode] of Object.entries(graph.nodes)) {
|
||||||
const targets: Record<string, Target> = {};
|
const targets: Record<string, Target> = {};
|
||||||
for (const [targetName, targetConfig] of Object.entries(
|
for (const [targetName, targetConfig] of Object.entries(
|
||||||
projectNode.data.targets
|
projectNode.data.targets ?? {}
|
||||||
)) {
|
)) {
|
||||||
targets[targetName] = {
|
targets[targetName] = {
|
||||||
executor: targetConfig.executor,
|
executor: targetConfig.executor,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ function writeFile() {
|
|||||||
label: id,
|
label: id,
|
||||||
projectGraphUrl: join('assets/generated-project-graphs/', filename),
|
projectGraphUrl: join('assets/generated-project-graphs/', filename),
|
||||||
taskGraphUrl: join('assets/generated-task-graphs/', filename),
|
taskGraphUrl: join('assets/generated-task-graphs/', filename),
|
||||||
|
taskInputsUrl: join('assets/generated-task-inputs/', filename),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -50,6 +51,7 @@ function writeFile() {
|
|||||||
label: id,
|
label: id,
|
||||||
projectGraphUrl: join('assets/project-graphs/', filename),
|
projectGraphUrl: join('assets/project-graphs/', filename),
|
||||||
taskGraphUrl: join('assets/task-graphs/', filename),
|
taskGraphUrl: join('assets/task-graphs/', filename),
|
||||||
|
taskInputsUrl: join('assets/task-inputs/', filename),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -32,12 +32,19 @@ async function generateGraph(directory: string, name: string) {
|
|||||||
/window.taskGraphResponse = (.*?);/
|
/window.taskGraphResponse = (.*?);/
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const expandedTaskInputsReponse = environmentJs.match(
|
||||||
|
/window.expandedTaskInputsResponse = (.*?);/
|
||||||
|
);
|
||||||
|
|
||||||
ensureDirSync(
|
ensureDirSync(
|
||||||
join(__dirname, '../graph/client/src/assets/generated-project-graphs/')
|
join(__dirname, '../graph/client/src/assets/generated-project-graphs/')
|
||||||
);
|
);
|
||||||
ensureDirSync(
|
ensureDirSync(
|
||||||
join(__dirname, '../graph/client/src/assets/generated-task-graphs/')
|
join(__dirname, '../graph/client/src/assets/generated-task-graphs/')
|
||||||
);
|
);
|
||||||
|
ensureDirSync(
|
||||||
|
join(__dirname, '../graph/client/src/assets/generated-task-inputs/')
|
||||||
|
);
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(
|
join(
|
||||||
@ -56,6 +63,15 @@ async function generateGraph(directory: string, name: string) {
|
|||||||
),
|
),
|
||||||
taskGraphResponse[1]
|
taskGraphResponse[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
'../graph/client/src/assets/generated-task-inputs/',
|
||||||
|
`${name}.json`
|
||||||
|
),
|
||||||
|
expandedTaskInputsReponse[1]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user