feat(graph): add sync generators to target details in project details view (#27639)

Add a `Sync Generators` section to the target details in the PDV.

### Default


![image](https://github.com/user-attachments/assets/0390c301-a833-4230-8c6a-0d452a7b8c57)

### Heading tooltip


![image](https://github.com/user-attachments/assets/b8b1c19f-20f8-42e6-a914-16e4dd0b11f2)

### Source map


![image](https://github.com/user-attachments/assets/3b66d2e1-88b7-42e8-9771-5732a32b4cc3)

![image](https://github.com/user-attachments/assets/8b3b0f15-c2f1-411f-8eb6-1df6ff320c5c)

### Disabled sync generator tooltip


![image](https://github.com/user-attachments/assets/9fd35041-35fc-41c4-92b5-b613fed9e0ae)


<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
<!-- Fixes NXC-802 -->

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-09-19 18:17:55 +02:00 committed by GitHub
parent 7d0d834e42
commit a1f69e3a01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 202 additions and 12 deletions

View File

@ -84,6 +84,7 @@ const projectDetailsLoader = async (
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
errors?: GraphError[]; errors?: GraphError[];
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}> => { }> => {
const workspaceData = await workspaceDataLoader(selectedWorkspaceId); const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId); const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
@ -104,6 +105,7 @@ const projectDetailsLoader = async (
sourceMap: sourceMaps[project.data.root], sourceMap: sourceMaps[project.data.root],
errors: workspaceData.errors, errors: workspaceData.errors,
connectedToCloud: workspaceData.connectedToCloud, connectedToCloud: workspaceData.connectedToCloud,
disabledTaskSyncGenerators: workspaceData.disabledTaskSyncGenerators,
}; };
}; };

View File

@ -21,14 +21,21 @@ import {
import { ProjectDetailsHeader } from './project-details-header'; import { ProjectDetailsHeader } from './project-details-header';
export function ProjectDetailsPage() { export function ProjectDetailsPage() {
const { project, sourceMap, hash, errors, connectedToCloud } = const {
useRouteLoaderData('selectedProjectDetails') as { project,
hash: string; sourceMap,
project: ProjectGraphProjectNode; hash,
sourceMap: Record<string, string[]>; errors,
errors?: GraphError[]; connectedToCloud,
connectedToCloud?: boolean; disabledTaskSyncGenerators,
}; } = useRouteLoaderData('selectedProjectDetails') as {
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
};
const { environment, watch, appConfig } = useEnvironmentConfig(); const { environment, watch, appConfig } = useEnvironmentConfig();
@ -65,6 +72,7 @@ export function ProjectDetailsPage() {
sourceMap={sourceMap} sourceMap={sourceMap}
errors={errors} errors={errors}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
></ProjectDetailsWrapper> ></ProjectDetailsWrapper>
</div> </div>
</div> </div>

View File

@ -22,6 +22,7 @@ interface ProjectDetailsProps {
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
errors?: GraphError[]; errors?: GraphError[];
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
} }
export function ProjectDetailsWrapper({ export function ProjectDetailsWrapper({
@ -29,6 +30,7 @@ export function ProjectDetailsWrapper({
sourceMap, sourceMap,
errors, errors,
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
}: ProjectDetailsProps) { }: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment; const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService(); const externalApiService = getExternalApiService();
@ -174,6 +176,7 @@ export function ProjectDetailsWrapper({
} }
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined} onNxConnect={environment === 'nx-console' ? handleNxConnect : undefined}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
/> />
<ErrorToast errors={errors} /> <ErrorToast errors={errors} />
</> </>

View File

@ -83,6 +83,11 @@ export const Primary = {
], ],
}, },
configurations: {}, configurations: {},
syncGenerators: [
'@nx/js:typescript-sync',
'@foo/bar:sync',
'@baz/qux:sync',
],
}, },
build: { build: {
dependsOn: ['build-base', 'build-native'], dependsOn: ['build-base', 'build-native'],
@ -210,6 +215,7 @@ export const Primary = {
'nx-core-build-project-json-nodes', 'nx-core-build-project-json-nodes',
], ],
}, },
disabledTaskSyncGenerators: ['@foo/bar:sync'],
}, },
}; };

View File

@ -18,6 +18,7 @@ export interface ProjectDetailsProps {
errors?: GraphError[]; errors?: GraphError[];
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
onViewInProjectGraph?: (data: { projectName: string }) => void; onViewInProjectGraph?: (data: { projectName: string }) => void;
onViewInTaskGraph?: (data: { onViewInTaskGraph?: (data: {
projectName: string; projectName: string;
@ -44,6 +45,7 @@ export const ProjectDetails = ({
onNxConnect, onNxConnect,
viewInProjectGraphPosition = 'top', viewInProjectGraphPosition = 'top',
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
}: ProjectDetailsProps) => { }: ProjectDetailsProps) => {
const projectData = project.data; const projectData = project.data;
const isCompact = variant === 'compact'; const isCompact = variant === 'compact';
@ -153,6 +155,7 @@ export const ProjectDetails = ({
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
onNxConnect={onNxConnect} onNxConnect={onNxConnect}
/> />
</div> </div>

View File

@ -18,6 +18,7 @@ export interface TargetConfigurationGroupListProps {
}) => void; }) => void;
onNxConnect?: () => void; onNxConnect?: () => void;
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
className?: string; className?: string;
} }
@ -30,6 +31,7 @@ export function TargetConfigurationGroupList({
onNxConnect, onNxConnect,
className = '', className = '',
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
}: TargetConfigurationGroupListProps) { }: TargetConfigurationGroupListProps) {
const targetsGroup = useMemo(() => groupTargets(project), [project]); const targetsGroup = useMemo(() => groupTargets(project), [project]);
const hasGroups = useMemo(() => { const hasGroups = useMemo(() => {
@ -56,6 +58,7 @@ export function TargetConfigurationGroupList({
project={project} project={project}
sourceMap={sourceMap} sourceMap={sourceMap}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant} variant={variant}
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}
@ -82,6 +85,7 @@ export function TargetConfigurationGroupList({
project={project} project={project}
sourceMap={sourceMap} sourceMap={sourceMap}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant} variant={variant}
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}
@ -105,6 +109,7 @@ export function TargetConfigurationGroupList({
project={project} project={project}
sourceMap={sourceMap} sourceMap={sourceMap}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
variant={variant} variant={variant}
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}

View File

@ -7,6 +7,7 @@ export interface TargetConfigurationDetailsListItemProps {
project: ProjectGraphProjectNode; project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
onRunTarget?: (data: { projectName: string; targetName: string }) => void; onRunTarget?: (data: { projectName: string; targetName: string }) => void;
onViewInTaskGraph?: (data: { onViewInTaskGraph?: (data: {
@ -23,6 +24,7 @@ export function TargetConfigurationDetailsListItem({
variant, variant,
sourceMap, sourceMap,
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
onRunTarget, onRunTarget,
onViewInTaskGraph, onViewInTaskGraph,
onNxConnect, onNxConnect,
@ -42,6 +44,7 @@ export function TargetConfigurationDetailsListItem({
targetConfiguration={target} targetConfiguration={target}
sourceMap={sourceMap} sourceMap={sourceMap}
connectedToCloud={connectedToCloud} connectedToCloud={connectedToCloud}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}
onNxConnect={onNxConnect} onNxConnect={onNxConnect}

View File

@ -14,6 +14,7 @@ import { TargetExecutorTitle } from '../target-executor/target-executor-title';
import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key'; import { getTargetExecutorSourceMapKey } from '../target-source-info/get-target-executor-source-map-key';
import { TargetSourceInfo } from '../target-source-info/target-source-info'; import { TargetSourceInfo } from '../target-source-info/target-source-info';
import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration'; import { getDisplayHeaderFromTargetConfiguration } from '../utils/get-display-header-from-target-configuration';
import { getTaskSyncGenerators } from '../utils/sync-generators';
import { FadingCollapsible } from './fading-collapsible'; import { FadingCollapsible } from './fading-collapsible';
import { TargetConfigurationProperty } from './target-configuration-property'; import { TargetConfigurationProperty } from './target-configuration-property';
import { TooltipTriggerText } from './tooltip-trigger-text'; import { TooltipTriggerText } from './tooltip-trigger-text';
@ -24,6 +25,7 @@ interface TargetConfigurationDetailsProps {
targetConfiguration: TargetConfiguration; targetConfiguration: TargetConfiguration;
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
onCollapse?: (targetName: string) => void; onCollapse?: (targetName: string) => void;
onExpand?: (targetName: string) => void; onExpand?: (targetName: string) => void;
@ -43,6 +45,7 @@ export default function TargetConfigurationDetails({
targetConfiguration, targetConfiguration,
sourceMap, sourceMap,
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
onViewInTaskGraph, onViewInTaskGraph,
onRunTarget, onRunTarget,
onNxConnect, onNxConnect,
@ -84,6 +87,9 @@ export default function TargetConfigurationDetails({
? Object.keys(configurations).length ? Object.keys(configurations).length
: true); : true);
const { enabledSyncGenerators, disabledSyncGenerators } =
getTaskSyncGenerators(targetConfiguration, disabledTaskSyncGenerators);
return ( return (
<div className="relative rounded-md border border-slate-200 dark:border-slate-700/60"> <div className="relative rounded-md border border-slate-200 dark:border-slate-700/60">
<TargetConfigurationDetailsHeader <TargetConfigurationDetailsHeader
@ -374,6 +380,68 @@ export default function TargetConfigurationDetails({
</div> </div>
</div> </div>
) : null} ) : null}
{enabledSyncGenerators.length > 0 && (
<div className="group">
<h4 className="mb-4">
<Tooltip
openAction="hover"
content={
(<PropertyInfoTooltip type="syncGenerators" />) as any
}
>
<span className="font-medium">
<TooltipTriggerText>Sync Generators</TooltipTriggerText>
</span>
</Tooltip>
</h4>
<ul className="mb-4 list-disc pl-5">
{enabledSyncGenerators.map((generator, idx) => (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`syncGenerators-${idx}`}
>
<TargetConfigurationProperty data={generator}>
<TargetSourceInfo
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.syncGenerators`}
sourceMap={sourceMap}
/>
</TargetConfigurationProperty>
</li>
))}
{disabledSyncGenerators.length > 0 &&
disabledSyncGenerators.map((generator, idx) => (
<li
className="group/line overflow-hidden whitespace-nowrap"
key={`syncGenerators-${idx}`}
>
<TargetConfigurationProperty
data={generator}
disabled={true}
disabledTooltip={
<p className="max-w-sm whitespace-pre-wrap py-2 font-mono text-sm normal-case text-slate-700 dark:text-slate-400">
The Sync Generator is disabled in the{' '}
<code className="font-bold italic">
sync.disabledTaskSyncGenerators
</code>{' '}
property in the{' '}
<code className="font-bold italic">nx.json</code>{' '}
file.
</p>
}
>
<TargetSourceInfo
className="min-w-0 flex-1 pl-4 opacity-0 transition-opacity duration-150 ease-in-out group-hover/line:opacity-100"
propertyKey={`targets.${targetName}.syncGenerators`}
sourceMap={sourceMap}
/>
</TargetConfigurationProperty>
</li>
))}
</ul>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -0,0 +1,31 @@
import { Tooltip } from '@nx/graph/ui-tooltips';
import { JSX, ReactNode } from 'react';
import { TooltipTriggerText } from './tooltip-trigger-text';
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
interface TargetConfigurationPropertyTextProps {
content: ReactNode;
disabled?: boolean;
disabledTooltip?: ReactNode;
}
export function TargetConfigurationPropertyText({
content,
disabled,
disabledTooltip,
}: TargetConfigurationPropertyTextProps): JSX.Element | null {
return (
<>
<span className={disabled ? 'opacity-50' : ''}>{content}</span>
{disabledTooltip && (
<Tooltip openAction="hover" content={disabledTooltip}>
<span className="pl-2 font-medium">
<TooltipTriggerText>
<QuestionMarkCircleIcon className="inline h-4 w-4" />
</TooltipTriggerText>
</span>
</Tooltip>
)}
</>
);
}

View File

@ -1,18 +1,27 @@
import { JSX, ReactNode } from 'react'; import { JSX, ReactNode } from 'react';
import { TargetConfigurationPropertyText } from './target-configuration-property-text';
interface RenderPropertyProps { interface RenderPropertyProps {
data: string | Record<string, any> | any[]; data: string | Record<string, any> | any[];
disabled?: boolean;
disabledTooltip?: ReactNode;
children?: ReactNode; children?: ReactNode;
} }
export function TargetConfigurationProperty({ export function TargetConfigurationProperty({
data, data,
children, children,
disabled,
disabledTooltip,
}: RenderPropertyProps): JSX.Element | null { }: RenderPropertyProps): JSX.Element | null {
if (typeof data === 'string') { if (typeof data === 'string') {
return ( return (
<span className="flex font-mono text-sm"> <span className="flex font-mono text-sm">
{data} <TargetConfigurationPropertyText
content={data}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children} {children}
</span> </span>
); );
@ -21,7 +30,11 @@ export function TargetConfigurationProperty({
<ul> <ul>
{data.map((item, index) => ( {data.map((item, index) => (
<li key={index} className="flex font-mono text-sm"> <li key={index} className="flex font-mono text-sm">
{String(item)} <TargetConfigurationPropertyText
content={String(item)}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children} {children}
</li> </li>
))} ))}
@ -32,7 +45,15 @@ export function TargetConfigurationProperty({
<ul> <ul>
{Object.entries(data).map(([key, value], index) => ( {Object.entries(data).map(([key, value], index) => (
<li key={index} className="flex font-mono text-sm"> <li key={index} className="flex font-mono text-sm">
<strong>{key}</strong>: {String(value)} <TargetConfigurationPropertyText
content={
<>
<strong>{key}</strong>: {String(value)}
</>
}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
{children} {children}
</li> </li>
))} ))}

View File

@ -0,0 +1,34 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { TargetConfiguration } from '@nx/devkit';
export function getTaskSyncGenerators(
targetConfiguration: TargetConfiguration,
disabledTaskSyncGenerators: string[] | undefined
): {
enabledSyncGenerators: string[];
disabledSyncGenerators: string[];
} {
const enabledSyncGenerators: string[] = [];
const disabledSyncGenerators: string[] = [];
if (!targetConfiguration.syncGenerators?.length) {
return { enabledSyncGenerators, disabledSyncGenerators };
}
if (!disabledTaskSyncGenerators?.length) {
enabledSyncGenerators.push(...targetConfiguration.syncGenerators);
return { enabledSyncGenerators, disabledSyncGenerators };
}
const disabledGeneratorsSet = new Set(disabledTaskSyncGenerators);
for (const generator of targetConfiguration.syncGenerators) {
if (disabledGeneratorsSet.has(generator)) {
disabledSyncGenerators.push(generator);
} else {
enabledSyncGenerators.push(generator);
}
}
return { enabledSyncGenerators, disabledSyncGenerators };
}

View File

@ -78,6 +78,7 @@ export interface ProjectGraphClientResponse {
isPartial: boolean; isPartial: boolean;
errors?: GraphError[]; errors?: GraphError[];
connectedToCloud?: boolean; connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
} }
export interface TaskGraphClientResponse { export interface TaskGraphClientResponse {
@ -773,13 +774,16 @@ async function createProjectGraphAndSourceMapClientResponse(
let isPartial = false; let isPartial = false;
let errors: GraphError[] | undefined; let errors: GraphError[] | undefined;
let connectedToCloud: boolean | undefined; let connectedToCloud: boolean | undefined;
let disabledTaskSyncGenerators: string[] | undefined;
try { try {
const projectGraphAndSourceMaps = const projectGraphAndSourceMaps =
await createProjectGraphAndSourceMapsAsync({ exitOnError: false }); await createProjectGraphAndSourceMapsAsync({ exitOnError: false });
projectGraph = projectGraphAndSourceMaps.projectGraph; projectGraph = projectGraphAndSourceMaps.projectGraph;
sourceMaps = projectGraphAndSourceMaps.sourceMaps; sourceMaps = projectGraphAndSourceMaps.sourceMaps;
connectedToCloud = isNxCloudUsed(readNxJson()); const nxJson = readNxJson();
connectedToCloud = isNxCloudUsed(nxJson);
disabledTaskSyncGenerators = nxJson.sync?.disabledTaskSyncGenerators;
} catch (e) { } catch (e) {
if (e instanceof ProjectGraphError) { if (e instanceof ProjectGraphError) {
projectGraph = e.getPartialProjectGraph(); projectGraph = e.getPartialProjectGraph();
@ -820,6 +824,7 @@ async function createProjectGraphAndSourceMapClientResponse(
sourceMaps, sourceMaps,
errors, errors,
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
}) })
); );
@ -851,6 +856,7 @@ async function createProjectGraphAndSourceMapClientResponse(
isPartial, isPartial,
errors, errors,
connectedToCloud, connectedToCloud,
disabledTaskSyncGenerators,
}, },
sourceMapResponse: sourceMaps, sourceMapResponse: sourceMaps,
}; };