@lightdash/cli 0.3422.1 → 0.3423.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.
@@ -13,7 +13,7 @@ vitest_1.vi.mock('./dbt/apiClient', async (importOriginal) => ({
13
13
  ...(await importOriginal()),
14
14
  lightdashApi: vitest_1.vi.fn(),
15
15
  }));
16
- const { assertUniqueSpacePaths, downloadSpaces, getDashboardChartSlugs, getFlatSpaceFileNames, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
16
+ const { assertUniqueSpacePaths, downloadSpaces, getDashboardChartSlugs, getFlatSpaceFileNames, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, summarizeUploadChanges, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
17
17
  const makeLooseDashboard = (slug, chartSlugs) => ({
18
18
  slug,
19
19
  name: slug,
@@ -31,6 +31,24 @@ const writeFolderDashboard = async (baseDir, slug, chartSlugs) => {
31
31
  const yaml = `contentType: dashboard\nname: ${slug}\nslug: ${slug}\nspaceSlug: test-space\ntiles:\n${tilesYaml}\nversion: 1\n`;
32
32
  await fs_1.promises.writeFile(path.join(baseDir, 'dashboards', `${slug}.yml`), yaml);
33
33
  };
34
+ describe('summarizeUploadChanges', () => {
35
+ it('reports only phase deltas and warns when resources fail', () => {
36
+ expect(summarizeUploadChanges({ 'charts created': 1, 'spaces skipped': 3 }, {
37
+ 'charts created': 3,
38
+ 'spaces skipped': 3,
39
+ 'charts skipped': 2,
40
+ 'charts with errors': 1,
41
+ })).toEqual({
42
+ detail: '2 created, 2 skipped, 1 with errors',
43
+ variant: 'warning',
44
+ });
45
+ });
46
+ it('reports an empty phase without inventing a resource count', () => {
47
+ expect(summarizeUploadChanges({}, {})).toEqual({
48
+ detail: 'no changes',
49
+ });
50
+ });
51
+ });
34
52
  const pivotedSeries = (pivotValue, overrides = {}) => ({
35
53
  type: common_1.CartesianSeriesType.BAR,
36
54
  encode: {
@@ -378,7 +396,9 @@ spaceName: Finance
378
396
  version: 1
379
397
  `);
380
398
  try {
381
- await (0, download_1.downloadContent)(['pivoted-chart'], 'charts', 'project-uuid', 'project', tmpDir, false, false, false, false, false);
399
+ const onProgress = vitest_1.vi.fn();
400
+ await (0, download_1.downloadContent)(['pivoted-chart'], 'charts', 'project-uuid', 'project', tmpDir, false, false, false, false, false, onProgress);
401
+ expect(onProgress).toHaveBeenCalledWith('1 of 1 charts downloaded');
382
402
  await expect(readSpaceFiles(tmpDir)).resolves.toEqual([
383
403
  expect.objectContaining({
384
404
  filePath: existingFile,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,KAAK,0BAA0B,GAAG;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AA6BF,eAAO,MAAM,4BAA4B,GAAI,aAAa,MAAM,KAAG,MAClC,CAAC;AAElC,eAAO,MAAM,2BAA2B,GAAU,yBAG/C,0BAA0B,KAAG,OAAO,CAAC,IAAI,CA0H3C,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAU,sCAI7C,0BAA0B,KAAG,OAAO,CAAC,IAAI,CAgK3C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,KAAK,0BAA0B,GAAG;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAkBF,eAAO,MAAM,4BAA4B,GAAI,aAAa,MAAM,KAAG,MAClC,CAAC;AAElC,eAAO,MAAM,2BAA2B,GAAU,yBAG/C,0BAA0B,KAAG,OAAO,CAAC,IAAI,CA2F3C,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAU,sCAI7C,0BAA0B,KAAG,OAAO,CAAC,IAAI,CA0I3C,CAAC"}
@@ -20,12 +20,6 @@ const isPartialUploadError = resource_1.isCodeResourcePhaseError;
20
20
  const isGroupServiceDisabledError = (error) => error instanceof common_1.LightdashError &&
21
21
  error.statusCode === 403 &&
22
22
  error.message === 'Group service is not enabled';
23
- const getElapsedMilliseconds = (startedAt) => Date.now() - startedAt;
24
- const measureAction = async (action) => {
25
- const startedAt = Date.now();
26
- const value = await action();
27
- return { value, durationMs: getElapsedMilliseconds(startedAt) };
28
- };
29
23
  const getOrganizationContentFolder = (customPath) => (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
30
24
  exports.getOrganizationContentFolder = getOrganizationContentFolder;
31
25
  const downloadOrganizationContent = async ({ customPath, config, }) => {
@@ -46,44 +40,29 @@ const downloadOrganizationContent = async ({ customPath, config, }) => {
46
40
  const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
47
41
  operation: 'download',
48
42
  scope: 'organization',
49
- initialLabel: 'Custom roles',
50
43
  });
51
44
  try {
52
- const { value: customRolesTotal, durationMs: customRolesDurationMs } = await measureAction(() => (0, customRoles_1.downloadCustomRoles)(organizationUuid, organizationContentPath));
53
- output.completeItem({
45
+ const customRolesTotal = await output.runItem({
54
46
  label: 'Custom roles',
55
- detail: `${customRolesTotal} downloaded`,
56
- durationMs: customRolesDurationMs,
57
- }, 'Users');
58
- const { value: usersTotal, durationMs: usersDurationMs } = await measureAction(() => (0, users_1.downloadUsers)(organizationUuid, organizationContentPath));
59
- output.completeItem({
47
+ action: () => (0, customRoles_1.downloadCustomRoles)(organizationUuid, organizationContentPath),
48
+ detail: (total) => `${total} downloaded`,
49
+ });
50
+ const usersTotal = await output.runItem({
60
51
  label: 'Users',
61
- detail: `${usersTotal} downloaded`,
62
- durationMs: usersDurationMs,
63
- }, 'Groups');
64
- const groupsStartedAt = Date.now();
52
+ action: () => (0, users_1.downloadUsers)(organizationUuid, organizationContentPath),
53
+ detail: (total) => `${total} downloaded`,
54
+ });
65
55
  let groupsTotal = 0;
66
- let groupsDurationMs;
56
+ output.startItem('Groups');
67
57
  try {
68
58
  groupsTotal = await (0, groups_1.downloadGroups)(organizationUuid, organizationContentPath);
69
- groupsDurationMs = getElapsedMilliseconds(groupsStartedAt);
70
- output.completeItem({
71
- label: 'Groups',
72
- detail: `${groupsTotal} downloaded`,
73
- durationMs: groupsDurationMs,
74
- }, null);
59
+ output.completeItem(`${groupsTotal} downloaded`);
75
60
  }
76
61
  catch (error) {
77
- groupsDurationMs = getElapsedMilliseconds(groupsStartedAt);
78
62
  if (!isGroupServiceDisabledError(error)) {
79
63
  throw error;
80
64
  }
81
- output.completeItem({
82
- label: 'Groups',
83
- detail: 'service unavailable',
84
- durationMs: groupsDurationMs,
85
- variant: 'warning',
86
- }, null);
65
+ output.completeItem('service unavailable', 'warning');
87
66
  globalState_1.default.debug('> Warning: groups were not downloaded because the group service is not enabled');
88
67
  }
89
68
  const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
@@ -137,10 +116,10 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
137
116
  const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
138
117
  operation: 'upload',
139
118
  scope: 'organization',
140
- initialLabel: 'Custom roles',
141
119
  });
142
120
  try {
143
- const { value: summary, durationMs: customRolesDurationMs } = await measureAction(() => (0, customRoles_1.uploadCustomRoles)(organizationUuid, organizationContentPath));
121
+ output.startItem('Custom roles');
122
+ const summary = await (0, customRoles_1.uploadCustomRoles)(organizationUuid, organizationContentPath);
144
123
  const summaryMessage = (0, customRoles_1.formatCustomRoleUploadSummary)(summary);
145
124
  if (summary.failed > 0) {
146
125
  summary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
@@ -149,12 +128,9 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
149
128
  globalState_1.default.log(styles.warning(`Skipped users and ${skippedGroups} groups because custom roles failed`));
150
129
  throw createCustomRolePartialUploadError(`Processed custom roles: ${summaryMessage}`);
151
130
  }
152
- output.completeItem({
153
- label: 'Custom roles',
154
- detail: summaryMessage,
155
- durationMs: customRolesDurationMs,
156
- }, 'Users');
157
- const { value: userSummary, durationMs: usersDurationMs } = await measureAction(() => (0, users_1.uploadUsers)(organizationUuid, organizationContentPath, sendInvites));
131
+ output.completeItem(summaryMessage);
132
+ output.startItem('Users');
133
+ const userSummary = await (0, users_1.uploadUsers)(organizationUuid, organizationContentPath, sendInvites);
158
134
  const userSummaryMessage = (0, users_1.formatUserUploadSummary)(userSummary);
159
135
  if (userSummary.failed > 0) {
160
136
  userSummary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
@@ -163,23 +139,16 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
163
139
  globalState_1.default.log(styles.warning(`Skipped ${skippedGroups} groups because users failed`));
164
140
  throw createUserPartialUploadError(`Processed users: ${userSummaryMessage}`);
165
141
  }
166
- output.completeItem({
167
- label: 'Users',
168
- detail: userSummaryMessage,
169
- durationMs: usersDurationMs,
170
- }, 'Groups');
171
- const { value: groupSummary, durationMs: groupsDurationMs } = await measureAction(() => (0, groups_1.uploadGroups)(organizationUuid, organizationContentPath));
142
+ output.completeItem(userSummaryMessage);
143
+ output.startItem('Groups');
144
+ const groupSummary = await (0, groups_1.uploadGroups)(organizationUuid, organizationContentPath);
172
145
  const groupSummaryMessage = (0, groups_1.formatGroupUploadSummary)(groupSummary);
173
146
  if (groupSummary.failed > 0) {
174
147
  groupSummary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
175
148
  output.prepareForFailureDetails();
176
149
  throw createGroupPartialUploadError(`Processed groups: ${groupSummaryMessage}`);
177
150
  }
178
- output.completeItem({
179
- label: 'Groups',
180
- detail: groupSummaryMessage,
181
- durationMs: groupsDurationMs,
182
- }, null);
151
+ output.completeItem(groupSummaryMessage);
183
152
  const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
184
153
  if (!renderedSummary) {
185
154
  globalState_1.default.log(styles.success(`Uploaded organization content from ${organizationContentPath}`));
@@ -28,6 +28,7 @@ describe('organization content download', () => {
28
28
  const spinner = {
29
29
  start: vi.fn(),
30
30
  succeed: vi.fn(),
31
+ warn: vi.fn(),
31
32
  stop: vi.fn(),
32
33
  fail: vi.fn(),
33
34
  };
@@ -55,7 +56,7 @@ describe('organization content download', () => {
55
56
  data: {},
56
57
  }));
57
58
  await expect((0, index_1.downloadOrganizationContent)({ config })).resolves.toBeUndefined();
58
- expect(spinner.stop).toHaveBeenCalledOnce();
59
+ expect(spinner.warn).toHaveBeenCalledOnce();
59
60
  expect(spinner.fail).not.toHaveBeenCalled();
60
61
  expect(globalState_1.default.debug).toHaveBeenCalledWith('> Warning: groups were not downloaded because the group service is not enabled');
61
62
  });
@@ -74,6 +75,7 @@ describe('organization content upload sequencing', () => {
74
75
  const spinner = {
75
76
  start: vi.fn(),
76
77
  succeed: vi.fn(),
78
+ warn: vi.fn(),
77
79
  stop: vi.fn(),
78
80
  fail: vi.fn(),
79
81
  };
@@ -14,25 +14,35 @@ export type ContentAsCodeOutputProps = {
14
14
  };
15
15
  type ContentAsCodeProgressProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope' | 'items'> & {
16
16
  activeLabel: string;
17
+ activeDetail?: string;
17
18
  };
18
19
  type ContentAsCodeFailureProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope' | 'elapsedSeconds' | 'items'> & {
19
20
  failedItem: Omit<ContentAsCodeOutputItem, 'variant'>;
20
21
  };
21
22
  export declare const formatDurationTag: (durationMs: number) => string;
22
23
  export declare const formatContentAsCodeAction: ({ label, detail, durationMs, }: Pick<ContentAsCodeOutputItem, "label" | "detail" | "durationMs">) => string;
23
- export declare const formatContentAsCodeProgress: ({ operation, scope, items, activeLabel, }: ContentAsCodeProgressProps) => string;
24
+ export declare const formatContentAsCodeProgress: ({ operation, scope, items, activeLabel, activeDetail, }: ContentAsCodeProgressProps) => string;
24
25
  export declare const formatContentAsCodeFailure: ({ operation, scope, elapsedSeconds, items, failedItem, }: ContentAsCodeFailureProps) => string;
25
26
  export declare const formatContentAsCodeComplete: ({ operation, scope, path, elapsedSeconds, items, }: ContentAsCodeOutputProps) => string;
26
27
  export declare const canRenderContentAsCodeTree: () => boolean;
28
+ export declare const logContentAsCodeDiscovery: (message: string) => void;
27
29
  export declare const renderContentAsCodeComplete: (props: ContentAsCodeOutputProps) => boolean;
28
- type CreateContentAsCodeOutputProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope'> & {
29
- initialLabel: string;
30
+ type CreateContentAsCodeOutputProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope'>;
31
+ type RunContentAsCodeItemProps<T> = {
32
+ label: string;
33
+ action: () => Promise<T>;
34
+ detail: (value: T) => string;
35
+ variant?: (value: T) => ContentAsCodeOutputVariant | undefined;
30
36
  };
31
- export declare const createContentAsCodeOutput: ({ operation, scope, initialLabel, }: CreateContentAsCodeOutputProps) => {
32
- completeItem: (item: ContentAsCodeOutputItem, nextLabel: string | null) => void;
37
+ export declare const createContentAsCodeOutput: ({ operation, scope, }: CreateContentAsCodeOutputProps) => {
38
+ startItem: (label: string, detail?: string) => void;
39
+ updateActive: (detail: string) => void;
40
+ completeItem: (detail: string, variant?: ContentAsCodeOutputVariant) => void;
41
+ runItem: <T>({ label, action, detail, variant, }: RunContentAsCodeItemProps<T>) => Promise<T>;
33
42
  prepareForFailureDetails: () => void;
34
43
  complete: (path: string, elapsedSeconds: number) => boolean;
35
44
  fail: (detail: string, elapsedSeconds: number, showFallback: boolean) => void;
36
45
  };
46
+ export type ContentAsCodeOutput = ReturnType<typeof createContentAsCodeOutput>;
37
47
  export {};
38
48
  //# sourceMappingURL=contentAsCodeOutput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contentAsCodeOutput.d.ts","sourceRoot":"","sources":["../../src/terminal/contentAsCodeOutput.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,uBAAuB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,uBAAuB,EAAE,CAAC;CACpC,CAAC;AAEF,KAAK,0BAA0B,GAAG,IAAI,CAClC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,OAAO,CAClC,GAAG;IACA,WAAW,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CACrD,GAAG;IACA,UAAU,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;CACxD,CAAC;AAkBF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,WACR,CAAC;AAE7C,eAAO,MAAM,yBAAyB,GAAI,gCAIvC,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,CAAC,WAG3B,CAAC;AAczC,eAAO,MAAM,2BAA2B,GAAI,2CAKzC,0BAA0B,WAU5B,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,0DAMxC,yBAAyB,WAY3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,oDAMzC,wBAAwB,WAW1B,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,OAOzC,CAAC;AAEN,eAAO,MAAM,2BAA2B,GACpC,OAAO,wBAAwB,KAChC,OAOF,CAAC;AAEF,KAAK,8BAA8B,GAAG,IAAI,CACtC,wBAAwB,EACxB,WAAW,GAAG,OAAO,CACxB,GAAG;IACA,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,qCAIvC,8BAA8B;yBAkCf,uBAAuB,aAClB,MAAM,GAAG,IAAI;;qBAmBX,MAAM,kBAAkB,MAAM,KAAG,OAAO;mBAc7C,MAAM,kBACE,MAAM,gBACR,OAAO;CAuBhC,CAAC"}
1
+ {"version":3,"file":"contentAsCodeOutput.d.ts","sourceRoot":"","sources":["../../src/terminal/contentAsCodeOutput.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,uBAAuB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,uBAAuB,EAAE,CAAC;CACpC,CAAC;AAEF,KAAK,0BAA0B,GAAG,IAAI,CAClC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,OAAO,CAClC,GAAG;IACA,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CACrD,GAAG;IACA,UAAU,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;CACxD,CAAC;AAkBF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,WACR,CAAC;AAE7C,eAAO,MAAM,yBAAyB,GAAI,gCAIvC,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,CAAC,WAG3B,CAAC;AAczC,eAAO,MAAM,2BAA2B,GAAI,yDAMzC,0BAA0B,WAe5B,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,0DAMxC,yBAAyB,WAY3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,oDAMzC,wBAAwB,WAW1B,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,OAOzC,CAAC;AAEN,eAAO,MAAM,yBAAyB,GAAI,SAAS,MAAM,KAAG,IAM3D,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACpC,OAAO,wBAAwB,KAChC,OAOF,CAAC;AAEF,KAAK,8BAA8B,GAAG,IAAI,CACtC,wBAAwB,EACxB,WAAW,GAAG,OAAO,CACxB,CAAC;AAEF,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,0BAA0B,GAAG,SAAS,CAAC;CAClE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,uBAGvC,8BAA8B;uBASH,MAAM,WAAW,MAAM;2BAwBnB,MAAM;2BAcxB,MAAM,YACJ,0BAA0B;cAkBjB,CAAC,uCAKrB,yBAAyB,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC;;qBAiBvB,MAAM,kBAAkB,MAAM,KAAG,OAAO;mBAc7C,MAAM,kBACE,MAAM,gBACR,OAAO;CA0BhC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createContentAsCodeOutput = exports.renderContentAsCodeComplete = exports.canRenderContentAsCodeTree = exports.formatContentAsCodeComplete = exports.formatContentAsCodeFailure = exports.formatContentAsCodeProgress = exports.formatContentAsCodeAction = exports.formatDurationTag = void 0;
3
+ exports.createContentAsCodeOutput = exports.renderContentAsCodeComplete = exports.logContentAsCodeDiscovery = exports.canRenderContentAsCodeTree = exports.formatContentAsCodeComplete = exports.formatContentAsCodeFailure = exports.formatContentAsCodeProgress = exports.formatContentAsCodeAction = exports.formatDurationTag = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
6
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
@@ -21,10 +21,11 @@ const formatTreeItem = ({ label, detail, durationMs, variant = 'success', }) =>
21
21
  const status = variant === 'success' ? chalk_1.default.green('✓') : chalk_1.default.yellow('⚠');
22
22
  return ` ${TREE_CONNECTOR} ${status} ${chalk_1.default.bold(label)} ${chalk_1.default.gray(`· ${detail}`)} ${(0, exports.formatDurationTag)(durationMs)}`;
23
23
  };
24
- const formatContentAsCodeProgress = ({ operation, scope, items, activeLabel, }) => {
24
+ const formatContentAsCodeProgress = ({ operation, scope, items, activeLabel, activeDetail, }) => {
25
25
  const header = `${chalk_1.default.bold(getOperationLabel(operation, 'running'))} content as code ${chalk_1.default.gray(`(${scope})`)}`;
26
26
  const completedItems = items.map(formatTreeItem);
27
- const activeItem = ` ${TREE_CONNECTOR} ${chalk_1.default.yellow('◐')} ${chalk_1.default.bold(activeLabel)} ${chalk_1.default.gray(`· ${getOperationLabel(operation, 'running').toLowerCase()}…`)}`;
27
+ const activeItem = ` ${TREE_CONNECTOR} ${chalk_1.default.yellow('◐')} ${chalk_1.default.bold(activeLabel)} ${chalk_1.default.gray(`· ${activeDetail ??
28
+ `${getOperationLabel(operation, 'running').toLowerCase()}…`}`)}`;
28
29
  return [header, ...completedItems, activeItem].join('\n');
29
30
  };
30
31
  exports.formatContentAsCodeProgress = formatContentAsCodeProgress;
@@ -49,6 +50,15 @@ const canRenderContentAsCodeTree = () => Boolean(process.stderr.isTTY &&
49
50
  process.env.NO_UNICODE !== '1' &&
50
51
  process.env.NO_UNICODE !== 'true');
51
52
  exports.canRenderContentAsCodeTree = canRenderContentAsCodeTree;
53
+ const logContentAsCodeDiscovery = (message) => {
54
+ if ((0, exports.canRenderContentAsCodeTree)()) {
55
+ globalState_1.default.debug(message);
56
+ }
57
+ else {
58
+ globalState_1.default.log(message);
59
+ }
60
+ };
61
+ exports.logContentAsCodeDiscovery = logContentAsCodeDiscovery;
52
62
  const renderContentAsCodeComplete = (props) => {
53
63
  if (!(0, exports.canRenderContentAsCodeTree)()) {
54
64
  return false;
@@ -57,52 +67,80 @@ const renderContentAsCodeComplete = (props) => {
57
67
  return true;
58
68
  };
59
69
  exports.renderContentAsCodeComplete = renderContentAsCodeComplete;
60
- const createContentAsCodeOutput = ({ operation, scope, initialLabel, }) => {
70
+ const createContentAsCodeOutput = ({ operation, scope, }) => {
61
71
  const useTree = (0, exports.canRenderContentAsCodeTree)();
62
72
  const items = [];
63
- let activeLabel = initialLabel;
73
+ let activeLabel = 'Content';
74
+ let activeDetail;
64
75
  let activeStartedAt = Date.now();
65
76
  const runningLabel = getOperationLabel(operation, 'running');
66
- const spinner = globalState_1.default.startSpinner(useTree
67
- ? (0, exports.formatContentAsCodeProgress)({
68
- operation,
69
- scope,
70
- items,
71
- activeLabel,
72
- })
73
- : `${runningLabel} ${activeLabel.toLowerCase()}`);
74
- const setActive = (label) => {
77
+ let spinner;
78
+ const startItem = (label, detail) => {
75
79
  activeLabel = label;
80
+ activeDetail = detail;
76
81
  activeStartedAt = Date.now();
82
+ const text = useTree
83
+ ? (0, exports.formatContentAsCodeProgress)({
84
+ operation,
85
+ scope,
86
+ items,
87
+ activeLabel,
88
+ activeDetail,
89
+ })
90
+ : `${runningLabel} ${activeLabel.toLowerCase()}`;
91
+ if (!spinner) {
92
+ spinner = globalState_1.default.startSpinner(text);
93
+ return;
94
+ }
77
95
  if (useTree) {
96
+ spinner.text = text;
97
+ }
98
+ else {
99
+ spinner.start(text);
100
+ }
101
+ };
102
+ const updateActive = (detail) => {
103
+ activeDetail = detail;
104
+ if (useTree && spinner) {
78
105
  spinner.text = (0, exports.formatContentAsCodeProgress)({
79
106
  operation,
80
107
  scope,
81
108
  items,
82
109
  activeLabel,
110
+ activeDetail,
83
111
  });
84
112
  }
85
- else {
86
- spinner.start(`${runningLabel} ${activeLabel.toLowerCase()}`);
87
- }
88
113
  };
89
- return {
90
- completeItem: (item, nextLabel) => {
91
- items.push(item);
92
- if (!useTree) {
93
- if (item.variant === 'warning') {
94
- spinner.stop();
95
- }
96
- else {
97
- spinner.succeed((0, exports.formatContentAsCodeAction)(item));
98
- }
114
+ const completeItem = (detail, variant) => {
115
+ const item = {
116
+ label: activeLabel,
117
+ detail,
118
+ durationMs: Date.now() - activeStartedAt,
119
+ variant,
120
+ };
121
+ items.push(item);
122
+ if (!useTree && spinner) {
123
+ if (item.variant === 'warning') {
124
+ spinner.warn((0, exports.formatContentAsCodeAction)(item));
99
125
  }
100
- if (nextLabel !== null) {
101
- setActive(nextLabel);
126
+ else {
127
+ spinner.succeed((0, exports.formatContentAsCodeAction)(item));
102
128
  }
103
- },
129
+ }
130
+ };
131
+ const runItem = async ({ label, action, detail, variant, }) => {
132
+ startItem(label);
133
+ const value = await action();
134
+ completeItem(detail(value), variant?.(value));
135
+ return value;
136
+ };
137
+ return {
138
+ startItem,
139
+ updateActive,
140
+ completeItem,
141
+ runItem,
104
142
  prepareForFailureDetails: () => {
105
- if (!useTree) {
143
+ if (!useTree && spinner) {
106
144
  spinner.stop();
107
145
  }
108
146
  },
@@ -110,7 +148,7 @@ const createContentAsCodeOutput = ({ operation, scope, initialLabel, }) => {
110
148
  if (!useTree) {
111
149
  return false;
112
150
  }
113
- spinner.stop();
151
+ spinner?.stop();
114
152
  return (0, exports.renderContentAsCodeComplete)({
115
153
  operation,
116
154
  scope,
@@ -121,7 +159,7 @@ const createContentAsCodeOutput = ({ operation, scope, initialLabel, }) => {
121
159
  },
122
160
  fail: (detail, elapsedSeconds, showFallback) => {
123
161
  if (useTree) {
124
- spinner.fail((0, exports.formatContentAsCodeFailure)({
162
+ const failure = (0, exports.formatContentAsCodeFailure)({
125
163
  operation,
126
164
  scope,
127
165
  elapsedSeconds,
@@ -131,10 +169,20 @@ const createContentAsCodeOutput = ({ operation, scope, initialLabel, }) => {
131
169
  detail,
132
170
  durationMs: Date.now() - activeStartedAt,
133
171
  },
134
- }));
172
+ });
173
+ if (spinner) {
174
+ spinner.fail(failure);
175
+ }
176
+ else {
177
+ process.stderr.write(`${failure}\n`);
178
+ }
135
179
  }
136
180
  else if (showFallback) {
137
- spinner.fail(`Failed to ${operation} ${scope} content: ${detail}`);
181
+ const failure = `Failed to ${operation} ${scope} content: ${detail}`;
182
+ if (spinner)
183
+ spinner.fail(failure);
184
+ else
185
+ process.stderr.write(`${failure}\n`);
138
186
  }
139
187
  },
140
188
  };
@@ -98,6 +98,7 @@ describe('content as code terminal output', () => {
98
98
  text: '',
99
99
  start: vi.fn(),
100
100
  succeed: vi.fn(),
101
+ warn: vi.fn(),
101
102
  stop: vi.fn(),
102
103
  fail: vi.fn(),
103
104
  };
@@ -108,13 +109,10 @@ describe('content as code terminal output', () => {
108
109
  const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
109
110
  operation: 'download',
110
111
  scope: 'organization',
111
- initialLabel: 'Custom roles',
112
112
  });
113
- output.completeItem({
114
- label: 'Custom roles',
115
- detail: '34 downloaded',
116
- durationMs: 85,
117
- }, 'Users');
113
+ output.startItem('Custom roles');
114
+ output.completeItem('34 downloaded');
115
+ output.startItem('Users');
118
116
  expect(spinner.succeed).not.toHaveBeenCalled();
119
117
  expect(spinner.text).toContain('└ ◐ Users · downloading…');
120
118
  expect(output.complete('/tmp/lightdash', 0.6)).toBe(true);
@@ -127,4 +125,64 @@ describe('content as code terminal output', () => {
127
125
  vi.unstubAllEnvs();
128
126
  vi.restoreAllMocks();
129
127
  });
128
+ it('updates the active tree item with resource progress', () => {
129
+ const originalIsTTY = process.stderr.isTTY;
130
+ Object.defineProperty(process.stderr, 'isTTY', {
131
+ configurable: true,
132
+ value: true,
133
+ });
134
+ vi.stubEnv('CI', 'false');
135
+ vi.stubEnv('TERM', 'xterm');
136
+ vi.stubEnv('NO_UNICODE', 'false');
137
+ const spinner = {
138
+ text: '',
139
+ start: vi.fn(),
140
+ succeed: vi.fn(),
141
+ warn: vi.fn(),
142
+ stop: vi.fn(),
143
+ fail: vi.fn(),
144
+ };
145
+ vi.spyOn(globalState_1.default, 'startSpinner').mockReturnValue(spinner);
146
+ const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
147
+ operation: 'download',
148
+ scope: 'project',
149
+ });
150
+ output.startItem('Charts');
151
+ output.updateActive('25 of 100 downloaded');
152
+ expect(spinner.text).toContain('└ ◐ Charts · 25 of 100 downloaded');
153
+ Object.defineProperty(process.stderr, 'isTTY', {
154
+ configurable: true,
155
+ value: originalIsTTY,
156
+ });
157
+ vi.unstubAllEnvs();
158
+ vi.restoreAllMocks();
159
+ });
160
+ it('hides routine discovery in tree mode unless verbose output is enabled', () => {
161
+ const originalIsTTY = process.stderr.isTTY;
162
+ Object.defineProperty(process.stderr, 'isTTY', {
163
+ configurable: true,
164
+ value: true,
165
+ });
166
+ vi.stubEnv('CI', 'false');
167
+ vi.stubEnv('TERM', 'xterm');
168
+ vi.stubEnv('NO_UNICODE', 'false');
169
+ const debug = vi
170
+ .spyOn(globalState_1.default, 'debug')
171
+ .mockImplementation(() => undefined);
172
+ const log = vi
173
+ .spyOn(globalState_1.default, 'log')
174
+ .mockImplementation(() => undefined);
175
+ (0, contentAsCodeOutput_1.logContentAsCodeDiscovery)('Found 3 chart files');
176
+ expect(debug).toHaveBeenCalledWith('Found 3 chart files');
177
+ expect(log).not.toHaveBeenCalled();
178
+ vi.stubEnv('CI', 'true');
179
+ (0, contentAsCodeOutput_1.logContentAsCodeDiscovery)('Found 3 chart files');
180
+ expect(log).toHaveBeenCalledWith('Found 3 chart files');
181
+ Object.defineProperty(process.stderr, 'isTTY', {
182
+ configurable: true,
183
+ value: originalIsTTY,
184
+ });
185
+ vi.unstubAllEnvs();
186
+ vi.restoreAllMocks();
187
+ });
130
188
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.3422.1",
3
+ "version": "0.3423.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "unique-names-generator": "4.7.1",
42
42
  "uuid": "11.1.1",
43
43
  "yaml": "2.8.3",
44
- "@lightdash/common": "0.3422.1",
45
- "@lightdash/warehouses": "0.3422.1"
44
+ "@lightdash/common": "0.3423.0",
45
+ "@lightdash/warehouses": "0.3423.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",