feat(graph): Create Migrate UI (#30734)
This PR introduces a new UI in Nx Console designed to assist users with managing migrations more effectively. Each migration is now presented with its status and actions, allowing users to interact directly. If any issues arise, users can address them in isolation without disrupting the overall flow. The migrate ui provides a clear overview of the migration state, helping users track progress and understand what actions are required at each step.
This commit is contained in:
parent
67732d6217
commit
a911318017
3
.gitignore
vendored
3
.gitignore
vendored
@ -66,3 +66,6 @@ target
|
|||||||
/wasi-sdk*
|
/wasi-sdk*
|
||||||
|
|
||||||
vite.config.*.timestamp*
|
vite.config.*.timestamp*
|
||||||
|
|
||||||
|
|
||||||
|
storybook-static
|
||||||
@ -1414,6 +1414,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
"path": "/recipes/nx-console/console-run-command",
|
"path": "/recipes/nx-console/console-run-command",
|
||||||
@ -2772,6 +2780,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
"path": "/recipes/nx-console/console-run-command",
|
"path": "/recipes/nx-console/console-run-command",
|
||||||
@ -2823,6 +2839,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"isExternal": false,
|
||||||
|
"children": [],
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
"path": "/recipes/nx-console/console-run-command",
|
"path": "/recipes/nx-console/console-run-command",
|
||||||
|
|||||||
@ -1937,6 +1937,17 @@
|
|||||||
"path": "/recipes/nx-console/console-generate-command",
|
"path": "/recipes/nx-console/console-generate-command",
|
||||||
"tags": ["editor-setup"]
|
"tags": ["editor-setup"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/console-migrate-ui",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"tags": ["editor-setup"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "console-run-command",
|
"id": "console-run-command",
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
@ -3798,6 +3809,17 @@
|
|||||||
"path": "/recipes/nx-console/console-generate-command",
|
"path": "/recipes/nx-console/console-generate-command",
|
||||||
"tags": ["editor-setup"]
|
"tags": ["editor-setup"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/console-migrate-ui",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"tags": ["editor-setup"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "console-run-command",
|
"id": "console-run-command",
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
@ -3869,6 +3891,17 @@
|
|||||||
"path": "/recipes/nx-console/console-generate-command",
|
"path": "/recipes/nx-console/console-generate-command",
|
||||||
"tags": ["editor-setup"]
|
"tags": ["editor-setup"]
|
||||||
},
|
},
|
||||||
|
"/recipes/nx-console/console-migrate-ui": {
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"description": "",
|
||||||
|
"mediaImage": "",
|
||||||
|
"file": "shared/recipes/console-migrate-ui",
|
||||||
|
"itemList": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui",
|
||||||
|
"tags": ["editor-setup"]
|
||||||
|
},
|
||||||
"/recipes/nx-console/console-run-command": {
|
"/recipes/nx-console/console-run-command": {
|
||||||
"id": "console-run-command",
|
"id": "console-run-command",
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
|
|||||||
@ -28,6 +28,13 @@
|
|||||||
"name": "Generate Command",
|
"name": "Generate Command",
|
||||||
"path": "/recipes/nx-console/console-generate-command"
|
"path": "/recipes/nx-console/console-generate-command"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "",
|
||||||
|
"file": "shared/recipes/console-migrate-ui",
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"path": "/recipes/nx-console/console-migrate-ui"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "",
|
"description": "",
|
||||||
"file": "shared/recipes/console-run-command",
|
"file": "shared/recipes/console-run-command",
|
||||||
|
|||||||
@ -631,6 +631,12 @@
|
|||||||
"tags": ["editor-setup"],
|
"tags": ["editor-setup"],
|
||||||
"file": "shared/recipes/console-generate-command"
|
"file": "shared/recipes/console-generate-command"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Migrate UI",
|
||||||
|
"id": "console-migrate-ui",
|
||||||
|
"tags": ["editor-setup"],
|
||||||
|
"file": "shared/recipes/console-migrate-ui"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Run Command",
|
"name": "Run Command",
|
||||||
"id": "console-run-command",
|
"id": "console-run-command",
|
||||||
|
|||||||
40
docs/shared/recipes/console-migrate-ui.md
Normal file
40
docs/shared/recipes/console-migrate-ui.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: 'Nx Console Migrate UI'
|
||||||
|
description: Overview over the Migrate UI feature for guided migrations in Nx Console
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nx Console Migrate UI
|
||||||
|
|
||||||
|
The Nx Console Migrate UI provides a visual, guided way to execute migrations in your workspace while following Nx best practices. This tool simplifies the process of upgrading your Nx workspace by offering an easy-to-use interface that walks you through each step of the migration process.
|
||||||
|
|
||||||
|
## Accessing the Migrate UI
|
||||||
|
|
||||||
|
The Migrate UI is available on the latest version of Nx Console in VSCode or Cursor if you have enabled the `nxConsole.enableMigrateUi` setting. If enabled, a dedicated `Nx Migrate` view will appear in the sidebar. Unless you're on the latest version of Nx, you'll notice a button prompting you to start the migration.
|
||||||
|
|
||||||
|
{% callout type="note" %}
|
||||||
|
Ensure you have stashed or committed all your changes before initiating a migration.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
## Starting a Migration
|
||||||
|
|
||||||
|
By default, clicking the migration button starts the migration process by upgrading to the recommended Nx version — the latest version of the next major release. This method ensures you upgrade one major version at a time in order to [avoid breakages](recipes/tips-n-tricks/advanced-update#one-major-version-at-a-time-small-steps)
|
||||||
|
|
||||||
|
{%callout type="note"%}
|
||||||
|
If you need more control, a smaller edit button is provided. You can use it to customize the migration process by specifying an exact version and passing custom CLI options like `--to` or `--from`.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
Once you start the migration, Nx Console runs the `nx migrate` command to update your dependency versions and generate a `migrations.json` file. You'll be prompted to inspect the changes made to your `package.json` before installing them and proceedig.
|
||||||
|
|
||||||
|
## The Migration Process
|
||||||
|
|
||||||
|
After confirming the changes, the Migrate UI opens and guides you step-by-step through each migration action. Simply click on `Run Migrations` and each migration will be executed in order.
|
||||||
|
|
||||||
|
If a migration step encounters an error, the process pauses so you can inspect the error details.
|
||||||
|
You can click through to view the migration source code, giving you the opportunity to patch it for your specific use-case or make necessary adjustments to your repository before rerunning the migration.
|
||||||
|
Alternatively, you may choose to skip a problematic migration.
|
||||||
|
|
||||||
|
For successful migration steps that modify files, the UI pauses to let you review the changes. You must approve these changes before the migration continues.
|
||||||
|
|
||||||
|
## Finalizing the Migration
|
||||||
|
|
||||||
|
When all migration steps are complete or you don't want to run further migrations, you can finish the process by clicking the Finish button. By default, this will squash all commits created during the migration together, but you can opt into preserving them.
|
||||||
@ -96,6 +96,7 @@
|
|||||||
- [Telemetry](/recipes/nx-console/console-telemetry)
|
- [Telemetry](/recipes/nx-console/console-telemetry)
|
||||||
- [Project Details View](/recipes/nx-console/console-project-details)
|
- [Project Details View](/recipes/nx-console/console-project-details)
|
||||||
- [Generate Command](/recipes/nx-console/console-generate-command)
|
- [Generate Command](/recipes/nx-console/console-generate-command)
|
||||||
|
- [Migrate UI](/recipes/nx-console/console-migrate-ui)
|
||||||
- [Run Command](/recipes/nx-console/console-run-command)
|
- [Run Command](/recipes/nx-console/console-run-command)
|
||||||
- [Nx Cloud Integration](/recipes/nx-console/console-nx-cloud)
|
- [Nx Cloud Integration](/recipes/nx-console/console-nx-cloud)
|
||||||
- [Troubleshooting](/recipes/nx-console/console-troubleshooting)
|
- [Troubleshooting](/recipes/nx-console/console-troubleshooting)
|
||||||
|
|||||||
117
graph/client/src/app/console-migrate/migrate.app.tsx
Normal file
117
graph/client/src/app/console-migrate/migrate.app.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { type FileChange } from 'nx/src/devkit-exports';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { MigrateUI } from '@nx/graph-migrate';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import { getExternalApiService } from '@nx/graph/legacy/shared';
|
||||||
|
import { useSelector } from '@xstate/react';
|
||||||
|
import { Interpreter } from 'xstate';
|
||||||
|
import { MigrateEvents, MigrateState } from './migrate.machine';
|
||||||
|
|
||||||
|
export function MigrateApp({
|
||||||
|
service,
|
||||||
|
}: {
|
||||||
|
service: Interpreter<MigrateState, any, MigrateEvents>;
|
||||||
|
}) {
|
||||||
|
const externalApiService = getExternalApiService();
|
||||||
|
|
||||||
|
const onRunMigration = (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
configuration: {
|
||||||
|
createCommits: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'run-migration',
|
||||||
|
payload: {
|
||||||
|
migration,
|
||||||
|
configuration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRunMany = (
|
||||||
|
migrations: MigrationDetailsWithId[],
|
||||||
|
configuration: {
|
||||||
|
createCommits: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'run-many',
|
||||||
|
payload: {
|
||||||
|
migrations,
|
||||||
|
configuration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'cancel',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = (squashCommits: boolean) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'finish',
|
||||||
|
payload: {
|
||||||
|
squashCommits,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrations = useSelector(service, (state) => state.context.migrations);
|
||||||
|
const nxConsoleMetadata = useSelector(
|
||||||
|
service,
|
||||||
|
(state) => state.context.nxConsoleMetadata
|
||||||
|
);
|
||||||
|
const onFileClick = (migration: MigrationDetailsWithId, file: FileChange) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'file-click',
|
||||||
|
payload: {
|
||||||
|
path: file.path,
|
||||||
|
migration: migration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSkipMigration = (migration: MigrationDetailsWithId) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'skip-migration',
|
||||||
|
payload: { migration },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onViewImplementation = (migration: MigrationDetailsWithId) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'view-implementation',
|
||||||
|
payload: { migration },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onViewDocumentation = (migration: MigrationDetailsWithId) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'view-documentation',
|
||||||
|
payload: { migration },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MigrateUI
|
||||||
|
migrations={migrations}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
onRunMigration={onRunMigration}
|
||||||
|
onRunMany={onRunMany}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onFinish={onFinish}
|
||||||
|
onFileClick={onFileClick}
|
||||||
|
onSkipMigration={onSkipMigration}
|
||||||
|
onViewImplementation={onViewImplementation}
|
||||||
|
onViewDocumentation={onViewDocumentation}
|
||||||
|
></MigrateUI>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
graph/client/src/app/console-migrate/migrate.machine.ts
Normal file
63
graph/client/src/app/console-migrate/migrate.machine.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import type {
|
||||||
|
GeneratedMigrationDetails,
|
||||||
|
MigrationDetailsWithId,
|
||||||
|
} from 'nx/src/config/misc-interfaces';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
import { createMachine } from 'xstate';
|
||||||
|
export interface MigrateState {
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MigrateEvents = {
|
||||||
|
type: 'loadData';
|
||||||
|
migrations: GeneratedMigrationDetails[];
|
||||||
|
'nx-console': MigrationsJsonMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialContext: MigrateState = {
|
||||||
|
migrations: [],
|
||||||
|
nxConsoleMetadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const migrateMachine = createMachine<MigrateState, MigrateEvents>({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QFsCWUBOBDALmAxADYD2WEAIrlgNoAMAuoqAA7Gyo6rEB2TIAHogAsAJgA0IAJ6IAjAGYAnADoAHDJFCAbAFYVtGTNpHtAXzMTuxCHD5pMuMH1btOPPoIQBaTROlfNSkZBwSEA7OYgdth4SqgQhI5IIM4cXLxJHpoySiIqcira2gqitEJBPlKIakpyIiIKMrql6kIqKhFRDkokZJBObKluGbKaynU6oXKKIjKaU+KVCLMiSqG02uvF2jqaeiJmZkA */
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
id: 'migrate',
|
||||||
|
initial: 'idle',
|
||||||
|
context: initialContext,
|
||||||
|
states: {
|
||||||
|
idle: {},
|
||||||
|
loaded: {},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
loadData: [
|
||||||
|
{
|
||||||
|
target: 'loaded',
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.migrations = event.migrations.map((migration, index) => {
|
||||||
|
const duplicateCount = event.migrations
|
||||||
|
.slice(0, index)
|
||||||
|
.filter((m) => m.name === migration.name).length;
|
||||||
|
return {
|
||||||
|
...migration,
|
||||||
|
id:
|
||||||
|
duplicateCount === 0
|
||||||
|
? migration.name
|
||||||
|
: `${migration.name}-${duplicateCount}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
ctx.nxConsoleMetadata = event['nx-console'];
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
3
graph/client/src/globals.d.ts
vendored
3
graph/client/src/globals.d.ts
vendored
@ -33,6 +33,9 @@ export declare global {
|
|||||||
data: any
|
data: any
|
||||||
) => Interpreter<ProjectDetailsState, any, ProjectDetailsEvents>;
|
) => Interpreter<ProjectDetailsState, any, ProjectDetailsEvents>;
|
||||||
renderError?: (data: any) => void;
|
renderError?: (data: any) => void;
|
||||||
|
renderMigrate?: (
|
||||||
|
data: any
|
||||||
|
) => Interpreter<MigrateState, any, MigrateEvents>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declare module 'cytoscape' {
|
declare module 'cytoscape' {
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import { projectDetailsMachine } from './app/console-project-details/project-det
|
|||||||
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { GraphError } from 'nx/src/command-line/graph/graph';
|
import type { GraphError } from 'nx/src/command-line/graph/graph';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { GeneratedMigrationDetails } from 'nx/src/config/misc-interfaces';
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { inspect } from '@xstate/inspect';
|
import { inspect } from '@xstate/inspect';
|
||||||
@ -19,6 +23,8 @@ import { render } from 'preact';
|
|||||||
import { ErrorPage } from './app/ui-components/error-page';
|
import { ErrorPage } from './app/ui-components/error-page';
|
||||||
import { ProjectDetailsApp } from './app/console-project-details/project-details.app';
|
import { ProjectDetailsApp } from './app/console-project-details/project-details.app';
|
||||||
import { interpret } from 'xstate';
|
import { interpret } from 'xstate';
|
||||||
|
import { MigrateApp } from './app/console-migrate/migrate.app';
|
||||||
|
import { migrateMachine } from './app/console-migrate/migrate.machine';
|
||||||
|
|
||||||
if (window.__NX_RENDER_GRAPH__ === false) {
|
if (window.__NX_RENDER_GRAPH__ === false) {
|
||||||
window.externalApi = new ExternalApiImpl();
|
window.externalApi = new ExternalApiImpl();
|
||||||
@ -58,6 +64,27 @@ if (window.__NX_RENDER_GRAPH__ === false) {
|
|||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.renderMigrate = (data: {
|
||||||
|
migrations: GeneratedMigrationDetails[];
|
||||||
|
'nx-console': MigrationsJsonMetadata;
|
||||||
|
}) => {
|
||||||
|
const service = interpret(migrateMachine).start();
|
||||||
|
|
||||||
|
service.send({
|
||||||
|
type: 'loadData',
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<StrictMode>
|
||||||
|
<MigrateApp service={service} />
|
||||||
|
</StrictMode>,
|
||||||
|
document.getElementById('app')
|
||||||
|
);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
if (window.useXstateInspect === true) {
|
if (window.useXstateInspect === true) {
|
||||||
inspect({
|
inspect({
|
||||||
|
|||||||
12
graph/migrate/.babelrc
Normal file
12
graph/migrate/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nx/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
18
graph/migrate/.eslintrc.json
Normal file
18
graph/migrate/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*", "storybook-static"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
graph/migrate/.storybook/main.ts
Normal file
29
graph/migrate/.storybook/main.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||||
|
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||||
|
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
viteFinal: async (config) =>
|
||||||
|
mergeConfig(config, {
|
||||||
|
plugins: [nxViteTsPaths()],
|
||||||
|
}),
|
||||||
|
|
||||||
|
docs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
||||||
|
// To customize your Vite configuration you can use the viteFinal field.
|
||||||
|
// Check https://storybook.js.org/docs/react/builders/vite#configuration
|
||||||
|
// and https://nx.dev/recipes/storybook/custom-builder-configs
|
||||||
2
graph/migrate/.storybook/preview.ts
Normal file
2
graph/migrate/.storybook/preview.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import './tailwind.css';
|
||||||
|
export const tags = ['autodocs'];
|
||||||
33
graph/migrate/.storybook/tailwind.css
Normal file
33
graph/migrate/.storybook/tailwind.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@tailwind components;
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeOut {
|
||||||
|
animation: fadeOut 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
7
graph/migrate/README.md
Normal file
7
graph/migrate/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# graph-migrate
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test graph-migrate` to execute the unit tests via [Jest](https://jestjs.io).
|
||||||
19
graph/migrate/jest.config.ts
Normal file
19
graph/migrate/jest.config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
const nxPreset = require('@nx/jest/preset').default;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...nxPreset,
|
||||||
|
displayName: 'graph-migrate',
|
||||||
|
transform: {
|
||||||
|
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||||
|
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/graph/migrate',
|
||||||
|
// The mock for widnow.matchMedia has to be in a separete file and imported before the components to test
|
||||||
|
// for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
'/graph/client/src/app/machines/match-media-mock.spec.ts',
|
||||||
|
],
|
||||||
|
};
|
||||||
15
graph/migrate/postcss.config.js
Normal file
15
graph/migrate/postcss.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
||||||
|
// option from your application's configuration (i.e. project.json).
|
||||||
|
//
|
||||||
|
// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {
|
||||||
|
config: join(__dirname, 'tailwind.config.js'),
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
9
graph/migrate/project.json
Normal file
9
graph/migrate/project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "graph-migrate",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "graph/migrate/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"// targets": "to see all targets run: nx show project graph-migrate --web",
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
1
graph/migrate/src/index.ts
Normal file
1
graph/migrate/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './lib/migrate';
|
||||||
96
graph/migrate/src/lib/components/automatic-migration.tsx
Normal file
96
graph/migrate/src/lib/components/automatic-migration.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import { FileChange } from '@nx/devkit';
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
import type { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
import { useSelector } from '@xstate/react';
|
||||||
|
import {
|
||||||
|
currentMigrationHasChanges,
|
||||||
|
currentMigrationHasFailed,
|
||||||
|
currentMigrationHasSucceeded,
|
||||||
|
} from '../state/automatic/selectors';
|
||||||
|
import { MigrationTimeline } from './migration-timeline';
|
||||||
|
import { Interpreter } from 'xstate';
|
||||||
|
import type {
|
||||||
|
AutomaticMigrationEvents,
|
||||||
|
AutomaticMigrationState,
|
||||||
|
} from '../state/automatic/types';
|
||||||
|
|
||||||
|
export function AutomaticMigration(props: {
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onFileClick: (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
file: Omit<FileChange, 'content'>
|
||||||
|
) => void;
|
||||||
|
onViewImplementation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onViewDocumentation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
actor: Interpreter<
|
||||||
|
AutomaticMigrationState,
|
||||||
|
any,
|
||||||
|
AutomaticMigrationEvents,
|
||||||
|
any,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
}) {
|
||||||
|
const currentMigration = useSelector(
|
||||||
|
props.actor,
|
||||||
|
(state) => state.context.currentMigration
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMigrationIndex = props.migrations.findIndex(
|
||||||
|
(migration) => migration.id === currentMigration?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMigrationRunning = useSelector(
|
||||||
|
props.actor,
|
||||||
|
(state) => state.context.currentMigrationRunning
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMigrationFailed = useSelector(props.actor, (state) =>
|
||||||
|
currentMigrationHasFailed(state.context)
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMigrationSuccess = useSelector(props.actor, (state) =>
|
||||||
|
currentMigrationHasSucceeded(state.context)
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMigrationChanges = useSelector(props.actor, (state) =>
|
||||||
|
currentMigrationHasChanges(state.context)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDone = useSelector(props.actor, (state) => state.matches('done'));
|
||||||
|
|
||||||
|
const isInit = useSelector(props.actor, (state) => state.matches('init'));
|
||||||
|
|
||||||
|
const handleReviewMigration = (migrationId: string) => {
|
||||||
|
props.actor.send({
|
||||||
|
type: 'reviewMigration',
|
||||||
|
migrationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MigrationTimeline
|
||||||
|
migrations={props.migrations}
|
||||||
|
nxConsoleMetadata={props.nxConsoleMetadata}
|
||||||
|
currentMigrationIndex={
|
||||||
|
currentMigrationIndex >= 0 ? currentMigrationIndex : 0
|
||||||
|
}
|
||||||
|
currentMigrationRunning={currentMigrationRunning}
|
||||||
|
currentMigrationFailed={currentMigrationFailed}
|
||||||
|
currentMigrationSuccess={currentMigrationSuccess}
|
||||||
|
currentMigrationHasChanges={currentMigrationChanges}
|
||||||
|
isDone={isDone}
|
||||||
|
isInit={isInit}
|
||||||
|
onRunMigration={props.onRunMigration}
|
||||||
|
onSkipMigration={props.onSkipMigration}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
onViewImplementation={props.onViewImplementation}
|
||||||
|
onViewDocumentation={props.onViewDocumentation}
|
||||||
|
onReviewMigration={handleReviewMigration}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
graph/migrate/src/lib/components/index.ts
Normal file
7
graph/migrate/src/lib/components/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './automatic-migration';
|
||||||
|
export * from './migration-card';
|
||||||
|
export * from './migration-list';
|
||||||
|
export * from './migration-done';
|
||||||
|
export * from './migration-init';
|
||||||
|
export * from './migration-settings-panel';
|
||||||
|
export * from './migration-timeline';
|
||||||
279
graph/migrate/src/lib/components/migration-card.tsx
Normal file
279
graph/migrate/src/lib/components/migration-card.tsx
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
import { FileChange } from 'nx/src/devkit-exports';
|
||||||
|
import type { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowPathIcon,
|
||||||
|
CodeBracketIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
PlayIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { Pill } from '@nx/graph-internal/ui-project-details';
|
||||||
|
import { useState, forwardRef, useImperativeHandle, useEffect } from 'react';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export interface MigrationCardHandle {
|
||||||
|
expand: () => void;
|
||||||
|
collapse: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MigrationCard = forwardRef<
|
||||||
|
MigrationCardHandle,
|
||||||
|
{
|
||||||
|
migration: MigrationDetailsWithId;
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelect?: (isSelected: boolean) => void;
|
||||||
|
onRunMigration?: () => void;
|
||||||
|
onFileClick: (file: Omit<FileChange, 'content'>) => void;
|
||||||
|
onViewImplementation: () => void;
|
||||||
|
onViewDocumentation: () => void;
|
||||||
|
forceIsRunning?: boolean;
|
||||||
|
isExpanded?: boolean;
|
||||||
|
}
|
||||||
|
>(function MigrationCard(
|
||||||
|
{
|
||||||
|
migration,
|
||||||
|
nxConsoleMetadata,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
onRunMigration,
|
||||||
|
onFileClick,
|
||||||
|
onViewImplementation,
|
||||||
|
onViewDocumentation,
|
||||||
|
forceIsRunning,
|
||||||
|
isExpanded: isExpandedProp,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(isExpandedProp ?? false);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
expand: () => setIsExpanded(true),
|
||||||
|
collapse: () => setIsExpanded(false),
|
||||||
|
toggle: () => setIsExpanded((prev) => !prev),
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExpandedProp !== undefined) {
|
||||||
|
setIsExpanded(isExpandedProp);
|
||||||
|
}
|
||||||
|
}, [isExpandedProp]);
|
||||||
|
|
||||||
|
const migrationResult = nxConsoleMetadata.completedMigrations?.[migration.id];
|
||||||
|
const succeeded = migrationResult?.type === 'successful';
|
||||||
|
const failed = migrationResult?.type === 'failed';
|
||||||
|
const skipped = migrationResult?.type === 'skipped';
|
||||||
|
const inProgress = nxConsoleMetadata.runningMigrations?.includes(
|
||||||
|
migration.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const madeChanges = succeeded && !!migrationResult?.changedFiles.length;
|
||||||
|
|
||||||
|
const renderSelectBox = onSelect && isSelected !== undefined;
|
||||||
|
|
||||||
|
const isNxMigration =
|
||||||
|
migration.package.startsWith('@nx') || migration.package.startsWith('nx');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={migration.id}
|
||||||
|
className={`gap-2 rounded-md p-2 transition-colors`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{renderSelectBox && (
|
||||||
|
<div className="h-4 w-4">
|
||||||
|
<input
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={(e) => onSelect((e.target as any).checked)}
|
||||||
|
id={migration.id}
|
||||||
|
name={migration.id}
|
||||||
|
value={migration.id}
|
||||||
|
type="checkbox"
|
||||||
|
className={`h-4 w-4 ${
|
||||||
|
succeeded
|
||||||
|
? 'accent-green-600 dark:accent-green-500'
|
||||||
|
: failed
|
||||||
|
? 'accent-red-600 dark:accent-red-500'
|
||||||
|
: 'accent-blue-500 dark:accent-sky-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`flex flex-col gap-1`}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 ${
|
||||||
|
isNxMigration ? 'cursor-pointer gap-1 hover:underline' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (isNxMigration) {
|
||||||
|
onViewDocumentation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <div>{migration.name}</div>
|
||||||
|
{isNxMigration && (
|
||||||
|
<ArrowTopRightOnSquareIcon className="h-4 w-4" />
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
<span className="mb-2 text-sm">{migration.description}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{migration.package && (
|
||||||
|
<Pill
|
||||||
|
text={`${migration.package}: ${migration.version}`}
|
||||||
|
color={'grey'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{' '}
|
||||||
|
{succeeded && !madeChanges && (
|
||||||
|
<Pill text="No changes made" color="green" />
|
||||||
|
)}
|
||||||
|
{succeeded && madeChanges && (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pill
|
||||||
|
key="changes"
|
||||||
|
text={`${migrationResult?.changedFiles.length} changes`}
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{failed && (
|
||||||
|
<div>
|
||||||
|
<Pill text="Failed" color="red" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{skipped && (
|
||||||
|
<div>
|
||||||
|
<Pill text="Skipped" color="grey" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(onRunMigration || forceIsRunning) && (
|
||||||
|
<span
|
||||||
|
className={`rounded-md p-1 text-sm ring-1 ring-inset transition-colors ${
|
||||||
|
succeeded
|
||||||
|
? 'bg-green-50 text-green-700 ring-green-200 hover:bg-green-100 dark:bg-green-900/20 dark:text-green-500 dark:ring-green-900/30 dark:hover:bg-green-900/30'
|
||||||
|
: failed
|
||||||
|
? 'bg-red-50 text-red-700 ring-red-200 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-500 dark:ring-red-900/30 dark:hover:bg-red-900/30'
|
||||||
|
: 'bg-inherit text-slate-600 ring-slate-400/40 hover:bg-slate-200 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-700/60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{inProgress || forceIsRunning ? (
|
||||||
|
<ArrowPathIcon
|
||||||
|
className="h-6 w-6 animate-spin cursor-not-allowed text-blue-500"
|
||||||
|
aria-label="Migration in progress"
|
||||||
|
/>
|
||||||
|
) : !succeeded && !failed ? (
|
||||||
|
<PlayIcon
|
||||||
|
onClick={onRunMigration}
|
||||||
|
className="h-6 w-6 !cursor-pointer"
|
||||||
|
aria-label="Run migration"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ArrowPathIcon
|
||||||
|
onClick={onRunMigration}
|
||||||
|
className="h-6 w-6 !cursor-pointer"
|
||||||
|
aria-label="Rerun migration"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onViewImplementation()}
|
||||||
|
className="flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
<CodeBracketIcon className="h-4 w-4" />
|
||||||
|
View Source
|
||||||
|
</button>
|
||||||
|
{failed && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon className="h-4 w-4" />
|
||||||
|
{isExpanded ? 'Hide Errors' : 'View Errors'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{succeeded && madeChanges && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListBulletIcon className="h-4 w-4" />
|
||||||
|
{isExpanded ? 'Hide Changes' : 'View Changes'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{failed && isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: isExpanded ? 'auto' : 0 }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
className="flex overflow-hidden pt-2"
|
||||||
|
>
|
||||||
|
<pre>{migrationResult?.error}</pre>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{succeeded && madeChanges && isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: isExpanded ? 'auto' : 0 }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="my-2 border-t border-slate-200 dark:border-slate-700/60"></div>
|
||||||
|
<span className="pb-2 text-sm font-bold">File Changes</span>
|
||||||
|
<ul className="flex flex-col gap-2">
|
||||||
|
{migrationResult?.changedFiles.map((file) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className="cursor-pointer text-sm hover:underline"
|
||||||
|
key={`${migration.id}-${file.path}`}
|
||||||
|
onClick={() => {
|
||||||
|
onFileClick(file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file.path}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
144
graph/migrate/src/lib/components/migration-done.tsx
Normal file
144
graph/migrate/src/lib/components/migration-done.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Popover } from '@nx/graph/ui-common';
|
||||||
|
import { PrimaryAction } from '../migrate';
|
||||||
|
|
||||||
|
export interface MigrationDoneProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onFinish: (squash: boolean) => void;
|
||||||
|
shouldSquashCommits: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigrationDone({
|
||||||
|
onCancel,
|
||||||
|
onFinish,
|
||||||
|
shouldSquashCommits,
|
||||||
|
}: MigrationDoneProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [squashCommits, setSquashCommits] = useState(shouldSquashCommits);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
className="my-10 flex flex-col items-center justify-center gap-4"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 10,
|
||||||
|
delay: 0.3,
|
||||||
|
}}
|
||||||
|
className="flex h-16 w-16 items-center justify-center text-6xl"
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="checkmark">
|
||||||
|
<CheckCircleIcon className="h-12 w-12 text-green-700 dark:text-green-400" />
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||||
|
className="rounded-md border border-green-500/30 bg-green-50 px-6 py-5 text-green-700 shadow-lg dark:border-green-900/30 dark:bg-green-900/10 dark:text-green-500"
|
||||||
|
>
|
||||||
|
<h2 className="flex items-center gap-3 text-xl font-bold">
|
||||||
|
All migrations completed
|
||||||
|
</h2>
|
||||||
|
</motion.div>
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-4">
|
||||||
|
<motion.button
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
onClick={onCancel}
|
||||||
|
className="flex w-full items-center rounded-md border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</motion.button>
|
||||||
|
<div className="flex">
|
||||||
|
<motion.button
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
onClick={() => onFinish(squashCommits)}
|
||||||
|
type="button"
|
||||||
|
title={
|
||||||
|
squashCommits
|
||||||
|
? PrimaryAction.FinishSquashingCommits
|
||||||
|
: PrimaryAction.FinishWithoutSquashingCommits
|
||||||
|
}
|
||||||
|
className="whitespace-nowrap rounded-l-md border border-blue-700 bg-blue-500 px-6 py-3 text-sm font-medium text-white shadow-sm hover:bg-blue-600 dark:border-blue-700 dark:bg-blue-600 dark:text-white hover:dark:bg-blue-700"
|
||||||
|
>
|
||||||
|
{squashCommits
|
||||||
|
? PrimaryAction.FinishSquashingCommits
|
||||||
|
: PrimaryAction.FinishWithoutSquashingCommits}
|
||||||
|
</motion.button>
|
||||||
|
<div className="relative flex">
|
||||||
|
<motion.button
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
className="border-l-1 flex items-center rounded-r-md border border-blue-700 bg-blue-500 px-2 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 dark:border-blue-700 dark:bg-blue-700 dark:text-white hover:dark:bg-blue-800"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</motion.button>
|
||||||
|
<Popover
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
position={{
|
||||||
|
left: '-2rem',
|
||||||
|
top: '-6.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul className="p-2">
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setSquashCommits(true);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={squashCommits ? 'inline-block' : 'opacity-0'}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>Squash commits</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setSquashCommits(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={!squashCommits ? 'inline-block' : 'opacity-0'}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>Do not squash commits</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
graph/migrate/src/lib/components/migration-init.tsx
Normal file
55
graph/migrate/src/lib/components/migration-init.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export function MigrationInit({ onStart }: { onStart: () => void }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
className="flex h-full flex-col items-center justify-center gap-6 px-6 py-12 text-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.2 }}
|
||||||
|
className="text-6xl"
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="tools">
|
||||||
|
<WrenchScrewdriverIcon className="h-12 w-12" />
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.4 }}
|
||||||
|
className="text-2xl font-semibold text-gray-800 dark:text-white"
|
||||||
|
>
|
||||||
|
Ready to Migrate
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.6 }}
|
||||||
|
className="max-w-xl text-base text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Welcome to the Migrate UI. This tool will guide you through updating
|
||||||
|
your workspace. Click the button below to start running migrations.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 }}
|
||||||
|
onClick={onStart}
|
||||||
|
className="mt-4 rounded-md bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-md transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Start Migration
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
graph/migrate/src/lib/components/migration-list.tsx
Normal file
161
graph/migrate/src/lib/components/migration-list.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import { FileChange } from '@nx/devkit';
|
||||||
|
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import { PlayIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { MigrationCard } from './migration-card';
|
||||||
|
|
||||||
|
export function MigrationList(props: {
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onRunMany: (migrations: MigrationDetailsWithId[]) => void;
|
||||||
|
onFileClick: (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
file: Omit<FileChange, 'content'>
|
||||||
|
) => void;
|
||||||
|
onViewImplementation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onViewDocumentation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
}) {
|
||||||
|
const [selectedMigrations, setSelectedMigrations] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>(
|
||||||
|
props.migrations.reduce((acc, migration) => {
|
||||||
|
acc[migration.id] = false;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>)
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberSelected = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(selectedMigrations).filter((selected) => selected).length,
|
||||||
|
[selectedMigrations]
|
||||||
|
);
|
||||||
|
|
||||||
|
const anySelected = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(selectedMigrations).filter((selected) => selected).length >
|
||||||
|
0,
|
||||||
|
[selectedMigrations]
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSelected = useMemo(
|
||||||
|
() => props.migrations.length === numberSelected,
|
||||||
|
[props.migrations, numberSelected]
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberFailed = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(props.nxConsoleMetadata.completedMigrations ?? {}).filter(
|
||||||
|
(migration) => migration.type === 'failed'
|
||||||
|
).length,
|
||||||
|
[props.nxConsoleMetadata.completedMigrations]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderCheckboxClick = () => {
|
||||||
|
const newSelectedState = !anySelected;
|
||||||
|
setSelectedMigrations(
|
||||||
|
Object.keys(selectedMigrations).reduce((acc, migrationId) => {
|
||||||
|
acc[migrationId] = newSelectedState;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAllCheckboxRef = useCallback(
|
||||||
|
(el: HTMLInputElement | null) => {
|
||||||
|
if (!el) return;
|
||||||
|
el.checked = allSelected;
|
||||||
|
el.indeterminate = anySelected && !allSelected;
|
||||||
|
},
|
||||||
|
[allSelected, anySelected]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRunMany = () => {
|
||||||
|
props.onRunMany(
|
||||||
|
props.migrations.filter((migration) => selectedMigrations[migration.id])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRerunFailed = () => {
|
||||||
|
props.onRunMany(
|
||||||
|
props.migrations.filter(
|
||||||
|
(migration) =>
|
||||||
|
props.nxConsoleMetadata.completedMigrations?.[migration.id]?.type ===
|
||||||
|
'failed'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`my-2 gap-2 rounded-md border border-slate-200 p-2 dark:border-slate-700/60`}
|
||||||
|
>
|
||||||
|
<div className="flex h-4 w-full items-center gap-4">
|
||||||
|
<input
|
||||||
|
ref={selectAllCheckboxRef}
|
||||||
|
onClick={handleHeaderCheckboxClick}
|
||||||
|
id="select-all"
|
||||||
|
name="select-all"
|
||||||
|
value="select-all"
|
||||||
|
type="checkbox"
|
||||||
|
className={`h-4 w-4 accent-blue-500 dark:accent-sky-500`}
|
||||||
|
/>
|
||||||
|
<label htmlFor="select-all">
|
||||||
|
{allSelected || anySelected
|
||||||
|
? `${numberSelected} selected`
|
||||||
|
: 'Select all'}
|
||||||
|
</label>
|
||||||
|
{anySelected && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-md border border-slate-300 bg-white px-2 py-0.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={handleRunMany}
|
||||||
|
>
|
||||||
|
<PlayIcon className="h-5 w-5"></PlayIcon>
|
||||||
|
Run selected migrations
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{numberFailed > 0 && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 rounded-md border border-slate-300 bg-white px-2 py-0.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={handleRerunFailed}
|
||||||
|
>
|
||||||
|
<PlayIcon className="h-5 w-5"></PlayIcon>
|
||||||
|
Rerun failed migrations
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{props.migrations.map((migration) => (
|
||||||
|
<MigrationCard
|
||||||
|
key={migration.id}
|
||||||
|
migration={migration}
|
||||||
|
nxConsoleMetadata={props.nxConsoleMetadata}
|
||||||
|
isSelected={selectedMigrations[migration.id]}
|
||||||
|
onSelect={(isSelected) =>
|
||||||
|
setSelectedMigrations({
|
||||||
|
...selectedMigrations,
|
||||||
|
[migration.id]: isSelected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onRunMigration={() => props.onRunMigration(migration)}
|
||||||
|
onViewImplementation={() => {
|
||||||
|
props.onViewImplementation(migration);
|
||||||
|
}}
|
||||||
|
onViewDocumentation={() => {
|
||||||
|
props.onViewDocumentation(migration);
|
||||||
|
}}
|
||||||
|
onFileClick={(file) => {
|
||||||
|
props.onFileClick(migration, file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Popover } from '@nx/graph/ui-common';
|
||||||
|
|
||||||
|
export interface MigrationSettingsPanelProps {
|
||||||
|
createCommits: boolean;
|
||||||
|
setCreateCommits: (createCommits: boolean) => void;
|
||||||
|
commitPrefix: string;
|
||||||
|
setCommitPrefix: (commitPrefix: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigrationSettingsPanel({
|
||||||
|
createCommits,
|
||||||
|
setCreateCommits,
|
||||||
|
commitPrefix,
|
||||||
|
setCommitPrefix,
|
||||||
|
}: MigrationSettingsPanelProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
className="flex w-full items-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
position={{ left: '-12rem', top: '2.75rem' }}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-2 py-2">
|
||||||
|
<input
|
||||||
|
checked={createCommits}
|
||||||
|
onChange={(e) => setCreateCommits(e.target.checked)}
|
||||||
|
id="create-commits"
|
||||||
|
name="create-commits"
|
||||||
|
value="create-commits"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<label htmlFor="create-commits">Create commits</label>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2 border-b border-slate-200/25 dark:border-slate-700/25"></div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label htmlFor="commit-prefix">Commit prefix</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="chore: [nx migration] "
|
||||||
|
className="block w-full flex-1 rounded-md border border-slate-300/[0.25] bg-white p-1.5 font-light placeholder:font-light placeholder:text-slate-400 dark:border-slate-900 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700"
|
||||||
|
data-cy="textFilterInput"
|
||||||
|
name="filter"
|
||||||
|
onChange={(event) => setCommitPrefix(event.currentTarget.value)}
|
||||||
|
value={commitPrefix}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
564
graph/migrate/src/lib/components/migration-timeline.tsx
Normal file
564
graph/migrate/src/lib/components/migration-timeline.tsx
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import { FileChange } from '@nx/devkit';
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
import type { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronUpIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
MinusIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import { MigrationCard, MigrationCardHandle } from './migration-card';
|
||||||
|
import { Collapsible } from '@nx/graph/ui-common';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface MigrationTimelineProps {
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
currentMigrationIndex: number;
|
||||||
|
currentMigrationRunning?: boolean;
|
||||||
|
currentMigrationFailed?: boolean;
|
||||||
|
currentMigrationSuccess?: boolean;
|
||||||
|
currentMigrationHasChanges?: boolean;
|
||||||
|
isDone?: boolean;
|
||||||
|
isInit: boolean;
|
||||||
|
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onFileClick: (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
file: Omit<FileChange, 'content'>
|
||||||
|
) => void;
|
||||||
|
onViewImplementation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onViewDocumentation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
onReviewMigration: (migrationId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigrationTimeline({
|
||||||
|
migrations,
|
||||||
|
nxConsoleMetadata,
|
||||||
|
currentMigrationIndex,
|
||||||
|
currentMigrationRunning,
|
||||||
|
currentMigrationFailed,
|
||||||
|
currentMigrationSuccess,
|
||||||
|
currentMigrationHasChanges,
|
||||||
|
onRunMigration,
|
||||||
|
onSkipMigration,
|
||||||
|
onFileClick,
|
||||||
|
onViewImplementation,
|
||||||
|
onViewDocumentation,
|
||||||
|
onCancel,
|
||||||
|
onReviewMigration,
|
||||||
|
}: MigrationTimelineProps) {
|
||||||
|
const [showAllPastMigrations, setShowAllPastMigrations] = useState(false);
|
||||||
|
const [showAllFutureMigrations, setShowAllFutureMigrations] = useState(false);
|
||||||
|
const [expandedMigrations, setExpandedMigrations] = useState<{
|
||||||
|
[key: string]: boolean;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const currentMigration = migrations[currentMigrationIndex];
|
||||||
|
const pastMigrations = migrations.slice(0, currentMigrationIndex);
|
||||||
|
const futureMigrations = migrations.slice(currentMigrationIndex + 1);
|
||||||
|
|
||||||
|
// Number of visible migrations when collapsed
|
||||||
|
const visiblePastCount = 0;
|
||||||
|
const visibleFutureCount = 2;
|
||||||
|
const visiblePastMigrations = showAllPastMigrations
|
||||||
|
? pastMigrations
|
||||||
|
: pastMigrations.slice(
|
||||||
|
Math.max(0, pastMigrations.length - visiblePastCount)
|
||||||
|
);
|
||||||
|
const visibleFutureMigrations = showAllFutureMigrations
|
||||||
|
? futureMigrations
|
||||||
|
: futureMigrations.slice(0, visibleFutureCount);
|
||||||
|
|
||||||
|
const hasPastMigrationsHidden =
|
||||||
|
pastMigrations.length > visiblePastCount && !showAllPastMigrations;
|
||||||
|
const hasFutureMigrationsHidden =
|
||||||
|
futureMigrations.length > visibleFutureCount && !showAllFutureMigrations;
|
||||||
|
|
||||||
|
const currentMigrationRef = useRef<MigrationCardHandle>(null);
|
||||||
|
|
||||||
|
// Auto-expand when entering a failed migration or requires review
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentMigrationFailed || currentMigrationHasChanges) {
|
||||||
|
toggleMigrationExpanded(currentMigration.id, true);
|
||||||
|
}
|
||||||
|
}, [currentMigrationHasChanges, currentMigrationFailed, currentMigration]);
|
||||||
|
|
||||||
|
const toggleMigrationExpanded = (migrationId: string, state?: boolean) => {
|
||||||
|
setExpandedMigrations((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[migrationId]: state ?? !prev[migrationId],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mb-6 flex w-full justify-between">
|
||||||
|
{onCancel && (
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50"
|
||||||
|
>
|
||||||
|
Cancel the migration
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full pl-10">
|
||||||
|
{/* Timeline lines */}
|
||||||
|
{/* Solid line for visible migrations */}
|
||||||
|
<div
|
||||||
|
className="absolute left-10 top-0 w-0.5 bg-slate-200"
|
||||||
|
style={{
|
||||||
|
height: hasFutureMigrationsHidden ? 'calc(100% - 15%)' : '100%',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* Dashed line for the section after the last visible migration */}
|
||||||
|
{hasFutureMigrationsHidden && (
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-10 w-0.5 border-l-2 border-dashed border-slate-200"
|
||||||
|
style={{
|
||||||
|
height: '15%',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timeline container */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* Past migrations section */}
|
||||||
|
{pastMigrations.length > 0 && (
|
||||||
|
<>
|
||||||
|
{showAllPastMigrations && (
|
||||||
|
<div
|
||||||
|
key="show-past-migrations"
|
||||||
|
className="relative mb-6 w-full"
|
||||||
|
>
|
||||||
|
<TimelineButton
|
||||||
|
icon={ChevronDownIcon}
|
||||||
|
onClick={() => setShowAllPastMigrations(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="ml-6">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center"
|
||||||
|
onClick={() => setShowAllPastMigrations(false)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-slate-600">
|
||||||
|
Hide Past Migrations
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{visiblePastMigrations.map((migration) => (
|
||||||
|
<div key={migration.id} className="relative mb-6 w-full">
|
||||||
|
<MigrationStateCircle
|
||||||
|
migration={migration}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
onClick={() => toggleMigrationExpanded(migration.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`ml-6 mt-1`,
|
||||||
|
expandedMigrations[currentMigration.id] ? '-mt-1' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex w-full items-center gap-4 font-medium">
|
||||||
|
<span
|
||||||
|
onClick={() => toggleMigrationExpanded(migration.id)}
|
||||||
|
className={`flex-shrink-0 cursor-pointer whitespace-nowrap text-base ${
|
||||||
|
nxConsoleMetadata.completedMigrations?.[
|
||||||
|
migration.id
|
||||||
|
]?.type === 'successful'
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{migration.name}
|
||||||
|
</span>
|
||||||
|
{!expandedMigrations[migration.id] && (
|
||||||
|
<span className="w-0 flex-1 truncate text-sm text-slate-600/50">
|
||||||
|
{' '}
|
||||||
|
{migration.description}{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expandedMigrations[migration.id] && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{nxConsoleMetadata.completedMigrations?.[migration.id]
|
||||||
|
?.type === 'failed' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(migration.id);
|
||||||
|
onRunMigration(migration);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border border-red-500 bg-red-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-600 dark:border-red-700 dark:bg-red-600 dark:text-white hover:dark:bg-red-700"
|
||||||
|
>
|
||||||
|
Rerun
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
isOpen={expandedMigrations[migration.id]}
|
||||||
|
className="mt-2 w-full rounded-md border border-slate-300 p-3"
|
||||||
|
>
|
||||||
|
<MigrationCard
|
||||||
|
migration={migration}
|
||||||
|
isExpanded={expandedMigrations[migration.id]}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
onFileClick={(file) => onFileClick(migration, file)}
|
||||||
|
onViewImplementation={() =>
|
||||||
|
onViewImplementation(migration)
|
||||||
|
}
|
||||||
|
onViewDocumentation={() =>
|
||||||
|
onViewDocumentation(migration)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasPastMigrationsHidden && (
|
||||||
|
<div
|
||||||
|
key="show-past-migrations"
|
||||||
|
className="relative mb-6 w-full"
|
||||||
|
>
|
||||||
|
<TimelineButton
|
||||||
|
icon={ChevronUpIcon}
|
||||||
|
onClick={() => setShowAllPastMigrations(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="ml-6">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center"
|
||||||
|
onClick={() => setShowAllPastMigrations(true)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-slate-600">
|
||||||
|
Show Past Migrations
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Current migration */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* TODO: Change this to be a clickable element li, button etc... */}
|
||||||
|
<div>
|
||||||
|
<MigrationStateCircle
|
||||||
|
migration={migrations[currentMigrationIndex]}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
isRunning={currentMigrationRunning}
|
||||||
|
onClick={() => toggleMigrationExpanded(currentMigration.id)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`ml-6 mt-1`,
|
||||||
|
expandedMigrations[currentMigration.id] ? '-mt-1' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex w-full items-center gap-4 font-medium">
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 cursor-pointer whitespace-nowrap"
|
||||||
|
onClick={() =>
|
||||||
|
toggleMigrationExpanded(currentMigration.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentMigration.name}
|
||||||
|
</span>
|
||||||
|
{!expandedMigrations[currentMigration.id] && (
|
||||||
|
<p className="w-0 flex-1 truncate text-sm">
|
||||||
|
{currentMigration.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expandedMigrations[currentMigration.id] && (
|
||||||
|
<div className="flex flex-shrink-0 gap-2">
|
||||||
|
{currentMigrationFailed && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(currentMigration.id);
|
||||||
|
onRunMigration(currentMigration);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border border-red-500 bg-red-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-600 dark:border-red-700 dark:bg-red-600 dark:text-white hover:dark:bg-red-700"
|
||||||
|
>
|
||||||
|
Rerun
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!currentMigrationSuccess && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(currentMigration.id);
|
||||||
|
onSkipMigration(currentMigration);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border border-slate-500 bg-slate-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-slate-600 dark:border-slate-600 dark:bg-slate-600 dark:text-white hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentMigrationHasChanges && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(currentMigration.id);
|
||||||
|
onReviewMigration(currentMigration.id);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="flex items-center rounded-md border border-green-500 bg-green-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-600 dark:border-green-700 dark:bg-green-600 dark:text-white hover:dark:bg-green-700"
|
||||||
|
>
|
||||||
|
Approve Changes
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
className="mt-2 w-full rounded-md border border-slate-300/60"
|
||||||
|
isOpen={expandedMigrations[currentMigration.id]}
|
||||||
|
>
|
||||||
|
{/* Migration Card */}
|
||||||
|
<MigrationCard
|
||||||
|
ref={currentMigrationRef}
|
||||||
|
migration={currentMigration}
|
||||||
|
isExpanded={expandedMigrations[currentMigration.id]}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
onFileClick={(file) => onFileClick(currentMigration, file)}
|
||||||
|
forceIsRunning={currentMigrationRunning}
|
||||||
|
onViewImplementation={() =>
|
||||||
|
onViewImplementation(currentMigration)
|
||||||
|
}
|
||||||
|
onViewDocumentation={() =>
|
||||||
|
onViewDocumentation(currentMigration)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Future migrations */}
|
||||||
|
{futureMigrations.length > 0 && (
|
||||||
|
<>
|
||||||
|
{visibleFutureMigrations.map((migration) => (
|
||||||
|
<div key={migration.id} className="relative mt-6 w-full">
|
||||||
|
<MigrationStateCircle
|
||||||
|
migration={migration}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
onClick={() => toggleMigrationExpanded(migration.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
`ml-6 mt-1`,
|
||||||
|
expandedMigrations[migration.id] &&
|
||||||
|
!nxConsoleMetadata.completedMigrations?.[migration.id]
|
||||||
|
? '-mt-1'
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex w-full items-center gap-4">
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 cursor-pointer whitespace-nowrap"
|
||||||
|
onClick={() => toggleMigrationExpanded(migration.id)}
|
||||||
|
>
|
||||||
|
{migration.name}
|
||||||
|
</span>
|
||||||
|
{!expandedMigrations[migration.id] && (
|
||||||
|
<span className="w-0 flex-1 truncate text-sm text-slate-600/50">
|
||||||
|
{migration.description}{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* ONLY SHOW BUTTONS FOR PENDING MIGRATIONS */}
|
||||||
|
{expandedMigrations[migration.id] &&
|
||||||
|
!nxConsoleMetadata.completedMigrations?.[
|
||||||
|
migration.id
|
||||||
|
] && (
|
||||||
|
<div className="flex flex-shrink-0 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(migration.id);
|
||||||
|
onSkipMigration(migration);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border border-slate-500 bg-slate-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-slate-600 dark:border-slate-600 dark:bg-slate-600 dark:text-white hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Collapsible
|
||||||
|
isOpen={expandedMigrations[migration.id]}
|
||||||
|
className="mt-2 w-full rounded-md border border-slate-300/50 p-3"
|
||||||
|
>
|
||||||
|
<MigrationCard
|
||||||
|
migration={migration}
|
||||||
|
nxConsoleMetadata={nxConsoleMetadata}
|
||||||
|
isExpanded={expandedMigrations[migration.id]}
|
||||||
|
onFileClick={(file) => onFileClick(migration, file)}
|
||||||
|
onViewImplementation={() =>
|
||||||
|
onViewImplementation(migration)
|
||||||
|
}
|
||||||
|
onViewDocumentation={() =>
|
||||||
|
onViewDocumentation(migration)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasFutureMigrationsHidden && (
|
||||||
|
<div
|
||||||
|
key="show-future-migrations"
|
||||||
|
className="relative mb-1 mt-9 w-full"
|
||||||
|
>
|
||||||
|
<TimelineButton
|
||||||
|
icon={ChevronDownIcon}
|
||||||
|
onClick={() => setShowAllFutureMigrations(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="ml-6">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center"
|
||||||
|
onClick={() => setShowAllFutureMigrations(true)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-slate-600">
|
||||||
|
Show more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAllFutureMigrations && (
|
||||||
|
<div
|
||||||
|
key="show-future-migrations"
|
||||||
|
className="relative mb-1 mt-6 w-full"
|
||||||
|
>
|
||||||
|
<TimelineButton
|
||||||
|
icon={ChevronUpIcon}
|
||||||
|
onClick={() => setShowAllFutureMigrations(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="ml-6">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center"
|
||||||
|
onClick={() => setShowAllFutureMigrations(false)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-slate-600">
|
||||||
|
Show fewer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimelineButtonProps {
|
||||||
|
icon: React.ElementType;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimelineButton({ icon: Icon, onClick }: TimelineButtonProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 flex h-6 w-6 -translate-x-1/2 cursor-pointer items-center justify-center rounded-full bg-slate-300 text-slate-700"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MigrationStateCircleProps {
|
||||||
|
migration: MigrationDetailsWithId;
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
isRunning?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MigrationStateCircle({
|
||||||
|
migration,
|
||||||
|
nxConsoleMetadata,
|
||||||
|
isRunning,
|
||||||
|
onClick,
|
||||||
|
}: MigrationStateCircleProps) {
|
||||||
|
let bgColor = '';
|
||||||
|
let textColor = '';
|
||||||
|
let Icon = ClockIcon;
|
||||||
|
|
||||||
|
// Check if this migration is in the completed migrations
|
||||||
|
const completedMigration =
|
||||||
|
nxConsoleMetadata.completedMigrations?.[migration.id];
|
||||||
|
|
||||||
|
const isSkipped = completedMigration?.type === 'skipped';
|
||||||
|
const isError = completedMigration?.type === 'failed';
|
||||||
|
const isSuccess = completedMigration?.type === 'successful';
|
||||||
|
|
||||||
|
if (isSkipped) {
|
||||||
|
bgColor = 'bg-slate-300';
|
||||||
|
textColor = 'text-slate-700';
|
||||||
|
Icon = MinusIcon;
|
||||||
|
} else if (isError) {
|
||||||
|
bgColor = 'bg-red-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
Icon = ExclamationCircleIcon;
|
||||||
|
} else if (isRunning) {
|
||||||
|
bgColor = 'bg-blue-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
Icon = ClockIcon;
|
||||||
|
} else if (isSuccess) {
|
||||||
|
bgColor = 'bg-green-500';
|
||||||
|
textColor = 'text-white';
|
||||||
|
Icon = CheckCircleIcon;
|
||||||
|
} else {
|
||||||
|
// Future migration (none of the states above)
|
||||||
|
bgColor = 'bg-slate-300';
|
||||||
|
textColor = 'text-slate-700';
|
||||||
|
Icon = ClockIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 top-0 flex h-8 w-8 -translate-x-1/2 cursor-pointer items-center justify-center rounded-full ${bgColor} ${textColor}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{isRunning ? (
|
||||||
|
<span className="inline-block h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||||
|
) : (
|
||||||
|
<Icon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
236
graph/migrate/src/lib/migrate.stories.tsx
Normal file
236
graph/migrate/src/lib/migrate.stories.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { MigrateUI } from './migrate';
|
||||||
|
|
||||||
|
const meta: Meta<typeof MigrateUI> = {
|
||||||
|
component: MigrateUI,
|
||||||
|
title: 'MigrateUI',
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof MigrateUI>;
|
||||||
|
|
||||||
|
export const Automatic: Story = {
|
||||||
|
args: {
|
||||||
|
migrations: [
|
||||||
|
{
|
||||||
|
id: 'migration-1',
|
||||||
|
name: 'migration-1',
|
||||||
|
description: 'This is a migration that does a thing labeled with one.',
|
||||||
|
version: '1.0.0',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: './src/migrations/migration-1.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-2',
|
||||||
|
name: 'migration-2',
|
||||||
|
description:
|
||||||
|
'Funnily, this is another migration that does a thing labeled with two.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/react',
|
||||||
|
implementation: './src/migrations/migration-2.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3',
|
||||||
|
name: 'migration-3',
|
||||||
|
description:
|
||||||
|
'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: './src/migrations/migration-3.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-4',
|
||||||
|
name: 'migration-4',
|
||||||
|
description: 'This is a migration that does a thing labeled with four.',
|
||||||
|
version: '1.0.2',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: './src/migrations/migration-4.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3-1',
|
||||||
|
name: 'migration-3',
|
||||||
|
description:
|
||||||
|
'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: './src/migrations/migration-3.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-6',
|
||||||
|
name: 'migration-6',
|
||||||
|
description: 'This migration performs updates labeled as number six.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/workspace',
|
||||||
|
implementation: './src/migrations/migration-6.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-7',
|
||||||
|
name: 'migration-7',
|
||||||
|
description:
|
||||||
|
'Lucky number seven migration that updates configurations.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/devkit',
|
||||||
|
implementation: './src/migrations/migration-7.ts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nxConsoleMetadata: {
|
||||||
|
completedMigrations: {
|
||||||
|
'migration-1': {
|
||||||
|
name: 'migration-1',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '123',
|
||||||
|
},
|
||||||
|
'migration-2': {
|
||||||
|
type: 'skipped',
|
||||||
|
},
|
||||||
|
'migration-3': {
|
||||||
|
name: 'migration-3',
|
||||||
|
type: 'failed',
|
||||||
|
error: 'This is an error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
},
|
||||||
|
onFinish: (squash: boolean) => {
|
||||||
|
console.log('finished', squash);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllCompleted: Story = {
|
||||||
|
args: {
|
||||||
|
migrations: [
|
||||||
|
{
|
||||||
|
id: 'migration-1',
|
||||||
|
name: 'migration-1',
|
||||||
|
description: 'This is a migration that does a thing labeled with one.',
|
||||||
|
version: '1.0.0',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: './src/migrations/migration-1.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-2',
|
||||||
|
name: 'migration-2',
|
||||||
|
description:
|
||||||
|
'Funnily, this is another migration that does a thing labeled with two.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/react',
|
||||||
|
implementation: './src/migrations/migration-2.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3',
|
||||||
|
name: 'migration-3',
|
||||||
|
description:
|
||||||
|
'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: './src/migrations/migration-3.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-4',
|
||||||
|
name: 'migration-4',
|
||||||
|
description: 'This is a migration that does a thing labeled with four.',
|
||||||
|
version: '1.0.2',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: './src/migrations/migration-4.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3-1',
|
||||||
|
name: 'migration-3',
|
||||||
|
description:
|
||||||
|
'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: './src/migrations/migration-3.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-6',
|
||||||
|
name: 'migration-6',
|
||||||
|
description: 'This migration performs updates labeled as number six.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/workspace',
|
||||||
|
implementation: './src/migrations/migration-6.ts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-7',
|
||||||
|
name: 'migration-7',
|
||||||
|
description:
|
||||||
|
'Lucky number seven migration that updates configurations.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/devkit',
|
||||||
|
implementation: './src/migrations/migration-7.ts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nxConsoleMetadata: {
|
||||||
|
completedMigrations: {
|
||||||
|
'migration-1': {
|
||||||
|
name: 'migration-1',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '123',
|
||||||
|
},
|
||||||
|
'migration-2': {
|
||||||
|
name: 'migration-2',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '124',
|
||||||
|
},
|
||||||
|
'migration-3': {
|
||||||
|
name: 'migration-3',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '125',
|
||||||
|
},
|
||||||
|
'migration-4': {
|
||||||
|
name: 'migration-4',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '126',
|
||||||
|
},
|
||||||
|
'migration-3-1': {
|
||||||
|
name: 'migration-3',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '127',
|
||||||
|
},
|
||||||
|
'migration-6': {
|
||||||
|
name: 'migration-6',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '128',
|
||||||
|
},
|
||||||
|
'migration-7': {
|
||||||
|
name: 'migration-7',
|
||||||
|
type: 'successful',
|
||||||
|
changedFiles: [],
|
||||||
|
ref: '129',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
},
|
||||||
|
onRunMigration: (migration) => {
|
||||||
|
console.log('run migration', migration);
|
||||||
|
},
|
||||||
|
onRunMany: (migrations, configuration) => {
|
||||||
|
console.log('run many migrations', migrations, configuration);
|
||||||
|
},
|
||||||
|
onSkipMigration: (migration) => {
|
||||||
|
console.log('skip migration', migration);
|
||||||
|
},
|
||||||
|
onFileClick: (migration, file) => {
|
||||||
|
console.log('file click', migration, file);
|
||||||
|
},
|
||||||
|
onViewImplementation: (migration) => {
|
||||||
|
console.log('view implementation', migration);
|
||||||
|
},
|
||||||
|
onViewDocumentation: (migration) => {
|
||||||
|
console.log('view documentation', migration);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
console.log('cancel');
|
||||||
|
},
|
||||||
|
onFinish: (squash: boolean) => {
|
||||||
|
console.log('finished', squash);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
319
graph/migrate/src/lib/migrate.tsx
Normal file
319
graph/migrate/src/lib/migrate.tsx
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { type FileChange } from 'nx/src/devkit-exports';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Popover } from '@nx/graph/ui-common';
|
||||||
|
import { useInterpret, useSelector } from '@xstate/react';
|
||||||
|
import { machine as automaticMigrationMachine } from './state/automatic/machine';
|
||||||
|
import {
|
||||||
|
MigrationInit,
|
||||||
|
MigrationDone,
|
||||||
|
MigrationSettingsPanel,
|
||||||
|
AutomaticMigration,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
|
export interface MigrateUIProps {
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
|
onRunMigration: (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
configuration: {
|
||||||
|
createCommits: boolean;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
onRunMany: (
|
||||||
|
migrations: MigrationDetailsWithId[],
|
||||||
|
configuration: {
|
||||||
|
createCommits: boolean;
|
||||||
|
commitPrefix: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onFinish: (squashCommits: boolean) => void;
|
||||||
|
onFileClick: (
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
file: Omit<FileChange, 'content'>
|
||||||
|
) => void;
|
||||||
|
onViewImplementation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onViewDocumentation: (migration: MigrationDetailsWithId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PrimaryAction {
|
||||||
|
RunMigrations = 'Run Migrations',
|
||||||
|
PauseMigrations = 'Pause Migrations',
|
||||||
|
FinishWithoutSquashingCommits = 'Finish without squashing commits',
|
||||||
|
FinishSquashingCommits = 'Finish (squash commits)',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigrateUI(props: MigrateUIProps) {
|
||||||
|
const [createCommits, setCreateCommits] = useState(true);
|
||||||
|
const [commitPrefix, setCommitPrefix] = useState('');
|
||||||
|
const [primaryAction, setPrimaryAction] = useState<PrimaryAction>(
|
||||||
|
PrimaryAction.RunMigrations
|
||||||
|
);
|
||||||
|
// For popover
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const actor = useInterpret(automaticMigrationMachine, {
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
console.log('runMigration', ctx.currentMigration);
|
||||||
|
if (ctx.currentMigration) {
|
||||||
|
props.onRunMigration(ctx.currentMigration, { createCommits });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isDone = useSelector(actor, (state) => state.matches('done'));
|
||||||
|
const isInit = useSelector(actor, (state) => state.matches('init'));
|
||||||
|
const running = useSelector(actor, (state) => state.matches('running'));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
actor.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: props.migrations,
|
||||||
|
metadata: props.nxConsoleMetadata,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- only load initial data when migrations change
|
||||||
|
}, [JSON.stringify(props.migrations)]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
actor.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata: props.nxConsoleMetadata,
|
||||||
|
});
|
||||||
|
}, [props.nxConsoleMetadata, actor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDone) {
|
||||||
|
setPrimaryAction(PrimaryAction.FinishSquashingCommits);
|
||||||
|
}
|
||||||
|
}, [isDone, primaryAction]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
(primaryAction === PrimaryAction.RunMigrations ||
|
||||||
|
primaryAction === PrimaryAction.PauseMigrations) &&
|
||||||
|
!isInit
|
||||||
|
) {
|
||||||
|
setPrimaryAction(
|
||||||
|
running ? PrimaryAction.PauseMigrations : PrimaryAction.RunMigrations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [running, primaryAction, isInit]);
|
||||||
|
|
||||||
|
const handlePauseResume = () => {
|
||||||
|
if (running) {
|
||||||
|
actor.send({ type: 'pause' });
|
||||||
|
} else {
|
||||||
|
actor.send({ type: 'startRunning' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrimaryActionSelection = () => {
|
||||||
|
if (
|
||||||
|
primaryAction === PrimaryAction.RunMigrations ||
|
||||||
|
primaryAction === PrimaryAction.PauseMigrations
|
||||||
|
) {
|
||||||
|
handlePauseResume();
|
||||||
|
} else if (
|
||||||
|
primaryAction === PrimaryAction.FinishWithoutSquashingCommits ||
|
||||||
|
primaryAction === PrimaryAction.FinishSquashingCommits
|
||||||
|
) {
|
||||||
|
props.onFinish(primaryAction === PrimaryAction.FinishSquashingCommits);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInit) {
|
||||||
|
return (
|
||||||
|
<MigrationInit onStart={() => actor.send({ type: 'startRunning' })} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone) {
|
||||||
|
return (
|
||||||
|
<MigrationDone
|
||||||
|
onCancel={props.onCancel}
|
||||||
|
onFinish={props.onFinish}
|
||||||
|
shouldSquashCommits={createCommits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col overflow-hidden p-2">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="flex shrink-0 items-center justify-between pb-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
Migrating to {props.nxConsoleMetadata.targetVersion}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Migration Controls */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
<MigrationSettingsPanel
|
||||||
|
createCommits={createCommits}
|
||||||
|
setCreateCommits={setCreateCommits}
|
||||||
|
commitPrefix={commitPrefix}
|
||||||
|
setCommitPrefix={setCommitPrefix}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<AutomaticMigration
|
||||||
|
actor={actor}
|
||||||
|
migrations={props.migrations}
|
||||||
|
nxConsoleMetadata={props.nxConsoleMetadata}
|
||||||
|
onRunMigration={(migration) =>
|
||||||
|
props.onRunMigration(migration, { createCommits })
|
||||||
|
}
|
||||||
|
onSkipMigration={(migration) => props.onSkipMigration(migration)}
|
||||||
|
onViewImplementation={(migration) =>
|
||||||
|
props.onViewImplementation(migration)
|
||||||
|
}
|
||||||
|
onViewDocumentation={(migration) =>
|
||||||
|
props.onViewDocumentation(migration)
|
||||||
|
}
|
||||||
|
onFileClick={props.onFileClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bottom-0 flex shrink-0 justify-end gap-2 bg-transparent py-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={props.onCancel}
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<div className="flex">
|
||||||
|
<button
|
||||||
|
onClick={handlePrimaryActionSelection}
|
||||||
|
type="button"
|
||||||
|
title="Finish"
|
||||||
|
className="whitespace-nowrap rounded-l-md border border-blue-700 bg-blue-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-600 dark:border-blue-700 dark:bg-blue-600 dark:text-white hover:dark:bg-blue-700"
|
||||||
|
>
|
||||||
|
{primaryAction}
|
||||||
|
</button>
|
||||||
|
<div className="relative flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
className="border-l-1 flex items-center rounded-r-md border border-blue-700 bg-blue-500 px-2 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 dark:border-blue-700 dark:bg-blue-700 dark:text-white hover:dark:bg-blue-800"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
position={{
|
||||||
|
left: '-14rem',
|
||||||
|
top: isDone ? '2.75rem' : '-9.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ul className="p-2">
|
||||||
|
{!isDone && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
{!running && (
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setPrimaryAction(PrimaryAction.RunMigrations);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
primaryAction === PrimaryAction.RunMigrations
|
||||||
|
? 'inline-block'
|
||||||
|
: 'opacity-0'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>{'Run Migrations'}</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{running && (
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setPrimaryAction(PrimaryAction.PauseMigrations);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
primaryAction === PrimaryAction.PauseMigrations
|
||||||
|
? 'inline-block'
|
||||||
|
: 'opacity-0'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>{'Pause Migrations'}</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<div className="my-1 h-0.5 w-full bg-slate-300/30" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setPrimaryAction(PrimaryAction.FinishSquashingCommits);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
primaryAction === PrimaryAction.FinishSquashingCommits
|
||||||
|
? 'inline-block'
|
||||||
|
: 'opacity-0'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>Squash commits</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="flex cursor-pointer items-center gap-2 p-2 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setPrimaryAction(
|
||||||
|
PrimaryAction.FinishWithoutSquashingCommits
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
primaryAction ===
|
||||||
|
PrimaryAction.FinishWithoutSquashingCommits
|
||||||
|
? 'inline-block'
|
||||||
|
: 'opacity-0'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<span>Do not squash commits</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MigrateUI;
|
||||||
0
graph/migrate/src/lib/state/automatic/actions.ts
Normal file
0
graph/migrate/src/lib/state/automatic/actions.ts
Normal file
49
graph/migrate/src/lib/state/automatic/guards.ts
Normal file
49
graph/migrate/src/lib/state/automatic/guards.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { AutomaticMigrationState } from './types';
|
||||||
|
import {
|
||||||
|
currentMigrationCanLeaveReview,
|
||||||
|
currentMigrationHasChanges,
|
||||||
|
currentMigrationHasFailed,
|
||||||
|
currentMigrationIsSkipped,
|
||||||
|
} from './selectors';
|
||||||
|
|
||||||
|
export const guards = {
|
||||||
|
canStartRunningCurrentMigration: (ctx: AutomaticMigrationState) => {
|
||||||
|
return (
|
||||||
|
!!ctx.currentMigration &&
|
||||||
|
!ctx.currentMigrationRunning &&
|
||||||
|
((!currentMigrationHasFailed(ctx) && !currentMigrationHasChanges(ctx)) ||
|
||||||
|
currentMigrationIsSkipped(ctx))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
currentMigrationIsDone: (ctx: AutomaticMigrationState) => {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!ctx.currentMigration &&
|
||||||
|
!ctx.currentMigrationRunning &&
|
||||||
|
currentMigrationCanLeaveReview(ctx)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
currentMigrationCanLeaveReview: (ctx: AutomaticMigrationState) =>
|
||||||
|
currentMigrationCanLeaveReview(ctx),
|
||||||
|
lastMigrationIsDone: (ctx: AutomaticMigrationState) => {
|
||||||
|
if (!ctx.migrations) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const currentMigrationIndex = ctx.migrations.findIndex(
|
||||||
|
(migration) => migration.id === ctx.currentMigration?.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
currentMigrationIndex === ctx.migrations.length - 1 &&
|
||||||
|
currentMigrationCanLeaveReview(ctx)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
needsReview: (ctx: AutomaticMigrationState) => {
|
||||||
|
return (
|
||||||
|
!ctx.currentMigrationRunning &&
|
||||||
|
!guards.canStartRunningCurrentMigration(ctx)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
365
graph/migrate/src/lib/state/automatic/machine.spec.ts
Normal file
365
graph/migrate/src/lib/state/automatic/machine.spec.ts
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import {
|
||||||
|
addFailedMigration,
|
||||||
|
addSkippedMigration,
|
||||||
|
addSuccessfulMigration,
|
||||||
|
MigrationsJsonMetadata,
|
||||||
|
} from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import { interpret } from 'xstate';
|
||||||
|
import { machine } from './machine';
|
||||||
|
|
||||||
|
const dummyMigrations: MigrationDetailsWithId[] = [
|
||||||
|
{
|
||||||
|
id: 'migration-1',
|
||||||
|
name: 'migration-1',
|
||||||
|
description: 'This is a migration that does a thing labeled with one.',
|
||||||
|
version: '1.0.0',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: '/path/to/migration-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-2',
|
||||||
|
name: 'migration-2',
|
||||||
|
description:
|
||||||
|
'Funnily, this is another migration that does a thing labeled with two.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/react',
|
||||||
|
implementation: '/path/to/migration-2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3',
|
||||||
|
name: 'migration-3',
|
||||||
|
description: 'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: '/path/to/migration-3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-4',
|
||||||
|
name: 'migration-4',
|
||||||
|
description: 'This is a migration that does a thing labeled with four.',
|
||||||
|
version: '1.0.2',
|
||||||
|
package: 'nx',
|
||||||
|
implementation: '/path/to/migration-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-3-1',
|
||||||
|
name: 'migration-3',
|
||||||
|
description: 'This is a migration that does a thing labeled with three.',
|
||||||
|
version: '1.0.1',
|
||||||
|
package: '@nx/js',
|
||||||
|
implementation: '/path/to/migration-3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-6',
|
||||||
|
name: 'migration-6',
|
||||||
|
description: 'This migration performs updates labeled as number six.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/workspace',
|
||||||
|
implementation: '/path/to/migration-6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration-7',
|
||||||
|
name: 'migration-7',
|
||||||
|
description: 'Lucky number seven migration that updates configurations.',
|
||||||
|
version: '1.0.3',
|
||||||
|
package: '@nx/devkit',
|
||||||
|
implementation: '/path/to/migration-7',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Automatic Migration Machine', () => {
|
||||||
|
it('should start in init state', () => {
|
||||||
|
const service = interpret(machine);
|
||||||
|
service.start();
|
||||||
|
expect(service.getSnapshot().value).toBe('init');
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep running migrations until one fails', async () => {
|
||||||
|
let metadata: MigrationsJsonMetadata = {
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
};
|
||||||
|
const service = interpret(
|
||||||
|
machine.withConfig({
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
const migration = ctx.currentMigration;
|
||||||
|
if (!migration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('running migration', migration.id);
|
||||||
|
if (migration.id !== 'migration-3') {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
} else {
|
||||||
|
metadata = addFailedMigration(migration.id, 'error')(metadata);
|
||||||
|
}
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
service.start();
|
||||||
|
service.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: dummyMigrations,
|
||||||
|
metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.send('startRunning');
|
||||||
|
expect(service.getSnapshot().value).toBe('needsReview');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue running migrations after failed one is skipped', async () => {
|
||||||
|
let metadata: MigrationsJsonMetadata = {
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
};
|
||||||
|
const service = interpret(
|
||||||
|
machine.withConfig({
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
const migration = ctx.currentMigration;
|
||||||
|
if (!migration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('running migration', migration.id);
|
||||||
|
if (migration.id !== 'migration-3') {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
} else {
|
||||||
|
metadata = addFailedMigration(migration.id, 'error')(metadata);
|
||||||
|
}
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
service.start();
|
||||||
|
service.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: dummyMigrations,
|
||||||
|
metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.send('startRunning');
|
||||||
|
expect(service.getSnapshot().value).toBe('needsReview');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
metadata = addSkippedMigration('migration-3')(metadata);
|
||||||
|
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.getSnapshot().value).toBe('done');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-7'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep running migrations until one has changes', async () => {
|
||||||
|
let metadata: MigrationsJsonMetadata = {
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
};
|
||||||
|
const service = interpret(
|
||||||
|
machine.withConfig({
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
const migration = ctx.currentMigration;
|
||||||
|
if (!migration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('running migration', migration.id);
|
||||||
|
if (migration.id !== 'migration-3') {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
} else {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'UPDATE',
|
||||||
|
path: 'apps/app/tsconfig.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
}
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
service.start();
|
||||||
|
service.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: dummyMigrations,
|
||||||
|
metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.send('startRunning');
|
||||||
|
expect(service.getSnapshot().value).toBe('needsReview');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep running migrations after changes are reviewed', async () => {
|
||||||
|
let metadata: MigrationsJsonMetadata = {
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
};
|
||||||
|
const service = interpret(
|
||||||
|
machine.withConfig({
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
const migration = ctx.currentMigration;
|
||||||
|
if (!migration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('running migration', migration.id);
|
||||||
|
if (migration.id !== 'migration-3') {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
} else {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'UPDATE',
|
||||||
|
path: 'apps/app/tsconfig.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
}
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
service.start();
|
||||||
|
service.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: dummyMigrations,
|
||||||
|
metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.send('startRunning');
|
||||||
|
expect(service.getSnapshot().value).toBe('needsReview');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.send({
|
||||||
|
type: 'reviewMigration',
|
||||||
|
migrationId: 'migration-3',
|
||||||
|
});
|
||||||
|
expect(service.getSnapshot().value).toBe('done');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-7'
|
||||||
|
);
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not continue running migrations after failed one is skipped if state is paused', async () => {
|
||||||
|
let metadata: MigrationsJsonMetadata = {
|
||||||
|
targetVersion: '20.3.2',
|
||||||
|
};
|
||||||
|
const service = interpret(
|
||||||
|
machine.withConfig({
|
||||||
|
actions: {
|
||||||
|
runMigration: (ctx) => {
|
||||||
|
const migration = ctx.currentMigration;
|
||||||
|
if (!migration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('running migration', migration.id);
|
||||||
|
if (migration.id !== 'migration-3') {
|
||||||
|
metadata = addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
[],
|
||||||
|
'commit-123'
|
||||||
|
)(metadata);
|
||||||
|
} else {
|
||||||
|
metadata = addFailedMigration(migration.id, 'error')(metadata);
|
||||||
|
}
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
service.start();
|
||||||
|
service.send({
|
||||||
|
type: 'loadInitialData',
|
||||||
|
migrations: dummyMigrations,
|
||||||
|
metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.send('startRunning');
|
||||||
|
expect(service.getSnapshot().value).toBe('needsReview');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.send('pause');
|
||||||
|
|
||||||
|
metadata = addSkippedMigration('migration-3')(metadata);
|
||||||
|
|
||||||
|
service.send({
|
||||||
|
type: 'updateMetadata',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.getSnapshot().value).toBe('paused');
|
||||||
|
expect(service.getSnapshot().context.currentMigration?.id).toEqual(
|
||||||
|
'migration-3'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
157
graph/migrate/src/lib/state/automatic/machine.ts
Normal file
157
graph/migrate/src/lib/state/automatic/machine.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { createMachine } from 'xstate';
|
||||||
|
import { guards } from './guards';
|
||||||
|
import type {
|
||||||
|
AutomaticMigrationEvents,
|
||||||
|
AutomaticMigrationState,
|
||||||
|
} from './types';
|
||||||
|
import { findFirstIncompleteMigration } from './selectors';
|
||||||
|
|
||||||
|
export const machine = createMachine<
|
||||||
|
AutomaticMigrationState,
|
||||||
|
AutomaticMigrationEvents
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QFsCWUBOBDALmAxADYD2WEAkgHao6paEAiuWA2gAwC6ioADsbDVTFK3EAA9EbAHQAmAIwAOBXLkB2AJyqALADYtbLTIA0IAJ6IdO9VJ0GAzGx0yZOgKwGAvh5NpMuAgCuPBD+ALJgOGTM7FxIIHwCtMKiEgiKOlJauuqu6mzqCqquyibmCG4ZMppsMmxsdupy6k5ePujYeFI8WAGwkPiwkRg4AEoBlNSUUDGiCYLJcalyupmqVa6ubjLKdtqliHIyNgquLpZshzraWnatIL4dYFIY45NQ+N29YDNxc0kiiwOqjkUk2qjsWjUkMsy32aQUdlkWRceS06js2wxdwe-mer1QU3wP14-HmANAqWKINUOgUVS0SkU+nUcO0oI0lgxllcWTU2PauJeEwJ7xYcliJMSQnJ4kQm0R8kMtg2+RkuzhyxBclcTVcawMbFcENU-L8nSFbyJMgl8VJ-xScrsiMMevUMlUbGBemMZkQLkRbo2BQ2Gl0rlNjzxwsJLDsNr+0odCFy1jq2jkTlpCNUCjhLqk6kLymKVXyWhpEdxlDAkFgIzAADdUGAAO4fHp9Ym2qULCmIAC0y1BhuUarUPIUzVccIcCikRScwLYk6KDRN3nuAs61dr9abraJnFmdsTgIQ7qOwPcziyy9pPrK+mk6Lccgh2rkBgUOi8G8oxAgOBRBxPBjx7GVUn7JxhypMcigZKc4X7NkdF2Nx8mKBDNErTpPj6CAwLJJN+2sKwXCURdilpcs4TdKQMTsJwZB1eoaTpHCngtEVCPtM9GMqZQ6TdHQVEMAxWTnL9tTydQshzdEOKkHcIDrRtmxbHjTz7BA7CaKRs3cXZCyqSw4UM0E1TyXTyLsBQtEUiBhDATTe1lBAdn01RgWcK4GiyLQ8znYp6i0TZJ3cE51F-DwgA */
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
id: 'migrate',
|
||||||
|
initial: 'init',
|
||||||
|
context: {
|
||||||
|
reviewedMigrations: [],
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
init: {
|
||||||
|
on: {
|
||||||
|
startRunning: [
|
||||||
|
{
|
||||||
|
target: 'running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
on: {
|
||||||
|
startRunning: [
|
||||||
|
{
|
||||||
|
target: 'running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
on: {
|
||||||
|
pause: [
|
||||||
|
{
|
||||||
|
target: 'paused',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
cond: 'lastMigrationIsDone',
|
||||||
|
target: 'done',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cond: 'currentMigrationIsDone',
|
||||||
|
target: 'running',
|
||||||
|
actions: ['incrementCurrentMigration'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cond: 'canStartRunningCurrentMigration',
|
||||||
|
actions: ['setCurrentMigrationRunning', 'runMigration'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'needsReview',
|
||||||
|
cond: 'needsReview',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
needsReview: {
|
||||||
|
on: {
|
||||||
|
pause: [
|
||||||
|
{
|
||||||
|
target: 'paused',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
cond: 'currentMigrationCanLeaveReview',
|
||||||
|
target: 'running',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
done: {},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
loadInitialData: [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.migrations = event.migrations;
|
||||||
|
ctx.nxConsoleMetadata = event.metadata;
|
||||||
|
ctx.currentMigration = findFirstIncompleteMigration(
|
||||||
|
event.migrations,
|
||||||
|
event.metadata
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updateMetadata: [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.nxConsoleMetadata = event.metadata;
|
||||||
|
|
||||||
|
if (
|
||||||
|
ctx.currentMigration &&
|
||||||
|
ctx.nxConsoleMetadata.completedMigrations?.[
|
||||||
|
ctx.currentMigration.id
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
ctx.currentMigrationRunning = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reviewMigration: [
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.reviewedMigrations = [
|
||||||
|
...ctx.reviewedMigrations,
|
||||||
|
event.migrationId,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
guards,
|
||||||
|
actions: {
|
||||||
|
incrementCurrentMigration: assign((ctx) => {
|
||||||
|
if (!ctx.migrations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentMigrationIndex = ctx.migrations?.findIndex(
|
||||||
|
(migration) => migration.id === ctx.currentMigration?.id
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentMigrationIndex === undefined ||
|
||||||
|
currentMigrationIndex === ctx.migrations.length - 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.currentMigration = ctx.migrations?.[currentMigrationIndex + 1];
|
||||||
|
}),
|
||||||
|
setCurrentMigrationRunning: assign((ctx) => {
|
||||||
|
ctx.currentMigrationRunning = true;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
78
graph/migrate/src/lib/state/automatic/selectors.ts
Normal file
78
graph/migrate/src/lib/state/automatic/selectors.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
import { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
import type { AutomaticMigrationState } from './types';
|
||||||
|
|
||||||
|
// this is where the ui should start off on
|
||||||
|
// we assume completed migrations in the past are reviewed
|
||||||
|
export function findFirstIncompleteMigration(
|
||||||
|
migrations: MigrationDetailsWithId[],
|
||||||
|
nxConsoleMetadata: MigrationsJsonMetadata
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
migrations.find(
|
||||||
|
(migration) =>
|
||||||
|
nxConsoleMetadata.completedMigrations?.[migration.id]?.type !==
|
||||||
|
'successful' &&
|
||||||
|
nxConsoleMetadata.completedMigrations?.[migration.id]?.type !==
|
||||||
|
'skipped'
|
||||||
|
) ?? migrations[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationHasSucceeded(ctx: AutomaticMigrationState) {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const completedMigration =
|
||||||
|
ctx.nxConsoleMetadata?.completedMigrations?.[ctx.currentMigration.id];
|
||||||
|
return completedMigration?.type === 'successful';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationHasChanges(ctx: AutomaticMigrationState) {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const completedMigration =
|
||||||
|
ctx.nxConsoleMetadata?.completedMigrations?.[ctx.currentMigration.id];
|
||||||
|
return (
|
||||||
|
completedMigration?.type === 'successful' &&
|
||||||
|
completedMigration.changedFiles.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationIsSkipped(ctx: AutomaticMigrationState) {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const completedMigration =
|
||||||
|
ctx.nxConsoleMetadata?.completedMigrations?.[ctx.currentMigration.id];
|
||||||
|
return completedMigration?.type === 'skipped';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationIsReviewed(ctx: AutomaticMigrationState) {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ctx.reviewedMigrations.includes(ctx.currentMigration.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationCanLeaveReview(ctx: AutomaticMigrationState) {
|
||||||
|
return (
|
||||||
|
currentMigrationIsReviewed(ctx) ||
|
||||||
|
currentMigrationIsSkipped(ctx) ||
|
||||||
|
(currentMigrationHasSucceeded(ctx) && !currentMigrationHasChanges(ctx))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentMigrationHasFailed(
|
||||||
|
ctx: AutomaticMigrationState
|
||||||
|
): boolean {
|
||||||
|
if (!ctx.currentMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const completedMigration =
|
||||||
|
ctx.nxConsoleMetadata?.completedMigrations?.[ctx.currentMigration.id];
|
||||||
|
return completedMigration?.type === 'failed';
|
||||||
|
}
|
||||||
33
graph/migrate/src/lib/state/automatic/types.ts
Normal file
33
graph/migrate/src/lib/state/automatic/types.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
|
||||||
|
import { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
export type AutomaticMigrationState = {
|
||||||
|
migrations?: MigrationDetailsWithId[];
|
||||||
|
nxConsoleMetadata?: MigrationsJsonMetadata;
|
||||||
|
currentMigration?: MigrationDetailsWithId;
|
||||||
|
currentMigrationRunning?: boolean;
|
||||||
|
reviewedMigrations: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AutomaticMigrationEvents =
|
||||||
|
| {
|
||||||
|
type: 'loadInitialData';
|
||||||
|
migrations: MigrationDetailsWithId[];
|
||||||
|
metadata: MigrationsJsonMetadata;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'updateMetadata';
|
||||||
|
metadata: MigrationsJsonMetadata;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'pause';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'startRunning';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'reviewMigration';
|
||||||
|
migrationId: string;
|
||||||
|
};
|
||||||
20
graph/migrate/tailwind.config.js
Normal file
20
graph/migrate/tailwind.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
||||||
|
),
|
||||||
|
...createGlobPatternsForDependencies(__dirname),
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
23
graph/migrate/tsconfig.json
Normal file
23
graph/migrate/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": false,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.storybook.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
||||||
28
graph/migrate/tsconfig.lib.json
Normal file
28
graph/migrate/tsconfig.lib.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
"lib": ["DOM"]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"**/*.stories.ts",
|
||||||
|
"**/*.stories.js",
|
||||||
|
"**/*.stories.jsx",
|
||||||
|
"**/*.stories.tsx"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
20
graph/migrate/tsconfig.spec.json
Normal file
20
graph/migrate/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
31
graph/migrate/tsconfig.storybook.json
Normal file
31
graph/migrate/tsconfig.storybook.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"outDir": ""
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.test.js"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.stories.ts",
|
||||||
|
"src/**/*.stories.js",
|
||||||
|
"src/**/*.stories.jsx",
|
||||||
|
"src/**/*.stories.tsx",
|
||||||
|
"src/**/*.stories.mdx",
|
||||||
|
".storybook/*.js",
|
||||||
|
".storybook/*.ts"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nx/react/typings/styled-jsx.d.ts",
|
||||||
|
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nx/react/typings/image.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
graph/ui-common/.eslintrc.json
Normal file
18
graph/ui-common/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
graph/ui-common/README.md
Normal file
3
graph/ui-common/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# ui-common
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
9
graph/ui-common/project.json
Normal file
9
graph/ui-common/project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "ui-common",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "graph/ui-common/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"// targets": "to see all targets run: nx show project ui-common --web",
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
3
graph/ui-common/src/index.ts
Normal file
3
graph/ui-common/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './lib/collapsible';
|
||||||
|
export * from './lib/popover';
|
||||||
|
export * from './lib/button';
|
||||||
31
graph/ui-common/src/lib/button.tsx
Normal file
31
graph/ui-common/src/lib/button.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
className = '',
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`rounded-lg px-4 py-2 font-semibold transition ${
|
||||||
|
disabled
|
||||||
|
? 'cursor-not-allowed bg-gray-400'
|
||||||
|
: 'bg-blue-500 text-white hover:bg-blue-600'
|
||||||
|
} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
graph/ui-common/src/lib/collapsible.tsx
Normal file
26
graph/ui-common/src/lib/collapsible.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface CollapsibleProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Collapsible({ isOpen, children, className }: CollapsibleProps) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
className={`overflow-hidden ${className}`}
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
graph/ui-common/src/lib/popover.tsx
Normal file
33
graph/ui-common/src/lib/popover.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useFloating, useDismiss, useInteractions } from '@floating-ui/react';
|
||||||
|
|
||||||
|
export interface PopoverProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
position?: { top?: string; left?: string; right?: string; bottom?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Popover({ isOpen, onClose, children, position }: PopoverProps) {
|
||||||
|
const { refs, floatingStyles, context } = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismiss = useDismiss(context, { referencePress: false });
|
||||||
|
const { getFloatingProps } = useInteractions([dismiss]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{ ...floatingStyles, ...position }}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
className="animate-fadeIn absolute z-50 flex w-64 flex-col gap-1 rounded-md border border-slate-300/[0.25] bg-white text-slate-700 shadow-lg transition-opacity duration-200 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
graph/ui-common/tsconfig.json
Normal file
20
graph/ui-common/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
graph/ui-common/tsconfig.lib.json
Normal file
19
graph/ui-common/tsconfig.lib.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nx/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"jest.config.ts"
|
||||||
|
],
|
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './lib/project-details/project-details';
|
export * from './lib/project-details/project-details';
|
||||||
export * from './lib/utils/group-targets';
|
export * from './lib/utils/group-targets';
|
||||||
|
export * from './lib/pill';
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export function Pill({
|
|||||||
tooltip,
|
tooltip,
|
||||||
}: {
|
}: {
|
||||||
text: string;
|
text: string;
|
||||||
color?: 'grey' | 'green' | 'yellow';
|
color?: 'grey' | 'green' | 'yellow' | 'red';
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@ -19,7 +19,9 @@ export function Pill({
|
|||||||
color === 'green' &&
|
color === 'green' &&
|
||||||
'bg-green-400/10 text-green-500 ring-green-500/40 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
|
'bg-green-400/10 text-green-500 ring-green-500/40 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
|
||||||
color === 'yellow' &&
|
color === 'yellow' &&
|
||||||
'bg-yellow-50 text-yellow-600 ring-yellow-500/40 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-500/20'
|
'bg-yellow-50 text-yellow-600 ring-yellow-500/40 dark:bg-yellow-900/30 dark:text-yellow-400 dark:ring-yellow-500/20',
|
||||||
|
color === 'red' &&
|
||||||
|
'bg-red-50 text-red-600 ring-red-500/40 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/20'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
18
nx.json
18
nx.json
@ -240,7 +240,16 @@
|
|||||||
"serveStaticTargetName": "serve-static"
|
"serveStaticTargetName": "serve-static"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nx/powerpack-enterprise-cloud"
|
"@nx/powerpack-enterprise-cloud",
|
||||||
|
{
|
||||||
|
"plugin": "@nx/storybook/plugin",
|
||||||
|
"options": {
|
||||||
|
"serveStorybookTargetName": "serve:storybook",
|
||||||
|
"buildStorybookTargetName": "build:storybook",
|
||||||
|
"testStorybookTargetName": "test-storybook",
|
||||||
|
"staticStorybookTargetName": "static:storybook"
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"nxCloudId": "62d013ea0852fe0a2df74438",
|
"nxCloudId": "62d013ea0852fe0a2df74438",
|
||||||
"nxCloudUrl": "https://staging.nx.app",
|
"nxCloudUrl": "https://staging.nx.app",
|
||||||
@ -371,5 +380,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"generators": {
|
||||||
|
"@nx/react": {
|
||||||
|
"library": {
|
||||||
|
"unitTestRunner": "jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
package.json
18
package.json
@ -114,14 +114,15 @@
|
|||||||
"@rspack/dev-server": "1.0.9",
|
"@rspack/dev-server": "1.0.9",
|
||||||
"@rspack/plugin-minify": "^0.7.5",
|
"@rspack/plugin-minify": "^0.7.5",
|
||||||
"@rspack/plugin-react-refresh": "^1.0.0",
|
"@rspack/plugin-react-refresh": "^1.0.0",
|
||||||
"@schematics/angular": "19.2.0",
|
"@schematics/angular": "~19.2.0",
|
||||||
"@storybook/addon-essentials": "8.6.12",
|
"@storybook/addon-essentials": "8.4.6",
|
||||||
"@storybook/addon-interactions": "8.6.12",
|
"@storybook/addon-interactions": "8.4.6",
|
||||||
"@storybook/core-server": "8.6.12",
|
"@storybook/core-server": "8.4.6",
|
||||||
"@storybook/react": "8.6.12",
|
"@storybook/react": "8.4.6",
|
||||||
"@storybook/react-vite": "8.6.12",
|
"@storybook/react-vite": "8.4.6",
|
||||||
"@storybook/react-webpack5": "8.6.12",
|
"@storybook/react-webpack5": "8.4.6",
|
||||||
"@storybook/types": "8.6.12",
|
"@storybook/test": "^8.5.1",
|
||||||
|
"@storybook/types": "^8.4.6",
|
||||||
"@supabase/supabase-js": "^2.26.0",
|
"@supabase/supabase-js": "^2.26.0",
|
||||||
"@svgr/rollup": "^8.1.0",
|
"@svgr/rollup": "^8.1.0",
|
||||||
"@svgr/webpack": "^8.0.1",
|
"@svgr/webpack": "^8.0.1",
|
||||||
@ -164,6 +165,7 @@
|
|||||||
"@typescript-eslint/type-utils": "^8.19.0",
|
"@typescript-eslint/type-utils": "^8.19.0",
|
||||||
"@typescript-eslint/utils": "^8.19.0",
|
"@typescript-eslint/utils": "^8.19.0",
|
||||||
"@webcontainer/api": "1.5.1",
|
"@webcontainer/api": "1.5.1",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"@xstate/immer": "0.3.1",
|
"@xstate/immer": "0.3.1",
|
||||||
"@xstate/inspect": "0.7.0",
|
"@xstate/inspect": "0.7.0",
|
||||||
"@xstate/react": "3.0.1",
|
"@xstate/react": "3.0.1",
|
||||||
|
|||||||
321
packages/nx/src/command-line/migrate/migrate-ui-api.ts
Normal file
321
packages/nx/src/command-line/migrate/migrate-ui-api.ts
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||||
|
import { join, resolve } from 'path';
|
||||||
|
import type { MigrationDetailsWithId } from '../../config/misc-interfaces';
|
||||||
|
import type { FileChange } from '../../generators/tree';
|
||||||
|
import {
|
||||||
|
getImplementationPath as getMigrationImplementationPath,
|
||||||
|
nxCliPath,
|
||||||
|
readMigrationCollection,
|
||||||
|
} from './migrate';
|
||||||
|
|
||||||
|
export type MigrationsJsonMetadata = {
|
||||||
|
completedMigrations?: Record<
|
||||||
|
string,
|
||||||
|
SuccessfulMigration | FailedMigration | SkippedMigration
|
||||||
|
>;
|
||||||
|
runningMigrations?: string[];
|
||||||
|
initialGitRef?: {
|
||||||
|
ref: string;
|
||||||
|
subject: string;
|
||||||
|
};
|
||||||
|
confirmedPackageUpdates?: boolean;
|
||||||
|
targetVersion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SuccessfulMigration = {
|
||||||
|
type: 'successful';
|
||||||
|
name: string;
|
||||||
|
changedFiles: Omit<FileChange, 'content'>[];
|
||||||
|
ref: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FailedMigration = {
|
||||||
|
type: 'failed';
|
||||||
|
name: string;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SkippedMigration = {
|
||||||
|
type: 'skipped';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function recordInitialMigrationMetadata(
|
||||||
|
workspacePath: string,
|
||||||
|
versionToMigrateTo: string
|
||||||
|
) {
|
||||||
|
const migrationsJsonPath = join(workspacePath, 'migrations.json');
|
||||||
|
const parsedMigrationsJson = JSON.parse(
|
||||||
|
readFileSync(migrationsJsonPath, 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
const gitRef = execSync('git rev-parse HEAD', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
const gitSubject = execSync('git log -1 --pretty=%s', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
parsedMigrationsJson['nx-console'] = {
|
||||||
|
initialGitRef: {
|
||||||
|
ref: gitRef,
|
||||||
|
subject: gitSubject,
|
||||||
|
},
|
||||||
|
targetVersion: versionToMigrateTo,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
migrationsJsonPath,
|
||||||
|
JSON.stringify(parsedMigrationsJson, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function finishMigrationProcess(
|
||||||
|
workspacePath: string,
|
||||||
|
squashCommits: boolean,
|
||||||
|
commitMessage: string
|
||||||
|
) {
|
||||||
|
const migrationsJsonPath = join(workspacePath, 'migrations.json');
|
||||||
|
const parsedMigrationsJson = JSON.parse(
|
||||||
|
readFileSync(migrationsJsonPath, 'utf-8')
|
||||||
|
);
|
||||||
|
const initialGitRef = (
|
||||||
|
parsedMigrationsJson['nx-console'] as MigrationsJsonMetadata
|
||||||
|
).initialGitRef;
|
||||||
|
|
||||||
|
if (existsSync(migrationsJsonPath)) {
|
||||||
|
rmSync(migrationsJsonPath);
|
||||||
|
}
|
||||||
|
execSync('git add .', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
execSync(`git commit -m "${commitMessage}" --no-verify`, {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (squashCommits && initialGitRef) {
|
||||||
|
execSync(`git reset --soft ${initialGitRef.ref}`, {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
execSync(`git commit -m "${commitMessage}" --no-verify`, {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSingleMigration(
|
||||||
|
workspacePath: string,
|
||||||
|
migration: MigrationDetailsWithId,
|
||||||
|
configuration: {
|
||||||
|
createCommits: boolean;
|
||||||
|
commitPrefix?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath,
|
||||||
|
addRunningMigration(migration.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const gitRefBefore = execSync('git rev-parse HEAD', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
const cliPath = nxCliPath(workspacePath);
|
||||||
|
const updatedMigrateLocation = resolve(
|
||||||
|
cliPath,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'nx',
|
||||||
|
'src',
|
||||||
|
'command-line',
|
||||||
|
'migrate',
|
||||||
|
'migrate.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedMigrateModule: typeof import('./migrate') = await import(
|
||||||
|
updatedMigrateLocation
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileChanges = await updatedMigrateModule.runNxOrAngularMigration(
|
||||||
|
workspacePath,
|
||||||
|
migration,
|
||||||
|
false,
|
||||||
|
configuration.createCommits,
|
||||||
|
configuration.commitPrefix || 'chore: [nx migration] ',
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const gitRefAfter = execSync('git rev-parse HEAD', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath,
|
||||||
|
addSuccessfulMigration(
|
||||||
|
migration.id,
|
||||||
|
fileChanges.map((change) => ({
|
||||||
|
path: change.path,
|
||||||
|
type: change.type,
|
||||||
|
})),
|
||||||
|
gitRefAfter
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gitRefBefore !== gitRefAfter) {
|
||||||
|
execSync('git add migrations.json', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
execSync('git commit --amend --no-verify --no-edit', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath,
|
||||||
|
addFailedMigration(migration.id, e.message)
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath,
|
||||||
|
removeRunningMigration(migration.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
execSync('git add migrations.json', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImplementationPath(
|
||||||
|
workspacePath: string,
|
||||||
|
migration: MigrationDetailsWithId
|
||||||
|
) {
|
||||||
|
const { collection, collectionPath } = readMigrationCollection(
|
||||||
|
migration.package,
|
||||||
|
workspacePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const { path } = getMigrationImplementationPath(
|
||||||
|
collection,
|
||||||
|
collectionPath,
|
||||||
|
migration.name
|
||||||
|
);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath: string,
|
||||||
|
modify: (
|
||||||
|
migrationsJsonMetadata: MigrationsJsonMetadata
|
||||||
|
) => MigrationsJsonMetadata
|
||||||
|
) {
|
||||||
|
const migrationsJsonPath = join(workspacePath, 'migrations.json');
|
||||||
|
const migrationsJson = JSON.parse(readFileSync(migrationsJsonPath, 'utf-8'));
|
||||||
|
migrationsJson['nx-console'] = modify(migrationsJson['nx-console']);
|
||||||
|
writeFileSync(migrationsJsonPath, JSON.stringify(migrationsJson, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSuccessfulMigration(
|
||||||
|
id: string,
|
||||||
|
fileChanges: Omit<FileChange, 'content'>[],
|
||||||
|
ref: string
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
migrationsJsonMetadata: MigrationsJsonMetadata
|
||||||
|
): MigrationsJsonMetadata => {
|
||||||
|
const copied = { ...migrationsJsonMetadata };
|
||||||
|
if (!copied.completedMigrations) {
|
||||||
|
copied.completedMigrations = {};
|
||||||
|
}
|
||||||
|
copied.completedMigrations = {
|
||||||
|
...copied.completedMigrations,
|
||||||
|
[id]: {
|
||||||
|
type: 'successful',
|
||||||
|
name: id,
|
||||||
|
changedFiles: fileChanges,
|
||||||
|
ref,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return copied;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFailedMigration(id: string, error: string) {
|
||||||
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
|
const copied = { ...migrationsJsonMetadata };
|
||||||
|
if (!copied.completedMigrations) {
|
||||||
|
copied.completedMigrations = {};
|
||||||
|
}
|
||||||
|
copied.completedMigrations = {
|
||||||
|
...copied.completedMigrations,
|
||||||
|
[id]: {
|
||||||
|
type: 'failed',
|
||||||
|
name: id,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return copied;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSkippedMigration(id: string) {
|
||||||
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
|
const copied = { ...migrationsJsonMetadata };
|
||||||
|
if (!copied.completedMigrations) {
|
||||||
|
copied.completedMigrations = {};
|
||||||
|
}
|
||||||
|
copied.completedMigrations = {
|
||||||
|
...copied.completedMigrations,
|
||||||
|
[id]: {
|
||||||
|
type: 'skipped',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return copied;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRunningMigration(id: string) {
|
||||||
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
|
migrationsJsonMetadata.runningMigrations = [
|
||||||
|
...(migrationsJsonMetadata.runningMigrations ?? []),
|
||||||
|
id,
|
||||||
|
];
|
||||||
|
return migrationsJsonMetadata;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRunningMigration(id: string) {
|
||||||
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
|
migrationsJsonMetadata.runningMigrations =
|
||||||
|
migrationsJsonMetadata.runningMigrations?.filter((n) => n !== id);
|
||||||
|
if (migrationsJsonMetadata.runningMigrations?.length === 0) {
|
||||||
|
delete migrationsJsonMetadata.runningMigrations;
|
||||||
|
}
|
||||||
|
return migrationsJsonMetadata;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readMigrationsJsonMetadata(
|
||||||
|
workspacePath: string
|
||||||
|
): MigrationsJsonMetadata {
|
||||||
|
const migrationsJsonPath = join(workspacePath, 'migrations.json');
|
||||||
|
const migrationsJson = JSON.parse(readFileSync(migrationsJsonPath, 'utf-8'));
|
||||||
|
return migrationsJson['nx-console'];
|
||||||
|
}
|
||||||
@ -23,7 +23,12 @@ import {
|
|||||||
PackageJsonUpdates,
|
PackageJsonUpdates,
|
||||||
} from '../../config/misc-interfaces';
|
} from '../../config/misc-interfaces';
|
||||||
import { NxJsonConfiguration } from '../../config/nx-json';
|
import { NxJsonConfiguration } from '../../config/nx-json';
|
||||||
import { flushChanges, FsTree, printChanges } from '../../generators/tree';
|
import {
|
||||||
|
FileChange,
|
||||||
|
flushChanges,
|
||||||
|
FsTree,
|
||||||
|
printChanges,
|
||||||
|
} from '../../generators/tree';
|
||||||
import {
|
import {
|
||||||
extractFileFromTarball,
|
extractFileFromTarball,
|
||||||
fileExists,
|
fileExists,
|
||||||
@ -46,6 +51,8 @@ import {
|
|||||||
createTempNpmDirectory,
|
createTempNpmDirectory,
|
||||||
detectPackageManager,
|
detectPackageManager,
|
||||||
getPackageManagerCommand,
|
getPackageManagerCommand,
|
||||||
|
PackageManager,
|
||||||
|
PackageManagerCommands,
|
||||||
packageRegistryPack,
|
packageRegistryPack,
|
||||||
packageRegistryView,
|
packageRegistryView,
|
||||||
resolvePackageVersionUsingRegistry,
|
resolvePackageVersionUsingRegistry,
|
||||||
@ -1311,31 +1318,38 @@ async function generateMigrationsJsonAndUpdatePackageJson(
|
|||||||
// If for some reason it fails, it shouldn't affect the overall migration process
|
// If for some reason it fails, it shouldn't affect the overall migration process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bodyLines = process.env['NX_CONSOLE']
|
||||||
|
? [
|
||||||
|
'- Inspect the package.json changes in the built-in diff editor [Click to open]',
|
||||||
|
'- Confirm the changes to install the new dependencies and continue the migration',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`- Make sure package.json changes make sense and then run '${pmc.install}',`,
|
||||||
|
...(migrations.length > 0
|
||||||
|
? [`- Run '${pmc.exec} nx migrate --run-migrations'`]
|
||||||
|
: []),
|
||||||
|
...(opts.interactive && minVersionWithSkippedUpdates
|
||||||
|
? [
|
||||||
|
`- You opted out of some migrations for now. Write the following command down somewhere to apply these migrations later:`,
|
||||||
|
` nx migrate ${opts.targetVersion} --from ${opts.targetPackage}@${minVersionWithSkippedUpdates} --exclude-applied-migrations`,
|
||||||
|
`- To learn more go to https://nx.dev/recipes/tips-n-tricks/advanced-update`,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`- To learn more go to https://nx.dev/features/automate-updating-dependencies`,
|
||||||
|
]),
|
||||||
|
...(showConnectToCloudMessage()
|
||||||
|
? [
|
||||||
|
`- You may run '${pmc.run(
|
||||||
|
'nx',
|
||||||
|
'connect-to-nx-cloud'
|
||||||
|
)}' to get faster builds, GitHub integration, and more. Check out https://nx.app`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
output.log({
|
output.log({
|
||||||
title: 'Next steps:',
|
title: 'Next steps:',
|
||||||
bodyLines: [
|
bodyLines,
|
||||||
`- Make sure package.json changes make sense and then run '${pmc.install}',`,
|
|
||||||
...(migrations.length > 0
|
|
||||||
? [`- Run '${pmc.exec} nx migrate --run-migrations'`]
|
|
||||||
: []),
|
|
||||||
...(opts.interactive && minVersionWithSkippedUpdates
|
|
||||||
? [
|
|
||||||
`- You opted out of some migrations for now. Write the following command down somewhere to apply these migrations later:`,
|
|
||||||
` nx migrate ${opts.targetVersion} --from ${opts.targetPackage}@${minVersionWithSkippedUpdates} --exclude-applied-migrations`,
|
|
||||||
`- To learn more go to https://nx.dev/recipes/tips-n-tricks/advanced-update`,
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
`- To learn more go to https://nx.dev/features/automate-updating-dependencies`,
|
|
||||||
]),
|
|
||||||
...(showConnectToCloudMessage()
|
|
||||||
? [
|
|
||||||
`- You may run '${pmc.run(
|
|
||||||
'nx',
|
|
||||||
'connect-to-nx-cloud'
|
|
||||||
)}' to get faster builds, GitHub integration, and more. Check out https://nx.app`,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
output.error({
|
output.error({
|
||||||
@ -1402,17 +1416,28 @@ function showConnectToCloudMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runInstall() {
|
function runInstall(nxWorkspaceRoot?: string) {
|
||||||
const pmCommands = getPackageManagerCommand();
|
let packageManager: PackageManager;
|
||||||
|
let pmCommands: PackageManagerCommands;
|
||||||
|
if (nxWorkspaceRoot) {
|
||||||
|
packageManager = detectPackageManager(nxWorkspaceRoot);
|
||||||
|
pmCommands = getPackageManagerCommand(packageManager, nxWorkspaceRoot);
|
||||||
|
} else {
|
||||||
|
pmCommands = getPackageManagerCommand();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove this
|
// TODO: remove this
|
||||||
if (detectPackageManager() === 'npm') {
|
if (packageManager ?? detectPackageManager() === 'npm') {
|
||||||
process.env.npm_config_legacy_peer_deps ??= 'true';
|
process.env.npm_config_legacy_peer_deps ??= 'true';
|
||||||
}
|
}
|
||||||
output.log({
|
output.log({
|
||||||
title: `Running '${pmCommands.install}' to make sure necessary packages are installed`,
|
title: `Running '${pmCommands.install}' to make sure necessary packages are installed`,
|
||||||
});
|
});
|
||||||
execSync(pmCommands.install, { stdio: [0, 1, 2], windowsHide: false });
|
execSync(pmCommands.install, {
|
||||||
|
stdio: [0, 1, 2],
|
||||||
|
windowsHide: false,
|
||||||
|
cwd: nxWorkspaceRoot ?? process.cwd(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeMigrations(
|
export async function executeMigrations(
|
||||||
@ -1428,14 +1453,7 @@ export async function executeMigrations(
|
|||||||
shouldCreateCommits: boolean,
|
shouldCreateCommits: boolean,
|
||||||
commitPrefix: string
|
commitPrefix: string
|
||||||
) {
|
) {
|
||||||
let initialDeps = getStringifiedPackageJsonDeps(root);
|
const changedDepInstaller = new ChangedDepInstaller(root);
|
||||||
const installDepsIfChanged = () => {
|
|
||||||
const currentDeps = getStringifiedPackageJsonDeps(root);
|
|
||||||
if (initialDeps !== currentDeps) {
|
|
||||||
runInstall();
|
|
||||||
}
|
|
||||||
initialDeps = currentDeps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const migrationsWithNoChanges: typeof migrations = [];
|
const migrationsWithNoChanges: typeof migrations = [];
|
||||||
const sortedMigrations = migrations.sort((a, b) => {
|
const sortedMigrations = migrations.sort((a, b) => {
|
||||||
@ -1461,75 +1479,16 @@ export async function executeMigrations(
|
|||||||
for (const m of sortedMigrations) {
|
for (const m of sortedMigrations) {
|
||||||
logger.info(`Running migration ${m.package}: ${m.name}`);
|
logger.info(`Running migration ${m.package}: ${m.name}`);
|
||||||
try {
|
try {
|
||||||
const { collection, collectionPath } = readMigrationCollection(
|
const changes = await runNxOrAngularMigration(
|
||||||
m.package,
|
root,
|
||||||
root
|
m,
|
||||||
|
isVerbose,
|
||||||
|
shouldCreateCommits,
|
||||||
|
commitPrefix,
|
||||||
|
() => changedDepInstaller.installDepsIfChanged()
|
||||||
);
|
);
|
||||||
if (!isAngularMigration(collection, collectionPath, m.name)) {
|
if (changes.length === 0) {
|
||||||
const changes = await runNxMigration(
|
migrationsWithNoChanges.push(m);
|
||||||
root,
|
|
||||||
collectionPath,
|
|
||||||
collection,
|
|
||||||
m.name
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Ran ${m.name} from ${m.package}`);
|
|
||||||
logger.info(` ${m.description}\n`);
|
|
||||||
if (changes.length < 1) {
|
|
||||||
logger.info(`No changes were made\n`);
|
|
||||||
migrationsWithNoChanges.push(m);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Changes:');
|
|
||||||
printChanges(changes, ' ');
|
|
||||||
logger.info('');
|
|
||||||
} else {
|
|
||||||
const ngCliAdapter = await getNgCompatLayer();
|
|
||||||
const { madeChanges, loggingQueue } = await ngCliAdapter.runMigration(
|
|
||||||
root,
|
|
||||||
m.package,
|
|
||||||
m.name,
|
|
||||||
readProjectsConfigurationFromProjectGraph(
|
|
||||||
await createProjectGraphAsync()
|
|
||||||
).projects,
|
|
||||||
isVerbose
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`Ran ${m.name} from ${m.package}`);
|
|
||||||
logger.info(` ${m.description}\n`);
|
|
||||||
if (!madeChanges) {
|
|
||||||
logger.info(`No changes were made\n`);
|
|
||||||
migrationsWithNoChanges.push(m);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Changes:');
|
|
||||||
loggingQueue.forEach((log) => logger.info(' ' + log));
|
|
||||||
logger.info('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldCreateCommits) {
|
|
||||||
installDepsIfChanged();
|
|
||||||
|
|
||||||
const commitMessage = `${commitPrefix}${m.name}`;
|
|
||||||
try {
|
|
||||||
const committedSha = commitChanges(commitMessage);
|
|
||||||
|
|
||||||
if (committedSha) {
|
|
||||||
logger.info(
|
|
||||||
chalk.dim(`- Commit created for changes: ${committedSha}`)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
chalk.red(
|
|
||||||
`- A commit could not be created/retrieved for an unknown reason`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.info(chalk.red(`- ${e.message}`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logger.info(`---------------------------------------------------------`);
|
logger.info(`---------------------------------------------------------`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1541,12 +1500,119 @@ export async function executeMigrations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldCreateCommits) {
|
if (!shouldCreateCommits) {
|
||||||
installDepsIfChanged();
|
changedDepInstaller.installDepsIfChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrationsWithNoChanges;
|
return migrationsWithNoChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChangedDepInstaller {
|
||||||
|
private initialDeps: string;
|
||||||
|
constructor(private readonly root: string) {
|
||||||
|
this.initialDeps = getStringifiedPackageJsonDeps(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public installDepsIfChanged() {
|
||||||
|
const currentDeps = getStringifiedPackageJsonDeps(this.root);
|
||||||
|
if (this.initialDeps !== currentDeps) {
|
||||||
|
runInstall(this.root);
|
||||||
|
}
|
||||||
|
this.initialDeps = currentDeps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runNxOrAngularMigration(
|
||||||
|
root: string,
|
||||||
|
migration: {
|
||||||
|
package: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
version: string;
|
||||||
|
cli?: 'nx' | 'angular';
|
||||||
|
},
|
||||||
|
isVerbose: boolean,
|
||||||
|
shouldCreateCommits: boolean,
|
||||||
|
commitPrefix: string,
|
||||||
|
installDepsIfChanged?: () => void,
|
||||||
|
handleInstallDeps = false
|
||||||
|
): Promise<FileChange[]> {
|
||||||
|
if (!installDepsIfChanged) {
|
||||||
|
const changedDepInstaller = new ChangedDepInstaller(root);
|
||||||
|
installDepsIfChanged = () => changedDepInstaller.installDepsIfChanged();
|
||||||
|
}
|
||||||
|
const { collection, collectionPath } = readMigrationCollection(
|
||||||
|
migration.package,
|
||||||
|
root
|
||||||
|
);
|
||||||
|
let changes: FileChange[] = [];
|
||||||
|
if (!isAngularMigration(collection, collectionPath, migration.name)) {
|
||||||
|
changes = await runNxMigration(
|
||||||
|
root,
|
||||||
|
collectionPath,
|
||||||
|
collection,
|
||||||
|
migration.name
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Ran ${migration.name} from ${migration.package}`);
|
||||||
|
logger.info(` ${migration.description}\n`);
|
||||||
|
if (changes.length < 1) {
|
||||||
|
logger.info(`No changes were made\n`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Changes:');
|
||||||
|
printChanges(changes, ' ');
|
||||||
|
logger.info('');
|
||||||
|
} else {
|
||||||
|
const ngCliAdapter = await getNgCompatLayer();
|
||||||
|
const { madeChanges, loggingQueue } = await ngCliAdapter.runMigration(
|
||||||
|
root,
|
||||||
|
migration.package,
|
||||||
|
migration.name,
|
||||||
|
readProjectsConfigurationFromProjectGraph(await createProjectGraphAsync())
|
||||||
|
.projects,
|
||||||
|
isVerbose
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Ran ${migration.name} from ${migration.package}`);
|
||||||
|
logger.info(` ${migration.description}\n`);
|
||||||
|
if (!madeChanges) {
|
||||||
|
logger.info(`No changes were made\n`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Changes:');
|
||||||
|
loggingQueue.forEach((log) => logger.info(' ' + log));
|
||||||
|
logger.info('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCreateCommits) {
|
||||||
|
installDepsIfChanged();
|
||||||
|
|
||||||
|
const commitMessage = `${commitPrefix}${migration.name}`;
|
||||||
|
try {
|
||||||
|
const committedSha = commitChanges(commitMessage, root);
|
||||||
|
|
||||||
|
if (committedSha) {
|
||||||
|
logger.info(chalk.dim(`- Commit created for changes: ${committedSha}`));
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
chalk.red(
|
||||||
|
`- A commit could not be created/retrieved for an unknown reason`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(chalk.red(`- ${e.message}`));
|
||||||
|
}
|
||||||
|
// if we are running this function alone, we need to install deps internally
|
||||||
|
} else if (handleInstallDeps) {
|
||||||
|
installDepsIfChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
async function runMigrations(
|
async function runMigrations(
|
||||||
root: string,
|
root: string,
|
||||||
opts: { runMigrations: string; ifExists: boolean },
|
opts: { runMigrations: string; ifExists: boolean },
|
||||||
@ -1711,7 +1777,7 @@ export function runMigration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMigrationCollection(packageName: string, root: string) {
|
export function readMigrationCollection(packageName: string, root: string) {
|
||||||
const collectionPath = readPackageMigrationConfig(
|
const collectionPath = readPackageMigrationConfig(
|
||||||
packageName,
|
packageName,
|
||||||
root
|
root
|
||||||
@ -1724,7 +1790,7 @@ function readMigrationCollection(packageName: string, root: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImplementationPath(
|
export function getImplementationPath(
|
||||||
collection: MigrationsJson,
|
collection: MigrationsJson,
|
||||||
collectionPath: string,
|
collectionPath: string,
|
||||||
name: string
|
name: string
|
||||||
@ -1755,7 +1821,7 @@ function getImplementationPath(
|
|||||||
return { path: implPath, fnSymbol };
|
return { path: implPath, fnSymbol };
|
||||||
}
|
}
|
||||||
|
|
||||||
function nxCliPath() {
|
export function nxCliPath(nxWorkspaceRoot?: string) {
|
||||||
const version = process.env.NX_MIGRATE_CLI_VERSION || 'latest';
|
const version = process.env.NX_MIGRATE_CLI_VERSION || 'latest';
|
||||||
try {
|
try {
|
||||||
const packageManager = detectPackageManager();
|
const packageManager = detectPackageManager();
|
||||||
@ -1769,7 +1835,10 @@ function nxCliPath() {
|
|||||||
},
|
},
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
});
|
});
|
||||||
copyPackageManagerConfigurationFiles(workspaceRoot, tmpDir);
|
copyPackageManagerConfigurationFiles(
|
||||||
|
nxWorkspaceRoot ?? workspaceRoot,
|
||||||
|
tmpDir
|
||||||
|
);
|
||||||
if (pmc.preInstall) {
|
if (pmc.preInstall) {
|
||||||
// ensure package.json and repo in tmp folder is set to a proper package manager state
|
// ensure package.json and repo in tmp folder is set to a proper package manager state
|
||||||
execSync(pmc.preInstall, {
|
execSync(pmc.preInstall, {
|
||||||
@ -1795,7 +1864,7 @@ function nxCliPath() {
|
|||||||
|
|
||||||
// Set NODE_PATH so that these modules can be used for module resolution
|
// Set NODE_PATH so that these modules can be used for module resolution
|
||||||
addToNodePath(join(tmpDir, 'node_modules'));
|
addToNodePath(join(tmpDir, 'node_modules'));
|
||||||
addToNodePath(join(workspaceRoot, 'node_modules'));
|
addToNodePath(join(nxWorkspaceRoot ?? workspaceRoot, 'node_modules'));
|
||||||
|
|
||||||
return join(tmpDir, `node_modules`, '.bin', 'nx');
|
return join(tmpDir, `node_modules`, '.bin', 'nx');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -75,6 +75,17 @@ export interface MigrationsJsonEntry {
|
|||||||
requires?: Record<string, string>;
|
requires?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MigrationDetailsWithId = GeneratedMigrationDetails & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
export interface GeneratedMigrationDetails {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
package: string;
|
||||||
|
description: string;
|
||||||
|
implementation: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MigrationsJson {
|
export interface MigrationsJson {
|
||||||
name?: string;
|
name?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
|||||||
@ -298,7 +298,11 @@ export function commitChanges(
|
|||||||
directory?: string
|
directory?: string
|
||||||
): string | null {
|
): string | null {
|
||||||
try {
|
try {
|
||||||
execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' });
|
execSync('git add -A', {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
cwd: directory,
|
||||||
|
});
|
||||||
execSync('git commit --no-verify -F -', {
|
execSync('git commit --no-verify -F -', {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
@ -318,15 +322,16 @@ export function commitChanges(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getLatestCommitSha();
|
return getLatestCommitSha(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestCommitSha(): string | null {
|
export function getLatestCommitSha(directory?: string): string | null {
|
||||||
try {
|
try {
|
||||||
return execSync('git rev-parse HEAD', {
|
return execSync('git rev-parse HEAD', {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
windowsHide: false,
|
windowsHide: false,
|
||||||
|
cwd: directory,
|
||||||
}).trim();
|
}).trim();
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
2078
pnpm-lock.yaml
generated
2078
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -43,6 +43,8 @@
|
|||||||
"graph/ui-project-details/src/index.ts"
|
"graph/ui-project-details/src/index.ts"
|
||||||
],
|
],
|
||||||
"@nx/graph-internal/ui-theme": ["graph/ui-theme/src/index.ts"],
|
"@nx/graph-internal/ui-theme": ["graph/ui-theme/src/index.ts"],
|
||||||
|
"@nx/graph-migrate": ["graph/migrate/src/index.ts"],
|
||||||
|
"@nx/graph/ui-common": ["graph/ui-common/src/index.ts"],
|
||||||
"@nx/jest": ["packages/jest"],
|
"@nx/jest": ["packages/jest"],
|
||||||
"@nx/jest/*": ["packages/jest/*"],
|
"@nx/jest/*": ["packages/jest/*"],
|
||||||
"@nx/js": ["packages/js/src/index.ts"],
|
"@nx/js": ["packages/js/src/index.ts"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user