@lightdash/cli 1.53.0 → 1.55.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 +6 -0
- package/dist/handlers/apps/appCodeFiles.d.ts.map +1 -1
- package/dist/handlers/apps/appCodeFiles.js +42 -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/download.d.ts +16 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +349 -203
- 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
|
@@ -12,6 +12,7 @@ export declare const buildImportBody: (code: DataAppCode, targetProjectUuid: str
|
|
|
12
12
|
app?: string;
|
|
13
13
|
space?: string;
|
|
14
14
|
createNew?: boolean;
|
|
15
|
+
force?: boolean;
|
|
15
16
|
}) => ImportAppCodeRequestBody;
|
|
16
17
|
/**
|
|
17
18
|
* The persistent slug from a slug-aware server is the folder name (stable
|
|
@@ -53,6 +54,11 @@ export declare const applySdkMirrorToTemplateDeps: (templateDependencies: Record
|
|
|
53
54
|
* deps (payload stays identical to today's format).
|
|
54
55
|
*/
|
|
55
56
|
export declare const attachDependenciesToCode: (code: DataAppCode, customDeps: Record<string, string>, deps: DataAppDependencies) => DataAppCode;
|
|
57
|
+
/**
|
|
58
|
+
* Mirrors the chart/dashboard rule: changed when a file that actually gets
|
|
59
|
+
* uploaded has drifted more than 30s from the manifest's downloadedAt.
|
|
60
|
+
*/
|
|
61
|
+
export declare const appFolderNeedsUpdating: (dir: string, manifest: DataAppManifest) => Promise<boolean>;
|
|
56
62
|
export declare const readManifestFromDir: (dir: string) => Promise<DataAppManifest>;
|
|
57
63
|
export declare const readBundleFromDir: (dir: string) => Promise<DataAppCode>;
|
|
58
64
|
//# sourceMappingURL=appCodeFiles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;
|
|
1
|
+
{"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;IACF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB,KACF,wBAqBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,aACnB,eAAe,gBACX,GAAG,CAAC,MAAM,CAAC,KAC1B,MAgBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,QACL,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC3B,MAAM,KACZ,OAAO,CAAC,oBAAoB,GAAG,IAAI,CA6BrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,wBACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,MAAM,EAOH,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,yBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,eAC/B,MAAM,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAC3B,WAAW,cACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,mBAAmB,KAC1B,WAC4E,CAAC;AAuBhF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,YACD,eAAe,KAC1B,OAAO,CAAC,OAAO,CAqBjB,CAAC;AAEF,eAAO,MAAM,mBAAmB,QACvB,MAAM,KACZ,OAAO,CAAC,eAAe,CAYzB,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAe,MAAM,KAAG,OAAO,CAAC,WAAW,CAYxE,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.readBundleFromDir = exports.readManifestFromDir = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.resolveAppFolderName = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
|
|
3
|
+
exports.readBundleFromDir = exports.readManifestFromDir = exports.appFolderNeedsUpdating = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.resolveAppFolderName = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const common_1 = require("@lightdash/common");
|
|
6
6
|
const fs_1 = require("fs");
|
|
@@ -105,6 +105,7 @@ const buildImportBody = (code, targetProjectUuid, opts) => {
|
|
|
105
105
|
targetAppUuid,
|
|
106
106
|
spaceUuid: opts.space,
|
|
107
107
|
...(opts.createNew ? { createNew: true } : {}),
|
|
108
|
+
...(opts.force ? { force: true } : {}),
|
|
108
109
|
};
|
|
109
110
|
};
|
|
110
111
|
exports.buildImportBody = buildImportBody;
|
|
@@ -209,6 +210,46 @@ exports.applySdkMirrorToTemplateDeps = applySdkMirrorToTemplateDeps;
|
|
|
209
210
|
*/
|
|
210
211
|
const attachDependenciesToCode = (code, customDeps, deps) => Object.keys(customDeps).length > 0 ? { ...code, dependencies: deps } : code;
|
|
211
212
|
exports.attachDependenciesToCode = attachDependenciesToCode;
|
|
213
|
+
const CHANGE_TOLERANCE_MS = 30_000;
|
|
214
|
+
const collectMtimes = async (dir) => {
|
|
215
|
+
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
|
|
216
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
217
|
+
const entryPath = path.join(dir, entry.name);
|
|
218
|
+
if (entry.isDirectory())
|
|
219
|
+
return collectMtimes(entryPath);
|
|
220
|
+
const stats = await fs_1.promises.stat(entryPath);
|
|
221
|
+
return [stats.mtime.getTime()];
|
|
222
|
+
}));
|
|
223
|
+
return nested.flat();
|
|
224
|
+
};
|
|
225
|
+
const statMtime = async (file) => fs_1.promises
|
|
226
|
+
.stat(file)
|
|
227
|
+
.then((stats) => [stats.mtime.getTime()])
|
|
228
|
+
.catch(() => []);
|
|
229
|
+
/**
|
|
230
|
+
* Mirrors the chart/dashboard rule: changed when a file that actually gets
|
|
231
|
+
* uploaded has drifted more than 30s from the manifest's downloadedAt.
|
|
232
|
+
*/
|
|
233
|
+
const appFolderNeedsUpdating = async (dir, manifest) => {
|
|
234
|
+
if (!manifest.downloadedAt)
|
|
235
|
+
return true;
|
|
236
|
+
const downloadedAt = new Date(manifest.downloadedAt).getTime();
|
|
237
|
+
if (Number.isNaN(downloadedAt))
|
|
238
|
+
return true;
|
|
239
|
+
const srcDir = path.join(dir, 'src');
|
|
240
|
+
const srcExists = await fs_1.promises
|
|
241
|
+
.stat(srcDir)
|
|
242
|
+
.then((s) => s.isDirectory())
|
|
243
|
+
.catch(() => false);
|
|
244
|
+
const mtimes = [
|
|
245
|
+
...(srcExists ? await collectMtimes(srcDir) : []),
|
|
246
|
+
...(await statMtime(path.join(dir, 'package.json'))),
|
|
247
|
+
...(await statMtime(path.join(dir, 'pnpm-lock.yaml'))),
|
|
248
|
+
...(await statMtime(path.join(dir, MANIFEST_FILENAME))),
|
|
249
|
+
];
|
|
250
|
+
return mtimes.some((mtime) => Math.abs(mtime - downloadedAt) > CHANGE_TOLERANCE_MS);
|
|
251
|
+
};
|
|
252
|
+
exports.appFolderNeedsUpdating = appFolderNeedsUpdating;
|
|
212
253
|
const readManifestFromDir = async (dir) => {
|
|
213
254
|
const manifestRaw = await fs_1.promises.readFile(path.join(dir, MANIFEST_FILENAME), 'utf-8');
|
|
214
255
|
try {
|
|
@@ -349,3 +349,63 @@ describe('applySdkMirrorToTemplateDeps', () => {
|
|
|
349
349
|
expect((0, appCodeFiles_1.applySdkMirrorToTemplateDeps)(template, 'not-json')).toEqual(template);
|
|
350
350
|
});
|
|
351
351
|
});
|
|
352
|
+
// ─── appFolderNeedsUpdating ────────────────────────────────────────────────────
|
|
353
|
+
describe('appFolderNeedsUpdating', () => {
|
|
354
|
+
const DOWNLOADED_AT = '2026-07-30T12:00:00.000Z';
|
|
355
|
+
const manifest = { downloadedAt: DOWNLOADED_AT };
|
|
356
|
+
const buildFolder = () => {
|
|
357
|
+
const dir = (0, fs_1.mkdtempSync)(path.join(os.tmpdir(), 'ld-app-folder-'));
|
|
358
|
+
(0, fs_1.mkdirSync)(path.join(dir, 'src'), { recursive: true });
|
|
359
|
+
(0, fs_1.writeFileSync)(path.join(dir, 'src', 'App.tsx'), 'x');
|
|
360
|
+
(0, fs_1.writeFileSync)(path.join(dir, 'lightdash-app.yml'), 'slug: x');
|
|
361
|
+
(0, fs_1.mkdirSync)(path.join(dir, '.lightdash', 'context'), {
|
|
362
|
+
recursive: true,
|
|
363
|
+
});
|
|
364
|
+
(0, fs_1.writeFileSync)(path.join(dir, '.lightdash', 'context', 'semantic-layer.yml'), 'models: []');
|
|
365
|
+
const at = new Date(DOWNLOADED_AT);
|
|
366
|
+
[
|
|
367
|
+
path.join(dir, 'src', 'App.tsx'),
|
|
368
|
+
path.join(dir, 'lightdash-app.yml'),
|
|
369
|
+
path.join(dir, '.lightdash', 'context', 'semantic-layer.yml'),
|
|
370
|
+
].forEach((file) => (0, fs_1.utimesSync)(file, at, at));
|
|
371
|
+
return dir;
|
|
372
|
+
};
|
|
373
|
+
it('is false for a freshly downloaded folder', async () => {
|
|
374
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(buildFolder(), manifest)).resolves.toBe(false);
|
|
375
|
+
});
|
|
376
|
+
it('is true when a src file was edited', async () => {
|
|
377
|
+
const dir = buildFolder();
|
|
378
|
+
const later = new Date('2026-07-30T13:00:00.000Z');
|
|
379
|
+
(0, fs_1.utimesSync)(path.join(dir, 'src', 'App.tsx'), later, later);
|
|
380
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(dir, manifest)).resolves.toBe(true);
|
|
381
|
+
});
|
|
382
|
+
it('is true when the manifest was edited', async () => {
|
|
383
|
+
const dir = buildFolder();
|
|
384
|
+
const later = new Date('2026-07-30T13:00:00.000Z');
|
|
385
|
+
(0, fs_1.utimesSync)(path.join(dir, 'lightdash-app.yml'), later, later);
|
|
386
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(dir, manifest)).resolves.toBe(true);
|
|
387
|
+
});
|
|
388
|
+
it('ignores regenerated context files', async () => {
|
|
389
|
+
const dir = buildFolder();
|
|
390
|
+
const later = new Date('2026-07-30T13:00:00.000Z');
|
|
391
|
+
(0, fs_1.utimesSync)(path.join(dir, '.lightdash', 'context', 'semantic-layer.yml'), later, later);
|
|
392
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(dir, manifest)).resolves.toBe(false);
|
|
393
|
+
});
|
|
394
|
+
it('is true when downloadedAt is missing', async () => {
|
|
395
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(buildFolder(), {})).resolves.toBe(true);
|
|
396
|
+
});
|
|
397
|
+
it('is true when downloadedAt is unparseable', async () => {
|
|
398
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(buildFolder(), {
|
|
399
|
+
downloadedAt: 'not-a-date',
|
|
400
|
+
})).resolves.toBe(true);
|
|
401
|
+
});
|
|
402
|
+
it('is true when a nested src file was edited', async () => {
|
|
403
|
+
const dir = buildFolder();
|
|
404
|
+
(0, fs_1.mkdirSync)(path.join(dir, 'src', 'components'), { recursive: true });
|
|
405
|
+
const nested = path.join(dir, 'src', 'components', 'Chart.tsx');
|
|
406
|
+
(0, fs_1.writeFileSync)(nested, 'x');
|
|
407
|
+
const later = new Date('2026-07-30T13:00:00.000Z');
|
|
408
|
+
(0, fs_1.utimesSync)(nested, later, later);
|
|
409
|
+
await expect((0, appCodeFiles_1.appFolderNeedsUpdating)(dir, manifest)).resolves.toBe(true);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -61,6 +61,16 @@ export declare const capListedApps: (listedUuids: string[], limit: number) => {
|
|
|
61
61
|
appUuids: string[];
|
|
62
62
|
truncatedCount: number;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Data apps a dashboard needs that aren't already covered by an explicit
|
|
66
|
+
* --apps ref, or by a (possibly --apps-limit-truncated) --include-apps listing.
|
|
67
|
+
*/
|
|
68
|
+
export declare const computeLinkedAppSlugs: (args: {
|
|
69
|
+
appSlugs: string[];
|
|
70
|
+
explicitRefs: Set<string>;
|
|
71
|
+
includeApps: boolean;
|
|
72
|
+
cappedAppSlugs: Set<string>;
|
|
73
|
+
}) => string[];
|
|
64
74
|
/**
|
|
65
75
|
* Pre-context servers return a download payload without `context`.
|
|
66
76
|
*/
|
|
@@ -73,6 +83,25 @@ export declare const preSlugUploadHint: (args: {
|
|
|
73
83
|
folder: string;
|
|
74
84
|
slug: string | undefined;
|
|
75
85
|
}) => string;
|
|
86
|
+
export type AppPresence = {
|
|
87
|
+
kind: 'known';
|
|
88
|
+
slugs: Set<string>;
|
|
89
|
+
} | {
|
|
90
|
+
kind: 'unknown';
|
|
91
|
+
targetProjectUuid: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Auto-pushed apps normally upload only when the folder changed, because every
|
|
95
|
+
* upload triggers a sandbox rebuild. The exception is an app the target project
|
|
96
|
+
* does not have yet — without it a dashboard moved to a new project or instance
|
|
97
|
+
* lands with its tile skipped.
|
|
98
|
+
*/
|
|
99
|
+
export declare const shouldAutoPushApp: (args: {
|
|
100
|
+
manifest: Pick<DataAppManifest, 'slug' | 'projectUuid'>;
|
|
101
|
+
presence: AppPresence;
|
|
102
|
+
folderChanged: boolean;
|
|
103
|
+
force: boolean;
|
|
104
|
+
}) => boolean;
|
|
76
105
|
export type AppDownloadFailure = {
|
|
77
106
|
appRef: string;
|
|
78
107
|
message: string;
|
|
@@ -106,4 +135,24 @@ export declare const appsDownloadSummary: (successCount: number, total: number,
|
|
|
106
135
|
message: string;
|
|
107
136
|
failureLines: string[];
|
|
108
137
|
};
|
|
138
|
+
export type AppsDownloadOutcome = {
|
|
139
|
+
successCount: number;
|
|
140
|
+
skippedNotBuiltCount: number;
|
|
141
|
+
failures: AppDownloadFailure[];
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Downloads each app to its own folder under appsDir, classifying "no built
|
|
145
|
+
* version" 404s as skips rather than failures. Everything the loop needs
|
|
146
|
+
* (fetching, versioning, progress reporting) is injected so this stays free
|
|
147
|
+
* of any dependency on the download handler.
|
|
148
|
+
*/
|
|
149
|
+
export declare const downloadAppsToDir: (args: {
|
|
150
|
+
appRefs: string[];
|
|
151
|
+
projectId: string;
|
|
152
|
+
appsDir: string;
|
|
153
|
+
takenFolders: Set<string>;
|
|
154
|
+
cliVersion: string;
|
|
155
|
+
fetchApp: (projectId: string, appRef: string) => Promise<DataAppCodeDownload>;
|
|
156
|
+
onProgress?: (processed: number, total: number) => void;
|
|
157
|
+
}) => Promise<AppsDownloadOutcome>;
|
|
109
158
|
//# sourceMappingURL=appsDownload.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.appsDownloadSummary = exports.shouldWarnAllSkipped = exports.computeUpsertedTotal = exports.classifyAppDownloadError = exports.preSlugUploadHint = exports.ensureDownloadedAppContext = exports.capListedApps = exports.selectAppsToDownload = exports.shouldFallBackToSpaceScopedListing = exports.resolveAppsLimit = exports.preSlugServerHint = exports.unmatchedUploadRefsWarning = exports.matchedUploadRefs = exports.uploadFilterMatches = exports.getDataAppUploadFilter = exports.getDataAppReference = exports.DEFAULT_APPS_LIMIT = void 0;
|
|
3
|
+
exports.downloadAppsToDir = exports.appsDownloadSummary = exports.shouldWarnAllSkipped = exports.computeUpsertedTotal = exports.classifyAppDownloadError = exports.shouldAutoPushApp = exports.preSlugUploadHint = exports.ensureDownloadedAppContext = exports.computeLinkedAppSlugs = exports.capListedApps = exports.selectAppsToDownload = exports.shouldFallBackToSpaceScopedListing = exports.resolveAppsLimit = exports.preSlugServerHint = exports.unmatchedUploadRefsWarning = exports.matchedUploadRefs = exports.uploadFilterMatches = exports.getDataAppUploadFilter = exports.getDataAppReference = exports.DEFAULT_APPS_LIMIT = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
4
5
|
const common_1 = require("@lightdash/common");
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
5
7
|
const uuid_1 = require("uuid");
|
|
8
|
+
const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
|
|
9
|
+
const styles = tslib_1.__importStar(require("../../styles"));
|
|
10
|
+
const appCodeFiles_1 = require("./appCodeFiles");
|
|
11
|
+
const scaffolding_1 = require("./scaffolding");
|
|
6
12
|
exports.DEFAULT_APPS_LIMIT = 50;
|
|
7
13
|
const getDataAppReference = (reference) => {
|
|
8
14
|
let url;
|
|
@@ -106,6 +112,16 @@ const capListedApps = (listedUuids, limit) => ({
|
|
|
106
112
|
truncatedCount: Math.max(0, listedUuids.length - limit),
|
|
107
113
|
});
|
|
108
114
|
exports.capListedApps = capListedApps;
|
|
115
|
+
/**
|
|
116
|
+
* Data apps a dashboard needs that aren't already covered by an explicit
|
|
117
|
+
* --apps ref, or by a (possibly --apps-limit-truncated) --include-apps listing.
|
|
118
|
+
*/
|
|
119
|
+
const computeLinkedAppSlugs = (args) => {
|
|
120
|
+
const { appSlugs, explicitRefs, includeApps, cappedAppSlugs } = args;
|
|
121
|
+
return appSlugs.filter((slug) => !explicitRefs.has(slug) &&
|
|
122
|
+
!(includeApps && cappedAppSlugs.has(slug)));
|
|
123
|
+
};
|
|
124
|
+
exports.computeLinkedAppSlugs = computeLinkedAppSlugs;
|
|
109
125
|
/**
|
|
110
126
|
* Pre-context servers return a download payload without `context`.
|
|
111
127
|
*/
|
|
@@ -124,6 +140,23 @@ const preSlugUploadHint = (args) => `${args.folder}/lightdash-app.yml predates s
|
|
|
124
140
|
? ` (or add \`slug: ${args.slug}\` to lightdash-app.yml)`
|
|
125
141
|
: ''}. Uploads keep working via uuid matching meanwhile.`;
|
|
126
142
|
exports.preSlugUploadHint = preSlugUploadHint;
|
|
143
|
+
/**
|
|
144
|
+
* Auto-pushed apps normally upload only when the folder changed, because every
|
|
145
|
+
* upload triggers a sandbox rebuild. The exception is an app the target project
|
|
146
|
+
* does not have yet — without it a dashboard moved to a new project or instance
|
|
147
|
+
* lands with its tile skipped.
|
|
148
|
+
*/
|
|
149
|
+
const shouldAutoPushApp = (args) => {
|
|
150
|
+
if (args.force || args.folderChanged)
|
|
151
|
+
return true;
|
|
152
|
+
if (args.manifest.slug === undefined)
|
|
153
|
+
return true;
|
|
154
|
+
if (args.presence.kind === 'known') {
|
|
155
|
+
return !args.presence.slugs.has(args.manifest.slug);
|
|
156
|
+
}
|
|
157
|
+
return args.manifest.projectUuid !== args.presence.targetProjectUuid;
|
|
158
|
+
};
|
|
159
|
+
exports.shouldAutoPushApp = shouldAutoPushApp;
|
|
127
160
|
/**
|
|
128
161
|
* A 404 saying the app has no ready version means it exists but never
|
|
129
162
|
* finished a successful build — nothing to download, so it's a skip, not a
|
|
@@ -185,3 +218,59 @@ const appsDownloadSummary = (successCount, total, failures, appsDir, skippedNotB
|
|
|
185
218
|
};
|
|
186
219
|
};
|
|
187
220
|
exports.appsDownloadSummary = appsDownloadSummary;
|
|
221
|
+
/**
|
|
222
|
+
* Downloads each app to its own folder under appsDir, classifying "no built
|
|
223
|
+
* version" 404s as skips rather than failures. Everything the loop needs
|
|
224
|
+
* (fetching, versioning, progress reporting) is injected so this stays free
|
|
225
|
+
* of any dependency on the download handler.
|
|
226
|
+
*/
|
|
227
|
+
const downloadAppsToDir = async (args) => {
|
|
228
|
+
const { appRefs, projectId, appsDir, takenFolders, cliVersion, fetchApp, onProgress, } = args;
|
|
229
|
+
let successCount = 0;
|
|
230
|
+
let skippedNotBuiltCount = 0;
|
|
231
|
+
const failures = [];
|
|
232
|
+
for (const appRef of appRefs) {
|
|
233
|
+
try {
|
|
234
|
+
const code = (0, exports.ensureDownloadedAppContext)(appRef,
|
|
235
|
+
// eslint-disable-next-line no-await-in-loop
|
|
236
|
+
await fetchApp(projectId, appRef));
|
|
237
|
+
const folder = (0, appCodeFiles_1.resolveAppFolderName)(code.manifest, takenFolders);
|
|
238
|
+
takenFolders.add(folder);
|
|
239
|
+
const appDir = path.join(appsDir, folder);
|
|
240
|
+
const manifest = {
|
|
241
|
+
...code.manifest,
|
|
242
|
+
scaffoldingVersion: cliVersion,
|
|
243
|
+
};
|
|
244
|
+
// eslint-disable-next-line no-await-in-loop
|
|
245
|
+
await (0, appCodeFiles_1.writeBundleToDir)(appDir, { ...code, manifest });
|
|
246
|
+
// eslint-disable-next-line no-await-in-loop
|
|
247
|
+
await (0, appCodeFiles_1.writeFilesToDir)(appDir, (0, scaffolding_1.buildStaticAuthoringFiles)({
|
|
248
|
+
appName: code.manifest.name,
|
|
249
|
+
sdkVersion: cliVersion,
|
|
250
|
+
}));
|
|
251
|
+
// Server-provided deps override the scaffold's
|
|
252
|
+
// template package.json so re-uploads round-trip.
|
|
253
|
+
if (code.dependencies) {
|
|
254
|
+
// eslint-disable-next-line no-await-in-loop
|
|
255
|
+
await (0, appCodeFiles_1.writeDependenciesToDir)(appDir, code.dependencies);
|
|
256
|
+
}
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
await (0, appCodeFiles_1.writeContextToDir)(appDir, code.context);
|
|
259
|
+
successCount += 1;
|
|
260
|
+
}
|
|
261
|
+
catch (appErr) {
|
|
262
|
+
const outcome = (0, exports.classifyAppDownloadError)(appErr);
|
|
263
|
+
if (outcome.kind === 'skip-not-built') {
|
|
264
|
+
skippedNotBuiltCount += 1;
|
|
265
|
+
globalState_1.default.debug(`> Skipped app ${appRef}: no built version to download`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
failures.push({ appRef, message: outcome.message });
|
|
269
|
+
globalState_1.default.log(styles.error(`Failed to download app ${appRef}: ${outcome.message}`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
onProgress?.(successCount + skippedNotBuiltCount + failures.length, appRefs.length);
|
|
273
|
+
}
|
|
274
|
+
return { successCount, skippedNotBuiltCount, failures };
|
|
275
|
+
};
|
|
276
|
+
exports.downloadAppsToDir = downloadAppsToDir;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
3
4
|
const common_1 = require("@lightdash/common");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const os = tslib_1.__importStar(require("os"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
4
8
|
const appsDownload_1 = require("./appsDownload");
|
|
5
9
|
describe('getDataAppReference', () => {
|
|
6
10
|
const appUuid = 'd3afc44c-6f0f-4d9f-a267-fb739efa31dd';
|
|
@@ -158,6 +162,59 @@ describe('capListedApps', () => {
|
|
|
158
162
|
});
|
|
159
163
|
});
|
|
160
164
|
});
|
|
165
|
+
describe('computeLinkedAppSlugs', () => {
|
|
166
|
+
it('returns dashboard-referenced slugs unchanged when apps are not opted in at all', () => {
|
|
167
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
168
|
+
appSlugs: ['revenue-explorer', 'sales-dash'],
|
|
169
|
+
explicitRefs: new Set(),
|
|
170
|
+
includeApps: false,
|
|
171
|
+
cappedAppSlugs: new Set(),
|
|
172
|
+
})).toEqual(['revenue-explorer', 'sales-dash']);
|
|
173
|
+
});
|
|
174
|
+
it('excludes slugs already covered by an explicit --apps ref', () => {
|
|
175
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
176
|
+
appSlugs: ['revenue-explorer', 'sales-dash'],
|
|
177
|
+
explicitRefs: new Set(['revenue-explorer']),
|
|
178
|
+
includeApps: false,
|
|
179
|
+
cappedAppSlugs: new Set(),
|
|
180
|
+
})).toEqual(['sales-dash']);
|
|
181
|
+
});
|
|
182
|
+
it('excludes everything already inside an untruncated --include-apps listing', () => {
|
|
183
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
184
|
+
appSlugs: ['revenue-explorer', 'sales-dash'],
|
|
185
|
+
explicitRefs: new Set(),
|
|
186
|
+
includeApps: true,
|
|
187
|
+
cappedAppSlugs: new Set(['revenue-explorer', 'sales-dash']),
|
|
188
|
+
})).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
// PROD-9089 regression: --include-apps caps its listing at --apps-limit,
|
|
191
|
+
// so a dashboard's app can fall outside the capped set. It must still be
|
|
192
|
+
// downloaded via the linked step rather than silently dropped.
|
|
193
|
+
it('keeps slugs that fell outside a truncated --include-apps listing', () => {
|
|
194
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
195
|
+
appSlugs: ['revenue-explorer', 'sales-dash'],
|
|
196
|
+
explicitRefs: new Set(),
|
|
197
|
+
includeApps: true,
|
|
198
|
+
cappedAppSlugs: new Set(['sales-dash']),
|
|
199
|
+
})).toEqual(['revenue-explorer']);
|
|
200
|
+
});
|
|
201
|
+
it('an explicit ref wins over include-apps capping (never double counted)', () => {
|
|
202
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
203
|
+
appSlugs: ['revenue-explorer'],
|
|
204
|
+
explicitRefs: new Set(['revenue-explorer']),
|
|
205
|
+
includeApps: true,
|
|
206
|
+
cappedAppSlugs: new Set(),
|
|
207
|
+
})).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
it('returns an empty array when no dashboards reference any app', () => {
|
|
210
|
+
expect((0, appsDownload_1.computeLinkedAppSlugs)({
|
|
211
|
+
appSlugs: [],
|
|
212
|
+
explicitRefs: new Set(['other-app']),
|
|
213
|
+
includeApps: true,
|
|
214
|
+
cappedAppSlugs: new Set(),
|
|
215
|
+
})).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
161
218
|
describe('resolveAppsLimit', () => {
|
|
162
219
|
it('defaults to DEFAULT_APPS_LIMIT when the flag is not passed', () => {
|
|
163
220
|
expect(appsDownload_1.DEFAULT_APPS_LIMIT).toBe(50);
|
|
@@ -400,3 +457,177 @@ describe('appsDownloadSummary', () => {
|
|
|
400
457
|
expect(summary.message).toContain('1 failed');
|
|
401
458
|
});
|
|
402
459
|
});
|
|
460
|
+
describe('downloadAppsToDir', () => {
|
|
461
|
+
const tmpDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'ld-apps-download-'));
|
|
462
|
+
const codeFor = (slug) => ({
|
|
463
|
+
manifest: {
|
|
464
|
+
codeVersion: 1,
|
|
465
|
+
projectUuid: 'project-uuid',
|
|
466
|
+
slug,
|
|
467
|
+
version: 1,
|
|
468
|
+
name: slug,
|
|
469
|
+
description: '',
|
|
470
|
+
template: null,
|
|
471
|
+
downloadedAt: '2026-07-30T00:00:00.000Z',
|
|
472
|
+
},
|
|
473
|
+
files: [
|
|
474
|
+
{
|
|
475
|
+
path: 'src/App.tsx',
|
|
476
|
+
contentBase64: Buffer.from('export default () => null;').toString('base64'),
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
context: {
|
|
480
|
+
semanticLayer: {
|
|
481
|
+
path: '.lightdash/context/semantic-layer.yml',
|
|
482
|
+
contentBase64: Buffer.from('models: []').toString('base64'),
|
|
483
|
+
},
|
|
484
|
+
parameters: null,
|
|
485
|
+
promptHistory: {
|
|
486
|
+
path: '.lightdash/context/prompt-history.md',
|
|
487
|
+
contentBase64: Buffer.from('# history').toString('base64'),
|
|
488
|
+
},
|
|
489
|
+
theme: { instructions: null, assets: [], skippedAssetCount: 0 },
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
it('writes one folder per app and counts successes', async () => {
|
|
493
|
+
const appsDir = tmpDir();
|
|
494
|
+
const outcome = await (0, appsDownload_1.downloadAppsToDir)({
|
|
495
|
+
appRefs: ['revenue-explorer'],
|
|
496
|
+
projectId: 'project-uuid',
|
|
497
|
+
appsDir,
|
|
498
|
+
takenFolders: new Set(),
|
|
499
|
+
cliVersion: '0.0.0-test',
|
|
500
|
+
fetchApp: async (_p, ref) => codeFor(ref),
|
|
501
|
+
});
|
|
502
|
+
expect(outcome).toEqual({
|
|
503
|
+
successCount: 1,
|
|
504
|
+
skippedNotBuiltCount: 0,
|
|
505
|
+
failures: [],
|
|
506
|
+
});
|
|
507
|
+
expect(fs.existsSync(path.join(appsDir, 'revenue-explorer', 'lightdash-app.yml'))).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
it('writes server-provided dependencies over the scaffold template package.json', async () => {
|
|
510
|
+
const appsDir = tmpDir();
|
|
511
|
+
const packageJson = JSON.stringify({
|
|
512
|
+
name: 'server-provided',
|
|
513
|
+
dependencies: {},
|
|
514
|
+
});
|
|
515
|
+
const lockfile = 'lockfileVersion: 6.0\nserver: provided\n';
|
|
516
|
+
const outcome = await (0, appsDownload_1.downloadAppsToDir)({
|
|
517
|
+
appRefs: ['deps-app'],
|
|
518
|
+
projectId: 'project-uuid',
|
|
519
|
+
appsDir,
|
|
520
|
+
takenFolders: new Set(),
|
|
521
|
+
cliVersion: '0.0.0-test',
|
|
522
|
+
fetchApp: async (_p, ref) => ({
|
|
523
|
+
...codeFor(ref),
|
|
524
|
+
dependencies: { packageJson, lockfile },
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
expect(outcome.successCount).toBe(1);
|
|
528
|
+
const writtenPackageJson = fs.readFileSync(path.join(appsDir, 'deps-app', 'package.json'), 'utf-8');
|
|
529
|
+
expect(writtenPackageJson).toBe(packageJson);
|
|
530
|
+
const writtenLockfile = fs.readFileSync(path.join(appsDir, 'deps-app', 'pnpm-lock.yaml'), 'utf-8');
|
|
531
|
+
expect(writtenLockfile).toBe(lockfile);
|
|
532
|
+
});
|
|
533
|
+
it('counts an app with no ready version as skipped, not failed', async () => {
|
|
534
|
+
const outcome = await (0, appsDownload_1.downloadAppsToDir)({
|
|
535
|
+
appRefs: ['half-built'],
|
|
536
|
+
projectId: 'project-uuid',
|
|
537
|
+
appsDir: tmpDir(),
|
|
538
|
+
takenFolders: new Set(),
|
|
539
|
+
cliVersion: '0.0.0-test',
|
|
540
|
+
fetchApp: async () => {
|
|
541
|
+
throw new common_1.LightdashError({
|
|
542
|
+
message: 'App has no ready version to download',
|
|
543
|
+
name: 'NotFoundError',
|
|
544
|
+
statusCode: 404,
|
|
545
|
+
data: {},
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
expect(outcome.skippedNotBuiltCount).toBe(1);
|
|
550
|
+
expect(outcome.failures).toEqual([]);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
describe('shouldAutoPushApp', () => {
|
|
554
|
+
const manifest = {
|
|
555
|
+
slug: 'revenue-explorer',
|
|
556
|
+
projectUuid: 'source-project',
|
|
557
|
+
};
|
|
558
|
+
const known = (...slugs) => ({
|
|
559
|
+
kind: 'known',
|
|
560
|
+
slugs: new Set(slugs),
|
|
561
|
+
});
|
|
562
|
+
it('pushes when the target project does not have the app', () => {
|
|
563
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
564
|
+
manifest,
|
|
565
|
+
presence: known(),
|
|
566
|
+
folderChanged: false,
|
|
567
|
+
force: false,
|
|
568
|
+
})).toBe(true);
|
|
569
|
+
});
|
|
570
|
+
it('pushes when the app exists and the folder changed', () => {
|
|
571
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
572
|
+
manifest,
|
|
573
|
+
presence: known('revenue-explorer'),
|
|
574
|
+
folderChanged: true,
|
|
575
|
+
force: false,
|
|
576
|
+
})).toBe(true);
|
|
577
|
+
});
|
|
578
|
+
it('skips when the app exists and nothing changed', () => {
|
|
579
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
580
|
+
manifest,
|
|
581
|
+
presence: known('revenue-explorer'),
|
|
582
|
+
folderChanged: false,
|
|
583
|
+
force: false,
|
|
584
|
+
})).toBe(false);
|
|
585
|
+
});
|
|
586
|
+
it('pushes an unchanged existing app under --force', () => {
|
|
587
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
588
|
+
manifest,
|
|
589
|
+
presence: known('revenue-explorer'),
|
|
590
|
+
folderChanged: false,
|
|
591
|
+
force: true,
|
|
592
|
+
})).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
it('falls back to the project comparison when presence is unknown', () => {
|
|
595
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
596
|
+
manifest,
|
|
597
|
+
presence: {
|
|
598
|
+
kind: 'unknown',
|
|
599
|
+
targetProjectUuid: 'other-project',
|
|
600
|
+
},
|
|
601
|
+
folderChanged: false,
|
|
602
|
+
force: false,
|
|
603
|
+
})).toBe(true);
|
|
604
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
605
|
+
manifest,
|
|
606
|
+
presence: {
|
|
607
|
+
kind: 'unknown',
|
|
608
|
+
targetProjectUuid: 'source-project',
|
|
609
|
+
},
|
|
610
|
+
folderChanged: false,
|
|
611
|
+
force: false,
|
|
612
|
+
})).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
it('pushes a slug-less manifest so a pre-slug bundle still lands', () => {
|
|
615
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
616
|
+
manifest: { slug: undefined, projectUuid: 'source-project' },
|
|
617
|
+
presence: known('revenue-explorer'),
|
|
618
|
+
folderChanged: false,
|
|
619
|
+
force: false,
|
|
620
|
+
})).toBe(true);
|
|
621
|
+
});
|
|
622
|
+
it('pushes a slug-less manifest against an older server with unknown presence', () => {
|
|
623
|
+
expect((0, appsDownload_1.shouldAutoPushApp)({
|
|
624
|
+
manifest: { slug: undefined, projectUuid: 'source-project' },
|
|
625
|
+
presence: {
|
|
626
|
+
kind: 'unknown',
|
|
627
|
+
targetProjectUuid: 'source-project',
|
|
628
|
+
},
|
|
629
|
+
folderChanged: false,
|
|
630
|
+
force: false,
|
|
631
|
+
})).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
@@ -59,7 +59,15 @@ type MetadataEntry = {
|
|
|
59
59
|
};
|
|
60
60
|
declare const sanitizeChartForDownload: (chart: ChartAsCode, stripPivotSeries: boolean) => ChartAsCode;
|
|
61
61
|
type DownloadContentType = 'charts' | 'dashboards' | 'sqlCharts';
|
|
62
|
-
|
|
62
|
+
declare const extractAppSlugsFromDashboards: (dashboards: DashboardAsCode[]) => string[];
|
|
63
|
+
export type DownloadContentResult = {
|
|
64
|
+
total: number;
|
|
65
|
+
chartSlugs: string[];
|
|
66
|
+
appSlugs: string[];
|
|
67
|
+
metadataEntries: MetadataEntry[];
|
|
68
|
+
spaces: SpaceAsCode[];
|
|
69
|
+
};
|
|
70
|
+
export declare const downloadContent: (ids: string[], type: DownloadContentType, projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean, skipSpaces?: boolean, stripPivotSeries?: boolean, rootSpaces?: boolean, onProgress?: (detail: string) => void) => Promise<DownloadContentResult>;
|
|
63
71
|
declare const upsertVirtualViews: (projectId: string, slugs: string[], changes: Record<string, number>, force: boolean, canUpload: boolean, customPath?: string) => Promise<Record<string, number>>;
|
|
64
72
|
export declare const getExternalConnectionSecretEnvVar: (slug: string) => string;
|
|
65
73
|
/**
|
|
@@ -84,16 +92,23 @@ declare const summarizeUploadChanges: (before: Record<string, number>, after: Re
|
|
|
84
92
|
declare const getDashboardChartSlugs: (dashboardSlugs: string[], customPath?: string, looseDashboards?: (DashboardAsCode & {
|
|
85
93
|
needsUpdating: boolean;
|
|
86
94
|
})[]) => Promise<string[]>;
|
|
95
|
+
declare const getDashboardAppSlugs: (dashboardSlugs: string[], customPath?: string, looseDashboards?: (DashboardAsCode & {
|
|
96
|
+
needsUpdating: boolean;
|
|
97
|
+
})[]) => Promise<string[]>;
|
|
98
|
+
declare const isFilteredWithNoDashboards: (hasFilters: boolean, dashboardSlugs: string[]) => boolean;
|
|
87
99
|
export declare const uploadHandler: (options: DownloadHandlerOptions) => Promise<void>;
|
|
88
100
|
export declare const testHelpers: {
|
|
89
101
|
assertUniqueSpacePaths: typeof assertUniqueSpacePaths;
|
|
90
102
|
downloadSpaces: typeof downloadSpaces;
|
|
103
|
+
extractAppSlugsFromDashboards: typeof extractAppSlugsFromDashboards;
|
|
91
104
|
getFlatSpaceFileNames: typeof getFlatSpaceFileNames;
|
|
105
|
+
getDashboardAppSlugs: typeof getDashboardAppSlugs;
|
|
92
106
|
getDashboardChartSlugs: typeof getDashboardChartSlugs;
|
|
93
107
|
hasContentFilters: typeof hasContentFilters;
|
|
94
108
|
isAiAgentsUnavailableError: typeof isAiAgentsUnavailableError;
|
|
95
109
|
isExternalConnectionsUnavailableError: typeof isExternalConnectionsUnavailableError;
|
|
96
110
|
downloadAiAgents: typeof downloadAiAgents;
|
|
111
|
+
isFilteredWithNoDashboards: typeof isFilteredWithNoDashboards;
|
|
97
112
|
readAiAgentFiles: typeof readAiAgentFiles;
|
|
98
113
|
readExternalConnectionFiles: typeof readExternalConnectionFiles;
|
|
99
114
|
readSpaceFiles: typeof readSpaceFiles;
|