@lightdash/cli 1.54.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers/apps/appCodeFiles.d.ts +5 -0
- package/dist/handlers/apps/appCodeFiles.d.ts.map +1 -1
- package/dist/handlers/apps/appCodeFiles.js +41 -1
- package/dist/handlers/apps/appCodeFiles.test.js +60 -0
- package/dist/handlers/apps/appsDownload.d.ts +49 -0
- package/dist/handlers/apps/appsDownload.d.ts.map +1 -1
- package/dist/handlers/apps/appsDownload.js +90 -1
- package/dist/handlers/apps/appsDownload.test.js +231 -0
- package/dist/handlers/apps/uploadRetry.d.ts +32 -0
- package/dist/handlers/apps/uploadRetry.d.ts.map +1 -0
- package/dist/handlers/apps/uploadRetry.js +60 -0
- package/dist/handlers/apps/uploadRetry.test.d.ts +2 -0
- package/dist/handlers/apps/uploadRetry.test.d.ts.map +1 -0
- package/dist/handlers/apps/uploadRetry.test.js +103 -0
- package/dist/handlers/download.d.ts +16 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +353 -202
- package/dist/handlers/download.test.js +92 -1
- package/dist/vendor/authoring/AGENTS.md.tmpl +1 -1
- package/dist/vendor/authoring/README.md.tmpl +1 -1
- package/dist/vendor/authoring/developing-data-apps-locally/SKILL.md +3 -3
- package/dist/vendor/template/package.json +1 -1
- package/package.json +3 -3
|
@@ -20,6 +20,7 @@ const contentAsCodeOutput_1 = require("../terminal/contentAsCodeOutput");
|
|
|
20
20
|
const appCodeFiles_1 = require("./apps/appCodeFiles");
|
|
21
21
|
const appsDownload_1 = require("./apps/appsDownload");
|
|
22
22
|
const scaffolding_1 = require("./apps/scaffolding");
|
|
23
|
+
const uploadRetry_1 = require("./apps/uploadRetry");
|
|
23
24
|
const projectResources_1 = require("./contentAsCode/projectResources");
|
|
24
25
|
const resource_1 = require("./contentAsCode/resource");
|
|
25
26
|
const contentAsCodePaths_1 = require("./contentAsCodePaths");
|
|
@@ -331,6 +332,15 @@ const extractChartSlugsFromDashboards = (dashboards) => dashboards.reduce((acc,
|
|
|
331
332
|
.filter((slug) => slug !== undefined);
|
|
332
333
|
return [...acc, ...slugs];
|
|
333
334
|
}, []);
|
|
335
|
+
const extractAppSlugsFromDashboards = (dashboards) => [
|
|
336
|
+
...new Set(dashboards.flatMap((dashboard) => dashboard.tiles.reduce((acc, tile) => {
|
|
337
|
+
if (tile.type !== common_1.DashboardTileTypes.DATA_APP)
|
|
338
|
+
return acc;
|
|
339
|
+
return tile.properties.appSlug
|
|
340
|
+
? [...acc, tile.properties.appSlug]
|
|
341
|
+
: acc;
|
|
342
|
+
}, []))),
|
|
343
|
+
];
|
|
334
344
|
const downloadContent = async (ids, type, projectId, projectName, customPath, languageMap = false, nested = false, skipSpaces = false, stripPivotSeries = false, rootSpaces = false, onProgress) => {
|
|
335
345
|
const contentFilters = parseContentFilters(ids);
|
|
336
346
|
const folderScheme = nested ? 'nested' : 'flat';
|
|
@@ -338,6 +348,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
|
|
|
338
348
|
let offset = 0;
|
|
339
349
|
let total = 0;
|
|
340
350
|
let chartSlugs = [];
|
|
351
|
+
let appSlugs = [];
|
|
341
352
|
let allMetadataEntries = [];
|
|
342
353
|
let allSpaces = [];
|
|
343
354
|
do {
|
|
@@ -399,6 +410,10 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
|
|
|
399
410
|
...chartSlugs,
|
|
400
411
|
...extractChartSlugsFromDashboards(results.dashboards),
|
|
401
412
|
];
|
|
413
|
+
appSlugs = [
|
|
414
|
+
...appSlugs,
|
|
415
|
+
...extractAppSlugsFromDashboards(results.dashboards),
|
|
416
|
+
];
|
|
402
417
|
}
|
|
403
418
|
else {
|
|
404
419
|
const chartsBySpace = groupBySpace(results.charts);
|
|
@@ -432,7 +447,13 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
|
|
|
432
447
|
];
|
|
433
448
|
await (0, spacesAsCode_1.writeSpaceFiles)(uniqueSpaces, projectName, customPath, folderScheme, rootSpaces ? 'root' : 'folder', true);
|
|
434
449
|
}
|
|
435
|
-
return
|
|
450
|
+
return {
|
|
451
|
+
total,
|
|
452
|
+
chartSlugs: [...new Set(chartSlugs)],
|
|
453
|
+
appSlugs: [...new Set(appSlugs)],
|
|
454
|
+
metadataEntries: allMetadataEntries,
|
|
455
|
+
spaces: allSpaces,
|
|
456
|
+
};
|
|
436
457
|
};
|
|
437
458
|
exports.downloadContent = downloadContent;
|
|
438
459
|
const getScheduledDeliveriesFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE.folderName);
|
|
@@ -836,8 +857,8 @@ const upsertScheduledContent = async (projectId, slugs, changes, force, contentT
|
|
|
836
857
|
};
|
|
837
858
|
// Space-scoped fallback listing for servers without the project-wide apps
|
|
838
859
|
// endpoint; omits apps that were never added to a space.
|
|
839
|
-
const
|
|
840
|
-
const
|
|
860
|
+
const listAppsViaContentApi = async (projectId) => {
|
|
861
|
+
const listedApps = [];
|
|
841
862
|
let page = 1;
|
|
842
863
|
let totalPageCount = 1;
|
|
843
864
|
do {
|
|
@@ -846,13 +867,13 @@ const listAppUuidsViaContentApi = async (projectId) => {
|
|
|
846
867
|
url: `/api/v2/content?projectUuids=${projectId}&contentTypes=data_app&page=${page}&pageSize=100`,
|
|
847
868
|
body: undefined,
|
|
848
869
|
});
|
|
849
|
-
|
|
870
|
+
listedApps.push(...contentResult.data
|
|
850
871
|
.filter((item) => item.contentType === 'data_app')
|
|
851
|
-
.map((item) => item.uuid));
|
|
872
|
+
.map((item) => ({ appUuid: item.uuid, slug: item.slug })));
|
|
852
873
|
totalPageCount = contentResult.pagination?.totalPageCount ?? 1;
|
|
853
874
|
page += 1;
|
|
854
875
|
} while (page <= totalPageCount);
|
|
855
|
-
return
|
|
876
|
+
return listedApps;
|
|
856
877
|
};
|
|
857
878
|
const downloadHandler = async (options) => {
|
|
858
879
|
globalState_1.default.setVerbose(options.verbose);
|
|
@@ -966,6 +987,13 @@ const downloadHandler = async (options) => {
|
|
|
966
987
|
});
|
|
967
988
|
try {
|
|
968
989
|
let allMetadataEntries = [];
|
|
990
|
+
// Shared across both apps-download steps so two different apps whose
|
|
991
|
+
// names collide under the pre-slug fallback naming don't clobber each other.
|
|
992
|
+
const downloadedAppFolders = new Set();
|
|
993
|
+
// App slugs referenced by downloaded dashboards' tiles, populated by
|
|
994
|
+
// the Dashboards step and consumed by the Linked data apps step.
|
|
995
|
+
let dashboardAppSlugs = [];
|
|
996
|
+
const explicitAppRefs = new Set((Array.isArray(options.apps) ? options.apps : []).map(appsDownload_1.getDataAppReference));
|
|
969
997
|
if (shouldDownloadSpaces) {
|
|
970
998
|
output.startItem('Spaces');
|
|
971
999
|
try {
|
|
@@ -999,19 +1027,19 @@ const downloadHandler = async (options) => {
|
|
|
999
1027
|
globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
|
|
1000
1028
|
}
|
|
1001
1029
|
else {
|
|
1002
|
-
const
|
|
1030
|
+
const { total: regularChartTotal, metadataEntries: regularChartMeta, } = await output.runItem({
|
|
1003
1031
|
label: 'Charts',
|
|
1004
1032
|
action: () => (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, options.stripPivotSeries, options.rootSpaces, output.updateActive),
|
|
1005
|
-
detail: (
|
|
1033
|
+
detail: ({ total }) => `${total} downloaded`,
|
|
1006
1034
|
});
|
|
1007
1035
|
allMetadataEntries = [
|
|
1008
1036
|
...allMetadataEntries,
|
|
1009
1037
|
...regularChartMeta,
|
|
1010
1038
|
];
|
|
1011
|
-
const
|
|
1039
|
+
const { total: sqlChartTotal, metadataEntries: sqlChartMeta } = await output.runItem({
|
|
1012
1040
|
label: 'SQL charts',
|
|
1013
1041
|
action: () => (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive),
|
|
1014
|
-
detail: (
|
|
1042
|
+
detail: ({ total }) => `${total} downloaded`,
|
|
1015
1043
|
});
|
|
1016
1044
|
allMetadataEntries = [...allMetadataEntries, ...sqlChartMeta];
|
|
1017
1045
|
chartTotal = regularChartTotal + sqlChartTotal;
|
|
@@ -1024,30 +1052,38 @@ const downloadHandler = async (options) => {
|
|
|
1024
1052
|
}
|
|
1025
1053
|
else {
|
|
1026
1054
|
let chartSlugs = [];
|
|
1055
|
+
let appSlugs = [];
|
|
1027
1056
|
let dashMeta;
|
|
1028
|
-
|
|
1057
|
+
({
|
|
1058
|
+
total: dashboardTotal,
|
|
1059
|
+
chartSlugs,
|
|
1060
|
+
appSlugs,
|
|
1061
|
+
metadataEntries: dashMeta,
|
|
1062
|
+
} = await output.runItem({
|
|
1029
1063
|
label: 'Dashboards',
|
|
1030
1064
|
action: () => (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive),
|
|
1031
|
-
detail: (
|
|
1032
|
-
});
|
|
1065
|
+
detail: ({ total }) => `${total} downloaded`,
|
|
1066
|
+
}));
|
|
1033
1067
|
allMetadataEntries = [...allMetadataEntries, ...dashMeta];
|
|
1034
1068
|
if (hasFilters &&
|
|
1035
1069
|
chartSlugs.length > 0 &&
|
|
1036
1070
|
!options.skipCharts) {
|
|
1037
1071
|
output.startItem('Linked charts');
|
|
1038
1072
|
output.updateActive(`${chartSlugs.length} dashboard dependencies`);
|
|
1039
|
-
const
|
|
1073
|
+
const { total: regularCharts, metadataEntries: linkedChartMeta, } = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, options.stripPivotSeries, options.rootSpaces, output.updateActive);
|
|
1040
1074
|
allMetadataEntries = [
|
|
1041
1075
|
...allMetadataEntries,
|
|
1042
1076
|
...linkedChartMeta,
|
|
1043
1077
|
];
|
|
1044
|
-
const
|
|
1078
|
+
const { total: sqlCharts, metadataEntries: linkedSqlMeta } = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive);
|
|
1045
1079
|
allMetadataEntries = [
|
|
1046
1080
|
...allMetadataEntries,
|
|
1047
1081
|
...linkedSqlMeta,
|
|
1048
1082
|
];
|
|
1049
1083
|
output.completeItem(`${regularCharts + sqlCharts} downloaded`);
|
|
1050
1084
|
}
|
|
1085
|
+
// Consumed after the explicit apps step (see cappedAppSlugs).
|
|
1086
|
+
dashboardAppSlugs = appSlugs;
|
|
1051
1087
|
}
|
|
1052
1088
|
}
|
|
1053
1089
|
if (!options.spacesOnly && shouldDownloadAiAgents(options)) {
|
|
@@ -1106,6 +1142,9 @@ const downloadHandler = async (options) => {
|
|
|
1106
1142
|
apps: Array.isArray(options.apps) ? options.apps : undefined,
|
|
1107
1143
|
includeApps,
|
|
1108
1144
|
});
|
|
1145
|
+
// Slugs covered by a (possibly --apps-limit-truncated) --include-apps
|
|
1146
|
+
// listing, so the Linked data apps step knows what fell outside the cap.
|
|
1147
|
+
let cappedAppSlugs = new Set();
|
|
1109
1148
|
if (appsSelection.mode !== 'none') {
|
|
1110
1149
|
output.startItem('Data apps');
|
|
1111
1150
|
let appRefsToDownload;
|
|
@@ -1116,14 +1155,17 @@ const downloadHandler = async (options) => {
|
|
|
1116
1155
|
else {
|
|
1117
1156
|
// List every app in the project (includes apps not in any space)
|
|
1118
1157
|
output.updateActive('listing project apps…');
|
|
1119
|
-
let
|
|
1158
|
+
let listedApps;
|
|
1120
1159
|
try {
|
|
1121
1160
|
const projectApps = await (0, apiClient_1.lightdashApi)({
|
|
1122
1161
|
method: 'GET',
|
|
1123
1162
|
url: `/api/v1/ee/projects/${projectId}/apps`,
|
|
1124
1163
|
body: undefined,
|
|
1125
1164
|
});
|
|
1126
|
-
|
|
1165
|
+
listedApps = projectApps.map((app) => ({
|
|
1166
|
+
appUuid: app.appUuid,
|
|
1167
|
+
slug: app.slug,
|
|
1168
|
+
}));
|
|
1127
1169
|
}
|
|
1128
1170
|
catch (listErr) {
|
|
1129
1171
|
if (!(0, appsDownload_1.shouldFallBackToSpaceScopedListing)(listErr)) {
|
|
@@ -1131,18 +1173,21 @@ const downloadHandler = async (options) => {
|
|
|
1131
1173
|
throw listErr;
|
|
1132
1174
|
}
|
|
1133
1175
|
appListingError = (0, common_1.getErrorMessage)(listErr);
|
|
1134
|
-
|
|
1176
|
+
listedApps = [];
|
|
1135
1177
|
}
|
|
1136
1178
|
else {
|
|
1137
1179
|
globalState_1.default.log(styles.warning('This server does not support project-wide app listing; only apps that are in a space will be included.'));
|
|
1138
|
-
|
|
1139
|
-
await listAppUuidsViaContentApi(projectId);
|
|
1180
|
+
listedApps = await listAppsViaContentApi(projectId);
|
|
1140
1181
|
}
|
|
1141
1182
|
}
|
|
1142
|
-
const { appUuids: cappedAppUuids, truncatedCount } = (0, appsDownload_1.capListedApps)(
|
|
1183
|
+
const { appUuids: cappedAppUuids, truncatedCount } = (0, appsDownload_1.capListedApps)(listedApps.map((app) => app.appUuid), appsLimit);
|
|
1143
1184
|
if (truncatedCount > 0) {
|
|
1144
|
-
globalState_1.default.log(styles.warning(`Found ${
|
|
1185
|
+
globalState_1.default.log(styles.warning(`Found ${listedApps.length} data apps, downloading the first ${appsLimit}. Pass --apps-limit <n> to raise the cap.`));
|
|
1145
1186
|
}
|
|
1187
|
+
const cappedAppUuidSet = new Set(cappedAppUuids);
|
|
1188
|
+
cappedAppSlugs = new Set(listedApps
|
|
1189
|
+
.filter((app) => cappedAppUuidSet.has(app.appUuid))
|
|
1190
|
+
.map((app) => app.slug));
|
|
1146
1191
|
appRefsToDownload = [
|
|
1147
1192
|
...new Set([
|
|
1148
1193
|
...cappedAppUuids,
|
|
@@ -1162,71 +1207,62 @@ const downloadHandler = async (options) => {
|
|
|
1162
1207
|
output.updateActive(`0 of ${appRefsToDownload.length} downloaded`);
|
|
1163
1208
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
1164
1209
|
const appsDir = path.join(baseDir, 'apps');
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
scaffoldingVersion: env_1.CLI_VERSION,
|
|
1183
|
-
};
|
|
1184
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1185
|
-
await (0, appCodeFiles_1.writeBundleToDir)(appDir, { ...code, manifest });
|
|
1186
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1187
|
-
await (0, appCodeFiles_1.writeFilesToDir)(appDir, (0, scaffolding_1.buildStaticAuthoringFiles)({
|
|
1188
|
-
appName: code.manifest.name,
|
|
1189
|
-
sdkVersion: env_1.CLI_VERSION,
|
|
1190
|
-
}));
|
|
1191
|
-
// Server-provided deps override the scaffold's
|
|
1192
|
-
// template package.json so re-uploads round-trip.
|
|
1193
|
-
if (code.dependencies) {
|
|
1194
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1195
|
-
await (0, appCodeFiles_1.writeDependenciesToDir)(appDir, code.dependencies);
|
|
1196
|
-
}
|
|
1197
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1198
|
-
await (0, appCodeFiles_1.writeContextToDir)(appDir, code.context);
|
|
1199
|
-
appSuccessCount += 1;
|
|
1200
|
-
}
|
|
1201
|
-
catch (appErr) {
|
|
1202
|
-
const outcome = (0, appsDownload_1.classifyAppDownloadError)(appErr);
|
|
1203
|
-
if (outcome.kind === 'skip-not-built') {
|
|
1204
|
-
appSkippedNotBuiltCount += 1;
|
|
1205
|
-
globalState_1.default.debug(`> Skipped app ${appRef}: no built version to download`);
|
|
1206
|
-
}
|
|
1207
|
-
else {
|
|
1208
|
-
appFailures.push({
|
|
1209
|
-
appRef,
|
|
1210
|
-
message: outcome.message,
|
|
1211
|
-
});
|
|
1212
|
-
globalState_1.default.log(styles.error(`Failed to download app ${appRef}: ${outcome.message}`));
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
output.updateActive(`${appSuccessCount +
|
|
1216
|
-
appSkippedNotBuiltCount +
|
|
1217
|
-
appFailures.length} of ${appRefsToDownload.length} processed`);
|
|
1218
|
-
}
|
|
1219
|
-
const summary = (0, appsDownload_1.appsDownloadSummary)(appSuccessCount, appRefsToDownload.length, appFailures, appsDir, appSkippedNotBuiltCount);
|
|
1220
|
-
output.completeItem(`${appSuccessCount} downloaded${appSkippedNotBuiltCount > 0
|
|
1221
|
-
? `, ${appSkippedNotBuiltCount} skipped`
|
|
1222
|
-
: ''}${appFailures.length > 0
|
|
1223
|
-
? `, ${appFailures.length} failed`
|
|
1224
|
-
: ''}`, summary.ok ? undefined : 'warning');
|
|
1210
|
+
const { successCount, skippedNotBuiltCount, failures } = await (0, appsDownload_1.downloadAppsToDir)({
|
|
1211
|
+
appRefs: appRefsToDownload,
|
|
1212
|
+
projectId,
|
|
1213
|
+
appsDir,
|
|
1214
|
+
takenFolders: downloadedAppFolders,
|
|
1215
|
+
cliVersion: env_1.CLI_VERSION,
|
|
1216
|
+
fetchApp: (fetchProjectId, appRef) => (0, apiClient_1.lightdashApi)({
|
|
1217
|
+
method: 'GET',
|
|
1218
|
+
url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
|
|
1219
|
+
body: undefined,
|
|
1220
|
+
}),
|
|
1221
|
+
onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
|
|
1222
|
+
});
|
|
1223
|
+
const summary = (0, appsDownload_1.appsDownloadSummary)(successCount, appRefsToDownload.length, failures, appsDir, skippedNotBuiltCount);
|
|
1224
|
+
output.completeItem(`${successCount} downloaded${skippedNotBuiltCount > 0
|
|
1225
|
+
? `, ${skippedNotBuiltCount} skipped`
|
|
1226
|
+
: ''}${failures.length > 0 ? `, ${failures.length} failed` : ''}`, summary.ok ? undefined : 'warning');
|
|
1225
1227
|
if (!summary.ok) {
|
|
1226
1228
|
summary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
|
|
1227
1229
|
}
|
|
1228
1230
|
}
|
|
1229
1231
|
}
|
|
1232
|
+
// Dashboard-referenced apps not already covered above (explicit
|
|
1233
|
+
// --apps ref, or a non-truncated slot in the --include-apps cap).
|
|
1234
|
+
const linkedAppSlugs = (0, appsDownload_1.computeLinkedAppSlugs)({
|
|
1235
|
+
appSlugs: dashboardAppSlugs,
|
|
1236
|
+
explicitRefs: explicitAppRefs,
|
|
1237
|
+
includeApps,
|
|
1238
|
+
cappedAppSlugs,
|
|
1239
|
+
});
|
|
1240
|
+
if (linkedAppSlugs.length > 0) {
|
|
1241
|
+
output.startItem('Linked data apps');
|
|
1242
|
+
const appsDir = path.join((0, contentAsCodePaths_1.getDownloadFolder)(options.path), 'apps');
|
|
1243
|
+
const outcome = await (0, appsDownload_1.downloadAppsToDir)({
|
|
1244
|
+
appRefs: linkedAppSlugs,
|
|
1245
|
+
projectId,
|
|
1246
|
+
appsDir,
|
|
1247
|
+
takenFolders: downloadedAppFolders,
|
|
1248
|
+
cliVersion: env_1.CLI_VERSION,
|
|
1249
|
+
fetchApp: (fetchProjectId, appRef) => (0, apiClient_1.lightdashApi)({
|
|
1250
|
+
method: 'GET',
|
|
1251
|
+
url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
|
|
1252
|
+
body: undefined,
|
|
1253
|
+
}),
|
|
1254
|
+
onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
|
|
1255
|
+
});
|
|
1256
|
+
const linkedSummary = (0, appsDownload_1.appsDownloadSummary)(outcome.successCount, linkedAppSlugs.length, outcome.failures, appsDir, outcome.skippedNotBuiltCount);
|
|
1257
|
+
output.completeItem(`${outcome.successCount} downloaded${outcome.skippedNotBuiltCount > 0
|
|
1258
|
+
? `, ${outcome.skippedNotBuiltCount} skipped`
|
|
1259
|
+
: ''}${outcome.failures.length > 0
|
|
1260
|
+
? `, ${outcome.failures.length} failed`
|
|
1261
|
+
: ''}`, linkedSummary.ok ? undefined : 'warning');
|
|
1262
|
+
if (!linkedSummary.ok) {
|
|
1263
|
+
linkedSummary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1230
1266
|
// Write metadata file with all downloadedAt timestamps
|
|
1231
1267
|
const metadataToWrite = {
|
|
1232
1268
|
version: 1,
|
|
@@ -1364,6 +1400,7 @@ const upsertSingleItem = async (item, type, projectId, changes, force, config, s
|
|
|
1364
1400
|
}),
|
|
1365
1401
|
});
|
|
1366
1402
|
globalState_1.default.debug(`${type} "${item.name}": ${upsertData[type]?.[0].action}`);
|
|
1403
|
+
(upsertData.warnings ?? []).forEach((warning) => globalState_1.default.log(styles.warning(` ⚠ ${item.slug}: ${warning}`)));
|
|
1367
1404
|
// Merge storeUploadChanges result into changes in-place
|
|
1368
1405
|
const updatedChanges = storeUploadChanges(changes, upsertData);
|
|
1369
1406
|
Object.keys(updatedChanges).forEach((key) => {
|
|
@@ -1524,21 +1561,31 @@ const upsertResources = async (type, projectId, changes, force, slugs, canUpload
|
|
|
1524
1561
|
}
|
|
1525
1562
|
return { changes, total: filteredItems.length };
|
|
1526
1563
|
};
|
|
1527
|
-
|
|
1564
|
+
// readCodeFiles walks the whole download folder recursively, so callers that
|
|
1565
|
+
// need both chart and app slugs should read once and reuse the result.
|
|
1566
|
+
const readDashboardItems = async (customPath, looseDashboards = []) => {
|
|
1528
1567
|
const folderDashboards = await readCodeFiles('dashboards', customPath);
|
|
1529
|
-
|
|
1530
|
-
const filteredDashboardItems = dashboardSlugs.length > 0
|
|
1531
|
-
? dashboardItems.filter((dashboard) => dashboardSlugs.includes(dashboard.slug))
|
|
1532
|
-
: dashboardItems;
|
|
1533
|
-
return filteredDashboardItems.reduce((acc, dashboard) => {
|
|
1534
|
-
const dashboardChartSlugs = dashboard.tiles
|
|
1535
|
-
.map((tile) => 'chartSlug' in tile.properties
|
|
1536
|
-
? tile.properties.chartSlug
|
|
1537
|
-
: undefined)
|
|
1538
|
-
.filter((dashboardChartSlug) => !!dashboardChartSlug);
|
|
1539
|
-
return [...acc, ...dashboardChartSlugs];
|
|
1540
|
-
}, []);
|
|
1568
|
+
return [...folderDashboards, ...looseDashboards];
|
|
1541
1569
|
};
|
|
1570
|
+
const selectDashboards = (dashboardItems, dashboardSlugs) => dashboardSlugs.length > 0
|
|
1571
|
+
? dashboardItems.filter((dashboard) => dashboardSlugs.includes(dashboard.slug))
|
|
1572
|
+
: dashboardItems;
|
|
1573
|
+
const selectDashboardChartSlugs = (dashboardItems, dashboardSlugs) => selectDashboards(dashboardItems, dashboardSlugs).reduce((acc, dashboard) => {
|
|
1574
|
+
const dashboardChartSlugs = dashboard.tiles
|
|
1575
|
+
.map((tile) => 'chartSlug' in tile.properties
|
|
1576
|
+
? tile.properties.chartSlug
|
|
1577
|
+
: undefined)
|
|
1578
|
+
.filter((dashboardChartSlug) => !!dashboardChartSlug);
|
|
1579
|
+
return [...acc, ...dashboardChartSlugs];
|
|
1580
|
+
}, []);
|
|
1581
|
+
const selectDashboardAppSlugs = (dashboardItems, dashboardSlugs) => [
|
|
1582
|
+
...new Set(extractAppSlugsFromDashboards(selectDashboards(dashboardItems, dashboardSlugs))),
|
|
1583
|
+
];
|
|
1584
|
+
const getDashboardChartSlugs = async (dashboardSlugs, customPath, looseDashboards = []) => selectDashboardChartSlugs(await readDashboardItems(customPath, looseDashboards), dashboardSlugs);
|
|
1585
|
+
const getDashboardAppSlugs = async (dashboardSlugs, customPath, looseDashboards = []) => selectDashboardAppSlugs(await readDashboardItems(customPath, looseDashboards), dashboardSlugs);
|
|
1586
|
+
// Mirrors the Dashboards phase's own guard: a filtered upload with no
|
|
1587
|
+
// dashboard slugs uploads no dashboards, so there is nothing to derive from.
|
|
1588
|
+
const isFilteredWithNoDashboards = (hasFilters, dashboardSlugs) => hasFilters && dashboardSlugs.length === 0;
|
|
1542
1589
|
const uploadHandler = async (options) => {
|
|
1543
1590
|
globalState_1.default.setVerbose(options.verbose);
|
|
1544
1591
|
if (options.spacesOnly && options.skipSpaces) {
|
|
@@ -1671,97 +1718,8 @@ const uploadHandler = async (options) => {
|
|
|
1671
1718
|
});
|
|
1672
1719
|
}
|
|
1673
1720
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
label: 'Charts',
|
|
1677
|
-
changes,
|
|
1678
|
-
action: async () => {
|
|
1679
|
-
const chartSlugs = options.includeCharts
|
|
1680
|
-
? Array.from(new Set([
|
|
1681
|
-
...options.charts,
|
|
1682
|
-
...(await getDashboardChartSlugs(options.dashboards, options.path, looseFiles.dashboards)),
|
|
1683
|
-
]))
|
|
1684
|
-
: options.charts;
|
|
1685
|
-
if (hasFilters && chartSlugs.length === 0) {
|
|
1686
|
-
globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
|
|
1687
|
-
return changes;
|
|
1688
|
-
}
|
|
1689
|
-
const result = await upsertResources('charts', projectId, changes, options.force, chartSlugs, uploadPermissions.charts, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.charts, spaceNames);
|
|
1690
|
-
chartTotal = result.total;
|
|
1691
|
-
return result.changes;
|
|
1692
|
-
},
|
|
1693
|
-
});
|
|
1694
|
-
changes = await runUploadChangesPhase({
|
|
1695
|
-
output,
|
|
1696
|
-
label: 'Dashboards',
|
|
1697
|
-
changes,
|
|
1698
|
-
action: async () => {
|
|
1699
|
-
if (hasFilters && options.dashboards.length === 0) {
|
|
1700
|
-
globalState_1.default.log(styles.warning(`No dashboard filters provided, skipping`));
|
|
1701
|
-
return changes;
|
|
1702
|
-
}
|
|
1703
|
-
const result = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, uploadPermissions.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.dashboards, spaceNames);
|
|
1704
|
-
dashboardTotal = result.total;
|
|
1705
|
-
return result.changes;
|
|
1706
|
-
},
|
|
1707
|
-
});
|
|
1708
|
-
if (!options.skipAgents) {
|
|
1709
|
-
if (hasFilters && options.agents.length === 0) {
|
|
1710
|
-
globalState_1.default.log(styles.warning(`No AI agent filters provided, skipping`));
|
|
1711
|
-
}
|
|
1712
|
-
else {
|
|
1713
|
-
try {
|
|
1714
|
-
changes = await runUploadChangesPhase({
|
|
1715
|
-
output,
|
|
1716
|
-
label: 'AI agents',
|
|
1717
|
-
changes,
|
|
1718
|
-
action: () => upsertAiAgents(projectId, options.agents, changes, options.force, options.path, options.agents.length === 0),
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
catch (error) {
|
|
1722
|
-
throw new AiAgentAsCodeUploadError(error);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
if (!options.skipAlerts) {
|
|
1727
|
-
if (hasFilters && options.alerts.length === 0) {
|
|
1728
|
-
globalState_1.default.log(styles.warning(`No alert filters provided, skipping`));
|
|
1729
|
-
}
|
|
1730
|
-
else {
|
|
1731
|
-
changes = await runUploadChangesPhase({
|
|
1732
|
-
output,
|
|
1733
|
-
label: 'Alerts',
|
|
1734
|
-
changes,
|
|
1735
|
-
action: () => upsertScheduledContent(projectId, options.alerts, changes, options.force, common_1.ContentAsCodeType.ALERT, uploadPermissions.alerts, options.path),
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
if (!options.skipScheduledDeliveries) {
|
|
1740
|
-
if (hasFilters && options.scheduledDeliveries.length === 0) {
|
|
1741
|
-
globalState_1.default.log(styles.warning(`No scheduled delivery filters provided, skipping`));
|
|
1742
|
-
}
|
|
1743
|
-
else {
|
|
1744
|
-
changes = await runUploadChangesPhase({
|
|
1745
|
-
output,
|
|
1746
|
-
label: 'Scheduled deliveries',
|
|
1747
|
-
changes,
|
|
1748
|
-
action: () => upsertScheduledContent(projectId, options.scheduledDeliveries, changes, options.force, common_1.ContentAsCodeType.SCHEDULED_DELIVERY, uploadPermissions.scheduledDeliveries, options.path),
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
if (!options.skipGoogleSheets) {
|
|
1753
|
-
if (hasFilters && options.googleSheets.length === 0) {
|
|
1754
|
-
globalState_1.default.log(styles.warning(`No Google Sheets sync filters provided, skipping`));
|
|
1755
|
-
}
|
|
1756
|
-
else {
|
|
1757
|
-
changes = await runUploadChangesPhase({
|
|
1758
|
-
output,
|
|
1759
|
-
label: 'Google Sheets syncs',
|
|
1760
|
-
changes,
|
|
1761
|
-
action: () => upsertScheduledContent(projectId, options.googleSheets, changes, options.force, common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC, uploadPermissions.googleSheets, options.path),
|
|
1762
|
-
});
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1721
|
+
// Apps resolve their external connection links by slug in the target
|
|
1722
|
+
// project, so connections must exist before any app is uploaded.
|
|
1765
1723
|
if (!options.skipExternalConnections) {
|
|
1766
1724
|
if (hasFilters && options.externalConnections.length === 0) {
|
|
1767
1725
|
globalState_1.default.log(styles.warning(`No external connection filters provided, skipping`));
|
|
@@ -1775,28 +1733,72 @@ const uploadHandler = async (options) => {
|
|
|
1775
1733
|
});
|
|
1776
1734
|
}
|
|
1777
1735
|
}
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1736
|
+
// Both the Data apps and Charts phases derive slugs from the same
|
|
1737
|
+
// dashboard YAML; read the download folder once and share it.
|
|
1738
|
+
let dashboardItemsPromise;
|
|
1739
|
+
const loadDashboardItems = () => {
|
|
1740
|
+
dashboardItemsPromise =
|
|
1741
|
+
dashboardItemsPromise ??
|
|
1742
|
+
readDashboardItems(options.path, looseFiles.dashboards);
|
|
1743
|
+
return dashboardItemsPromise;
|
|
1744
|
+
};
|
|
1745
|
+
// Upload data apps (enterprise; explicit --apps/--include-apps, or
|
|
1746
|
+
// auto-pushed for a dashboard's apps). Must land before dashboards.
|
|
1780
1747
|
const explicitAppReferences = Array.isArray(options.apps)
|
|
1781
1748
|
? options.apps
|
|
1782
1749
|
: [];
|
|
1783
|
-
const
|
|
1750
|
+
const isExplicitAppSelection = options.includeApps === true || explicitAppReferences.length > 0;
|
|
1751
|
+
const autoPushAppSlugs = isFilteredWithNoDashboards(hasFilters, options.dashboards)
|
|
1752
|
+
? []
|
|
1753
|
+
: selectDashboardAppSlugs(await loadDashboardItems(), options.dashboards);
|
|
1754
|
+
const shouldUploadApps = isExplicitAppSelection || autoPushAppSlugs.length > 0;
|
|
1784
1755
|
let appsCreated = 0;
|
|
1785
1756
|
let appsUpdated = 0;
|
|
1786
1757
|
let appsUnchanged = 0;
|
|
1787
1758
|
let appsFailed = 0;
|
|
1788
1759
|
let appsSkipped = 0;
|
|
1760
|
+
let eeAppRoutesUnavailable = false;
|
|
1789
1761
|
const changesBeforeApps = { ...changes };
|
|
1790
1762
|
if (shouldUploadApps && !uploadPermissions.dataApps) {
|
|
1791
1763
|
output.startItem('Data apps');
|
|
1792
|
-
globalState_1.default.log(styles.
|
|
1764
|
+
globalState_1.default.log(styles.warning(`Skipping data apps: create:DataApp or manage:DataApp permission is required. Dashboard tiles will resolve only if their apps already exist in this project.`));
|
|
1793
1765
|
output.completeItem('permission denied', 'warning');
|
|
1794
1766
|
}
|
|
1795
1767
|
else if (shouldUploadApps) {
|
|
1796
1768
|
output.startItem('Data apps');
|
|
1797
|
-
// --include-apps uploads
|
|
1798
|
-
//
|
|
1799
|
-
const uploadFilter =
|
|
1769
|
+
// Explicit refs filter by slug/appUuid; --include-apps uploads all.
|
|
1770
|
+
// A pure auto-push run applies no filter — gated per folder below.
|
|
1771
|
+
const uploadFilter = isExplicitAppSelection
|
|
1772
|
+
? (0, appsDownload_1.getDataAppUploadFilter)(explicitAppReferences, options.includeApps === true)
|
|
1773
|
+
: null;
|
|
1774
|
+
// The manifest always names the source project, so comparing it
|
|
1775
|
+
// to the target would re-push every run — list the target instead.
|
|
1776
|
+
let presence = {
|
|
1777
|
+
kind: 'unknown',
|
|
1778
|
+
targetProjectUuid: projectId,
|
|
1779
|
+
};
|
|
1780
|
+
// Only the auto-push gate below (skipped entirely when explicit)
|
|
1781
|
+
// reads presence, so an explicit run never needs the listing.
|
|
1782
|
+
if (!isExplicitAppSelection && autoPushAppSlugs.length > 0) {
|
|
1783
|
+
try {
|
|
1784
|
+
const projectApps = await (0, apiClient_1.lightdashApi)({
|
|
1785
|
+
method: 'GET',
|
|
1786
|
+
url: `/api/v1/ee/projects/${projectId}/apps`,
|
|
1787
|
+
body: undefined,
|
|
1788
|
+
});
|
|
1789
|
+
// An older server answers without slugs; that is not an
|
|
1790
|
+
// error, but it is not an answer either.
|
|
1791
|
+
if (projectApps.every((app) => app.slug !== undefined)) {
|
|
1792
|
+
presence = {
|
|
1793
|
+
kind: 'known',
|
|
1794
|
+
slugs: new Set(projectApps.map((app) => app.slug)),
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
catch (listErr) {
|
|
1799
|
+
globalState_1.default.debug(`Could not list target project apps: ${(0, common_1.getErrorMessage)(listErr)}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1800
1802
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
1801
1803
|
const appsDir = path.join(baseDir, 'apps');
|
|
1802
1804
|
let appFolderEntries;
|
|
@@ -1807,7 +1809,6 @@ const uploadHandler = async (options) => {
|
|
|
1807
1809
|
}
|
|
1808
1810
|
catch (err) {
|
|
1809
1811
|
if (err.code === 'ENOENT') {
|
|
1810
|
-
globalState_1.default.log(styles.warning(`No apps directory found at ${appsDir}. Run 'lightdash download --include-apps' first.`));
|
|
1811
1812
|
appFolderEntries = [];
|
|
1812
1813
|
}
|
|
1813
1814
|
else {
|
|
@@ -1816,22 +1817,53 @@ const uploadHandler = async (options) => {
|
|
|
1816
1817
|
}
|
|
1817
1818
|
const subDirs = appFolderEntries.filter((e) => e.isDirectory());
|
|
1818
1819
|
if (subDirs.length === 0) {
|
|
1819
|
-
globalState_1.default.log(styles.warning(
|
|
1820
|
+
globalState_1.default.log(styles.warning(isExplicitAppSelection
|
|
1821
|
+
? `No app folders found in ${appsDir}. Run 'lightdash download --include-apps' first.`
|
|
1822
|
+
: `No app folders found in ${appsDir} for the dashboard(s) being uploaded. Re-run 'lightdash download' to fetch their apps.`));
|
|
1820
1823
|
}
|
|
1821
1824
|
const matchedRefs = new Set();
|
|
1825
|
+
const buildWaitState = (0, uploadRetry_1.createBuildLimitWaitState)();
|
|
1822
1826
|
for (const subDir of subDirs) {
|
|
1823
1827
|
const folderPath = path.join(appsDir, subDir.name);
|
|
1824
1828
|
try {
|
|
1825
1829
|
// eslint-disable-next-line no-await-in-loop
|
|
1826
1830
|
const code = await (0, appCodeFiles_1.readBundleFromDir)(folderPath);
|
|
1827
1831
|
if (!(0, appsDownload_1.uploadFilterMatches)(uploadFilter, code.manifest)) {
|
|
1828
|
-
|
|
1832
|
+
const isAutoPushCandidate = code.manifest.slug !== undefined &&
|
|
1833
|
+
autoPushAppSlugs.includes(code.manifest.slug);
|
|
1834
|
+
if (isAutoPushCandidate) {
|
|
1835
|
+
globalState_1.default.log(styles.warning(`Skipping app "${subDir.name}" — excluded by --apps, but a dashboard being uploaded references it. Its tile may fail to resolve unless the app already exists in the target project.`));
|
|
1836
|
+
}
|
|
1837
|
+
else {
|
|
1838
|
+
globalState_1.default.debug(`Skipping app folder "${subDir.name}" (not in filter)`);
|
|
1839
|
+
}
|
|
1829
1840
|
// eslint-disable-next-line no-continue
|
|
1830
1841
|
continue;
|
|
1831
1842
|
}
|
|
1832
1843
|
if (uploadFilter) {
|
|
1833
1844
|
(0, appsDownload_1.matchedUploadRefs)(uploadFilter, code.manifest).forEach((ref) => matchedRefs.add(ref));
|
|
1834
1845
|
}
|
|
1846
|
+
if (!isExplicitAppSelection) {
|
|
1847
|
+
const isAutoPushCandidate = code.manifest.slug !== undefined &&
|
|
1848
|
+
autoPushAppSlugs.includes(code.manifest.slug);
|
|
1849
|
+
if (!isAutoPushCandidate) {
|
|
1850
|
+
// eslint-disable-next-line no-continue
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1854
|
+
const folderChanged = await (0, appCodeFiles_1.appFolderNeedsUpdating)(folderPath, code.manifest);
|
|
1855
|
+
if (!(0, appsDownload_1.shouldAutoPushApp)({
|
|
1856
|
+
manifest: code.manifest,
|
|
1857
|
+
presence,
|
|
1858
|
+
folderChanged,
|
|
1859
|
+
force: options.force === true,
|
|
1860
|
+
})) {
|
|
1861
|
+
globalState_1.default.debug(`Skipping app "${subDir.name}" — unchanged and already in the target project`);
|
|
1862
|
+
appsSkipped += 1;
|
|
1863
|
+
// eslint-disable-next-line no-continue
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1835
1867
|
// Read declared dependencies from the app folder (optional).
|
|
1836
1868
|
// eslint-disable-next-line no-await-in-loop
|
|
1837
1869
|
const rawDeps = await (0, appCodeFiles_1.readDependenciesFromDir)(folderPath);
|
|
@@ -1900,10 +1932,17 @@ const uploadHandler = async (options) => {
|
|
|
1900
1932
|
force: options.force,
|
|
1901
1933
|
});
|
|
1902
1934
|
// eslint-disable-next-line no-await-in-loop
|
|
1903
|
-
const { appUuid, version, action, slug, warnings } = await (0, apiClient_1.lightdashApi)({
|
|
1935
|
+
const { appUuid, version, action, slug, warnings } = await (0, uploadRetry_1.withBuildLimitRetry)(() => (0, apiClient_1.lightdashApi)({
|
|
1904
1936
|
method: 'POST',
|
|
1905
1937
|
url: `/api/v1/ee/projects/${projectId}/apps/upload`,
|
|
1906
1938
|
body: JSON.stringify(body),
|
|
1939
|
+
}), buildWaitState, {
|
|
1940
|
+
onWait: (attempt, delayMs) => {
|
|
1941
|
+
if (attempt === 1) {
|
|
1942
|
+
globalState_1.default.log(styles.warning(`Project build limit reached — waiting for builds to finish before uploading "${subDir.name}"…`));
|
|
1943
|
+
}
|
|
1944
|
+
globalState_1.default.debug(`> Build cap retry ${attempt} for "${subDir.name}" in ${delayMs}ms`);
|
|
1945
|
+
},
|
|
1907
1946
|
});
|
|
1908
1947
|
// e.g. a manifest external-connection link whose slug is
|
|
1909
1948
|
// missing in the target project was skipped
|
|
@@ -1939,13 +1978,26 @@ const uploadHandler = async (options) => {
|
|
|
1939
1978
|
}
|
|
1940
1979
|
}
|
|
1941
1980
|
catch (appErr) {
|
|
1942
|
-
appsFailed += 1;
|
|
1943
1981
|
const status = appErr instanceof common_1.LightdashError
|
|
1944
1982
|
? appErr.statusCode
|
|
1945
1983
|
: undefined;
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1984
|
+
// Auto-push is flag-free, so a server without the EE app
|
|
1985
|
+
// routes must not fail an upload the user never asked for.
|
|
1986
|
+
if (!isExplicitAppSelection && status === 404) {
|
|
1987
|
+
eeAppRoutesUnavailable = true;
|
|
1988
|
+
globalState_1.default.log(styles.warning(`Skipping data apps: the enterprise "data apps" feature is not available on this instance. Dashboard tiles will resolve only if their apps already exist in this project.`));
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
appsFailed += 1;
|
|
1992
|
+
let hint = '';
|
|
1993
|
+
if (status === 404) {
|
|
1994
|
+
hint =
|
|
1995
|
+
' — the enterprise "data apps" feature may not be enabled on this instance';
|
|
1996
|
+
}
|
|
1997
|
+
else if (status === 429) {
|
|
1998
|
+
hint =
|
|
1999
|
+
' — gave up waiting for a free build slot; re-run the upload once builds finish (unchanged apps are skipped)';
|
|
2000
|
+
}
|
|
1949
2001
|
globalState_1.default.log(styles.error(`Failed to upload app folder "${subDir.name}"${status ? ` [HTTP ${status}]` : ''}: ${(0, common_1.getErrorMessage)(appErr)}${hint}`));
|
|
1950
2002
|
}
|
|
1951
2003
|
}
|
|
@@ -1966,7 +2018,12 @@ const uploadHandler = async (options) => {
|
|
|
1966
2018
|
if (appsSkipped > 0)
|
|
1967
2019
|
changes['data apps skipped'] = appsSkipped;
|
|
1968
2020
|
const appSummary = summarizeUploadChanges(changesBeforeApps, changes);
|
|
1969
|
-
|
|
2021
|
+
if (eeAppRoutesUnavailable) {
|
|
2022
|
+
output.completeItem('not available on this server', 'warning');
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
output.completeItem(appSummary.detail, appSummary.variant);
|
|
2026
|
+
}
|
|
1970
2027
|
if (appsFailed > 0) {
|
|
1971
2028
|
// App uploads are fire-and-forget per folder, so failures are
|
|
1972
2029
|
// logged and tallied rather than thrown — but the process must
|
|
@@ -1975,6 +2032,97 @@ const uploadHandler = async (options) => {
|
|
|
1975
2032
|
process.exitCode = 1;
|
|
1976
2033
|
}
|
|
1977
2034
|
}
|
|
2035
|
+
changes = await runUploadChangesPhase({
|
|
2036
|
+
output,
|
|
2037
|
+
label: 'Charts',
|
|
2038
|
+
changes,
|
|
2039
|
+
action: async () => {
|
|
2040
|
+
const chartSlugs = options.includeCharts
|
|
2041
|
+
? Array.from(new Set([
|
|
2042
|
+
...options.charts,
|
|
2043
|
+
...selectDashboardChartSlugs(await loadDashboardItems(), options.dashboards),
|
|
2044
|
+
]))
|
|
2045
|
+
: options.charts;
|
|
2046
|
+
if (hasFilters && chartSlugs.length === 0) {
|
|
2047
|
+
globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
|
|
2048
|
+
return changes;
|
|
2049
|
+
}
|
|
2050
|
+
const result = await upsertResources('charts', projectId, changes, options.force, chartSlugs, uploadPermissions.charts, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.charts, spaceNames);
|
|
2051
|
+
chartTotal = result.total;
|
|
2052
|
+
return result.changes;
|
|
2053
|
+
},
|
|
2054
|
+
});
|
|
2055
|
+
changes = await runUploadChangesPhase({
|
|
2056
|
+
output,
|
|
2057
|
+
label: 'Dashboards',
|
|
2058
|
+
changes,
|
|
2059
|
+
action: async () => {
|
|
2060
|
+
if (hasFilters && options.dashboards.length === 0) {
|
|
2061
|
+
globalState_1.default.log(styles.warning(`No dashboard filters provided, skipping`));
|
|
2062
|
+
return changes;
|
|
2063
|
+
}
|
|
2064
|
+
const result = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, uploadPermissions.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.dashboards, spaceNames);
|
|
2065
|
+
dashboardTotal = result.total;
|
|
2066
|
+
return result.changes;
|
|
2067
|
+
},
|
|
2068
|
+
});
|
|
2069
|
+
if (!options.skipAgents) {
|
|
2070
|
+
if (hasFilters && options.agents.length === 0) {
|
|
2071
|
+
globalState_1.default.log(styles.warning(`No AI agent filters provided, skipping`));
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
try {
|
|
2075
|
+
changes = await runUploadChangesPhase({
|
|
2076
|
+
output,
|
|
2077
|
+
label: 'AI agents',
|
|
2078
|
+
changes,
|
|
2079
|
+
action: () => upsertAiAgents(projectId, options.agents, changes, options.force, options.path, options.agents.length === 0),
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
catch (error) {
|
|
2083
|
+
throw new AiAgentAsCodeUploadError(error);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (!options.skipAlerts) {
|
|
2088
|
+
if (hasFilters && options.alerts.length === 0) {
|
|
2089
|
+
globalState_1.default.log(styles.warning(`No alert filters provided, skipping`));
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
changes = await runUploadChangesPhase({
|
|
2093
|
+
output,
|
|
2094
|
+
label: 'Alerts',
|
|
2095
|
+
changes,
|
|
2096
|
+
action: () => upsertScheduledContent(projectId, options.alerts, changes, options.force, common_1.ContentAsCodeType.ALERT, uploadPermissions.alerts, options.path),
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (!options.skipScheduledDeliveries) {
|
|
2101
|
+
if (hasFilters && options.scheduledDeliveries.length === 0) {
|
|
2102
|
+
globalState_1.default.log(styles.warning(`No scheduled delivery filters provided, skipping`));
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
changes = await runUploadChangesPhase({
|
|
2106
|
+
output,
|
|
2107
|
+
label: 'Scheduled deliveries',
|
|
2108
|
+
changes,
|
|
2109
|
+
action: () => upsertScheduledContent(projectId, options.scheduledDeliveries, changes, options.force, common_1.ContentAsCodeType.SCHEDULED_DELIVERY, uploadPermissions.scheduledDeliveries, options.path),
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (!options.skipGoogleSheets) {
|
|
2114
|
+
if (hasFilters && options.googleSheets.length === 0) {
|
|
2115
|
+
globalState_1.default.log(styles.warning(`No Google Sheets sync filters provided, skipping`));
|
|
2116
|
+
}
|
|
2117
|
+
else {
|
|
2118
|
+
changes = await runUploadChangesPhase({
|
|
2119
|
+
output,
|
|
2120
|
+
label: 'Google Sheets syncs',
|
|
2121
|
+
changes,
|
|
2122
|
+
action: () => upsertScheduledContent(projectId, options.googleSheets, changes, options.force, common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC, uploadPermissions.googleSheets, options.path),
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1978
2126
|
const end = Date.now();
|
|
1979
2127
|
await analytics_1.LightdashAnalytics.track({
|
|
1980
2128
|
event: 'upload.completed',
|
|
@@ -2009,12 +2157,15 @@ exports.uploadHandler = uploadHandler;
|
|
|
2009
2157
|
exports.testHelpers = {
|
|
2010
2158
|
assertUniqueSpacePaths: spacesAsCode_1.assertUniqueSpacePaths,
|
|
2011
2159
|
downloadSpaces: spacesAsCode_1.downloadSpaces,
|
|
2160
|
+
extractAppSlugsFromDashboards,
|
|
2012
2161
|
getFlatSpaceFileNames: spacesAsCode_1.getFlatSpaceFileNames,
|
|
2162
|
+
getDashboardAppSlugs,
|
|
2013
2163
|
getDashboardChartSlugs,
|
|
2014
2164
|
hasContentFilters,
|
|
2015
2165
|
isAiAgentsUnavailableError,
|
|
2016
2166
|
isExternalConnectionsUnavailableError,
|
|
2017
2167
|
downloadAiAgents,
|
|
2168
|
+
isFilteredWithNoDashboards,
|
|
2018
2169
|
readAiAgentFiles,
|
|
2019
2170
|
readExternalConnectionFiles,
|
|
2020
2171
|
readSpaceFiles: spacesAsCode_1.readSpaceFiles,
|