feat(graph): show next steps for successful migrations (#30934)

This PR updates Migration UI to display "next steps" when they are
provided by a migration. This works by writing `nextSteps` into the Nx
Console meta in `migrations.json`.

If the `nextSteps` is missing or it's empty, then nothing will be shown.

<img width="1555" alt="Screenshot 2025-04-29 at 5 16 49 PM"
src="https://github.com/user-attachments/assets/88491632-9b33-421a-887a-b6fbb5676098"
/>

See: https://www.loom.com/share/c0a4a7dce9df46b5b023fce5e0a3bd2f
This commit is contained in:
Jack Hsu 2025-04-30 08:16:30 -04:00 committed by GitHub
parent 9dcab79b10
commit dcef5c7cf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 7 deletions

View File

@ -12,7 +12,13 @@ import {
PlayIcon, PlayIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { Pill } from '@nx/graph-internal/ui-project-details'; import { Pill } from '@nx/graph-internal/ui-project-details';
import { useState, forwardRef, useImperativeHandle, useEffect } from 'react'; import {
useState,
forwardRef,
useImperativeHandle,
useEffect,
type ReactNode,
} from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
export interface MigrationCardHandle { export interface MigrationCardHandle {
@ -21,6 +27,30 @@ export interface MigrationCardHandle {
toggle: () => void; toggle: () => void;
} }
function convertUrlsToLinks(text: string): ReactNode[] {
const urlRegex = /(https?:\/\/[^\s]+)/g;
const parts = text.split(urlRegex);
const urls = text.match(urlRegex) || [];
const result: ReactNode[] = [];
for (let i = 0; i < parts.length; i++) {
if (urls[i - 1]) {
result.push(
<a
key={i}
href={urls[i - 1]}
target="_blank"
className="text-blue-500 hover:underline"
>
{urls[i - 1]}
</a>
);
} else if (parts[i]) {
result.push(parts[i]);
}
}
return result;
}
export const MigrationCard = forwardRef< export const MigrationCard = forwardRef<
MigrationCardHandle, MigrationCardHandle,
{ {
@ -200,6 +230,23 @@ export const MigrationCard = forwardRef<
)} )}
</div> </div>
</div> </div>
{succeeded && migrationResult?.nextSteps?.length ? (
<div className="pt-2">
<div className="my-2 border-t border-slate-200 dark:border-slate-700/60" />
<span className="pb-2 text-sm font-bold">
More Information & Next Steps
</span>
<ul className="list-inside list-disc pl-2">
{migrationResult?.nextSteps.map((step, idx) => (
<li key={idx} className="text-sm">
{convertUrlsToLinks(step)}
</li>
))}
</ul>
<p></p>
</div>
) : null}
<div className="mt-4 flex justify-end gap-2"> <div className="mt-4 flex justify-end gap-2">
<button <button
onClick={() => onViewImplementation()} onClick={() => onViewImplementation()}
@ -266,7 +313,7 @@ export const MigrationCard = forwardRef<
onFileClick(file); onFileClick(file);
}} }}
> >
{file.path} <code>{file.path}</code>
</li> </li>
); );
})} })}

View File

@ -242,7 +242,7 @@ export const PendingApproval: Story = {
{ {
id: 'migration-1', id: 'migration-1',
name: 'migration-1', name: 'migration-1',
description: 'Migrate 1', description: 'This is a test migration',
version: '1.0.0', version: '1.0.0',
package: 'nx', package: 'nx',
implementation: './src/migrations/migration-1.ts', implementation: './src/migrations/migration-1.ts',
@ -250,7 +250,7 @@ export const PendingApproval: Story = {
{ {
id: 'migration-2', id: 'migration-2',
name: 'migration-2', name: 'migration-2',
description: 'Migrate 2', description: 'This is a test migration',
version: '1.0.1', version: '1.0.1',
package: '@nx/react', package: '@nx/react',
implementation: './src/migrations/migration-2.ts', implementation: './src/migrations/migration-2.ts',
@ -271,12 +271,17 @@ export const PendingApproval: Story = {
type: 'successful', type: 'successful',
changedFiles: [], changedFiles: [],
ref: '123', ref: '123',
nextSteps: [],
}, },
'migration-2': { 'migration-2': {
name: 'migration-2', name: 'migration-2',
type: 'successful', type: 'successful',
changedFiles: [{ path: 'foo.txt', type: 'CREATE' }], changedFiles: [{ path: 'foo.txt', type: 'CREATE' }],
ref: '124', ref: '124',
nextSteps: [
'Check something: https://nx.dev/docs',
'Check another thing: https://nx.dev/docs',
],
}, },
}, },
targetVersion: '20.3.2', targetVersion: '20.3.2',

View File

@ -27,6 +27,7 @@ export type SuccessfulMigration = {
name: string; name: string;
changedFiles: Omit<FileChange, 'content'>[]; changedFiles: Omit<FileChange, 'content'>[];
ref: string; ref: string;
nextSteps?: string[];
}; };
export type FailedMigration = { export type FailedMigration = {
@ -135,7 +136,7 @@ export async function runSingleMigration(
// 2. Bundled into Console, so the version is fixed to what we build Console with. // 2. Bundled into Console, so the version is fixed to what we build Console with.
const updatedMigrateModule = await import('./migrate.js'); const updatedMigrateModule = await import('./migrate.js');
const { changes: fileChanges } = const { changes: fileChanges, nextSteps } =
await updatedMigrateModule.runNxOrAngularMigration( await updatedMigrateModule.runNxOrAngularMigration(
workspacePath, workspacePath,
migration, migration,
@ -159,7 +160,8 @@ export async function runSingleMigration(
path: change.path, path: change.path,
type: change.type, type: change.type,
})), })),
gitRefAfter gitRefAfter,
nextSteps
) )
); );
@ -234,7 +236,8 @@ export function modifyMigrationsJsonMetadata(
export function addSuccessfulMigration( export function addSuccessfulMigration(
id: string, id: string,
fileChanges: Omit<FileChange, 'content'>[], fileChanges: Omit<FileChange, 'content'>[],
ref: string ref: string,
nextSteps: string[]
) { ) {
return ( return (
migrationsJsonMetadata: MigrationsJsonMetadata migrationsJsonMetadata: MigrationsJsonMetadata
@ -250,6 +253,7 @@ export function addSuccessfulMigration(
name: id, name: id,
changedFiles: fileChanges, changedFiles: fileChanges,
ref, ref,
nextSteps,
}, },
}; };
return copied; return copied;