feat(graph): add atomizer label to target groups (#26622)

## Current Behavior
Atomized Groups are treated just like any other groups in the PDV

## Expected Behavior
We want to let people know that something was created by the Atomizer
and also surface more information to users.
This commit is contained in:
MaxKless 2024-06-26 16:17:59 +02:00 committed by GitHub
parent ce3f7f4ed8
commit 6528da3bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 439 additions and 120 deletions

View File

@ -25,40 +25,10 @@ export class ExternalApiImpl extends ExternalApi {
console.log('graphInteractionEventListener not registered.'); console.log('graphInteractionEventListener not registered.');
return; return;
} }
if (type === 'file-click') { this.graphInteractionEventListener({
const url = `${payload.sourceRoot}/${payload.file}`; type,
this.graphInteractionEventListener({ payload,
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);
}
} }
); );

View File

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

View File

@ -69,8 +69,7 @@ export function TooltipDisplay() {
externalApiService.postEvent({ externalApiService.postEvent({
type: 'file-click', type: 'file-click',
payload: { payload: {
sourceRoot: currentTooltip.props.sourceRoot, url: `${currentTooltip.props.sourceRoot}/${url}`,
file: url,
}, },
}) })
: undefined; : undefined;

View File

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

View File

@ -21,12 +21,14 @@ interface ProjectDetailsProps {
project: ProjectGraphProjectNode; project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
errors?: GraphError[]; errors?: GraphError[];
connectedToCloud?: boolean;
} }
export function ProjectDetailsWrapper({ export function ProjectDetailsWrapper({
project, project,
sourceMap, sourceMap,
errors, errors,
connectedToCloud,
}: ProjectDetailsProps) { }: ProjectDetailsProps) {
const environment = useEnvironmentConfig()?.environment; const environment = useEnvironmentConfig()?.environment;
const externalApiService = getExternalApiService(); const externalApiService = getExternalApiService();
@ -95,6 +97,14 @@ export function ProjectDetailsWrapper({
[externalApiService] [externalApiService]
); );
const handleNxConnect = useCallback(
() =>
externalApiService.postEvent({
type: 'nx-connect',
}),
[externalApiService]
);
const updateSearchParams = ( const updateSearchParams = (
params: URLSearchParams, params: URLSearchParams,
targetNames?: string[] targetNames?: string[]
@ -162,6 +172,10 @@ export function ProjectDetailsWrapper({
viewInProjectGraphPosition={ viewInProjectGraphPosition={
environment === 'nx-console' ? 'bottom' : 'top' environment === 'nx-console' ? 'bottom' : 'top'
} }
connectedToCloud={connectedToCloud}
nxConnectCallback={
environment === 'nx-console' ? handleNxConnect : undefined
}
/> />
<ErrorToast errors={errors} /> <ErrorToast errors={errors} />
</> </>

View File

@ -9,10 +9,10 @@ export function getExternalApiService() {
} }
export class ExternalApiService { export class ExternalApiService {
private subscribers: Set<(event: { type: string; payload: any }) => void> = private subscribers: Set<(event: { type: string; payload?: any }) => void> =
new Set(); new Set();
postEvent(event: { type: string; payload: any }) { postEvent(event: { type: string; payload?: any }) {
this.subscribers.forEach((subscriber) => { this.subscribers.forEach((subscriber) => {
subscriber(event); subscriber(event);
}); });

View File

@ -1,2 +1,3 @@
export * from './lib/technology-icon'; export * from './lib/technology-icon';
export * from './lib/framework-icons'; export * from './lib/framework-icons';
export * from './lib/ nx-cloud-icon';

View File

@ -0,0 +1,17 @@
import { FC, SVGProps } from 'react';
export const NxCloudIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="transparent"
viewBox="0 0 24 24"
{...props}
>
<path
d="M22.167 7.167v-2.5a2.5 2.5 0 0 0-2.5-2.5h-15a2.5 2.5 0 0 0-2.5 2.5v15a2.5 2.5 0 0 0 2.5 2.5h2.5m15-15c-2.76 0-5 2.24-5 5s-2.24 5-5 5-5 2.24-5 5m15-15V19.59a2.577 2.577 0 0 1-2.576 2.576H7.167"
strokeWidth="2"
/>
</svg>
);

View File

@ -19,12 +19,14 @@ export interface ProjectDetailsProps {
sourceMap: Record<string, string[]>; sourceMap: Record<string, string[]>;
errors?: GraphError[]; errors?: GraphError[];
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
connectedToCloud?: boolean;
onViewInProjectGraph?: (data: { projectName: string }) => void; onViewInProjectGraph?: (data: { projectName: string }) => void;
onViewInTaskGraph?: (data: { onViewInTaskGraph?: (data: {
projectName: string; projectName: string;
targetName: string; targetName: string;
}) => void; }) => void;
onRunTarget?: (data: { projectName: string; targetName: string }) => void; onRunTarget?: (data: { projectName: string; targetName: string }) => void;
nxConnectCallback?: () => void;
viewInProjectGraphPosition?: 'top' | 'bottom'; viewInProjectGraphPosition?: 'top' | 'bottom';
} }
@ -41,7 +43,9 @@ export const ProjectDetails = ({
onViewInProjectGraph, onViewInProjectGraph,
onViewInTaskGraph, onViewInTaskGraph,
onRunTarget, onRunTarget,
nxConnectCallback,
viewInProjectGraphPosition = 'top', viewInProjectGraphPosition = 'top',
connectedToCloud,
}: ProjectDetailsProps) => { }: ProjectDetailsProps) => {
const projectData = project.data; const projectData = project.data;
const isCompact = variant === 'compact'; const isCompact = variant === 'compact';
@ -161,6 +165,8 @@ export const ProjectDetails = ({
variant={variant} variant={variant}
onRunTarget={onRunTarget} onRunTarget={onRunTarget}
onViewInTaskGraph={onViewInTaskGraph} onViewInTaskGraph={onViewInTaskGraph}
connectedToCloud={connectedToCloud}
nxConnectCallback={nxConnectCallback}
/> />
</div> </div>
</> </>

View File

@ -3,12 +3,18 @@ import { TargetConfigurationGroupHeader } from '../target-configuration-details-
export interface TargetConfigurationGroupContainerProps { export interface TargetConfigurationGroupContainerProps {
targetGroupName: string; targetGroupName: string;
targetsNumber: number; targetsNumber: number;
nonAtomizedTarget?: string;
connectedToCloud?: boolean;
nxConnectCallback?: () => void;
children: React.ReactNode; children: React.ReactNode;
} }
export function TargetConfigurationGroupContainer({ export function TargetConfigurationGroupContainer({
targetGroupName, targetGroupName,
targetsNumber, targetsNumber,
nonAtomizedTarget,
connectedToCloud,
nxConnectCallback,
children, children,
}: TargetConfigurationGroupContainerProps) { }: TargetConfigurationGroupContainerProps) {
return ( return (
@ -16,6 +22,9 @@ export function TargetConfigurationGroupContainer({
<TargetConfigurationGroupHeader <TargetConfigurationGroupHeader
targetGroupName={targetGroupName} targetGroupName={targetGroupName}
targetsNumber={targetsNumber} targetsNumber={targetsNumber}
nonAtomizedTarget={nonAtomizedTarget}
connectedToCloud={connectedToCloud}
nxConnectCallback={nxConnectCallback}
className="sticky top-0 z-10 bg-white dark:bg-slate-900" className="sticky top-0 z-10 bg-white dark:bg-slate-900"
/> />
<div className="rounded-md border border-slate-200 p-2 dark:border-slate-700"> <div className="rounded-md border border-slate-200 p-2 dark:border-slate-700">

View File

@ -15,3 +15,21 @@ export const Simple: Story = {
targetsNumber: 5, targetsNumber: 5,
}, },
}; };
export const AtomizerCloud: Story = {
args: {
targetGroupName: 'Target Group Name',
targetsNumber: 5,
nonAtomizedTarget: 'e2e',
connectedToCloud: true,
},
};
export const AtomizerNoCloud: Story = {
args: {
targetGroupName: 'Target Group Name',
targetsNumber: 5,
nonAtomizedTarget: 'e2e',
connectedToCloud: false,
},
};

View File

@ -1,25 +1,60 @@
import { AtomizerTooltip, Tooltip } from '@nx/graph/ui-tooltips';
import { Pill } from '../pill'; import { Pill } from '../pill';
import { Square3Stack3DIcon } from '@heroicons/react/24/outline';
export interface TargetConfigurationGroupHeaderProps { export interface TargetConfigurationGroupHeaderProps {
targetGroupName: string; targetGroupName: string;
targetsNumber: number; targetsNumber: number;
className?: string; className?: string;
nonAtomizedTarget?: string;
connectedToCloud?: boolean;
nxConnectCallback?: () => void;
showIcon?: boolean;
} }
export const TargetConfigurationGroupHeader = ({ export const TargetConfigurationGroupHeader = ({
targetGroupName, targetGroupName,
targetsNumber, targetsNumber,
nonAtomizedTarget,
connectedToCloud = true,
nxConnectCallback,
className = '', className = '',
}: TargetConfigurationGroupHeaderProps) => { }: TargetConfigurationGroupHeaderProps) => {
return ( return (
<header className={`px-4 py-2 text-lg capitalize ${className}`}> <header
className={`flex items-center gap-2 px-4 py-2 text-lg capitalize ${className}`}
>
{targetGroupName}{' '} {targetGroupName}{' '}
{nonAtomizedTarget && <Square3Stack3DIcon className="h-5 w-5" />}
<Pill <Pill
text={ text={
targetsNumber.toString() + targetsNumber.toString() +
(targetsNumber === 1 ? ' target' : ' targets') (targetsNumber === 1 ? ' target' : ' targets')
} }
/> />
{nonAtomizedTarget && (
<Tooltip
openAction="hover"
strategy="fixed"
usePortal={true}
content={
(
<AtomizerTooltip
connectedToCloud={connectedToCloud}
nonAtomizedTarget={nonAtomizedTarget}
nxConnectCallback={nxConnectCallback}
/>
) as any
}
>
<span className="inline-flex">
<Pill
color={connectedToCloud ? 'grey' : 'yellow'}
text={'Atomizer'}
/>
</span>
</Tooltip>
)}
</header> </header>
); );
}; };

View File

@ -4,7 +4,10 @@ import type { ProjectGraphProjectNode } from '@nx/devkit';
import { TargetConfigurationDetailsListItem } from '../target-configuration-details-list-item/target-configuration-details-list-item'; import { TargetConfigurationDetailsListItem } from '../target-configuration-details-list-item/target-configuration-details-list-item';
import { TargetConfigurationGroupContainer } from '../target-configuration-details-group-container/target-configuration-details-group-container'; import { TargetConfigurationGroupContainer } from '../target-configuration-details-group-container/target-configuration-details-group-container';
import { groupTargets } from '../utils/group-targets'; import {
getNonAtomizedTargetForGroup,
groupTargets,
} from '../utils/group-targets';
import { useMemo } from 'react'; import { useMemo } from 'react';
export interface TargetConfigurationGroupListProps { export interface TargetConfigurationGroupListProps {
@ -16,6 +19,8 @@ export interface TargetConfigurationGroupListProps {
projectName: string; projectName: string;
targetName: string; targetName: string;
}) => void; }) => void;
nxConnectCallback?: () => void;
connectedToCloud?: boolean;
className?: string; className?: string;
} }
@ -25,7 +30,9 @@ export function TargetConfigurationGroupList({
sourceMap, sourceMap,
onRunTarget, onRunTarget,
onViewInTaskGraph, onViewInTaskGraph,
nxConnectCallback,
className = '', className = '',
connectedToCloud,
}: TargetConfigurationGroupListProps) { }: TargetConfigurationGroupListProps) {
const targetsGroup = useMemo(() => groupTargets(project), [project]); const targetsGroup = useMemo(() => groupTargets(project), [project]);
const hasGroups = useMemo(() => { const hasGroups = useMemo(() => {
@ -47,6 +54,12 @@ export function TargetConfigurationGroupList({
<TargetConfigurationGroupContainer <TargetConfigurationGroupContainer
targetGroupName={targetGroupName} targetGroupName={targetGroupName}
targetsNumber={targets.length} targetsNumber={targets.length}
nonAtomizedTarget={getNonAtomizedTargetForGroup(
project,
targetGroupName
)}
connectedToCloud={connectedToCloud}
nxConnectCallback={nxConnectCallback}
key={targetGroupName} key={targetGroupName}
> >
<ul className={className}> <ul className={className}>

View File

@ -33,3 +33,18 @@ function sortNxReleasePublishLast(a: string, b: string) {
if (b === 'nx-release-publish') return -1; if (b === 'nx-release-publish') return -1;
return a.localeCompare(b); return a.localeCompare(b);
} }
export function getNonAtomizedTargetForGroup(
project: ProjectGraphProjectNode,
targetGroupName: string
): string | undefined {
const targetWithNonAtomizedEquivalent = project.data.metadata?.targetGroups?.[
targetGroupName
]?.find(
(target) => project.data.targets?.[target]?.metadata?.nonAtomizedTarget
);
return targetWithNonAtomizedEquivalent
? project.data.targets?.[targetWithNonAtomizedEquivalent]?.metadata
?.nonAtomizedTarget
: undefined;
}

View File

@ -6,3 +6,4 @@ export * from './lib/tooltip-button';
export * from './lib/property-info-tooltip'; export * from './lib/property-info-tooltip';
export * from './lib/sourcemap-info-tooltip'; export * from './lib/sourcemap-info-tooltip';
export * from './lib/external-link'; export * from './lib/external-link';
export * from './lib/atomizer-tooltip';

View File

@ -0,0 +1,72 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AtomizerTooltip, AtomizerTooltipProps } from './atomizer-tooltip';
import { Tooltip } from './tooltip';
const meta: Meta<typeof AtomizerTooltip> = {
component: AtomizerTooltip,
title: 'Tooltips/AtomizerTooltip',
};
export default meta;
type Story = StoryObj<typeof AtomizerTooltip>;
export const Cloud: Story = {
args: {
connectedToCloud: true,
nonAtomizedTarget: 'e2e',
} as AtomizerTooltipProps,
render: (args) => {
return (
<div className="flex w-full justify-center">
<Tooltip
open={true}
openAction="manual"
content={(<AtomizerTooltip {...args} />) as any}
>
<p>Internal Reference</p>
</Tooltip>
</div>
);
},
};
export const NoCloud: Story = {
args: {
connectedToCloud: false,
nonAtomizedTarget: 'e2e',
} as AtomizerTooltipProps,
render: (args) => {
return (
<div className="flex w-full justify-center">
<Tooltip
open={true}
openAction="manual"
content={(<AtomizerTooltip {...args} />) as any}
>
<p>Internal Reference</p>
</Tooltip>
</div>
);
},
};
export const NoCloudConsole: Story = {
args: {
connectedToCloud: false,
nonAtomizedTarget: 'e2e',
nxConnectCallback: () => console.log('nxConnectCallback'),
} as AtomizerTooltipProps,
render: (args) => {
return (
<div className="flex w-full justify-center">
<Tooltip
open={true}
openAction="manual"
content={(<AtomizerTooltip {...args} />) as any}
>
<p>Internal Reference</p>
</Tooltip>
</div>
);
},
};

View File

@ -0,0 +1,106 @@
import { NxCloudIcon } from '@nx/graph/ui-icons';
import { twMerge } from 'tailwind-merge';
export interface AtomizerTooltipProps {
connectedToCloud: boolean;
nonAtomizedTarget: string;
nxConnectCallback?: () => void;
}
export function AtomizerTooltip(props: AtomizerTooltipProps) {
return (
<div className="z-20 max-w-lg text-sm text-slate-700 dark:text-slate-400">
<h4 className="flex items-center justify-between border-b border-slate-200 text-base dark:border-slate-700/60">
<span className="font-mono">Atomizer</span>
</h4>
<div
className={twMerge(
'flex flex-col py-2 font-mono',
!props.connectedToCloud
? 'border-b border-slate-200 dark:border-slate-700/60'
: ''
)}
>
<p className="whitespace-pre-wrap normal-case">
{'Nx '}
<Link
href="https://nx.dev/ci/features/split-e2e-tasks"
text="automatically split"
/>
{
' this potentially slow task into separate tasks for each file. We recommend enabling '
}
{!props.connectedToCloud && (
<>
<Link href="https://nx.app/" text="Nx Cloud" />
{' and '}
</>
)}
<Link
href="https://nx.dev/ci/features/distribute-task-execution"
text="Nx Agents"
/>
{' to benefit from '}
<Link
href="https://nx.dev/ci/features/distribute-task-execution"
text="task distribution"
/>
{!props.connectedToCloud && (
<>
{', '}
<Link
href="https://nx.dev/ci/features/remote-cache"
text="remote caching"
/>
</>
)}
{' and '}
<Link
href="https://nx.dev/ci/features/flaky-tasks"
text="flaky task re-runs"
/>
. Use
<code className="mx-2 inline rounded bg-gray-100 px-1 font-mono text-gray-800 dark:bg-gray-700 dark:text-gray-300">
{props.nonAtomizedTarget}
</code>
when running without Nx Agents.
</p>
</div>
{!props.connectedToCloud && (
<div className="flex py-2">
<p className="pr-4 normal-case">
{props.nxConnectCallback ? (
<button
className="inline-flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-base text-slate-600 ring-2 ring-inset ring-slate-400/40 hover:bg-slate-50 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-800/60"
onClick={() => props.nxConnectCallback!()}
>
<NxCloudIcon className="h-5 w-5 "></NxCloudIcon>
<span>Connect to Nx Cloud</span>
</button>
) : (
<span className="font-mono">
{'Run'}
<code className="mx-2 inline rounded bg-gray-100 px-1 font-mono text-gray-800 dark:bg-gray-700 dark:text-gray-300">
nx connect
</code>
{'to connect to Nx Cloud'}
</span>
)}
</p>
</div>
)}
</div>
);
}
function Link({ href, text }: { href: string; text: string }) {
return (
<a
href={href}
className="inline text-slate-500 underline decoration-slate-700/50 decoration-dotted decoration-2 dark:text-slate-400 dark:decoration-slate-400/50"
target="_blank"
rel="noreferrer"
>
{text}
</a>
);
}

View File

@ -99,7 +99,7 @@ export function PropertyInfoTooltip({ type }: PropertyInfoTooltipProps) {
: '' : ''
)} )}
> >
<p className="flex grow items-center gap-2 whitespace-pre-wrap"> <p className="flex grow items-center gap-2 whitespace-pre-wrap normal-case">
{propertyInfo.description} {propertyInfo.description}
</p> </p>
</div> </div>

View File

@ -25,6 +25,7 @@ import {
useRole, useRole,
safePolygon, safePolygon,
useTransitionStyles, useTransitionStyles,
FloatingPortal,
} from '@floating-ui/react'; } from '@floating-ui/react';
export type TooltipProps = HTMLAttributes<HTMLDivElement> & { export type TooltipProps = HTMLAttributes<HTMLDivElement> & {
@ -37,6 +38,7 @@ export type TooltipProps = HTMLAttributes<HTMLDivElement> & {
buffer?: number; buffer?: number;
showTooltipArrow?: boolean; showTooltipArrow?: boolean;
strategy?: 'absolute' | 'fixed'; strategy?: 'absolute' | 'fixed';
usePortal?: boolean;
}; };
export function Tooltip({ export function Tooltip({
@ -49,6 +51,7 @@ export function Tooltip({
strategy = 'absolute', strategy = 'absolute',
buffer = 0, buffer = 0,
showTooltipArrow = true, showTooltipArrow = true,
usePortal = false,
}: TooltipProps) { }: TooltipProps) {
const [isOpen, setIsOpen] = useState(open); const [isOpen, setIsOpen] = useState(open);
const arrowRef = useRef(null); const arrowRef = useRef(null);
@ -123,41 +126,49 @@ export function Tooltip({
...getReferenceProps(), ...getReferenceProps(),
}; };
const renderTooltip = () => (
<div
ref={refs.setFloating}
style={{
position: appliedStrategy,
top: showTooltipArrow ? y : y + 8 ?? 0,
left: x ?? 0,
width: 'max-content',
...animationStyles,
}}
className="z-20 min-w-[250px] max-w-prose rounded-md border border-slate-500"
{...getFloatingProps()}
>
{showTooltipArrow && (
<div
style={{
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px',
}}
className="absolute -z-10 h-4 w-4 rotate-45 bg-slate-500"
ref={arrowRef}
></div>
)}
<div className="select-text rounded-md bg-white p-3 dark:bg-slate-900 dark:text-slate-400">
{content}
</div>
</div>
);
return ( return (
<> <>
{!externalReference && !!children {!externalReference && !!children
? cloneElement(children, cloneProps) ? cloneElement(children, cloneProps)
: children} : children}
{isOpen && isMounted ? ( {isOpen && isMounted ? (
<div usePortal ? (
ref={refs.setFloating} <FloatingPortal>{renderTooltip()}</FloatingPortal>
style={{ ) : (
position: appliedStrategy, renderTooltip()
top: showTooltipArrow ? y : y + 8 ?? 0, )
left: x ?? 0,
width: 'max-content',
...animationStyles,
}}
className="z-10 min-w-[250px] max-w-prose rounded-md border border-slate-500"
{...getFloatingProps()}
>
{showTooltipArrow && (
<div
style={{
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px',
}}
className="absolute -z-10 h-4 w-4 rotate-45 bg-slate-500"
ref={arrowRef}
></div>
)}
<div className="select-text rounded-md bg-white p-3 dark:bg-slate-900 dark:text-slate-400">
{content}
</div>
</div>
) : null} ) : null}
</> </>
); );

View File

@ -306,6 +306,7 @@ describe('@nx/cypress/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Cypress Tests in CI", "description": "Runs Cypress Tests in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"cypress", "cypress",
], ],
@ -329,6 +330,7 @@ describe('@nx/cypress/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Cypress Tests in src/test.cy.ts in CI", "description": "Runs Cypress Tests in src/test.cy.ts in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"cypress", "cypress",
], ],

View File

@ -270,6 +270,7 @@ async function buildCypressTargets(
metadata: { metadata: {
technologies: ['cypress'], technologies: ['cypress'],
description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`,
nonAtomizedTarget: options.targetName,
}, },
}; };
dependsOn.push({ dependsOn.push({
@ -288,6 +289,7 @@ async function buildCypressTargets(
metadata: { metadata: {
technologies: ['cypress'], technologies: ['cypress'],
description: 'Runs Cypress Tests in CI', description: 'Runs Cypress Tests in CI',
nonAtomizedTarget: options.targetName,
}, },
}; };
ciTargetGroup.push(options.ciTargetName); ciTargetGroup.push(options.ciTargetName);

View File

@ -179,6 +179,7 @@ describe('@nx/jest/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Run Jest Tests in CI", "description": "Run Jest Tests in CI",
"nonAtomizedTarget": "test",
"technologies": [ "technologies": [
"jest", "jest",
], ],
@ -201,6 +202,7 @@ describe('@nx/jest/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Run Jest Tests in src/unit.spec.ts", "description": "Run Jest Tests in src/unit.spec.ts",
"nonAtomizedTarget": "test",
"technologies": [ "technologies": [
"jest", "jest",
], ],
@ -304,48 +306,48 @@ describe('@nx/jest/plugin', () => {
); );
expect(results).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
[ [
[ [
"proj/jest.config.js", "proj/jest.config.js",
{ {
"projects": { "projects": {
"proj": { "proj": {
"metadata": undefined, "metadata": undefined,
"root": "proj", "root": "proj",
"targets": { "targets": {
"test": { "test": {
"cache": true, "cache": true,
"command": "jest", "command": "jest",
"inputs": [ "inputs": [
"default", "default",
"^production", "^production",
{ {
"externalDependencies": [ "externalDependencies": [
"jest", "jest",
"some-package", "some-package",
], ],
},
],
"metadata": {
"description": "Run Jest Tests",
"technologies": [
"jest",
],
},
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
},
},
}, },
],
"metadata": {
"description": "Run Jest Tests",
"technologies": [
"jest",
],
}, },
"options": {
"cwd": "proj",
},
"outputs": [
"{workspaceRoot}/coverage",
],
}, },
}, ],
}, ]
}, `);
},
],
]
`);
} }
); );
}); });

View File

@ -237,6 +237,7 @@ async function buildJestTargets(
metadata: { metadata: {
technologies: ['jest'], technologies: ['jest'],
description: 'Run Jest Tests in CI', description: 'Run Jest Tests in CI',
nonAtomizedTarget: options.targetName,
}, },
}; };
targetGroup.push(options.ciTargetName); targetGroup.push(options.ciTargetName);
@ -258,6 +259,7 @@ async function buildJestTargets(
metadata: { metadata: {
technologies: ['jest'], technologies: ['jest'],
description: `Run Jest Tests in ${relativePath}`, description: `Run Jest Tests in ${relativePath}`,
nonAtomizedTarget: options.targetName,
}, },
}; };
targetGroup.push(targetName); targetGroup.push(targetName);

View File

@ -56,6 +56,7 @@ import { createTaskHasher } from '../../hasher/create-task-hasher';
import { filterUsingGlobPatterns } from '../../hasher/task-hasher'; import { filterUsingGlobPatterns } from '../../hasher/task-hasher';
import { ProjectGraphError } from '../../project-graph/error-types'; import { ProjectGraphError } from '../../project-graph/error-types';
import { isNxCloudUsed } from '../../utils/nx-cloud-utils';
export interface GraphError { export interface GraphError {
message: string; message: string;
@ -78,6 +79,7 @@ export interface ProjectGraphClientResponse {
exclude: string[]; exclude: string[];
isPartial: boolean; isPartial: boolean;
errors?: GraphError[]; errors?: GraphError[];
connectedToCloud?: boolean;
} }
export interface TaskGraphClientResponse { export interface TaskGraphClientResponse {
@ -748,11 +750,14 @@ async function createProjectGraphAndSourceMapClientResponse(
let sourceMaps: ConfigurationSourceMaps; let sourceMaps: ConfigurationSourceMaps;
let isPartial = false; let isPartial = false;
let errors: GraphError[] | undefined; let errors: GraphError[] | undefined;
let connectedToCloud: boolean | 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());
} catch (e) { } catch (e) {
if (e instanceof ProjectGraphError) { if (e instanceof ProjectGraphError) {
projectGraph = e.getPartialProjectGraph(); projectGraph = e.getPartialProjectGraph();
@ -786,7 +791,14 @@ async function createProjectGraphAndSourceMapClientResponse(
const hasher = createHash('sha256'); const hasher = createHash('sha256');
hasher.update( hasher.update(
JSON.stringify({ layout, projects, dependencies, sourceMaps, errors }) JSON.stringify({
layout,
projects,
dependencies,
sourceMaps,
errors,
connectedToCloud,
})
); );
const hash = hasher.digest('hex'); const hash = hasher.digest('hex');
@ -816,6 +828,7 @@ async function createProjectGraphAndSourceMapClientResponse(
fileMap, fileMap,
isPartial, isPartial,
errors, errors,
connectedToCloud,
}, },
sourceMapResponse: sourceMaps, sourceMapResponse: sourceMaps,
}; };

View File

@ -128,6 +128,7 @@ export interface TargetMetadata {
[k: string]: any; [k: string]: any;
description?: string; description?: string;
technologies?: string[]; technologies?: string[];
nonAtomizedTarget?: string;
} }
export interface TargetDependencyConfig { export interface TargetDependencyConfig {

View File

@ -1,8 +1,8 @@
import { NxJsonConfiguration, readNxJson } from '../config/nx-json'; import { NxJsonConfiguration, readNxJson } from '../config/nx-json';
export function isNxCloudUsed(nxJson: NxJsonConfiguration) { export function isNxCloudUsed(nxJson: NxJsonConfiguration): boolean {
return ( return (
process.env.NX_CLOUD_ACCESS_TOKEN || !!process.env.NX_CLOUD_ACCESS_TOKEN ||
!!nxJson.nxCloudAccessToken || !!nxJson.nxCloudAccessToken ||
!!Object.values(nxJson.tasksRunnerOptions ?? {}).find( !!Object.values(nxJson.tasksRunnerOptions ?? {}).find(
(r) => r.runner == '@nrwl/nx-cloud' || r.runner == 'nx-cloud' (r) => r.runner == '@nrwl/nx-cloud' || r.runner == 'nx-cloud'

View File

@ -99,6 +99,7 @@ describe('@nx/playwright/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Playwright Tests in CI", "description": "Runs Playwright Tests in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"playwright", "playwright",
], ],
@ -191,6 +192,7 @@ describe('@nx/playwright/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Playwright Tests in CI", "description": "Runs Playwright Tests in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"playwright", "playwright",
], ],
@ -273,6 +275,7 @@ describe('@nx/playwright/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Playwright Tests in CI", "description": "Runs Playwright Tests in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"playwright", "playwright",
], ],
@ -297,6 +300,7 @@ describe('@nx/playwright/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Playwright Tests in tests/run-me.spec.ts in CI", "description": "Runs Playwright Tests in tests/run-me.spec.ts in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"playwright", "playwright",
], ],
@ -324,6 +328,7 @@ describe('@nx/playwright/plugin', () => {
], ],
"metadata": { "metadata": {
"description": "Runs Playwright Tests in tests/run-me-2.spec.ts in CI", "description": "Runs Playwright Tests in tests/run-me-2.spec.ts in CI",
"nonAtomizedTarget": "e2e",
"technologies": [ "technologies": [
"playwright", "playwright",
], ],

View File

@ -215,6 +215,7 @@ async function buildPlaywrightTargets(
metadata: { metadata: {
technologies: ['playwright'], technologies: ['playwright'],
description: `Runs Playwright Tests in ${relativeSpecFilePath} in CI`, description: `Runs Playwright Tests in ${relativeSpecFilePath} in CI`,
nonAtomizedTarget: options.targetName,
}, },
}; };
dependsOn.push({ dependsOn.push({
@ -241,6 +242,7 @@ async function buildPlaywrightTargets(
metadata: { metadata: {
technologies: ['playwright'], technologies: ['playwright'],
description: 'Runs Playwright Tests in CI', description: 'Runs Playwright Tests in CI',
nonAtomizedTarget: options.targetName,
}, },
}; };
ciTargetGroup.push(options.ciTargetName); ciTargetGroup.push(options.ciTargetName);