Skip to content

Commit accec1b

Browse files
authored
Merge pull request #127 from contentstack/feat/DX-6195
added asset progress manager ui update
2 parents 9b7a589 + 21c884b commit accec1b

28 files changed

Lines changed: 602 additions & 259 deletions

File tree

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fileignoreconfig:
1010
- filename: packages/contentstack-query-export/.env-example
1111
checksum: 922c7aa9c788ab60b987de2b0a2aee6d90843c463a8bbc29201e4efe31081187
1212
- filename: pnpm-lock.yaml
13-
checksum: bb5303f2fe64f90ae95d2738363267fb0bfcfeb71f025c2110d4cec87ff84d95
13+
checksum: 3d2eaabf1df366efee1759156465c6aefa68f30d372717de2cdc3e41946aa3d8
1414
- filename: packages/contentstack-import/src/utils/build-import-spaces-options.ts
1515
checksum: fe0cb6cb5903515982af1e3642f2a19233207d35f13dc205cebeda0aa399f8b5
1616
- filename: packages/contentstack-export/src/export/modules/stack.ts

packages/contentstack-asset-management/README.md

Lines changed: 0 additions & 49 deletions
This file was deleted.

packages/contentstack-asset-management/src/constants/index.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ export const CHUNK_FILE_SIZE_MB = FALLBACK_AM_CHUNK_FILE_SIZE_MB;
3535
export const AM_MAIN_PROCESS_NAME = 'Asset Management 2.0';
3636

3737
/**
38-
* Process names for Asset Management 2.0 export progress (for tick labels).
38+
* Process names for Asset Management 2.0 export/import progress.
39+
*
40+
* In the new per-space layout each entry below corresponds to a single row in
41+
* the multibar:
42+
* - {@link AM_FIELDS} / {@link AM_ASSET_TYPES} are the shared bootstrap rows
43+
* (one execution per org, ahead of per-space work).
44+
* - {@link AM_IMPORT_FIELDS} / {@link AM_IMPORT_ASSET_TYPES} are the import
45+
* equivalents.
46+
* - One additional row per space is added dynamically via
47+
* {@link getSpaceProcessName} and ticks include folders + metadata + asset
48+
* transfer for that space.
3949
*/
4050
export const PROCESS_NAMES = {
4151
AM_SPACE_METADATA: 'Space metadata',
@@ -51,6 +61,38 @@ export const PROCESS_NAMES = {
5161
AM_IMPORT_ASSETS: 'Import assets',
5262
} as const;
5363

64+
/**
65+
* Maximum visual length of a per-space process row label. The CLIProgressManager
66+
* truncates anything over 20 characters; reserve 6 chars for the `Space ` prefix
67+
* so the trailing space uid keeps 14 chars before truncation.
68+
*/
69+
const SPACE_PROCESS_NAME_PREFIX = 'Space ';
70+
const SPACE_PROCESS_NAME_MAX_UID_LEN = 14;
71+
72+
/**
73+
* Returns the multibar row label for a single AM 2.0 space.
74+
* The label is bounded so CLIProgressManager.formatProcessName doesn't truncate
75+
* it mid-string; the full uid is still used for tick item labels and structured
76+
* logs, only the row label itself is shortened for display.
77+
*/
78+
export function getSpaceProcessName(spaceUid: string): string {
79+
const safeUid = spaceUid ?? '';
80+
const trimmed =
81+
safeUid.length > SPACE_PROCESS_NAME_MAX_UID_LEN
82+
? safeUid.substring(0, SPACE_PROCESS_NAME_MAX_UID_LEN)
83+
: safeUid;
84+
return `${SPACE_PROCESS_NAME_PREFIX}${trimmed}`;
85+
}
86+
87+
/**
88+
* Detects whether a process name belongs to a per-space progress row, used by
89+
* the export/import strategy registries to aggregate counts for the final
90+
* summary across all spaces.
91+
*/
92+
export function isSpaceProcessName(processName: string): boolean {
93+
return typeof processName === 'string' && processName.startsWith(SPACE_PROCESS_NAME_PREFIX);
94+
}
95+
5496
/**
5597
* Status messages for each process (exporting, fetching, importing, failed).
5698
*/

packages/contentstack-asset-management/src/export/asset-types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { getArrayFromResponse } from '../utils/export-helpers';
77
import { PROCESS_NAMES } from '../constants/index';
88

99
export default class ExportAssetTypes extends AssetManagementExportAdapter {
10+
protected processName: string = PROCESS_NAMES.AM_ASSET_TYPES;
11+
1012
constructor(apiConfig: AssetManagementAPIConfig, exportContext: ExportContext) {
1113
super(apiConfig, exportContext);
1214
}
@@ -24,7 +26,13 @@ export default class ExportAssetTypes extends AssetManagementExportAdapter {
2426
} else {
2527
log.debug(`Writing ${items.length} shared asset types`, this.exportContext.context);
2628
}
27-
await this.writeItemsToChunkedJson(dir, 'asset-types.json', 'asset_types', ['uid', 'title', 'category', 'file_extension'], items);
28-
this.tick(true, PROCESS_NAMES.AM_ASSET_TYPES, null);
29+
await this.writeItemsToChunkedJson(
30+
dir,
31+
'asset-types.json',
32+
'asset_types',
33+
['uid', 'title', 'category', 'file_extension'],
34+
items,
35+
);
36+
this.tick(true, `asset_types (${items.length})`, null);
2937
}
3038
}

packages/contentstack-asset-management/src/export/assets.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@ export default class ExportAssets extends AssetManagementExportAdapter {
3232
this.getWorkspaceAssets(workspace.space_uid, workspace.uid),
3333
]);
3434

35+
const assetItems = getAssetItems(assetsData);
36+
const downloadableCount = assetItems.filter((asset) => Boolean(asset.url && (asset.uid ?? asset._uid))).length;
37+
// Per-space total: 1 folder write + 1 metadata write + N per-asset downloads.
38+
// The shared module-level total is just a placeholder before this point; update
39+
// it now so the multibar row shows real progress as downloads tick in.
40+
this.progressOrParent?.updateProcessTotal?.(this.processName, 2 + downloadableCount);
41+
3542
await writeFile(pResolve(assetsDir, 'folders.json'), JSON.stringify(folders, null, 2));
3643
this.tick(true, `folders: ${workspace.space_uid}`, null);
3744
log.debug(`Wrote folders.json for space ${workspace.space_uid}`, this.exportContext.context);
3845

39-
const assetItems = getAssetItems(assetsData);
4046
log.debug(
4147
assetItems.length === 0
4248
? `No assets for space ${workspace.space_uid}, wrote empty assets.json`
@@ -60,7 +66,7 @@ export default class ExportAssets extends AssetManagementExportAdapter {
6066
: `Wrote ${assetItems.length} asset metadata record(s) for space ${workspace.space_uid}`,
6167
this.exportContext.context,
6268
);
63-
this.tick(true, `assets: ${workspace.space_uid} (${assetItems.length})`, null);
69+
this.tick(true, `metadata: ${workspace.space_uid} (${assetItems.length})`, null);
6470

6571
log.debug(`Starting binary downloads for space ${workspace.space_uid}`, this.exportContext.context);
6672
await this.downloadWorkspaceAssets(assetsData, assetsDir, workspace.space_uid);
@@ -87,8 +93,6 @@ export default class ExportAssets extends AssetManagementExportAdapter {
8793
`Asset downloads: securedAssets=${securedAssets}, concurrency=${this.downloadAssetsBatchConcurrency}`,
8894
this.exportContext.context,
8995
);
90-
let lastError: string | null = null;
91-
let allSuccess = true;
9296
let downloadOk = 0;
9397
let downloadFail = 0;
9498

@@ -118,24 +122,25 @@ export default class ExportAssets extends AssetManagementExportAdapter {
118122
const filePath = pResolve(assetFolderPath, filename);
119123
await writeStreamToFile(nodeStream, filePath);
120124
downloadOk += 1;
125+
// Per-asset tick so the per-space progress bar moves in real time.
126+
this.tick(true, `asset: ${filename}`, null);
121127
log.debug(`Downloaded asset ${uid}${filePath}`, this.exportContext.context);
122128
} catch (e) {
123-
allSuccess = false;
124129
downloadFail += 1;
125-
lastError = (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].FAILED;
130+
const err = (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_DOWNLOADS].FAILED;
131+
this.tick(false, `asset: ${filename}`, err);
126132
log.debug(`Failed to download asset ${uid}: ${e}`, this.exportContext.context);
127133
}
128134
});
129135

130-
this.tick(allSuccess, `downloads: ${spaceUid}`, lastError);
131136
log.info(
132-
allSuccess
137+
downloadFail === 0
133138
? `Finished downloading ${downloadOk} asset file(s) for space ${spaceUid}`
134139
: `Asset downloads for space ${spaceUid} completed with errors: ${downloadOk} succeeded, ${downloadFail} failed`,
135140
this.exportContext.context,
136141
);
137142
log.debug(
138-
`Asset downloads finished for space ${spaceUid}: ok=${downloadOk}, failed=${downloadFail}, allSuccess=${allSuccess}`,
143+
`Asset downloads finished for space ${spaceUid}: ok=${downloadOk}, failed=${downloadFail}`,
139144
this.exportContext.context,
140145
);
141146
}

packages/contentstack-asset-management/src/export/base.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class AssetManagementExportAdapter extends AssetManagementAdapter {
1818
protected readonly exportContext: ExportContext;
1919
protected progressManager: CLIProgressManager | null = null;
2020
protected parentProgressManager: CLIProgressManager | null = null;
21-
protected readonly processName: string = AM_MAIN_PROCESS_NAME;
21+
protected processName: string = AM_MAIN_PROCESS_NAME;
2222

2323
constructor(apiConfig: AssetManagementAPIConfig, exportContext: ExportContext) {
2424
super(apiConfig);
@@ -30,6 +30,15 @@ export class AssetManagementExportAdapter extends AssetManagementAdapter {
3030
this.parentProgressManager = parent;
3131
}
3232

33+
/**
34+
* Override the default progress process name for {@link tick}/{@link updateStatus}
35+
* calls. Used by the per-space orchestrator so each module's ticks land on the
36+
* row for the space currently being exported.
37+
*/
38+
public setProcessName(name: string): void {
39+
this.processName = name;
40+
}
41+
3342
protected get progressOrParent(): CLIProgressManager | null {
3443
return this.parentProgressManager ?? this.progressManager;
3544
}

packages/contentstack-asset-management/src/export/fields.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { getArrayFromResponse } from '../utils/export-helpers';
77
import { PROCESS_NAMES } from '../constants/index';
88

99
export default class ExportFields extends AssetManagementExportAdapter {
10+
protected processName: string = PROCESS_NAMES.AM_FIELDS;
11+
1012
constructor(apiConfig: AssetManagementAPIConfig, exportContext: ExportContext) {
1113
super(apiConfig, exportContext);
1214
}
@@ -25,6 +27,6 @@ export default class ExportFields extends AssetManagementExportAdapter {
2527
log.debug(`Writing ${items.length} shared fields`, this.exportContext.context);
2628
}
2729
await this.writeItemsToChunkedJson(dir, 'fields.json', 'fields', ['uid', 'title', 'display_type'], items);
28-
this.tick(true, PROCESS_NAMES.AM_FIELDS, null);
30+
this.tick(true, `fields (${items.length})`, null);
2931
}
3032
}

packages/contentstack-asset-management/src/export/spaces.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { log, CLIProgressManager, configHandler, handleAndLogError } from '@cont
44

55
import type { AssetManagementExportOptions, AssetManagementAPIConfig } from '../types/asset-management-api';
66
import type { ExportContext } from '../types/export-types';
7-
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
8-
import { AM_MAIN_PROCESS_NAME, PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
7+
import { AM_MAIN_PROCESS_NAME, PROCESS_NAMES, getSpaceProcessName } from '../constants/index';
98
import ExportAssetTypes from './asset-types';
109
import ExportFields from './fields';
1110
import ExportWorkspace from './workspaces';
@@ -55,12 +54,18 @@ export class ExportSpaces {
5554
await mkdir(spacesRootPath, { recursive: true });
5655
log.debug(`Spaces root path: ${spacesRootPath}`, context);
5756

58-
const totalSteps = 2 + linkedWorkspaces.length * 4;
5957
const progress = this.createProgress();
60-
progress.addProcess(AM_MAIN_PROCESS_NAME, totalSteps);
61-
progress
62-
.startProcess(AM_MAIN_PROCESS_NAME)
63-
.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_FIELDS].FETCHING, AM_MAIN_PROCESS_NAME);
58+
// Multibar layout: two shared bootstrap rows + one row per space. Per-space
59+
// totals start at 1 and are bumped to (2 + downloadableCount) inside
60+
// ExportAssets.start once we know the asset count for that space.
61+
progress.addProcess(PROCESS_NAMES.AM_FIELDS, 1);
62+
progress.addProcess(PROCESS_NAMES.AM_ASSET_TYPES, 1);
63+
const spaceProcessNames = new Map<string, string>();
64+
for (const ws of linkedWorkspaces) {
65+
const spaceProcess = getSpaceProcessName(ws.space_uid);
66+
spaceProcessNames.set(ws.space_uid, spaceProcess);
67+
progress.addProcess(spaceProcess, 1);
68+
}
6469

6570
const apiConfig: AssetManagementAPIConfig = {
6671
baseURL: assetManagementUrl,
@@ -82,39 +87,67 @@ export class ExportSpaces {
8287
await mkdir(sharedAssetTypesDir, { recursive: true });
8388

8489
const firstSpaceUid = linkedWorkspaces[0].space_uid;
90+
let bootstrapFailed = false;
91+
let anySpaceFailed = false;
8592
try {
93+
progress.startProcess(PROCESS_NAMES.AM_FIELDS);
94+
progress.startProcess(PROCESS_NAMES.AM_ASSET_TYPES);
95+
8696
const exportAssetTypes = new ExportAssetTypes(apiConfig, exportContext);
8797
exportAssetTypes.setParentProgressManager(progress);
8898
const exportFields = new ExportFields(apiConfig, exportContext);
8999
exportFields.setParentProgressManager(progress);
90-
await Promise.all([exportAssetTypes.start(firstSpaceUid), exportFields.start(firstSpaceUid)]);
100+
try {
101+
await Promise.all([exportAssetTypes.start(firstSpaceUid), exportFields.start(firstSpaceUid)]);
102+
progress.completeProcess(PROCESS_NAMES.AM_FIELDS, true);
103+
progress.completeProcess(PROCESS_NAMES.AM_ASSET_TYPES, true);
104+
} catch (bootstrapErr) {
105+
bootstrapFailed = true;
106+
progress.completeProcess(PROCESS_NAMES.AM_FIELDS, false);
107+
progress.completeProcess(PROCESS_NAMES.AM_ASSET_TYPES, false);
108+
throw bootstrapErr;
109+
}
91110

92111
for (const ws of linkedWorkspaces) {
93-
progress.updateStatus(`Exporting space: ${ws.space_uid}...`, AM_MAIN_PROCESS_NAME);
112+
const spaceProcess = spaceProcessNames.get(ws.space_uid)!;
113+
progress.startProcess(spaceProcess);
94114
log.debug(`Exporting space: ${ws.space_uid}`, context);
95115
const spaceDir = pResolve(spacesRootPath, ws.space_uid);
96116
try {
97117
const exportWorkspace = new ExportWorkspace(apiConfig, exportContext);
98118
exportWorkspace.setParentProgressManager(progress);
99-
await exportWorkspace.start(ws, spaceDir, branchName || 'main');
119+
await exportWorkspace.start(ws, spaceDir, branchName || 'main', spaceProcess);
120+
progress.completeProcess(spaceProcess, true);
100121
log.debug(`Exported workspace structure for space ${ws.space_uid}`, context);
101122
} catch (err) {
123+
// Per-space failure: mark the row failed and continue with the next
124+
// space so partial export results are preserved (matches import).
125+
anySpaceFailed = true;
102126
log.debug(`Failed to export workspace for space ${ws.space_uid}: ${err}`, context);
103-
progress.tick(
104-
false,
105-
`space: ${ws.space_uid}`,
106-
(err as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_SPACE_METADATA].FAILED,
107-
AM_MAIN_PROCESS_NAME,
127+
handleAndLogError(
128+
err,
129+
{ ...(context as Record<string, unknown>), spaceUid: ws.space_uid },
130+
`Failed to export space ${ws.space_uid}`,
108131
);
109-
throw err;
132+
progress.completeProcess(spaceProcess, false);
110133
}
111134
}
112135

113-
progress.completeProcess(AM_MAIN_PROCESS_NAME, true);
114-
log.info('Asset Management export completed successfully', context);
136+
log.info(
137+
anySpaceFailed
138+
? 'Asset Management export completed with errors in one or more spaces'
139+
: 'Asset Management export completed successfully',
140+
context,
141+
);
115142
log.debug('Asset Management 2.0 export completed', context);
116143
} catch (err) {
117-
progress.completeProcess(AM_MAIN_PROCESS_NAME, false);
144+
if (!bootstrapFailed) {
145+
// Mark any spaces that hadn't been processed as failed so the multibar
146+
// doesn't leave dangling pending rows.
147+
for (const [, spaceProcess] of spaceProcessNames) {
148+
progress.completeProcess(spaceProcess, false);
149+
}
150+
}
118151
handleAndLogError(err, { ...(context as Record<string, unknown>) }, 'Asset Management export failed');
119152
throw err;
120153
}

0 commit comments

Comments
 (0)