@lightdash/cli 0.1421.0 → 0.1422.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/analytics/analytics.d.ts +29 -1
- package/dist/analytics/analytics.js +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.js +2 -1
- package/dist/handlers/download.js +167 -73
- package/dist/handlers/login.js +7 -4
- package/package.json +3 -3
@@ -190,11 +190,39 @@ type CliLogin = BaseTrack & {
|
|
190
190
|
event: 'login.started' | 'login.completed';
|
191
191
|
properties: {
|
192
192
|
userId?: string;
|
193
|
+
organizationId?: string;
|
193
194
|
method: string;
|
194
195
|
url: string;
|
195
196
|
};
|
196
197
|
};
|
197
|
-
type
|
198
|
+
type CliContentAsCode = BaseTrack & ({
|
199
|
+
event: 'download.started' | 'upload.started';
|
200
|
+
properties: {
|
201
|
+
userId?: string;
|
202
|
+
organizationId?: string;
|
203
|
+
projectId: string;
|
204
|
+
};
|
205
|
+
} | {
|
206
|
+
event: 'download.completed' | 'upload.completed';
|
207
|
+
properties: {
|
208
|
+
userId?: string;
|
209
|
+
organizationId?: string;
|
210
|
+
projectId: string;
|
211
|
+
chartsNum?: number;
|
212
|
+
dashboardsNum?: number;
|
213
|
+
timeToCompleted: number;
|
214
|
+
};
|
215
|
+
} | {
|
216
|
+
event: 'download.error' | 'upload.error';
|
217
|
+
properties: {
|
218
|
+
userId?: string;
|
219
|
+
organizationId?: string;
|
220
|
+
projectId: string;
|
221
|
+
type?: 'charts' | 'dashboards';
|
222
|
+
error: string;
|
223
|
+
};
|
224
|
+
});
|
225
|
+
type Track = CliGenerateStarted | CliGenerateCompleted | CliGenerateError | CliDbtCommand | CliDbtError | CliPreviewStarted | CliPreviewCompleted | CliPreviewStopped | CliPreviewError | CliRefreshStarted | CliRefreshCompleted | CliRefreshError | CliCompileStarted | CliCompileCompleted | CliCompileError | CliDeployTriggered | CliCreateStarted | CliCreateCompleted | CliCreateError | CliStartStopPreview | CliStopPreviewMissing | CliGenerateExposuresStarted | CliGenerateExposuresCompleted | CliGenerateExposuresError | CliLogin | CliContentAsCode;
|
198
226
|
export declare class LightdashAnalytics {
|
199
227
|
static track(payload: Track): Promise<void>;
|
200
228
|
}
|
package/dist/config.d.ts
CHANGED
@@ -3,6 +3,7 @@ export type Config = {
|
|
3
3
|
user?: {
|
4
4
|
userUuid?: string;
|
5
5
|
anonymousUuid?: string;
|
6
|
+
organizationUuid?: string;
|
6
7
|
};
|
7
8
|
context?: {
|
8
9
|
serverUrl?: string;
|
@@ -22,6 +23,6 @@ export declare const getConfig: () => Promise<Config>;
|
|
22
23
|
export declare const setProject: (projectUuid: string, projectName: string) => Promise<void>;
|
23
24
|
export declare const setPreviewProject: (projectUuid: string, name: string) => Promise<void>;
|
24
25
|
export declare const unsetPreviewProject: () => Promise<void>;
|
25
|
-
export declare const setDefaultUser: (userUuid: string) => Promise<void>;
|
26
|
+
export declare const setDefaultUser: (userUuid: string, organizationUuid: string) => Promise<void>;
|
26
27
|
export declare const setContext: (context: Config['context']) => Promise<void>;
|
27
28
|
export declare const setAnswer: (answer: Config['answers']) => Promise<void>;
|
package/dist/config.js
CHANGED
@@ -93,13 +93,14 @@ const unsetPreviewProject = async () => {
|
|
93
93
|
});
|
94
94
|
};
|
95
95
|
exports.unsetPreviewProject = unsetPreviewProject;
|
96
|
-
const setDefaultUser = async (userUuid) => {
|
96
|
+
const setDefaultUser = async (userUuid, organizationUuid) => {
|
97
97
|
const config = await getRawConfig();
|
98
98
|
await (0, exports.setConfig)({
|
99
99
|
...config,
|
100
100
|
user: {
|
101
101
|
...(config.user || {}),
|
102
102
|
userUuid,
|
103
|
+
organizationUuid,
|
103
104
|
},
|
104
105
|
});
|
105
106
|
};
|
@@ -8,6 +8,7 @@ const common_1 = require("@lightdash/common");
|
|
8
8
|
const fs_1 = require("fs");
|
9
9
|
const yaml = tslib_1.__importStar(require("js-yaml"));
|
10
10
|
const path = tslib_1.__importStar(require("path"));
|
11
|
+
const analytics_1 = require("../analytics/analytics");
|
11
12
|
const config_1 = require("../config");
|
12
13
|
const globalState_1 = tslib_1.__importDefault(require("../globalState"));
|
13
14
|
const styles = tslib_1.__importStar(require("../styles"));
|
@@ -95,64 +96,104 @@ const downloadHandler = async (options) => {
|
|
95
96
|
if (!projectId) {
|
96
97
|
throw new Error('No project selected. Run lightdash config set-project');
|
97
98
|
}
|
98
|
-
//
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
chartsAsCode
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
99
|
+
// For analytics
|
100
|
+
let chartTotal;
|
101
|
+
let dashboardTotal;
|
102
|
+
const start = Date.now();
|
103
|
+
await analytics_1.LightdashAnalytics.track({
|
104
|
+
event: 'download.started',
|
105
|
+
properties: {
|
106
|
+
userId: config.user?.userUuid,
|
107
|
+
organizationId: config.user?.organizationUuid,
|
108
|
+
projectId,
|
109
|
+
},
|
110
|
+
});
|
111
|
+
try {
|
112
|
+
// If any filter is provided, we skip those items without filters
|
113
|
+
// eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
|
114
|
+
const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
|
115
|
+
// Download charts
|
116
|
+
if (hasFilters && options.charts.length === 0) {
|
117
|
+
console.info(styles.warning(`No charts filters provided, skipping`));
|
118
|
+
}
|
119
|
+
else {
|
120
|
+
const spinner = globalState_1.default.startSpinner(`Downloading charts`);
|
121
|
+
const chartFilters = parseContentFilters(options.charts);
|
122
|
+
let chartsAsCode;
|
123
|
+
let offset = 0;
|
124
|
+
do {
|
125
|
+
globalState_1.default.debug(`Downloading charts with offset "${offset}" and filters "${chartFilters}"`);
|
126
|
+
const queryParams = chartFilters
|
127
|
+
? `${chartFilters}&offset=${offset}`
|
128
|
+
: `?offset=${offset}`;
|
129
|
+
chartsAsCode = await (0, apiClient_1.lightdashApi)({
|
130
|
+
method: 'GET',
|
131
|
+
url: `/api/v1/projects/${projectId}/charts/code${queryParams}`,
|
132
|
+
body: undefined,
|
133
|
+
});
|
134
|
+
spinner.start(`Downloaded ${chartsAsCode.offset} of ${chartsAsCode.total} charts`);
|
135
|
+
chartsAsCode.missingIds.forEach((missingId) => {
|
136
|
+
console.warn(styles.warning(`\nNo chart with id "${missingId}"`));
|
137
|
+
});
|
138
|
+
await dumpIntoFiles('charts', chartsAsCode.charts);
|
139
|
+
offset = chartsAsCode.offset;
|
140
|
+
} while (chartsAsCode.offset < chartsAsCode.total);
|
141
|
+
chartTotal = chartsAsCode.total;
|
142
|
+
spinner.succeed(`Downloaded ${chartsAsCode.total} charts`);
|
143
|
+
}
|
144
|
+
// Download dashboards
|
145
|
+
if (hasFilters && options.dashboards.length === 0) {
|
146
|
+
console.info(styles.warning(`No dashboards filters provided, skipping`));
|
147
|
+
}
|
148
|
+
else {
|
149
|
+
const spinner = globalState_1.default.startSpinner(`Downloading dashboards`);
|
150
|
+
const dashboardFilters = parseContentFilters(options.dashboards);
|
151
|
+
let offset = 0;
|
152
|
+
let dashboardsAsCode;
|
153
|
+
do {
|
154
|
+
globalState_1.default.debug(`Downloading dashboards with offset "${offset}" and filters "${dashboardFilters}"`);
|
155
|
+
const queryParams = dashboardFilters
|
156
|
+
? `${dashboardFilters}&offset=${offset}`
|
157
|
+
: `?offset=${offset}`;
|
158
|
+
dashboardsAsCode = await (0, apiClient_1.lightdashApi)({
|
159
|
+
method: 'GET',
|
160
|
+
url: `/api/v1/projects/${projectId}/dashboards/code${queryParams}`,
|
161
|
+
body: undefined,
|
162
|
+
});
|
163
|
+
dashboardsAsCode.missingIds.forEach((missingId) => {
|
164
|
+
console.warn(styles.warning(`\nNo dashboard with id "${missingId}"`));
|
165
|
+
});
|
166
|
+
spinner?.start(`Downloaded ${dashboardsAsCode.offset} of ${dashboardsAsCode.total} dashboards`);
|
167
|
+
await dumpIntoFiles('dashboards', dashboardsAsCode.dashboards);
|
168
|
+
offset = dashboardsAsCode.offset;
|
169
|
+
} while (dashboardsAsCode.offset < dashboardsAsCode.total);
|
170
|
+
dashboardTotal = dashboardsAsCode.total;
|
171
|
+
spinner.succeed(`Downloaded ${dashboardsAsCode.total} dashboards`);
|
172
|
+
}
|
173
|
+
const end = Date.now();
|
174
|
+
await analytics_1.LightdashAnalytics.track({
|
175
|
+
event: 'download.completed',
|
176
|
+
properties: {
|
177
|
+
userId: config.user?.userUuid,
|
178
|
+
organizationId: config.user?.organizationUuid,
|
179
|
+
projectId,
|
180
|
+
chartsNum: chartTotal,
|
181
|
+
dashboardsNum: dashboardTotal,
|
182
|
+
timeToCompleted: (end - start) / 1000, // in seconds
|
183
|
+
},
|
184
|
+
});
|
132
185
|
}
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
method: 'GET',
|
145
|
-
url: `/api/v1/projects/${projectId}/dashboards/code${queryParams}`,
|
146
|
-
body: undefined,
|
147
|
-
});
|
148
|
-
dashboardsAsCode.missingIds.forEach((missingId) => {
|
149
|
-
console.warn(styles.warning(`\nNo dashboard with id "${missingId}"`));
|
150
|
-
});
|
151
|
-
spinner?.start(`Downloaded ${dashboardsAsCode.offset} of ${dashboardsAsCode.total} dashboards`);
|
152
|
-
await dumpIntoFiles('dashboards', dashboardsAsCode.dashboards);
|
153
|
-
offset = dashboardsAsCode.offset;
|
154
|
-
} while (dashboardsAsCode.offset < dashboardsAsCode.total);
|
155
|
-
spinner.succeed(`Downloaded ${dashboardsAsCode.total} dashboards`);
|
186
|
+
catch (error) {
|
187
|
+
console.error(styles.error(`\nError downloading ${error}`));
|
188
|
+
await analytics_1.LightdashAnalytics.track({
|
189
|
+
event: 'download.error',
|
190
|
+
properties: {
|
191
|
+
userId: config.user?.userUuid,
|
192
|
+
organizationId: config.user?.organizationUuid,
|
193
|
+
projectId,
|
194
|
+
error: `${error}`,
|
195
|
+
},
|
196
|
+
});
|
156
197
|
}
|
157
198
|
// TODO delete files if chart don't exist ?*/
|
158
199
|
};
|
@@ -203,6 +244,7 @@ const logUploadChanges = (changes) => {
|
|
203
244
|
* @param slugs if slugs are provided, we only force upsert the charts/dashboards that match the slugs, if slugs are empty, we upload files that were locally updated
|
204
245
|
*/
|
205
246
|
const upsertResources = async (type, projectId, changes, force, slugs) => {
|
247
|
+
const config = await (0, config_1.getConfig)();
|
206
248
|
const items = await readCodeFiles(type);
|
207
249
|
console.info(`Found ${items.length} ${type} files`);
|
208
250
|
const hasFilter = slugs.length > 0;
|
@@ -241,10 +283,20 @@ const upsertResources = async (type, projectId, changes, force, slugs) => {
|
|
241
283
|
catch (error) {
|
242
284
|
changes[`${type} with errors`] =
|
243
285
|
(changes[`${type} with errors`] ?? 0) + 1;
|
244
|
-
console.error(`Error upserting ${type}
|
286
|
+
console.error(styles.error(`Error upserting ${type}: ${error}`));
|
287
|
+
await analytics_1.LightdashAnalytics.track({
|
288
|
+
event: 'download.error',
|
289
|
+
properties: {
|
290
|
+
userId: config.user?.userUuid,
|
291
|
+
organizationId: config.user?.organizationUuid,
|
292
|
+
projectId,
|
293
|
+
type,
|
294
|
+
error: `${error}`,
|
295
|
+
},
|
296
|
+
});
|
245
297
|
}
|
246
298
|
}
|
247
|
-
return changes;
|
299
|
+
return { changes, total: filteredItems.length };
|
248
300
|
};
|
249
301
|
const uploadHandler = async (options) => {
|
250
302
|
globalState_1.default.setVerbose(options.verbose);
|
@@ -258,21 +310,63 @@ const uploadHandler = async (options) => {
|
|
258
310
|
throw new Error('No project selected. Run lightdash config set-project');
|
259
311
|
}
|
260
312
|
let changes = {};
|
261
|
-
//
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
313
|
+
// For analytics
|
314
|
+
let chartTotal;
|
315
|
+
let dashboardTotal;
|
316
|
+
const start = Date.now();
|
317
|
+
await analytics_1.LightdashAnalytics.track({
|
318
|
+
event: 'upload.started',
|
319
|
+
properties: {
|
320
|
+
userId: config.user?.userUuid,
|
321
|
+
organizationId: config.user?.organizationUuid,
|
322
|
+
projectId,
|
323
|
+
},
|
324
|
+
});
|
325
|
+
try {
|
326
|
+
// If any filter is provided, we skip those items without filters
|
327
|
+
// eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
|
328
|
+
const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
|
329
|
+
if (hasFilters && options.charts.length === 0) {
|
330
|
+
console.info(styles.warning(`No charts filters provided, skipping`));
|
331
|
+
}
|
332
|
+
else {
|
333
|
+
const { changes: chartChanges, total } = await upsertResources('charts', projectId, changes, options.force, options.charts);
|
334
|
+
changes = chartChanges;
|
335
|
+
chartTotal = total;
|
336
|
+
}
|
337
|
+
if (hasFilters && options.dashboards.length === 0) {
|
338
|
+
console.info(styles.warning(`No dashboard filters provided, skipping`));
|
339
|
+
}
|
340
|
+
else {
|
341
|
+
const { changes: dashboardChanges, total } = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards);
|
342
|
+
changes = dashboardChanges;
|
343
|
+
dashboardTotal = total;
|
344
|
+
}
|
345
|
+
const end = Date.now();
|
346
|
+
await analytics_1.LightdashAnalytics.track({
|
347
|
+
event: 'upload.completed',
|
348
|
+
properties: {
|
349
|
+
userId: config.user?.userUuid,
|
350
|
+
organizationId: config.user?.organizationUuid,
|
351
|
+
projectId,
|
352
|
+
chartsNum: chartTotal,
|
353
|
+
dashboardsNum: dashboardTotal,
|
354
|
+
timeToCompleted: (end - start) / 1000, // in seconds
|
355
|
+
},
|
356
|
+
});
|
357
|
+
logUploadChanges(changes);
|
272
358
|
}
|
273
|
-
|
274
|
-
|
359
|
+
catch (error) {
|
360
|
+
console.error(styles.error(`\nError downloading ${error}`));
|
361
|
+
await analytics_1.LightdashAnalytics.track({
|
362
|
+
event: 'download.error',
|
363
|
+
properties: {
|
364
|
+
userId: config.user?.userUuid,
|
365
|
+
organizationId: config.user?.organizationUuid,
|
366
|
+
projectId,
|
367
|
+
error: `${error}`,
|
368
|
+
},
|
369
|
+
});
|
275
370
|
}
|
276
|
-
logUploadChanges(changes);
|
277
371
|
};
|
278
372
|
exports.uploadHandler = uploadHandler;
|
package/dist/handlers/login.js
CHANGED
@@ -30,9 +30,10 @@ const loginWithToken = async (url, token, proxyAuthorization) => {
|
|
30
30
|
throw new common_1.AuthorizationError(`Cannot sign in with token:\n${JSON.stringify(await response.json())}`);
|
31
31
|
}
|
32
32
|
const userBody = await response.json();
|
33
|
-
const { userUuid } = userBody;
|
33
|
+
const { userUuid, organizationUuid } = userBody;
|
34
34
|
return {
|
35
35
|
userUuid,
|
36
|
+
organizationUuid,
|
36
37
|
token,
|
37
38
|
};
|
38
39
|
};
|
@@ -71,7 +72,7 @@ const loginWithPassword = async (url) => {
|
|
71
72
|
if (header === null) {
|
72
73
|
throw new common_1.AuthorizationError(`Cannot sign in:\n${JSON.stringify(loginBody)}`);
|
73
74
|
}
|
74
|
-
const { userUuid } = loginBody.results;
|
75
|
+
const { userUuid, organizationUuid } = loginBody.results;
|
75
76
|
const cookie = header.split(';')[0].split('=')[1];
|
76
77
|
const patUrl = new url_1.URL(`/api/v1/user/me/personal-access-tokens`, url).href;
|
77
78
|
const now = new Date();
|
@@ -90,6 +91,7 @@ const loginWithPassword = async (url) => {
|
|
90
91
|
const { token } = patResponseBody.results;
|
91
92
|
return {
|
92
93
|
userUuid,
|
94
|
+
organizationUuid,
|
93
95
|
token,
|
94
96
|
};
|
95
97
|
};
|
@@ -109,7 +111,7 @@ const login = async (url, options) => {
|
|
109
111
|
console.error(`\n${styles.title('Warning')}: Login URL ${styles.secondary(url)} does not match a valid cloud server, perhaps you meant ${styles.secondary(cloudServer)} ?\n`);
|
110
112
|
}
|
111
113
|
const proxyAuthorization = process.env.LIGHTDASH_PROXY_AUTHORIZATION;
|
112
|
-
const { userUuid, token } = options.token
|
114
|
+
const { userUuid, token, organizationUuid } = options.token
|
113
115
|
? await loginWithToken(url, options.token, proxyAuthorization)
|
114
116
|
: await loginWithPassword(url);
|
115
117
|
globalState_1.default.debug(`> Logged in with userUuid: ${userUuid}`);
|
@@ -117,13 +119,14 @@ const login = async (url, options) => {
|
|
117
119
|
event: 'login.completed',
|
118
120
|
properties: {
|
119
121
|
userId: userUuid,
|
122
|
+
organizationId: organizationUuid,
|
120
123
|
url,
|
121
124
|
method: options.token ? 'token' : 'password',
|
122
125
|
},
|
123
126
|
});
|
124
127
|
await (0, config_1.setContext)({ serverUrl: url, apiKey: token });
|
125
128
|
globalState_1.default.debug(`> Saved config on: ${config_1.configFilePath}`);
|
126
|
-
await (0, config_1.setDefaultUser)(userUuid);
|
129
|
+
await (0, config_1.setDefaultUser)(userUuid, organizationUuid);
|
127
130
|
console.error(`\n ✅️ Login successful\n`);
|
128
131
|
try {
|
129
132
|
if (process.env.CI === 'true') {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lightdash/cli",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.1422.0",
|
4
4
|
"license": "MIT",
|
5
5
|
"bin": {
|
6
6
|
"lightdash": "dist/index.js"
|
@@ -11,8 +11,8 @@
|
|
11
11
|
],
|
12
12
|
"dependencies": {
|
13
13
|
"@actions/core": "^1.11.1",
|
14
|
-
"@lightdash/common": "^0.
|
15
|
-
"@lightdash/warehouses": "^0.
|
14
|
+
"@lightdash/common": "^0.1422.0",
|
15
|
+
"@lightdash/warehouses": "^0.1422.0",
|
16
16
|
"@types/columnify": "^1.5.1",
|
17
17
|
"ajv": "^8.11.0",
|
18
18
|
"ajv-formats": "^2.1.1",
|