feat(graph): add undo migration option when one is pending approval (#30878)
This PR adds a button for user to undo a migration that's already been applied and pending approval. See: https://www.loom.com/share/97286bdc80ea4538af76a914ef8f0f8b Also, fixes an existing issue where `migrations.json` did not record the correct git sha for each commit. ## Current Behavior When a migration is pending approval, the only option is to accept it. ## Expected Behavior Allow user to undo a migration if they don't want the changes. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
3b0c456bbe
commit
0dc4dbf499
@ -85,6 +85,13 @@ export function MigrateApp({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUndoMigration = (migration: MigrationDetailsWithId) => {
|
||||||
|
externalApiService.postEvent({
|
||||||
|
type: 'undo-migration',
|
||||||
|
payload: { migration },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onViewImplementation = (migration: MigrationDetailsWithId) => {
|
const onViewImplementation = (migration: MigrationDetailsWithId) => {
|
||||||
externalApiService.postEvent({
|
externalApiService.postEvent({
|
||||||
type: 'view-implementation',
|
type: 'view-implementation',
|
||||||
@ -109,6 +116,7 @@ export function MigrateApp({
|
|||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
onFileClick={onFileClick}
|
onFileClick={onFileClick}
|
||||||
onSkipMigration={onSkipMigration}
|
onSkipMigration={onSkipMigration}
|
||||||
|
onUndoMigration={onUndoMigration}
|
||||||
onViewImplementation={onViewImplementation}
|
onViewImplementation={onViewImplementation}
|
||||||
onViewDocumentation={onViewDocumentation}
|
onViewDocumentation={onViewDocumentation}
|
||||||
></MigrateUI>
|
></MigrateUI>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export function AutomaticMigration(props: {
|
|||||||
nxConsoleMetadata: MigrationsJsonMetadata;
|
nxConsoleMetadata: MigrationsJsonMetadata;
|
||||||
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onUndoMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
onFileClick: (
|
onFileClick: (
|
||||||
migration: MigrationDetailsWithId,
|
migration: MigrationDetailsWithId,
|
||||||
file: Omit<FileChange, 'content'>
|
file: Omit<FileChange, 'content'>
|
||||||
@ -87,6 +88,7 @@ export function AutomaticMigration(props: {
|
|||||||
isInit={isInit}
|
isInit={isInit}
|
||||||
onRunMigration={props.onRunMigration}
|
onRunMigration={props.onRunMigration}
|
||||||
onSkipMigration={props.onSkipMigration}
|
onSkipMigration={props.onSkipMigration}
|
||||||
|
onUndoMigration={props.onUndoMigration}
|
||||||
onFileClick={props.onFileClick}
|
onFileClick={props.onFileClick}
|
||||||
onViewImplementation={props.onViewImplementation}
|
onViewImplementation={props.onViewImplementation}
|
||||||
onViewDocumentation={props.onViewDocumentation}
|
onViewDocumentation={props.onViewDocumentation}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export interface MigrationTimelineProps {
|
|||||||
isInit: boolean;
|
isInit: boolean;
|
||||||
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
onRunMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onUndoMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
onFileClick: (
|
onFileClick: (
|
||||||
migration: MigrationDetailsWithId,
|
migration: MigrationDetailsWithId,
|
||||||
file: Omit<FileChange, 'content'>
|
file: Omit<FileChange, 'content'>
|
||||||
@ -53,6 +54,7 @@ export function MigrationTimeline({
|
|||||||
currentMigrationHasChanges,
|
currentMigrationHasChanges,
|
||||||
onRunMigration,
|
onRunMigration,
|
||||||
onSkipMigration,
|
onSkipMigration,
|
||||||
|
onUndoMigration,
|
||||||
onFileClick,
|
onFileClick,
|
||||||
onViewImplementation,
|
onViewImplementation,
|
||||||
onViewDocumentation,
|
onViewDocumentation,
|
||||||
@ -335,6 +337,18 @@ export function MigrationTimeline({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{currentMigrationHasChanges && (
|
{currentMigrationHasChanges && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
toggleMigrationExpanded(currentMigration.id);
|
||||||
|
onUndoMigration(currentMigration);
|
||||||
|
}}
|
||||||
|
type="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"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-5 w-5" aria-hidden="true" />{' '}
|
||||||
|
Undo and Skip
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleMigrationExpanded(currentMigration.id);
|
toggleMigrationExpanded(currentMigration.id);
|
||||||
@ -346,6 +360,7 @@ export function MigrationTimeline({
|
|||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />{' '}
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />{' '}
|
||||||
Approve Changes
|
Approve Changes
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -290,6 +290,9 @@ export const PendingApproval: Story = {
|
|||||||
onSkipMigration: (migration) => {
|
onSkipMigration: (migration) => {
|
||||||
console.log('skip migration', migration);
|
console.log('skip migration', migration);
|
||||||
},
|
},
|
||||||
|
onUndoMigration: (migration) => {
|
||||||
|
console.log('undo migration', migration);
|
||||||
|
},
|
||||||
onFileClick: (migration, file) => {
|
onFileClick: (migration, file) => {
|
||||||
console.log('file click', migration, file);
|
console.log('file click', migration, file);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export interface MigrateUIProps {
|
|||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
onSkipMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
|
onUndoMigration: (migration: MigrationDetailsWithId) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onFinish: (squashCommits: boolean) => void;
|
onFinish: (squashCommits: boolean) => void;
|
||||||
onFileClick: (
|
onFileClick: (
|
||||||
@ -188,13 +189,10 @@ export function MigrateUI(props: MigrateUIProps) {
|
|||||||
onRunMigration={(migration) =>
|
onRunMigration={(migration) =>
|
||||||
props.onRunMigration(migration, { createCommits })
|
props.onRunMigration(migration, { createCommits })
|
||||||
}
|
}
|
||||||
onSkipMigration={(migration) => props.onSkipMigration(migration)}
|
onSkipMigration={props.onSkipMigration}
|
||||||
onViewImplementation={(migration) =>
|
onUndoMigration={props.onUndoMigration}
|
||||||
props.onViewImplementation(migration)
|
onViewImplementation={props.onViewImplementation}
|
||||||
}
|
onViewDocumentation={props.onViewDocumentation}
|
||||||
onViewDocumentation={(migration) =>
|
|
||||||
props.onViewDocumentation(migration)
|
|
||||||
}
|
|
||||||
onFileClick={props.onFileClick}
|
onFileClick={props.onFileClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -171,6 +171,16 @@ export async function runSingleMigration(
|
|||||||
cwd: workspacePath,
|
cwd: workspacePath,
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
});
|
});
|
||||||
|
// The revision changes after the amend, so we need to update it
|
||||||
|
const amendedGitRef = execSync('git rev-parse HEAD', {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
modifyMigrationsJsonMetadata(
|
||||||
|
workspacePath,
|
||||||
|
updateRefForSuccessfulMigration(migration.id, amendedGitRef)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
modifyMigrationsJsonMetadata(
|
modifyMigrationsJsonMetadata(
|
||||||
@ -245,6 +255,24 @@ export function addSuccessfulMigration(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateRefForSuccessfulMigration(id: string, ref: string) {
|
||||||
|
return (
|
||||||
|
migrationsJsonMetadata: MigrationsJsonMetadata
|
||||||
|
): MigrationsJsonMetadata => {
|
||||||
|
const copied = { ...migrationsJsonMetadata };
|
||||||
|
if (!copied.completedMigrations) {
|
||||||
|
copied.completedMigrations = {};
|
||||||
|
}
|
||||||
|
const existing = copied.completedMigrations[id];
|
||||||
|
if (existing && existing.type === 'successful') {
|
||||||
|
existing.ref = ref;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Attempted to update ref for unsuccessful migration`);
|
||||||
|
}
|
||||||
|
return copied;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function addFailedMigration(id: string, error: string) {
|
export function addFailedMigration(id: string, error: string) {
|
||||||
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
const copied = { ...migrationsJsonMetadata };
|
const copied = { ...migrationsJsonMetadata };
|
||||||
@ -307,3 +335,19 @@ export function readMigrationsJsonMetadata(
|
|||||||
const migrationsJson = JSON.parse(readFileSync(migrationsJsonPath, 'utf-8'));
|
const migrationsJson = JSON.parse(readFileSync(migrationsJsonPath, 'utf-8'));
|
||||||
return migrationsJson['nx-console'];
|
return migrationsJson['nx-console'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function undoMigration(workspacePath: string, id: string) {
|
||||||
|
return (migrationsJsonMetadata: MigrationsJsonMetadata) => {
|
||||||
|
const existing = migrationsJsonMetadata.completedMigrations[id];
|
||||||
|
if (existing.type !== 'successful')
|
||||||
|
throw new Error(`undoMigration called on unsuccessful migration: ${id}`);
|
||||||
|
execSync(`git reset --hard ${existing.ref}^`, {
|
||||||
|
cwd: workspacePath,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
migrationsJsonMetadata.completedMigrations[id] = {
|
||||||
|
type: 'skipped',
|
||||||
|
};
|
||||||
|
return migrationsJsonMetadata;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user