@lightdash/cli 0.3408.0 → 0.3409.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/organizationContent/index.d.ts.map +1 -1
- package/dist/handlers/organizationContent/index.js +75 -27
- package/dist/handlers/organizationContent/index.test.js +10 -0
- package/dist/handlers/organizationContent/users.d.ts.map +1 -1
- package/dist/handlers/organizationContent/users.js +6 -16
- package/dist/handlers/organizationContent/users.test.js +14 -0
- package/dist/terminal/contentAsCodeOutput.d.ts +38 -0
- package/dist/terminal/contentAsCodeOutput.d.ts.map +1 -0
- package/dist/terminal/contentAsCodeOutput.js +142 -0
- package/dist/terminal/contentAsCodeOutput.test.d.ts +2 -0
- package/dist/terminal/contentAsCodeOutput.test.d.ts.map +1 -0
- package/dist/terminal/contentAsCodeOutput.test.js +130 -0
- package/package.json +3 -3
|
@@ -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;
|
|
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"}
|
|
@@ -6,6 +6,7 @@ const common_1 = require("@lightdash/common");
|
|
|
6
6
|
const analytics_1 = require("../../analytics/analytics");
|
|
7
7
|
const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
|
|
8
8
|
const styles = tslib_1.__importStar(require("../../styles"));
|
|
9
|
+
const contentAsCodeOutput_1 = require("../../terminal/contentAsCodeOutput");
|
|
9
10
|
const registry_1 = require("../contentAsCode/registry");
|
|
10
11
|
const resource_1 = require("../contentAsCode/resource");
|
|
11
12
|
const contentAsCodePaths_1 = require("../contentAsCodePaths");
|
|
@@ -19,6 +20,12 @@ const isPartialUploadError = resource_1.isCodeResourcePhaseError;
|
|
|
19
20
|
const isGroupServiceDisabledError = (error) => error instanceof common_1.LightdashError &&
|
|
20
21
|
error.statusCode === 403 &&
|
|
21
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
|
+
};
|
|
22
29
|
const getOrganizationContentFolder = (customPath) => (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
|
|
23
30
|
exports.getOrganizationContentFolder = getOrganizationContentFolder;
|
|
24
31
|
const downloadOrganizationContent = async ({ customPath, config, }) => {
|
|
@@ -36,27 +43,53 @@ const downloadOrganizationContent = async ({ customPath, config, }) => {
|
|
|
36
43
|
scope: 'organization',
|
|
37
44
|
},
|
|
38
45
|
});
|
|
39
|
-
const
|
|
46
|
+
const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
|
|
47
|
+
operation: 'download',
|
|
48
|
+
scope: 'organization',
|
|
49
|
+
initialLabel: 'Custom roles',
|
|
50
|
+
});
|
|
40
51
|
try {
|
|
41
|
-
const customRolesTotal = await (0, customRoles_1.downloadCustomRoles)(organizationUuid, organizationContentPath);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
const { value: customRolesTotal, durationMs: customRolesDurationMs } = await measureAction(() => (0, customRoles_1.downloadCustomRoles)(organizationUuid, organizationContentPath));
|
|
53
|
+
output.completeItem({
|
|
54
|
+
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({
|
|
60
|
+
label: 'Users',
|
|
61
|
+
detail: `${usersTotal} downloaded`,
|
|
62
|
+
durationMs: usersDurationMs,
|
|
63
|
+
}, 'Groups');
|
|
64
|
+
const groupsStartedAt = Date.now();
|
|
47
65
|
let groupsTotal = 0;
|
|
66
|
+
let groupsDurationMs;
|
|
48
67
|
try {
|
|
49
68
|
groupsTotal = await (0, groups_1.downloadGroups)(organizationUuid, organizationContentPath);
|
|
50
|
-
|
|
69
|
+
groupsDurationMs = getElapsedMilliseconds(groupsStartedAt);
|
|
70
|
+
output.completeItem({
|
|
71
|
+
label: 'Groups',
|
|
72
|
+
detail: `${groupsTotal} downloaded`,
|
|
73
|
+
durationMs: groupsDurationMs,
|
|
74
|
+
}, null);
|
|
51
75
|
}
|
|
52
76
|
catch (error) {
|
|
77
|
+
groupsDurationMs = getElapsedMilliseconds(groupsStartedAt);
|
|
53
78
|
if (!isGroupServiceDisabledError(error)) {
|
|
54
79
|
throw error;
|
|
55
80
|
}
|
|
56
|
-
|
|
81
|
+
output.completeItem({
|
|
82
|
+
label: 'Groups',
|
|
83
|
+
detail: 'service unavailable',
|
|
84
|
+
durationMs: groupsDurationMs,
|
|
85
|
+
variant: 'warning',
|
|
86
|
+
}, null);
|
|
57
87
|
globalState_1.default.debug('> Warning: groups were not downloaded because the group service is not enabled');
|
|
58
88
|
}
|
|
59
|
-
|
|
89
|
+
const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
|
|
90
|
+
if (!renderedSummary) {
|
|
91
|
+
globalState_1.default.log(styles.success(`Downloaded organization content saved to ${organizationContentPath}`));
|
|
92
|
+
}
|
|
60
93
|
await analytics_1.LightdashAnalytics.track({
|
|
61
94
|
event: 'download.completed',
|
|
62
95
|
properties: {
|
|
@@ -71,7 +104,7 @@ const downloadOrganizationContent = async ({ customPath, config, }) => {
|
|
|
71
104
|
});
|
|
72
105
|
}
|
|
73
106
|
catch (error) {
|
|
74
|
-
|
|
107
|
+
output.fail((0, common_1.getErrorMessage)(error), (Date.now() - start) / 1000, true);
|
|
75
108
|
await analytics_1.LightdashAnalytics.track({
|
|
76
109
|
event: 'download.error',
|
|
77
110
|
properties: {
|
|
@@ -101,39 +134,56 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
|
|
|
101
134
|
scope: 'organization',
|
|
102
135
|
},
|
|
103
136
|
});
|
|
104
|
-
const
|
|
137
|
+
const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
|
|
138
|
+
operation: 'upload',
|
|
139
|
+
scope: 'organization',
|
|
140
|
+
initialLabel: 'Custom roles',
|
|
141
|
+
});
|
|
105
142
|
try {
|
|
106
|
-
const summary = await (0, customRoles_1.uploadCustomRoles)(organizationUuid, organizationContentPath);
|
|
143
|
+
const { value: summary, durationMs: customRolesDurationMs } = await measureAction(() => (0, customRoles_1.uploadCustomRoles)(organizationUuid, organizationContentPath));
|
|
107
144
|
const summaryMessage = (0, customRoles_1.formatCustomRoleUploadSummary)(summary);
|
|
108
145
|
if (summary.failed > 0) {
|
|
109
146
|
summary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
|
|
110
|
-
|
|
147
|
+
output.prepareForFailureDetails();
|
|
111
148
|
const skippedGroups = await (0, groups_1.countDependencySkippedGroups)(organizationContentPath);
|
|
112
149
|
globalState_1.default.log(styles.warning(`Skipped users and ${skippedGroups} groups because custom roles failed`));
|
|
113
150
|
throw createCustomRolePartialUploadError(`Processed custom roles: ${summaryMessage}`);
|
|
114
151
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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));
|
|
118
158
|
const userSummaryMessage = (0, users_1.formatUserUploadSummary)(userSummary);
|
|
119
159
|
if (userSummary.failed > 0) {
|
|
120
160
|
userSummary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
|
|
121
|
-
|
|
161
|
+
output.prepareForFailureDetails();
|
|
122
162
|
const skippedGroups = await (0, groups_1.countDependencySkippedGroups)(organizationContentPath);
|
|
123
163
|
globalState_1.default.log(styles.warning(`Skipped ${skippedGroups} groups because users failed`));
|
|
124
164
|
throw createUserPartialUploadError(`Processed users: ${userSummaryMessage}`);
|
|
125
165
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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));
|
|
129
172
|
const groupSummaryMessage = (0, groups_1.formatGroupUploadSummary)(groupSummary);
|
|
130
173
|
if (groupSummary.failed > 0) {
|
|
131
174
|
groupSummary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
|
|
132
|
-
|
|
175
|
+
output.prepareForFailureDetails();
|
|
133
176
|
throw createGroupPartialUploadError(`Processed groups: ${groupSummaryMessage}`);
|
|
134
177
|
}
|
|
135
|
-
|
|
136
|
-
|
|
178
|
+
output.completeItem({
|
|
179
|
+
label: 'Groups',
|
|
180
|
+
detail: groupSummaryMessage,
|
|
181
|
+
durationMs: groupsDurationMs,
|
|
182
|
+
}, null);
|
|
183
|
+
const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
|
|
184
|
+
if (!renderedSummary) {
|
|
185
|
+
globalState_1.default.log(styles.success(`Uploaded organization content from ${organizationContentPath}`));
|
|
186
|
+
}
|
|
137
187
|
await analytics_1.LightdashAnalytics.track({
|
|
138
188
|
event: 'upload.completed',
|
|
139
189
|
properties: {
|
|
@@ -156,9 +206,7 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
|
|
|
156
206
|
});
|
|
157
207
|
}
|
|
158
208
|
catch (error) {
|
|
159
|
-
|
|
160
|
-
spinner.fail(`Failed to upload organization content: ${(0, common_1.getErrorMessage)(error)}`);
|
|
161
|
-
}
|
|
209
|
+
output.fail((0, common_1.getErrorMessage)(error), (Date.now() - start) / 1000, !isPartialUploadError(error));
|
|
162
210
|
await analytics_1.LightdashAnalytics.track({
|
|
163
211
|
event: 'upload.error',
|
|
164
212
|
properties: {
|
|
@@ -59,6 +59,11 @@ describe('organization content download', () => {
|
|
|
59
59
|
expect(spinner.fail).not.toHaveBeenCalled();
|
|
60
60
|
expect(globalState_1.default.debug).toHaveBeenCalledWith('> Warning: groups were not downloaded because the group service is not enabled');
|
|
61
61
|
});
|
|
62
|
+
it('reports a duration for every downloaded resource', async () => {
|
|
63
|
+
await (0, index_1.downloadOrganizationContent)({ config });
|
|
64
|
+
expect(spinner.succeed).toHaveBeenCalledTimes(3);
|
|
65
|
+
spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
|
|
66
|
+
});
|
|
62
67
|
it('still fails for other group download errors', async () => {
|
|
63
68
|
vi.mocked(groups_1.downloadGroups).mockRejectedValue(new Error('Network error'));
|
|
64
69
|
await expect((0, index_1.downloadOrganizationContent)({ config })).rejects.toThrow('Network error');
|
|
@@ -139,6 +144,11 @@ describe('organization content upload sequencing', () => {
|
|
|
139
144
|
'groups:end',
|
|
140
145
|
]);
|
|
141
146
|
});
|
|
147
|
+
it('reports a duration for every uploaded resource', async () => {
|
|
148
|
+
await (0, index_1.uploadOrganizationContent)({ config });
|
|
149
|
+
expect(spinner.succeed).toHaveBeenCalledTimes(3);
|
|
150
|
+
spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
|
|
151
|
+
});
|
|
142
152
|
it('does not start users or groups when custom roles fail', async () => {
|
|
143
153
|
vi.mocked(customRoles_1.uploadCustomRoles).mockResolvedValue(customRoleSummary(1));
|
|
144
154
|
await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed custom roles');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/users.ts"],"names":[],"mappings":"AACA,OAAO,EAUH,KAAK,UAAU,EAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAIH,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC9B,MAAM,2BAA2B,CAAC;AAGnC,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEhD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,sBAAsB,CAAC,UAAU,CAkBjE,CAAC;AAIF,eAAO,MAAM,cAAc,GAAI,yBAAyB,MAAM,KAAG,MACI,CAAC;AAEtE,eAAO,MAAM,uBAAuB,GAAI,SAAS,iBAAiB,KAAG,
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/users.ts"],"names":[],"mappings":"AACA,OAAO,EAUH,KAAK,UAAU,EAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAIH,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC9B,MAAM,2BAA2B,CAAC;AAGnC,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEhD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,sBAAsB,CAAC,UAAU,CAkBjE,CAAC;AAIF,eAAO,MAAM,cAAc,GAAI,yBAAyB,MAAM,KAAG,MACI,CAAC;AAEtE,eAAO,MAAM,uBAAuB,GAAI,SAAS,iBAAiB,KAAG,MAMrD,CAAC;AAEjB,eAAO,MAAM,aAAa,GACtB,yBAAyB,MAAM,KAChC,OAAO,CAAC,OAAO,EAAE,CAOnB,CAAC;AAkCF,eAAO,MAAM,WAAW,GACpB,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,EAC/B,cAAa,OAAe,KAC7B,OAAO,CAAC,iBAAiB,CAgG3B,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAcX,CAAC"}
|
|
@@ -28,22 +28,12 @@ exports.USER_CODE_RESOURCE = {
|
|
|
28
28
|
};
|
|
29
29
|
const getUsersFolder = (organizationContentPath) => path.join(organizationContentPath, exports.USER_CODE_RESOURCE.folderName);
|
|
30
30
|
exports.getUsersFolder = getUsersFolder;
|
|
31
|
-
const formatUserUploadSummary = (summary) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
];
|
|
38
|
-
return [
|
|
39
|
-
`${summary.created} created`,
|
|
40
|
-
`${summary.updated} updated`,
|
|
41
|
-
`${summary.unchanged} unchanged`,
|
|
42
|
-
`${summary.awaitingAuthentication} awaiting authentication`,
|
|
43
|
-
invitationSummary.join(', '),
|
|
44
|
-
`${summary.failed} failed`,
|
|
45
|
-
].join(', ');
|
|
46
|
-
};
|
|
31
|
+
const formatUserUploadSummary = (summary) => [
|
|
32
|
+
`${summary.created} created`,
|
|
33
|
+
`${summary.updated} updated`,
|
|
34
|
+
`${summary.unchanged} unchanged`,
|
|
35
|
+
`${summary.failed} failed`,
|
|
36
|
+
].join(', ');
|
|
47
37
|
exports.formatUserUploadSummary = formatUserUploadSummary;
|
|
48
38
|
const readUserFiles = async (organizationContentPath) => {
|
|
49
39
|
const result = await (0, resource_1.readCodeResourceFiles)({
|
|
@@ -35,6 +35,20 @@ describe('users as code', () => {
|
|
|
35
35
|
vitest_1.vi.restoreAllMocks();
|
|
36
36
|
await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
|
|
37
37
|
});
|
|
38
|
+
it('formats upload results using CRUD statuses only', () => {
|
|
39
|
+
expect((0, users_1.formatUserUploadSummary)({
|
|
40
|
+
created: 1,
|
|
41
|
+
updated: 2,
|
|
42
|
+
unchanged: 3,
|
|
43
|
+
awaitingAuthentication: 4,
|
|
44
|
+
invited: 5,
|
|
45
|
+
skippedAuthenticated: 6,
|
|
46
|
+
skippedDisabled: 7,
|
|
47
|
+
skippedValidInvite: 8,
|
|
48
|
+
failed: 9,
|
|
49
|
+
failures: [],
|
|
50
|
+
})).toBe('1 created, 2 updated, 3 unchanged, 9 failed');
|
|
51
|
+
});
|
|
38
52
|
it('downloads deterministic portable users and preserves unknown YAML', async () => {
|
|
39
53
|
const folder = (0, users_1.getUsersFolder)(tmpDir);
|
|
40
54
|
await fs_1.promises.mkdir(folder, { recursive: true });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type ContentAsCodeOutputVariant = 'success' | 'warning';
|
|
2
|
+
export type ContentAsCodeOutputItem = {
|
|
3
|
+
label: string;
|
|
4
|
+
detail: string;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
variant?: ContentAsCodeOutputVariant;
|
|
7
|
+
};
|
|
8
|
+
export type ContentAsCodeOutputProps = {
|
|
9
|
+
operation: 'download' | 'upload';
|
|
10
|
+
scope: 'organization' | 'project';
|
|
11
|
+
path: string;
|
|
12
|
+
elapsedSeconds: number;
|
|
13
|
+
items: ContentAsCodeOutputItem[];
|
|
14
|
+
};
|
|
15
|
+
type ContentAsCodeProgressProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope' | 'items'> & {
|
|
16
|
+
activeLabel: string;
|
|
17
|
+
};
|
|
18
|
+
type ContentAsCodeFailureProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope' | 'elapsedSeconds' | 'items'> & {
|
|
19
|
+
failedItem: Omit<ContentAsCodeOutputItem, 'variant'>;
|
|
20
|
+
};
|
|
21
|
+
export declare const formatDurationTag: (durationMs: number) => string;
|
|
22
|
+
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 formatContentAsCodeFailure: ({ operation, scope, elapsedSeconds, items, failedItem, }: ContentAsCodeFailureProps) => string;
|
|
25
|
+
export declare const formatContentAsCodeComplete: ({ operation, scope, path, elapsedSeconds, items, }: ContentAsCodeOutputProps) => string;
|
|
26
|
+
export declare const canRenderContentAsCodeTree: () => boolean;
|
|
27
|
+
export declare const renderContentAsCodeComplete: (props: ContentAsCodeOutputProps) => boolean;
|
|
28
|
+
type CreateContentAsCodeOutputProps = Pick<ContentAsCodeOutputProps, 'operation' | 'scope'> & {
|
|
29
|
+
initialLabel: string;
|
|
30
|
+
};
|
|
31
|
+
export declare const createContentAsCodeOutput: ({ operation, scope, initialLabel, }: CreateContentAsCodeOutputProps) => {
|
|
32
|
+
completeItem: (item: ContentAsCodeOutputItem, nextLabel: string | null) => void;
|
|
33
|
+
prepareForFailureDetails: () => void;
|
|
34
|
+
complete: (path: string, elapsedSeconds: number) => boolean;
|
|
35
|
+
fail: (detail: string, elapsedSeconds: number, showFallback: boolean) => void;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=contentAsCodeOutput.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
|
+
const globalState_1 = tslib_1.__importDefault(require("../globalState"));
|
|
7
|
+
const ACTION_LABEL_WIDTH = 14;
|
|
8
|
+
const TREE_CONNECTOR = chalk_1.default.gray('└');
|
|
9
|
+
const getDurationLabel = (durationMs) => `(${Math.round(durationMs)}ms)`;
|
|
10
|
+
const getOperationLabel = (operation, tense) => {
|
|
11
|
+
if (operation === 'download') {
|
|
12
|
+
return tense === 'running' ? 'Downloading' : 'Downloaded';
|
|
13
|
+
}
|
|
14
|
+
return tense === 'running' ? 'Uploading' : 'Uploaded';
|
|
15
|
+
};
|
|
16
|
+
const formatDurationTag = (durationMs) => chalk_1.default.gray(getDurationLabel(durationMs));
|
|
17
|
+
exports.formatDurationTag = formatDurationTag;
|
|
18
|
+
const formatContentAsCodeAction = ({ label, detail, durationMs, }) => `${chalk_1.default.bold(label.padEnd(ACTION_LABEL_WIDTH))} ${chalk_1.default.gray(detail)} ${(0, exports.formatDurationTag)(durationMs)}`;
|
|
19
|
+
exports.formatContentAsCodeAction = formatContentAsCodeAction;
|
|
20
|
+
const formatTreeItem = ({ label, detail, durationMs, variant = 'success', }) => {
|
|
21
|
+
const status = variant === 'success' ? chalk_1.default.green('✓') : chalk_1.default.yellow('⚠');
|
|
22
|
+
return ` ${TREE_CONNECTOR} ${status} ${chalk_1.default.bold(label)} ${chalk_1.default.gray(`· ${detail}`)} ${(0, exports.formatDurationTag)(durationMs)}`;
|
|
23
|
+
};
|
|
24
|
+
const formatContentAsCodeProgress = ({ operation, scope, items, activeLabel, }) => {
|
|
25
|
+
const header = `${chalk_1.default.bold(getOperationLabel(operation, 'running'))} content as code ${chalk_1.default.gray(`(${scope})`)}`;
|
|
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()}…`)}`;
|
|
28
|
+
return [header, ...completedItems, activeItem].join('\n');
|
|
29
|
+
};
|
|
30
|
+
exports.formatContentAsCodeProgress = formatContentAsCodeProgress;
|
|
31
|
+
const formatContentAsCodeFailure = ({ operation, scope, elapsedSeconds, items, failedItem, }) => {
|
|
32
|
+
const operationLabel = operation === 'download' ? 'download' : 'upload';
|
|
33
|
+
const header = `${chalk_1.default.bold(`Failed to ${operationLabel}`)} content as code ${chalk_1.default.gray(`(${scope}) · ${elapsedSeconds.toFixed(1)}s`)}`;
|
|
34
|
+
const failed = ` ${TREE_CONNECTOR} ${chalk_1.default.red('×')} ${chalk_1.default.bold(failedItem.label)} ${chalk_1.default.red(`· ${failedItem.detail}`)} ${chalk_1.default.red(getDurationLabel(failedItem.durationMs))}`;
|
|
35
|
+
return [header, ...items.map(formatTreeItem), failed].join('\n');
|
|
36
|
+
};
|
|
37
|
+
exports.formatContentAsCodeFailure = formatContentAsCodeFailure;
|
|
38
|
+
const formatContentAsCodeComplete = ({ operation, scope, path, elapsedSeconds, items, }) => {
|
|
39
|
+
const operationLabel = getOperationLabel(operation, 'complete');
|
|
40
|
+
const pathLabel = operation === 'download' ? 'Saved to' : 'Read from';
|
|
41
|
+
const header = `${chalk_1.default.green('✓')} ${chalk_1.default.bold(operationLabel)} content as code ${chalk_1.default.gray(`(${scope}) · ${elapsedSeconds.toFixed(1)}s`)}`;
|
|
42
|
+
const pathItem = ` ${TREE_CONNECTOR} ${chalk_1.default.gray(pathLabel)} ${chalk_1.default.bold(path)}`;
|
|
43
|
+
return [header, ...items.map(formatTreeItem), pathItem].join('\n');
|
|
44
|
+
};
|
|
45
|
+
exports.formatContentAsCodeComplete = formatContentAsCodeComplete;
|
|
46
|
+
const canRenderContentAsCodeTree = () => Boolean(process.stderr.isTTY &&
|
|
47
|
+
process.env.CI !== 'true' &&
|
|
48
|
+
process.env.TERM !== 'dumb' &&
|
|
49
|
+
process.env.NO_UNICODE !== '1' &&
|
|
50
|
+
process.env.NO_UNICODE !== 'true');
|
|
51
|
+
exports.canRenderContentAsCodeTree = canRenderContentAsCodeTree;
|
|
52
|
+
const renderContentAsCodeComplete = (props) => {
|
|
53
|
+
if (!(0, exports.canRenderContentAsCodeTree)()) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
process.stderr.write(`${(0, exports.formatContentAsCodeComplete)(props)}\n`);
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
59
|
+
exports.renderContentAsCodeComplete = renderContentAsCodeComplete;
|
|
60
|
+
const createContentAsCodeOutput = ({ operation, scope, initialLabel, }) => {
|
|
61
|
+
const useTree = (0, exports.canRenderContentAsCodeTree)();
|
|
62
|
+
const items = [];
|
|
63
|
+
let activeLabel = initialLabel;
|
|
64
|
+
let activeStartedAt = Date.now();
|
|
65
|
+
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) => {
|
|
75
|
+
activeLabel = label;
|
|
76
|
+
activeStartedAt = Date.now();
|
|
77
|
+
if (useTree) {
|
|
78
|
+
spinner.text = (0, exports.formatContentAsCodeProgress)({
|
|
79
|
+
operation,
|
|
80
|
+
scope,
|
|
81
|
+
items,
|
|
82
|
+
activeLabel,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
spinner.start(`${runningLabel} ${activeLabel.toLowerCase()}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
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
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (nextLabel !== null) {
|
|
101
|
+
setActive(nextLabel);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
prepareForFailureDetails: () => {
|
|
105
|
+
if (!useTree) {
|
|
106
|
+
spinner.stop();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
complete: (path, elapsedSeconds) => {
|
|
110
|
+
if (!useTree) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
spinner.stop();
|
|
114
|
+
return (0, exports.renderContentAsCodeComplete)({
|
|
115
|
+
operation,
|
|
116
|
+
scope,
|
|
117
|
+
path,
|
|
118
|
+
elapsedSeconds,
|
|
119
|
+
items,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
fail: (detail, elapsedSeconds, showFallback) => {
|
|
123
|
+
if (useTree) {
|
|
124
|
+
spinner.fail((0, exports.formatContentAsCodeFailure)({
|
|
125
|
+
operation,
|
|
126
|
+
scope,
|
|
127
|
+
elapsedSeconds,
|
|
128
|
+
items,
|
|
129
|
+
failedItem: {
|
|
130
|
+
label: activeLabel,
|
|
131
|
+
detail,
|
|
132
|
+
durationMs: Date.now() - activeStartedAt,
|
|
133
|
+
},
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
else if (showFallback) {
|
|
137
|
+
spinner.fail(`Failed to ${operation} ${scope} content: ${detail}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
exports.createContentAsCodeOutput = createContentAsCodeOutput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contentAsCodeOutput.test.d.ts","sourceRoot":"","sources":["../../src/terminal/contentAsCodeOutput.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const globalState_1 = tslib_1.__importDefault(require("../globalState"));
|
|
5
|
+
const contentAsCodeOutput_1 = require("./contentAsCodeOutput");
|
|
6
|
+
describe('content as code terminal output', () => {
|
|
7
|
+
it('formats fallback actions with aligned labels and muted metadata', () => {
|
|
8
|
+
const output = (0, contentAsCodeOutput_1.formatContentAsCodeAction)({
|
|
9
|
+
label: 'Custom roles',
|
|
10
|
+
detail: '34 downloaded',
|
|
11
|
+
durationMs: 245,
|
|
12
|
+
});
|
|
13
|
+
expect(output).toContain('Custom roles 34 downloaded (245ms)');
|
|
14
|
+
});
|
|
15
|
+
it('formats running progress as a tree', () => {
|
|
16
|
+
const output = (0, contentAsCodeOutput_1.formatContentAsCodeProgress)({
|
|
17
|
+
operation: 'download',
|
|
18
|
+
scope: 'organization',
|
|
19
|
+
activeLabel: 'Groups',
|
|
20
|
+
items: [
|
|
21
|
+
{
|
|
22
|
+
label: 'Custom roles',
|
|
23
|
+
detail: '34 downloaded',
|
|
24
|
+
durationMs: 85,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: 'Users',
|
|
28
|
+
detail: '9 downloaded',
|
|
29
|
+
durationMs: 107,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
expect(output).toContain('Downloading content as code (organization)');
|
|
34
|
+
expect(output).toContain('└ ✓ Custom roles · 34 downloaded (85ms)');
|
|
35
|
+
expect(output).toContain('└ ✓ Users · 9 downloaded (107ms)');
|
|
36
|
+
expect(output).toContain('└ ◐ Groups · downloading…');
|
|
37
|
+
});
|
|
38
|
+
it('renders one borderless completion tree', () => {
|
|
39
|
+
const output = (0, contentAsCodeOutput_1.formatContentAsCodeComplete)({
|
|
40
|
+
operation: 'upload',
|
|
41
|
+
scope: 'organization',
|
|
42
|
+
path: '/tmp/lightdash',
|
|
43
|
+
elapsedSeconds: 1.2,
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
label: 'Custom roles',
|
|
47
|
+
detail: '1 created, 2 unchanged',
|
|
48
|
+
durationMs: 245,
|
|
49
|
+
},
|
|
50
|
+
{ label: 'Users', detail: '4 updated', durationMs: 31 },
|
|
51
|
+
{
|
|
52
|
+
label: 'Groups',
|
|
53
|
+
detail: 'service unavailable',
|
|
54
|
+
durationMs: 12,
|
|
55
|
+
variant: 'warning',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
expect(output).toContain('✓ Uploaded content as code (organization) · 1.2s');
|
|
60
|
+
expect(output).toContain('└ ✓ Custom roles');
|
|
61
|
+
expect(output).toContain('(245ms)');
|
|
62
|
+
expect(output).toContain('└ ⚠ Groups');
|
|
63
|
+
expect(output).toContain('└ Read from /tmp/lightdash');
|
|
64
|
+
expect(output).not.toMatch(/[╭╮╰╯│─]/);
|
|
65
|
+
});
|
|
66
|
+
it('formats failures with completed and failed tree rows', () => {
|
|
67
|
+
const output = (0, contentAsCodeOutput_1.formatContentAsCodeFailure)({
|
|
68
|
+
operation: 'download',
|
|
69
|
+
scope: 'organization',
|
|
70
|
+
elapsedSeconds: 0.4,
|
|
71
|
+
items: [
|
|
72
|
+
{
|
|
73
|
+
label: 'Custom roles',
|
|
74
|
+
detail: '34 downloaded',
|
|
75
|
+
durationMs: 85,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
failedItem: {
|
|
79
|
+
label: 'Users',
|
|
80
|
+
detail: '403 forbidden',
|
|
81
|
+
durationMs: 52,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
expect(output).toContain('Failed to download content as code (organization) · 0.4s');
|
|
85
|
+
expect(output).toContain('└ ✓ Custom roles · 34 downloaded (85ms)');
|
|
86
|
+
expect(output).toContain('└ × Users · 403 forbidden (52ms)');
|
|
87
|
+
});
|
|
88
|
+
it('replaces running progress with one persistent completion tree', () => {
|
|
89
|
+
const originalIsTTY = process.stderr.isTTY;
|
|
90
|
+
Object.defineProperty(process.stderr, 'isTTY', {
|
|
91
|
+
configurable: true,
|
|
92
|
+
value: true,
|
|
93
|
+
});
|
|
94
|
+
vi.stubEnv('CI', 'false');
|
|
95
|
+
vi.stubEnv('TERM', 'xterm');
|
|
96
|
+
vi.stubEnv('NO_UNICODE', 'false');
|
|
97
|
+
const spinner = {
|
|
98
|
+
text: '',
|
|
99
|
+
start: vi.fn(),
|
|
100
|
+
succeed: vi.fn(),
|
|
101
|
+
stop: vi.fn(),
|
|
102
|
+
fail: vi.fn(),
|
|
103
|
+
};
|
|
104
|
+
vi.spyOn(globalState_1.default, 'startSpinner').mockReturnValue(spinner);
|
|
105
|
+
const write = vi
|
|
106
|
+
.spyOn(process.stderr, 'write')
|
|
107
|
+
.mockImplementation(() => true);
|
|
108
|
+
const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
|
|
109
|
+
operation: 'download',
|
|
110
|
+
scope: 'organization',
|
|
111
|
+
initialLabel: 'Custom roles',
|
|
112
|
+
});
|
|
113
|
+
output.completeItem({
|
|
114
|
+
label: 'Custom roles',
|
|
115
|
+
detail: '34 downloaded',
|
|
116
|
+
durationMs: 85,
|
|
117
|
+
}, 'Users');
|
|
118
|
+
expect(spinner.succeed).not.toHaveBeenCalled();
|
|
119
|
+
expect(spinner.text).toContain('└ ◐ Users · downloading…');
|
|
120
|
+
expect(output.complete('/tmp/lightdash', 0.6)).toBe(true);
|
|
121
|
+
expect(spinner.stop).toHaveBeenCalledOnce();
|
|
122
|
+
expect(write).toHaveBeenCalledOnce();
|
|
123
|
+
Object.defineProperty(process.stderr, 'isTTY', {
|
|
124
|
+
configurable: true,
|
|
125
|
+
value: originalIsTTY,
|
|
126
|
+
});
|
|
127
|
+
vi.unstubAllEnvs();
|
|
128
|
+
vi.restoreAllMocks();
|
|
129
|
+
});
|
|
130
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3409.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.
|
|
45
|
-
"@lightdash/warehouses": "0.
|
|
44
|
+
"@lightdash/common": "0.3409.0",
|
|
45
|
+
"@lightdash/warehouses": "0.3409.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/inquirer": "8.2.4",
|