@lightdash/cli 1.260.1 → 1.262.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/analytics/analytics.d.ts +1 -0
- package/dist/analytics/analytics.d.ts.map +1 -1
- package/dist/handlers/apps/appsDownload.d.ts +12 -6
- package/dist/handlers/apps/appsDownload.d.ts.map +1 -1
- package/dist/handlers/apps/appsDownload.js +36 -12
- package/dist/handlers/apps/appsDownload.test.js +25 -0
- package/dist/handlers/download.d.ts +5 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +324 -61
- package/dist/index.js +7 -0
- package/package.json +3 -3
|
@@ -33,7 +33,7 @@ const selectProject_1 = require("./selectProject");
|
|
|
33
33
|
const spacesAsCode_1 = require("./spacesAsCode");
|
|
34
34
|
const shouldDownloadAiAgents = ({ includeAll, includeAgents, agents, appsOnly, }) => appsOnly !== true &&
|
|
35
35
|
(includeAll === true || includeAgents === true || agents.length > 0);
|
|
36
|
-
const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, }) => !spacesOnly &&
|
|
36
|
+
const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, chartTypes, }) => !spacesOnly &&
|
|
37
37
|
[
|
|
38
38
|
charts,
|
|
39
39
|
dashboards,
|
|
@@ -44,6 +44,7 @@ const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, goo
|
|
|
44
44
|
virtualViews,
|
|
45
45
|
externalConnections,
|
|
46
46
|
apps ?? [],
|
|
47
|
+
chartTypes ?? [],
|
|
47
48
|
].some((filters) => filters.length > 0);
|
|
48
49
|
/*
|
|
49
50
|
This function is used to parse the content filters.
|
|
@@ -937,6 +938,9 @@ const listAppsViaContentApi = async (projectId) => {
|
|
|
937
938
|
const downloadHandler = async (options) => {
|
|
938
939
|
globalState_1.default.setVerbose(options.verbose);
|
|
939
940
|
const isOrganizationDownload = options.organization === true;
|
|
941
|
+
if (options.appsOnly && options.chartTypesOnly) {
|
|
942
|
+
throw new common_1.ParameterError('--apps-only cannot be combined with --chart-types-only.');
|
|
943
|
+
}
|
|
940
944
|
// Bare --apps-only means "all apps": imply --include-apps.
|
|
941
945
|
if (options.appsOnly &&
|
|
942
946
|
options.apps === undefined &&
|
|
@@ -944,13 +948,36 @@ const downloadHandler = async (options) => {
|
|
|
944
948
|
options.includeAll !== true) {
|
|
945
949
|
options.includeApps = true;
|
|
946
950
|
}
|
|
951
|
+
// Bare --chart-types-only means "all chart types" likewise.
|
|
952
|
+
if (options.chartTypesOnly &&
|
|
953
|
+
options.chartTypes === undefined &&
|
|
954
|
+
options.includeChartTypes !== true &&
|
|
955
|
+
options.includeAll !== true) {
|
|
956
|
+
options.includeChartTypes = true;
|
|
957
|
+
}
|
|
947
958
|
const includeAll = options.includeAll === true;
|
|
948
|
-
const includeApps = !options.spacesOnly &&
|
|
949
|
-
|
|
959
|
+
const includeApps = !options.spacesOnly &&
|
|
960
|
+
!options.chartTypesOnly &&
|
|
961
|
+
(options.includeApps === true || includeAll);
|
|
962
|
+
const includeChartTypes = !options.spacesOnly &&
|
|
963
|
+
!options.appsOnly &&
|
|
964
|
+
(options.includeChartTypes === true || includeAll);
|
|
965
|
+
const includeAllOptionalContent = includeAll &&
|
|
966
|
+
!options.appsOnly &&
|
|
967
|
+
!options.chartTypesOnly &&
|
|
968
|
+
!options.spacesOnly;
|
|
950
969
|
const { limit: appsLimit, noEffectWarning: appsLimitWarning } = (0, appsDownload_1.resolveAppsLimit)(options.appsLimit, includeApps);
|
|
951
970
|
if (appsLimitWarning) {
|
|
952
971
|
globalState_1.default.log(styles.warning(appsLimitWarning));
|
|
953
972
|
}
|
|
973
|
+
const { limit: chartTypesLimit, noEffectWarning: chartTypesLimitWarning } = (0, appsDownload_1.resolveAppsLimit)(options.chartTypesLimit, includeChartTypes, {
|
|
974
|
+
limitFlag: '--chart-types-limit',
|
|
975
|
+
includeFlag: '--include-chart-types',
|
|
976
|
+
refsFlag: '--chart-types',
|
|
977
|
+
});
|
|
978
|
+
if (chartTypesLimitWarning) {
|
|
979
|
+
globalState_1.default.log(styles.warning(chartTypesLimitWarning));
|
|
980
|
+
}
|
|
954
981
|
if (options.appsOnly) {
|
|
955
982
|
const appsOnlySelection = (0, appsDownload_1.selectAppsToDownload)({
|
|
956
983
|
apps: Array.isArray(options.apps) ? options.apps : undefined,
|
|
@@ -959,6 +986,28 @@ const downloadHandler = async (options) => {
|
|
|
959
986
|
if (appsOnlySelection.mode === 'none') {
|
|
960
987
|
throw new common_1.ParameterError('Nothing to download: --apps-only requires --apps <appReferences...>, --include-apps, or --include-all.');
|
|
961
988
|
}
|
|
989
|
+
options.chartTypes = undefined;
|
|
990
|
+
options.skipCharts = true;
|
|
991
|
+
options.skipDashboards = true;
|
|
992
|
+
options.skipSpaces = true;
|
|
993
|
+
options.includeAgents = false;
|
|
994
|
+
options.includeAlerts = false;
|
|
995
|
+
options.includeGoogleSheets = false;
|
|
996
|
+
options.includeScheduledDeliveries = false;
|
|
997
|
+
options.includeVirtualViews = false;
|
|
998
|
+
options.includeExternalConnections = false;
|
|
999
|
+
}
|
|
1000
|
+
if (options.chartTypesOnly) {
|
|
1001
|
+
const chartTypesOnlySelection = (0, appsDownload_1.selectAppsToDownload)({
|
|
1002
|
+
apps: Array.isArray(options.chartTypes)
|
|
1003
|
+
? options.chartTypes
|
|
1004
|
+
: undefined,
|
|
1005
|
+
includeApps: includeChartTypes,
|
|
1006
|
+
});
|
|
1007
|
+
if (chartTypesOnlySelection.mode === 'none') {
|
|
1008
|
+
throw new common_1.ParameterError('Nothing to download: --chart-types-only requires --chart-types <chartTypeReferences...>, --include-chart-types, or --include-all.');
|
|
1009
|
+
}
|
|
1010
|
+
options.apps = undefined;
|
|
962
1011
|
options.skipCharts = true;
|
|
963
1012
|
options.skipDashboards = true;
|
|
964
1013
|
options.skipSpaces = true;
|
|
@@ -978,6 +1027,7 @@ const downloadHandler = async (options) => {
|
|
|
978
1027
|
options.agents = [];
|
|
979
1028
|
options.alerts = [];
|
|
980
1029
|
options.apps = [];
|
|
1030
|
+
options.chartTypes = [];
|
|
981
1031
|
options.googleSheets = [];
|
|
982
1032
|
options.scheduledDeliveries = [];
|
|
983
1033
|
options.virtualViews = [];
|
|
@@ -1054,6 +1104,8 @@ const downloadHandler = async (options) => {
|
|
|
1054
1104
|
// Shared across both apps-download steps so two different apps whose
|
|
1055
1105
|
// names collide under the pre-slug fallback naming don't clobber each other.
|
|
1056
1106
|
const downloadedAppFolders = new Set();
|
|
1107
|
+
// Chart types live in their own folder, so they track their own names.
|
|
1108
|
+
const downloadedChartTypeFolders = new Set();
|
|
1057
1109
|
// App slugs referenced by downloaded dashboards' tiles, populated by
|
|
1058
1110
|
// the Dashboards step and consumed by the Linked data apps step.
|
|
1059
1111
|
let dashboardAppSlugs = [];
|
|
@@ -1314,7 +1366,7 @@ const downloadHandler = async (options) => {
|
|
|
1314
1366
|
output.updateActive(`0 of ${appRefsToDownload.length} downloaded`);
|
|
1315
1367
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
1316
1368
|
const appsDir = path.join(baseDir, 'apps');
|
|
1317
|
-
const { successCount, skippedNotBuiltCount, failures } = await (0, appsDownload_1.downloadAppsToDir)({
|
|
1369
|
+
const { successCount, skippedNotBuiltCount, skippedWrongKindCount, failures, } = await (0, appsDownload_1.downloadAppsToDir)({
|
|
1318
1370
|
appRefs: appRefsToDownload,
|
|
1319
1371
|
projectId,
|
|
1320
1372
|
appsDir,
|
|
@@ -1325,18 +1377,120 @@ const downloadHandler = async (options) => {
|
|
|
1325
1377
|
url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
|
|
1326
1378
|
body: undefined,
|
|
1327
1379
|
}),
|
|
1380
|
+
skipBundle: (manifest) => manifest.template === common_1.DATA_APP_VIZ_TEMPLATE
|
|
1381
|
+
? 'this is a custom chart type — download it with --chart-types or --include-chart-types'
|
|
1382
|
+
: null,
|
|
1328
1383
|
onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
|
|
1329
1384
|
});
|
|
1330
|
-
const summary = (0, appsDownload_1.appsDownloadSummary)(successCount, appRefsToDownload.length, failures, appsDir, skippedNotBuiltCount);
|
|
1385
|
+
const summary = (0, appsDownload_1.appsDownloadSummary)(successCount, appRefsToDownload.length, failures, appsDir, skippedNotBuiltCount + skippedWrongKindCount);
|
|
1331
1386
|
counts.appsNum = successCount;
|
|
1332
|
-
output.completeItem(`${successCount} downloaded${skippedNotBuiltCount > 0
|
|
1333
|
-
? `, ${skippedNotBuiltCount} skipped`
|
|
1387
|
+
output.completeItem(`${successCount} downloaded${skippedNotBuiltCount + skippedWrongKindCount > 0
|
|
1388
|
+
? `, ${skippedNotBuiltCount + skippedWrongKindCount} skipped`
|
|
1334
1389
|
: ''}${failures.length > 0 ? `, ${failures.length} failed` : ''}`, summary.ok ? undefined : 'warning');
|
|
1335
1390
|
if (!summary.ok) {
|
|
1336
1391
|
summary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
|
|
1337
1392
|
}
|
|
1338
1393
|
}
|
|
1339
1394
|
}
|
|
1395
|
+
// Download custom chart types (enterprise, opt-in via --chart-types /
|
|
1396
|
+
// --include-chart-types / --include-all) into chart-types/, kept
|
|
1397
|
+
// separate from data apps.
|
|
1398
|
+
const chartTypesSelection = (0, appsDownload_1.selectAppsToDownload)({
|
|
1399
|
+
apps: Array.isArray(options.chartTypes)
|
|
1400
|
+
? options.chartTypes
|
|
1401
|
+
: undefined,
|
|
1402
|
+
includeApps: includeChartTypes,
|
|
1403
|
+
});
|
|
1404
|
+
if (chartTypesSelection.mode !== 'none') {
|
|
1405
|
+
output.startItem('Custom chart types');
|
|
1406
|
+
let chartTypeRefsToDownload;
|
|
1407
|
+
let chartTypeListingError = null;
|
|
1408
|
+
if (chartTypesSelection.mode === 'explicit') {
|
|
1409
|
+
chartTypeRefsToDownload = chartTypesSelection.appRefs;
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
output.updateActive('listing project chart types…');
|
|
1413
|
+
let listedChartTypes = [];
|
|
1414
|
+
try {
|
|
1415
|
+
const projectChartTypes = await (0, apiClient_1.lightdashApi)({
|
|
1416
|
+
method: 'GET',
|
|
1417
|
+
url: `/api/v1/ee/projects/${projectId}/apps/chart-types`,
|
|
1418
|
+
body: undefined,
|
|
1419
|
+
});
|
|
1420
|
+
listedChartTypes = projectChartTypes.map((chartType) => ({
|
|
1421
|
+
appUuid: chartType.appUuid,
|
|
1422
|
+
slug: chartType.slug,
|
|
1423
|
+
}));
|
|
1424
|
+
}
|
|
1425
|
+
catch (listErr) {
|
|
1426
|
+
if ((0, appsDownload_1.shouldFallBackToSpaceScopedListing)(listErr)) {
|
|
1427
|
+
// 404: the server predates the chart-types listing
|
|
1428
|
+
// (or chart types entirely) — nothing to list.
|
|
1429
|
+
globalState_1.default.log(styles.warning('This server does not support listing custom chart types; pass explicit --chart-types references or upgrade the server.'));
|
|
1430
|
+
listedChartTypes = [];
|
|
1431
|
+
}
|
|
1432
|
+
else if (includeAllOptionalContent) {
|
|
1433
|
+
chartTypeListingError = (0, common_1.getErrorMessage)(listErr);
|
|
1434
|
+
listedChartTypes = [];
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
throw listErr;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const { appUuids: cappedChartTypeUuids, truncatedCount: chartTypesTruncated, } = (0, appsDownload_1.capListedApps)(listedChartTypes.map((chartType) => chartType.appUuid), chartTypesLimit);
|
|
1441
|
+
if (chartTypesTruncated > 0) {
|
|
1442
|
+
globalState_1.default.log(styles.warning(`Found ${listedChartTypes.length} custom chart types, downloading the first ${chartTypesLimit}. Pass --chart-types-limit <n> to raise the cap.`));
|
|
1443
|
+
}
|
|
1444
|
+
chartTypeRefsToDownload = [
|
|
1445
|
+
...new Set([
|
|
1446
|
+
...cappedChartTypeUuids,
|
|
1447
|
+
...chartTypesSelection.extraAppRefs,
|
|
1448
|
+
]),
|
|
1449
|
+
];
|
|
1450
|
+
}
|
|
1451
|
+
if (chartTypeRefsToDownload.length === 0) {
|
|
1452
|
+
counts.chartTypesNum = 0;
|
|
1453
|
+
if (chartTypeListingError === null) {
|
|
1454
|
+
output.completeItem('0 found');
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
output.completeItem(`listing failed: ${chartTypeListingError}`, 'warning');
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
output.updateActive(`0 of ${chartTypeRefsToDownload.length} downloaded`);
|
|
1462
|
+
const chartTypesDir = path.join((0, contentAsCodePaths_1.getDownloadFolder)(options.path), 'chart-types');
|
|
1463
|
+
const chartTypesOutcome = await (0, appsDownload_1.downloadAppsToDir)({
|
|
1464
|
+
appRefs: chartTypeRefsToDownload,
|
|
1465
|
+
projectId,
|
|
1466
|
+
appsDir: chartTypesDir,
|
|
1467
|
+
takenFolders: downloadedChartTypeFolders,
|
|
1468
|
+
cliVersion: env_1.CLI_VERSION,
|
|
1469
|
+
fetchApp: (fetchProjectId, appRef) => (0, apiClient_1.lightdashApi)({
|
|
1470
|
+
method: 'GET',
|
|
1471
|
+
url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
|
|
1472
|
+
body: undefined,
|
|
1473
|
+
}),
|
|
1474
|
+
skipBundle: (manifest) => manifest.template !== common_1.DATA_APP_VIZ_TEMPLATE
|
|
1475
|
+
? 'this is a data app — download it with --apps or --include-apps'
|
|
1476
|
+
: null,
|
|
1477
|
+
onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
|
|
1478
|
+
});
|
|
1479
|
+
const chartTypesSummary = (0, appsDownload_1.appsDownloadSummary)(chartTypesOutcome.successCount, chartTypeRefsToDownload.length, chartTypesOutcome.failures, chartTypesDir, chartTypesOutcome.skippedNotBuiltCount +
|
|
1480
|
+
chartTypesOutcome.skippedWrongKindCount, 'custom chart type');
|
|
1481
|
+
counts.chartTypesNum = chartTypesOutcome.successCount;
|
|
1482
|
+
const chartTypesSkipped = chartTypesOutcome.skippedNotBuiltCount +
|
|
1483
|
+
chartTypesOutcome.skippedWrongKindCount;
|
|
1484
|
+
output.completeItem(`${chartTypesOutcome.successCount} downloaded${chartTypesSkipped > 0
|
|
1485
|
+
? `, ${chartTypesSkipped} skipped`
|
|
1486
|
+
: ''}${chartTypesOutcome.failures.length > 0
|
|
1487
|
+
? `, ${chartTypesOutcome.failures.length} failed`
|
|
1488
|
+
: ''}`, chartTypesSummary.ok ? undefined : 'warning');
|
|
1489
|
+
if (!chartTypesSummary.ok) {
|
|
1490
|
+
chartTypesSummary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1340
1494
|
// Dashboard-referenced apps not already covered above (explicit
|
|
1341
1495
|
// --apps ref, or a non-truncated slot in the --include-apps cap).
|
|
1342
1496
|
const linkedAppSlugs = (0, appsDownload_1.computeLinkedAppSlugs)({
|
|
@@ -1359,13 +1513,16 @@ const downloadHandler = async (options) => {
|
|
|
1359
1513
|
url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
|
|
1360
1514
|
body: undefined,
|
|
1361
1515
|
}),
|
|
1516
|
+
// Dashboard data-app tiles reference apps, never chart types.
|
|
1517
|
+
skipBundle: (manifest) => manifest.template === common_1.DATA_APP_VIZ_TEMPLATE
|
|
1518
|
+
? 'this is a custom chart type — dashboard app tiles cannot reference it'
|
|
1519
|
+
: null,
|
|
1362
1520
|
onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
|
|
1363
1521
|
});
|
|
1364
|
-
const
|
|
1522
|
+
const linkedSkipped = outcome.skippedNotBuiltCount + outcome.skippedWrongKindCount;
|
|
1523
|
+
const linkedSummary = (0, appsDownload_1.appsDownloadSummary)(outcome.successCount, linkedAppSlugs.length, outcome.failures, appsDir, linkedSkipped);
|
|
1365
1524
|
counts.appsNum = (counts.appsNum ?? 0) + outcome.successCount;
|
|
1366
|
-
output.completeItem(`${outcome.successCount} downloaded${outcome.
|
|
1367
|
-
? `, ${outcome.skippedNotBuiltCount} skipped`
|
|
1368
|
-
: ''}${outcome.failures.length > 0
|
|
1525
|
+
output.completeItem(`${outcome.successCount} downloaded${linkedSkipped > 0 ? `, ${linkedSkipped} skipped` : ''}${outcome.failures.length > 0
|
|
1369
1526
|
? `, ${outcome.failures.length} failed`
|
|
1370
1527
|
: ''}`, linkedSummary.ok ? undefined : 'warning');
|
|
1371
1528
|
if (!linkedSummary.ok) {
|
|
@@ -1720,20 +1877,47 @@ const uploadHandler = async (options) => {
|
|
|
1720
1877
|
if (options.appsOnly && options.spacesOnly) {
|
|
1721
1878
|
throw new common_1.ParameterError('--apps-only cannot be combined with --spaces-only.');
|
|
1722
1879
|
}
|
|
1880
|
+
if (options.appsOnly && options.chartTypesOnly) {
|
|
1881
|
+
throw new common_1.ParameterError('--apps-only cannot be combined with --chart-types-only.');
|
|
1882
|
+
}
|
|
1883
|
+
if (options.chartTypesOnly && options.spacesOnly) {
|
|
1884
|
+
throw new common_1.ParameterError('--chart-types-only cannot be combined with --spaces-only.');
|
|
1885
|
+
}
|
|
1723
1886
|
if (options.appsOnly &&
|
|
1724
1887
|
(options.charts.length > 0 || options.dashboards.length > 0)) {
|
|
1725
1888
|
throw new common_1.ParameterError('--apps-only cannot be combined with --charts or --dashboards.');
|
|
1726
1889
|
}
|
|
1890
|
+
if (options.chartTypesOnly &&
|
|
1891
|
+
(options.charts.length > 0 || options.dashboards.length > 0)) {
|
|
1892
|
+
throw new common_1.ParameterError('--chart-types-only cannot be combined with --charts or --dashboards.');
|
|
1893
|
+
}
|
|
1727
1894
|
// Bare --apps-only means "all apps": imply --include-apps.
|
|
1728
1895
|
if (options.appsOnly &&
|
|
1729
1896
|
options.apps === undefined &&
|
|
1730
1897
|
options.includeApps !== true) {
|
|
1731
1898
|
options.includeApps = true;
|
|
1732
1899
|
}
|
|
1900
|
+
if (options.appsOnly) {
|
|
1901
|
+
options.chartTypes = undefined;
|
|
1902
|
+
options.includeChartTypes = false;
|
|
1903
|
+
}
|
|
1904
|
+
// Bare --chart-types-only means "all chart types" likewise.
|
|
1905
|
+
if (options.chartTypesOnly &&
|
|
1906
|
+
options.chartTypes === undefined &&
|
|
1907
|
+
options.includeChartTypes !== true) {
|
|
1908
|
+
options.includeChartTypes = true;
|
|
1909
|
+
}
|
|
1910
|
+
if (options.chartTypesOnly) {
|
|
1911
|
+
options.apps = undefined;
|
|
1912
|
+
options.includeApps = false;
|
|
1913
|
+
}
|
|
1733
1914
|
const isOrganizationUpload = options.organization === true;
|
|
1734
|
-
// --apps-only
|
|
1735
|
-
// skips exactly as it does when only
|
|
1736
|
-
|
|
1915
|
+
// --apps-only / --chart-types-only ride the existing filter machinery:
|
|
1916
|
+
// every other phase skips exactly as it does when only those refs are
|
|
1917
|
+
// passed.
|
|
1918
|
+
const hasFilters = hasContentFilters(options) ||
|
|
1919
|
+
options.appsOnly === true ||
|
|
1920
|
+
options.chartTypesOnly === true;
|
|
1737
1921
|
const shouldReconcileSpaces = !isOrganizationUpload && !options.skipSpaces && !hasFilters;
|
|
1738
1922
|
let preflightSpaceFiles = [];
|
|
1739
1923
|
if (shouldReconcileSpaces) {
|
|
@@ -1915,8 +2099,10 @@ const uploadHandler = async (options) => {
|
|
|
1915
2099
|
});
|
|
1916
2100
|
}
|
|
1917
2101
|
}
|
|
1918
|
-
// Upload data apps (enterprise
|
|
1919
|
-
// auto-pushed for a dashboard's
|
|
2102
|
+
// Upload data apps and custom chart types (enterprise). Data apps:
|
|
2103
|
+
// explicit --apps/--include-apps, or auto-pushed for a dashboard's
|
|
2104
|
+
// apps; must land before dashboards. Chart types: explicit
|
|
2105
|
+
// --chart-types/--include-chart-types, from their own folder.
|
|
1920
2106
|
const explicitAppReferences = Array.isArray(options.apps)
|
|
1921
2107
|
? options.apps
|
|
1922
2108
|
: [];
|
|
@@ -1924,26 +2110,70 @@ const uploadHandler = async (options) => {
|
|
|
1924
2110
|
const autoPushAppSlugs = isFilteredWithNoDashboards(hasFilters, options.dashboards)
|
|
1925
2111
|
? []
|
|
1926
2112
|
: selectDashboardAppSlugs(await loadDashboardItems(), options.dashboards);
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2113
|
+
const explicitChartTypeReferences = Array.isArray(options.chartTypes)
|
|
2114
|
+
? options.chartTypes
|
|
2115
|
+
: [];
|
|
2116
|
+
const isExplicitChartTypeSelection = options.includeChartTypes === true ||
|
|
2117
|
+
explicitChartTypeReferences.length > 0;
|
|
2118
|
+
const appsPhaseActive = isExplicitAppSelection || autoPushAppSlugs.length > 0;
|
|
2119
|
+
const bundleUploadPhases = [
|
|
2120
|
+
{
|
|
2121
|
+
label: 'Data apps',
|
|
2122
|
+
noun: 'data app',
|
|
2123
|
+
changesPrefix: 'data apps',
|
|
2124
|
+
dirName: 'apps',
|
|
2125
|
+
explicitRefs: explicitAppReferences,
|
|
2126
|
+
includeAllFolders: options.includeApps === true,
|
|
2127
|
+
isExplicitSelection: isExplicitAppSelection,
|
|
2128
|
+
autoPushSlugs: autoPushAppSlugs,
|
|
2129
|
+
useAppSpace: true,
|
|
2130
|
+
isChartTypes: false,
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
label: 'Custom chart types',
|
|
2134
|
+
noun: 'custom chart type',
|
|
2135
|
+
changesPrefix: 'chart types',
|
|
2136
|
+
dirName: 'chart-types',
|
|
2137
|
+
explicitRefs: explicitChartTypeReferences,
|
|
2138
|
+
includeAllFolders: options.includeChartTypes === true,
|
|
2139
|
+
isExplicitSelection: isExplicitChartTypeSelection,
|
|
2140
|
+
autoPushSlugs: [],
|
|
2141
|
+
useAppSpace: false,
|
|
2142
|
+
isChartTypes: true,
|
|
2143
|
+
},
|
|
2144
|
+
];
|
|
2145
|
+
for (const phase of bundleUploadPhases) {
|
|
2146
|
+
const shouldUploadPhase = phase.isExplicitSelection || phase.autoPushSlugs.length > 0;
|
|
2147
|
+
if (!shouldUploadPhase) {
|
|
2148
|
+
// eslint-disable-next-line no-continue
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
let appsCreated = 0;
|
|
2152
|
+
let appsUpdated = 0;
|
|
2153
|
+
let appsUnchanged = 0;
|
|
2154
|
+
let appsFailed = 0;
|
|
2155
|
+
let appsSkipped = 0;
|
|
2156
|
+
let eeAppRoutesUnavailable = false;
|
|
2157
|
+
const changesBeforeApps = { ...changes };
|
|
2158
|
+
if (!uploadPermissions.dataApps) {
|
|
2159
|
+
if (phase.isChartTypes) {
|
|
2160
|
+
counts.chartTypesNum = 0;
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
counts.appsNum = 0;
|
|
2164
|
+
}
|
|
2165
|
+
output.startItem(phase.label);
|
|
2166
|
+
globalState_1.default.log(styles.warning(`Skipping ${phase.changesPrefix}: create:DataApp or manage:DataApp permission is required for this project (the create:DataApp@preview and manage:DataApp@preview scopes only cover preview projects you created). Dashboard tiles will resolve only if their apps already exist in this project.`));
|
|
2167
|
+
output.completeItem('permission denied', 'warning');
|
|
2168
|
+
// eslint-disable-next-line no-continue
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
output.startItem(phase.label);
|
|
2172
|
+
// Explicit refs filter by slug/appUuid; include-all uploads every
|
|
2173
|
+
// folder. A pure auto-push run applies no filter — gated per
|
|
2174
|
+
// folder below.
|
|
2175
|
+
let uploadFilter = phase.isExplicitSelection
|
|
2176
|
+
? (0, appsDownload_1.getDataAppUploadFilter)(phase.explicitRefs, phase.includeAllFolders)
|
|
1947
2177
|
: null;
|
|
1948
2178
|
// uuid/URL --apps refs resolve to slugs against the target
|
|
1949
2179
|
// project's listing so they can match slug-identity local folders.
|
|
@@ -1952,7 +2182,9 @@ const uploadHandler = async (options) => {
|
|
|
1952
2182
|
try {
|
|
1953
2183
|
const projectApps = await (0, apiClient_1.lightdashApi)({
|
|
1954
2184
|
method: 'GET',
|
|
1955
|
-
|
|
2185
|
+
// Each kind resolves against its own listing — the
|
|
2186
|
+
// apps listing never includes chart types.
|
|
2187
|
+
url: `/api/v1/ee/projects/${projectId}/apps${phase.isChartTypes ? '/chart-types' : ''}`,
|
|
1956
2188
|
body: undefined,
|
|
1957
2189
|
});
|
|
1958
2190
|
uploadFilter = (0, appsDownload_1.resolveUploadFilterUuids)(uploadFilter, projectApps);
|
|
@@ -1962,9 +2194,10 @@ const uploadHandler = async (options) => {
|
|
|
1962
2194
|
}
|
|
1963
2195
|
}
|
|
1964
2196
|
// The server applies the space on creates only; existing apps
|
|
1965
|
-
// keep their space.
|
|
2197
|
+
// keep their space. Chart types are spaceless, so --app-space
|
|
2198
|
+
// never applies to them.
|
|
1966
2199
|
let appSpaceUuid;
|
|
1967
|
-
if (options.appSpace !== undefined) {
|
|
2200
|
+
if (phase.useAppSpace && options.appSpace !== undefined) {
|
|
1968
2201
|
if ((0, uuid_1.validate)(options.appSpace)) {
|
|
1969
2202
|
appSpaceUuid = options.appSpace;
|
|
1970
2203
|
}
|
|
@@ -1977,8 +2210,13 @@ const uploadHandler = async (options) => {
|
|
|
1977
2210
|
appSpaceUuid = (0, appsDownload_1.resolveAppSpaceUuid)(options.appSpace, spaces);
|
|
1978
2211
|
}
|
|
1979
2212
|
}
|
|
2213
|
+
else if (phase.isChartTypes &&
|
|
2214
|
+
options.appSpace !== undefined &&
|
|
2215
|
+
!appsPhaseActive) {
|
|
2216
|
+
globalState_1.default.log(styles.warning('--app-space does not apply to custom chart types — they are project-global and spaceless.'));
|
|
2217
|
+
}
|
|
1980
2218
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
1981
|
-
const appsDir = path.join(baseDir,
|
|
2219
|
+
const appsDir = path.join(baseDir, phase.dirName);
|
|
1982
2220
|
let appFolderEntries;
|
|
1983
2221
|
try {
|
|
1984
2222
|
appFolderEntries = await fs_1.promises.readdir(appsDir, {
|
|
@@ -1995,9 +2233,14 @@ const uploadHandler = async (options) => {
|
|
|
1995
2233
|
}
|
|
1996
2234
|
const subDirs = appFolderEntries.filter((e) => e.isDirectory());
|
|
1997
2235
|
if (subDirs.length === 0) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2236
|
+
if (phase.isChartTypes) {
|
|
2237
|
+
globalState_1.default.log(styles.warning(`No chart type folders found in ${appsDir}. Run 'lightdash download --include-chart-types' first.`));
|
|
2238
|
+
}
|
|
2239
|
+
else {
|
|
2240
|
+
globalState_1.default.log(styles.warning(phase.isExplicitSelection
|
|
2241
|
+
? `No app folders found in ${appsDir}. Run 'lightdash download --include-apps' first.`
|
|
2242
|
+
: `No app folders found in ${appsDir} for the dashboard(s) being uploaded. Re-run 'lightdash download' to fetch their apps.`));
|
|
2243
|
+
}
|
|
2001
2244
|
}
|
|
2002
2245
|
const matchedRefs = new Set();
|
|
2003
2246
|
const buildWaitState = (0, uploadRetry_1.createBuildLimitWaitState)();
|
|
@@ -2008,12 +2251,12 @@ const uploadHandler = async (options) => {
|
|
|
2008
2251
|
const code = await (0, appCodeFiles_1.readBundleFromDir)(folderPath);
|
|
2009
2252
|
if (!(0, appsDownload_1.uploadFilterMatches)(uploadFilter, code.manifest)) {
|
|
2010
2253
|
const isAutoPushCandidate = code.manifest.slug !== undefined &&
|
|
2011
|
-
|
|
2254
|
+
phase.autoPushSlugs.includes(code.manifest.slug);
|
|
2012
2255
|
if (isAutoPushCandidate) {
|
|
2013
2256
|
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.`));
|
|
2014
2257
|
}
|
|
2015
2258
|
else {
|
|
2016
|
-
globalState_1.default.debug(`Skipping
|
|
2259
|
+
globalState_1.default.debug(`Skipping ${phase.noun} folder "${subDir.name}" (not in filter)`);
|
|
2017
2260
|
}
|
|
2018
2261
|
// eslint-disable-next-line no-continue
|
|
2019
2262
|
continue;
|
|
@@ -2021,9 +2264,22 @@ const uploadHandler = async (options) => {
|
|
|
2021
2264
|
if (uploadFilter) {
|
|
2022
2265
|
(0, appsDownload_1.matchedUploadRefs)(uploadFilter, code.manifest).forEach((ref) => matchedRefs.add(ref));
|
|
2023
2266
|
}
|
|
2024
|
-
|
|
2267
|
+
// Folder-kind guard: the manifest governs what the server
|
|
2268
|
+
// creates, so a bundle filed under the wrong folder would
|
|
2269
|
+
// silently upload as the wrong kind. Skip and say where
|
|
2270
|
+
// it belongs instead.
|
|
2271
|
+
const isVizBundle = code.manifest.template === common_1.DATA_APP_VIZ_TEMPLATE;
|
|
2272
|
+
if (isVizBundle !== phase.isChartTypes) {
|
|
2273
|
+
globalState_1.default.log(styles.warning(isVizBundle
|
|
2274
|
+
? `Skipping "${subDir.name}": it is a custom chart type — move the folder to chart-types/ and upload with --chart-types or --include-chart-types.`
|
|
2275
|
+
: `Skipping "${subDir.name}": it is a data app — move the folder to apps/ and upload with --apps or --include-apps.`));
|
|
2276
|
+
appsSkipped += 1;
|
|
2277
|
+
// eslint-disable-next-line no-continue
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (!phase.isExplicitSelection) {
|
|
2025
2281
|
const isAutoPushCandidate = code.manifest.slug !== undefined &&
|
|
2026
|
-
|
|
2282
|
+
phase.autoPushSlugs.includes(code.manifest.slug);
|
|
2027
2283
|
if (!isAutoPushCandidate) {
|
|
2028
2284
|
// eslint-disable-next-line no-continue
|
|
2029
2285
|
continue;
|
|
@@ -2156,7 +2412,9 @@ const uploadHandler = async (options) => {
|
|
|
2156
2412
|
globalState_1.default.log(styles.warning((0, appsDownload_1.preSlugServerHint)(subDir.name)));
|
|
2157
2413
|
}
|
|
2158
2414
|
if (action === 'create') {
|
|
2159
|
-
globalState_1.default.log(
|
|
2415
|
+
globalState_1.default.log(phase.isChartTypes
|
|
2416
|
+
? `New chart type: ${config.context.serverUrl}/projects/${projectId}/chart-types/${appUuid}`
|
|
2417
|
+
: `New app: ${config.context.serverUrl}/projects/${projectId}/apps/${appUuid}`);
|
|
2160
2418
|
}
|
|
2161
2419
|
}
|
|
2162
2420
|
catch (appErr) {
|
|
@@ -2165,7 +2423,7 @@ const uploadHandler = async (options) => {
|
|
|
2165
2423
|
: undefined;
|
|
2166
2424
|
// Auto-push is flag-free, so a server without the EE app
|
|
2167
2425
|
// routes must not fail an upload the user never asked for.
|
|
2168
|
-
if (!
|
|
2426
|
+
if (!phase.isExplicitSelection && status === 404) {
|
|
2169
2427
|
eeAppRoutesUnavailable = true;
|
|
2170
2428
|
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.`));
|
|
2171
2429
|
break;
|
|
@@ -2190,21 +2448,26 @@ const uploadHandler = async (options) => {
|
|
|
2190
2448
|
}
|
|
2191
2449
|
}
|
|
2192
2450
|
if (appsCreated > 0)
|
|
2193
|
-
changes[
|
|
2451
|
+
changes[`${phase.changesPrefix} created`] = appsCreated;
|
|
2194
2452
|
if (appsUpdated > 0)
|
|
2195
|
-
changes[
|
|
2453
|
+
changes[`${phase.changesPrefix} updated`] = appsUpdated;
|
|
2196
2454
|
if (appsUnchanged > 0)
|
|
2197
|
-
changes[
|
|
2455
|
+
changes[`${phase.changesPrefix} unchanged`] = appsUnchanged;
|
|
2198
2456
|
if (appsFailed > 0)
|
|
2199
|
-
changes[
|
|
2457
|
+
changes[`${phase.changesPrefix} failed`] = appsFailed;
|
|
2200
2458
|
if (appsSkipped > 0)
|
|
2201
|
-
changes[
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2459
|
+
changes[`${phase.changesPrefix} skipped`] = appsSkipped;
|
|
2460
|
+
const phaseBundleTotal = appsCreated +
|
|
2461
|
+
appsUpdated +
|
|
2462
|
+
appsUnchanged +
|
|
2463
|
+
appsFailed +
|
|
2464
|
+
appsSkipped;
|
|
2465
|
+
if (phase.isChartTypes) {
|
|
2466
|
+
counts.chartTypesNum = phaseBundleTotal;
|
|
2467
|
+
}
|
|
2468
|
+
else {
|
|
2469
|
+
counts.appsNum = phaseBundleTotal;
|
|
2470
|
+
}
|
|
2208
2471
|
const appSummary = summarizeUploadChanges(changesBeforeApps, changes);
|
|
2209
2472
|
if (eeAppRoutesUnavailable) {
|
|
2210
2473
|
output.completeItem('not available on this server', 'warning');
|
|
@@ -2216,7 +2479,7 @@ const uploadHandler = async (options) => {
|
|
|
2216
2479
|
// App uploads are fire-and-forget per folder, so failures are
|
|
2217
2480
|
// logged and tallied rather than thrown — but the process must
|
|
2218
2481
|
// still exit non-zero or CI pipelines read the run as green.
|
|
2219
|
-
globalState_1.default.log(styles.error(`${appsFailed}
|
|
2482
|
+
globalState_1.default.log(styles.error(`${appsFailed} ${phase.noun} upload(s) failed — see errors above.`));
|
|
2220
2483
|
process.exitCode = 1;
|
|
2221
2484
|
}
|
|
2222
2485
|
}
|
package/dist/index.js
CHANGED
|
@@ -389,6 +389,10 @@ const downloadCommand = commander_1.program
|
|
|
389
389
|
.option('--include-apps', "Include all of the project's data apps (enterprise), capped at --apps-limit (default: 50)", false)
|
|
390
390
|
.option('--apps-limit <number>', 'Maximum number of data apps downloaded by --include-apps or --include-all (default: 50)', undefined)
|
|
391
391
|
.option('--apps-only', "Download only data apps (implies --skip-charts --skip-dashboards --skip-spaces). Bare --apps-only downloads all the project's apps; pass --apps <appReferences...> to select.", false)
|
|
392
|
+
.option('--chart-types <chartTypeReferences...>', 'Download only the specified custom chart types, by slug, URL, or UUID (enterprise).')
|
|
393
|
+
.option('--include-chart-types', "Include all of the project's custom chart types (enterprise), capped at --chart-types-limit (default: 50)", false)
|
|
394
|
+
.option('--chart-types-limit <number>', 'Maximum number of custom chart types downloaded by --include-chart-types or --include-all (default: 50)', undefined)
|
|
395
|
+
.option('--chart-types-only', "Download only custom chart types (implies --skip-charts --skip-dashboards --skip-spaces). Bare --chart-types-only downloads all the project's chart types; pass --chart-types <chartTypeReferences...> to select.", false)
|
|
392
396
|
.option('--organization', 'download all organization-scoped resources, including Data App themes, without selecting a project', false);
|
|
393
397
|
downloadCommand.action(withOrganizationMode(download_1.downloadHandler));
|
|
394
398
|
const uploadCommand = commander_1.program
|
|
@@ -425,6 +429,9 @@ const uploadCommand = commander_1.program
|
|
|
425
429
|
.option('--apps <appReferences...>', 'Upload only the specified data apps, by slug (the app folder name), app URL, or UUID (enterprise). URL and UUID refs are resolved against the target project.')
|
|
426
430
|
.option('--include-apps', 'Upload all app folders on disk (enterprise).', false)
|
|
427
431
|
.option('--apps-only', 'Upload only data apps, skipping charts, dashboards, and space reconciliation. Bare --apps-only uploads every app folder; pass --apps <appReferences...> to select.', false)
|
|
432
|
+
.option('--chart-types <chartTypeReferences...>', 'Upload only the specified custom chart types, by slug (the chart type folder name), URL, or UUID (enterprise). URL and UUID refs are resolved against the target project.')
|
|
433
|
+
.option('--include-chart-types', 'Upload all chart type folders on disk (enterprise).', false)
|
|
434
|
+
.option('--chart-types-only', 'Upload only custom chart types, skipping charts, dashboards, and space reconciliation. Bare --chart-types-only uploads every chart type folder; pass --chart-types <chartTypeReferences...> to select.', false)
|
|
428
435
|
.option('--allow-custom-dependencies', 'Approve uploading apps that declare custom dependencies without prompting. Required for non-interactive runs of such uploads.', false)
|
|
429
436
|
.option('--app-space <spaceRef>', 'Space (slug or UUID) for data apps this upload creates. Existing apps keep their space.')
|
|
430
437
|
.option('--create-new', 'Always create a new app from the uploaded code instead of updating the app referenced by lightdash-app.yml. The new app gets a fresh slug.', false)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.262.0",
|
|
4
4
|
"description": "Lightdash CLI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"unique-names-generator": "4.7.1",
|
|
46
46
|
"uuid": "11.1.1",
|
|
47
47
|
"yaml": "2.8.3",
|
|
48
|
-
"@lightdash/common": "1.
|
|
49
|
-
"@lightdash/warehouses": "1.
|
|
48
|
+
"@lightdash/common": "1.262.0",
|
|
49
|
+
"@lightdash/warehouses": "1.262.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/inquirer": "8.2.4",
|