@lightdash/cli 0.1418.1 → 0.1419.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -49,7 +49,13 @@ export declare const lightdashApi: <T extends string | boolean | unknown[] | imp
49
49
  owner: string;
50
50
  } | import("@lightdash/common").KnexPaginatedData<import("@lightdash/common").ApiMetricsCatalogResults> | import("@lightdash/common").MetricsExplorerQueryResults | import("@lightdash/common").KnexPaginatedData<import("@lightdash/common").Group[] | import("@lightdash/common").GroupWithMembers[]> | {
51
51
  tagUuid: string;
52
- } | import("@lightdash/common").ChartAsCode[] | {
52
+ } | {
53
+ charts: import("@lightdash/common").ChartAsCode[];
54
+ missingIds: string[];
55
+ } | {
56
+ dashboards: import("@lightdash/common").DashboardAsCode[];
57
+ missingIds: string[];
58
+ } | {
53
59
  edges: import("@lightdash/common").CatalogMetricsTreeEdge[];
54
60
  } | import("@lightdash/common").MetricTotalResults | null | undefined>({ method, url, body, }: LightdashApiProps) => Promise<T>;
55
61
  export declare const checkLightdashVersion: () => Promise<void>;
@@ -1,5 +1,7 @@
1
1
  export type DownloadHandlerOptions = {
2
2
  verbose: boolean;
3
+ charts: string[];
4
+ dashboards: string[];
3
5
  force: boolean;
4
6
  };
5
7
  export declare const downloadHandler: (options: DownloadHandlerOptions) => Promise<void>;
@@ -10,11 +10,26 @@ const yaml = tslib_1.__importStar(require("js-yaml"));
10
10
  const path = tslib_1.__importStar(require("path"));
11
11
  const config_1 = require("../config");
12
12
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
13
+ const styles = tslib_1.__importStar(require("../styles"));
13
14
  const apiClient_1 = require("./dbt/apiClient");
14
15
  const DOWNLOAD_FOLDER = 'lightdash';
16
+ /*
17
+ This function is used to parse the content filters.
18
+ It can be slugs, uuids or urls
19
+ We remove the URL part (if any) and return a list of `slugs or uuids` that can be used in the API call
20
+ */
21
+ const parseContentFilters = (items) => {
22
+ if (items.length === 0)
23
+ return '';
24
+ const parsedItems = items.map((item) => {
25
+ const uuidMatch = item.match(/https?:\/\/.+\/(?:saved|dashboards)\/([a-f0-9-]+)/i);
26
+ return uuidMatch ? uuidMatch[1] : item;
27
+ });
28
+ return `?${new URLSearchParams(parsedItems.map((item) => ['ids', item])).toString()}`;
29
+ };
15
30
  const dumpIntoFiles = async (folder, items) => {
16
31
  const outputDir = path.join(process.cwd(), DOWNLOAD_FOLDER, folder);
17
- console.info(`Writting ${items.length} ${folder} into ${outputDir}`);
32
+ console.info(`Writing ${items.length} ${folder} into ${outputDir}`);
18
33
  // Make directory
19
34
  const created = await fs_1.promises.mkdir(outputDir, { recursive: true });
20
35
  if (created)
@@ -80,22 +95,43 @@ const downloadHandler = async (options) => {
80
95
  if (!projectId) {
81
96
  throw new Error('No project selected. Run lightdash config set-project');
82
97
  }
98
+ // If any filter is provided, we skip those items without filters
99
+ // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
100
+ const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
83
101
  // Download charts
84
- globalState_1.default.debug('Downloading charts');
85
- const chartsAsCode = await (0, apiClient_1.lightdashApi)({
86
- method: 'GET',
87
- url: `/api/v1/projects/${projectId}/charts/code`,
88
- body: undefined,
89
- });
90
- await dumpIntoFiles('charts', chartsAsCode);
102
+ if (hasFilters && options.charts.length === 0) {
103
+ console.info(styles.warning(`No charts filters provided, skipping`));
104
+ }
105
+ else {
106
+ globalState_1.default.debug(`Downloading charts`);
107
+ const chartFilters = parseContentFilters(options.charts);
108
+ const chartsAsCode = await (0, apiClient_1.lightdashApi)({
109
+ method: 'GET',
110
+ url: `/api/v1/projects/${projectId}/charts/code${chartFilters}`,
111
+ body: undefined,
112
+ });
113
+ chartsAsCode.missingIds.forEach((missingId) => {
114
+ console.warn(styles.warning(`No chart with id "${missingId}"`));
115
+ });
116
+ await dumpIntoFiles('charts', chartsAsCode.charts);
117
+ }
91
118
  // Download dashboards
92
- globalState_1.default.debug('Downloading dashboards');
93
- const dashboardsAsCode = await (0, apiClient_1.lightdashApi)({
94
- method: 'GET',
95
- url: `/api/v1/projects/${projectId}/dashboards/code`,
96
- body: undefined,
97
- });
98
- await dumpIntoFiles('dashboards', dashboardsAsCode);
119
+ if (hasFilters && options.dashboards.length === 0) {
120
+ console.info(styles.warning(`No dashboards filters provided, skipping`));
121
+ }
122
+ else {
123
+ globalState_1.default.debug(`Downloading dashboards`);
124
+ const dashboardFilters = parseContentFilters(options.dashboards);
125
+ const dashboardsAsCode = await (0, apiClient_1.lightdashApi)({
126
+ method: 'GET',
127
+ url: `/api/v1/projects/${projectId}/dashboards/code${dashboardFilters}`,
128
+ body: undefined,
129
+ });
130
+ dashboardsAsCode.missingIds.forEach((missingId) => {
131
+ console.warn(styles.warning(`No dashboard with id "${missingId}"`));
132
+ });
133
+ await dumpIntoFiles('dashboards', dashboardsAsCode.dashboards);
134
+ }
99
135
  // TODO delete files if chart don't exist ?*/
100
136
  };
101
137
  exports.downloadHandler = downloadHandler;
@@ -127,13 +163,31 @@ const logUploadChanges = (changes) => {
127
163
  console.info(`Total ${key}: ${value} `);
128
164
  });
129
165
  };
130
- const upsertResources = async (type, projectId, changes, force) => {
166
+ /**
167
+ *
168
+ * @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
169
+ */
170
+ const upsertResources = async (type, projectId, changes, force, slugs) => {
131
171
  const items = await readCodeFiles(type);
132
172
  console.info(`Found ${items.length} ${type} files`);
133
- for (const item of items) {
173
+ const hasFilter = slugs.length > 0;
174
+ const filteredItems = hasFilter
175
+ ? items.filter((item) => slugs.includes(item.slug))
176
+ : items;
177
+ if (hasFilter) {
178
+ console.info(`Filtered ${filteredItems.length} ${type} with slugs: ${slugs.join(', ')}`);
179
+ const missingItems = slugs.filter((slug) => !items.find((item) => item.slug === slug));
180
+ missingItems.forEach((slug) => {
181
+ console.warn(styles.warning(`No ${type} with slug: "${slug}"`));
182
+ });
183
+ }
184
+ for (const item of filteredItems) {
134
185
  // If a chart fails to update, we keep updating the rest
135
186
  try {
136
187
  if (!force && !item.needsUpdating) {
188
+ if (hasFilter) {
189
+ console.warn(styles.warning(`Skipping ${type} "${item.slug}" with no local changes`));
190
+ }
137
191
  globalState_1.default.debug(`Skipping ${type} "${item.slug}" with no local changes`);
138
192
  changes[`${type} skipped`] =
139
193
  (changes[`${type} skipped`] ?? 0) + 1;
@@ -169,8 +223,8 @@ const uploadHandler = async (options) => {
169
223
  throw new Error('No project selected. Run lightdash config set-project');
170
224
  }
171
225
  let changes = {};
172
- changes = await upsertResources('charts', projectId, changes, options.force);
173
- changes = await upsertResources('dashboards', projectId, changes, options.force);
226
+ changes = await upsertResources('charts', projectId, changes, options.force, options.charts);
227
+ changes = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards);
174
228
  logUploadChanges(changes);
175
229
  };
176
230
  exports.uploadHandler = uploadHandler;
package/dist/index.js CHANGED
@@ -194,11 +194,15 @@ commander_1.program
194
194
  .command('download')
195
195
  .description('Downloads charts and dashboards as code')
196
196
  .option('--verbose', undefined, false)
197
+ .option('-c, --charts <charts...>', 'specify chart slugs, uuids, or urls to download', [])
198
+ .option('-d, --dashboards <dashboards...>', 'specify dashboard slugs, uuids or urls to download', [])
197
199
  .action(download_1.downloadHandler);
198
200
  commander_1.program
199
201
  .command('upload')
200
202
  .description('Uploads charts and dashboards as code')
201
203
  .option('--verbose', undefined, false)
204
+ .option('-c, --charts <charts...>', 'specify chart slugs to force upload', [])
205
+ .option('-d, --dashboards <dashboards...>', 'specify dashboard slugs to force upload', [])
202
206
  .option('--force', 'Force upload even if local files have not changed, use this when you want to upload files to a new project', false)
203
207
  .action(download_1.uploadHandler);
204
208
  commander_1.program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.1418.1",
3
+ "version": "0.1419.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.1418.1",
15
- "@lightdash/warehouses": "^0.1418.1",
14
+ "@lightdash/common": "^0.1419.0",
15
+ "@lightdash/warehouses": "^0.1419.0",
16
16
  "@types/columnify": "^1.5.1",
17
17
  "ajv": "^8.11.0",
18
18
  "ajv-formats": "^2.1.1",