metabase-exporter 1.0.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/README.md +45 -0
- package/metabase-exporter-1.0.0.tgz +0 -0
- package/package.json +14 -0
- package/src/index.js +102 -0
- package/src/services/CardService.js +57 -0
- package/src/services/MetabaseService.js +418 -0
- package/src/services/fieldMappeerService.js +75 -0
- package/structure.json +3531 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Metabase-Replicator
|
|
2
|
+
|
|
3
|
+
Export/import collections from one Metabase instance to one or more target Metabase instances.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Clone collections (with hierarchy) from source → multiple targets
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Node.js 18+
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage (CLI)
|
|
20
|
+
|
|
21
|
+
Run the tool with source credentials and one or more target credentials.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node src/index.js --source_creds '{
|
|
25
|
+
"baseUrl": "https://source.metabase.example",
|
|
26
|
+
"userName": "admin@source",
|
|
27
|
+
"password": "******"
|
|
28
|
+
}' --target_creds '[
|
|
29
|
+
{
|
|
30
|
+
"baseUrl": "https://target1.metabase.example",
|
|
31
|
+
"userName": "admin@target1",
|
|
32
|
+
"password": "******"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"baseUrl": "https://target2.metabase.example",
|
|
36
|
+
"userName": "admin@target2",
|
|
37
|
+
"password": "******"
|
|
38
|
+
}
|
|
39
|
+
]'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### CLI Flags
|
|
43
|
+
|
|
44
|
+
- `--source_creds` (required): JSON object with `baseUrl`, `userName`, `password`
|
|
45
|
+
- `--target_creds` (required): JSON array of objects, each with `baseUrl`, `userName`, `password`
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "metabase-exporter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"metabase-exporter": "./src/index.js"
|
|
6
|
+
},
|
|
7
|
+
"description": "This is the cli tool which clones reports from single source metabse account to multiple destination accounts.",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"axios": "^1.12.2",
|
|
10
|
+
"commander": "^14.0.2"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["cli"],
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
const MetabaseService = require('./services/MetabaseService');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function sortTheCollectionasedOnHierachy(collectionList){
|
|
10
|
+
const sortedCollectionList = [];
|
|
11
|
+
let visitedCount = collectionList.length;
|
|
12
|
+
const visitedCollection = new Set()
|
|
13
|
+
|
|
14
|
+
while (visitedCount > 0) {
|
|
15
|
+
for (const collection of collectionList) {
|
|
16
|
+
if ((visitedCollection.has(collection.parent_id) || collection.parent_id == null) && !visitedCollection.has(collection.id)) {
|
|
17
|
+
visitedCount--;
|
|
18
|
+
visitedCollection.add(collection.id);
|
|
19
|
+
sortedCollectionList.push(collection);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return sortedCollectionList;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
program
|
|
29
|
+
.name('metabase-migrate')
|
|
30
|
+
.description('Metabase migrate CLI (parsing only)')
|
|
31
|
+
.requiredOption('--source_creds <json>', 'JSON: {"baseUrl":"...","userName":"...","password":"..."}')
|
|
32
|
+
.requiredOption('--target_creds <json>', 'JSON array: [{"baseUrl":"...","userName":"...","password":"..."}]');
|
|
33
|
+
|
|
34
|
+
program.parse(process.argv);
|
|
35
|
+
|
|
36
|
+
const opts = program.opts();
|
|
37
|
+
|
|
38
|
+
const sourceCreds = JSON.parse(opts.source_creds);
|
|
39
|
+
const targetCreds = JSON.parse(opts.target_creds);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if (!(sourceCreds.baseUrl && sourceCreds.userName && sourceCreds.password)) {
|
|
43
|
+
throw new Error(`Invalid input formate`);
|
|
44
|
+
}
|
|
45
|
+
const source = new MetabaseService(sourceCreds);
|
|
46
|
+
|
|
47
|
+
if (!targetCreds.length) throw new Error(`Invalid input formate`);
|
|
48
|
+
|
|
49
|
+
await source.login();
|
|
50
|
+
|
|
51
|
+
const collectionListSummary = await source.getCollections();
|
|
52
|
+
const collectionList = [];
|
|
53
|
+
for (const clSummary of collectionListSummary) {
|
|
54
|
+
collectionList.push(await source.getCollection(clSummary.id));
|
|
55
|
+
}
|
|
56
|
+
// console.log(collectionList.map((cl) => { return { parentId: cl.parent_id, id: cl.id }; }));
|
|
57
|
+
const sortedList = sortTheCollectionasedOnHierachy(collectionList);
|
|
58
|
+
|
|
59
|
+
for (const targetCred of targetCreds) {
|
|
60
|
+
try {
|
|
61
|
+
const target = new MetabaseService(targetCred);
|
|
62
|
+
await target.login();
|
|
63
|
+
|
|
64
|
+
// This stores the card, field, and table IDs from both the source and target Metabase instances in a single class.
|
|
65
|
+
await source.mapEntities(target);
|
|
66
|
+
for (const collection of sortedList) {
|
|
67
|
+
if (!collection.is_sample
|
|
68
|
+
&& !collection.personal_owner_id
|
|
69
|
+
&& collection.name != 'Automatically Generated Dashboards'
|
|
70
|
+
&& !collection.is_personal
|
|
71
|
+
) {
|
|
72
|
+
await source.cloneCollection(source, collection.id, collection.parent_id, target);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await source.cloneRemainingDependentItems(source, target);
|
|
77
|
+
|
|
78
|
+
for (const collection of sortedList) {
|
|
79
|
+
if (!collection.is_sample
|
|
80
|
+
&& !collection.personal_owner_id
|
|
81
|
+
&& collection.name != 'Automatically Generated Dashboards'
|
|
82
|
+
&& !collection.is_personal
|
|
83
|
+
) {
|
|
84
|
+
await source.cloneDashboards(source, collection.id, target);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await target.logout();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error(err);
|
|
91
|
+
console.error(`Login failed for ${targetCred.baseUrl}`);
|
|
92
|
+
console.error(`Skipping the replication`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await source.logout();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.log('error occured', err);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
})();
|
|
102
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const mapperService = require('./fieldMappeerService');
|
|
2
|
+
|
|
3
|
+
class CardService {
|
|
4
|
+
CloningTheDatasetQuery(srcQuery) {
|
|
5
|
+
this.cloneQuery(srcQuery);
|
|
6
|
+
return srcQuery;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
cloneQuery(srcQuery) {
|
|
10
|
+
if (Array.isArray(srcQuery)) {
|
|
11
|
+
for (let i = 0; i < srcQuery.length; i++) {
|
|
12
|
+
if (Array.isArray(srcQuery[i])) this.cloneQuery(srcQuery[i]);
|
|
13
|
+
|
|
14
|
+
else if (typeof srcQuery[i] == 'object') {
|
|
15
|
+
this.cloneQuery(srcQuery[i]);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
if (srcQuery[i] == 'field') {
|
|
19
|
+
const targetField = mapperService.getFieldId(srcQuery[i + 1]);
|
|
20
|
+
if (targetField) srcQuery[i + 1] = targetField;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
else if(typeof srcQuery == 'object') {
|
|
27
|
+
const keys = Object.keys(srcQuery);
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (Array.isArray(srcQuery[key])) this.cloneQuery(srcQuery[key]);
|
|
30
|
+
|
|
31
|
+
else if (typeof srcQuery[key] == 'object') this.cloneQuery(srcQuery[key]);
|
|
32
|
+
|
|
33
|
+
else {
|
|
34
|
+
if (key == 'source-table') srcQuery[key] = this.createNewId(srcQuery[key]);
|
|
35
|
+
|
|
36
|
+
if (key == 'database') srcQuery[key] = mapperService.getDatabaseId(srcQuery[key]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
createNewId(sourceTableId) {
|
|
43
|
+
if (typeof sourceTableId == 'string') {
|
|
44
|
+
const suffix = sourceTableId.split("__")[1];
|
|
45
|
+
const prefix = sourceTableId.split("__")[0];
|
|
46
|
+
|
|
47
|
+
const tableId = `${prefix}__${mapperService.getCardId(parseInt(suffix))}`;
|
|
48
|
+
return tableId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return mapperService.getTableId(sourceTableId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
module.exports = new CardService();
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
const mapperService = require('./fieldMappeerService');
|
|
4
|
+
const CardService = require('./CardService')
|
|
5
|
+
|
|
6
|
+
class MetabaseService {
|
|
7
|
+
#token;
|
|
8
|
+
|
|
9
|
+
constructor({ baseUrl, userName, password, token } = {}) {
|
|
10
|
+
this.baseUrl = String(baseUrl || '').replace(/\/+$/, '');
|
|
11
|
+
this.userName = userName;
|
|
12
|
+
this.password = password;
|
|
13
|
+
this.#token = token;
|
|
14
|
+
this.parentIdsMapping = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get headers() {
|
|
18
|
+
return {
|
|
19
|
+
'X-Metabase-Session': this.#token,
|
|
20
|
+
'Content-Type': 'application/json'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async login(username = this.userName, password = this.password) {
|
|
25
|
+
if (this.#token) return;
|
|
26
|
+
const { data } = await axios.post(`${this.baseUrl}/api/session`, { username, password });
|
|
27
|
+
if (!data?.id) throw new Error('Login failed');
|
|
28
|
+
this.#token = data.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async logout() {
|
|
32
|
+
if (!this.#token) return;
|
|
33
|
+
try {
|
|
34
|
+
await axios.delete(`${this.baseUrl}/api/session`, { headers: this.headers });
|
|
35
|
+
} catch (_) { }
|
|
36
|
+
this.#token = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async listCollections() {
|
|
40
|
+
const { data } = await axios.get(`${this.baseUrl}/api/collection`, { headers: this.headers });
|
|
41
|
+
return (data || []).filter(c => c.namespace !== 'personal');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getCollection(collectionID) {
|
|
45
|
+
const { data } = await axios.get(`${this.baseUrl}/api/collection/${collectionID}`, {
|
|
46
|
+
headers: this.headers
|
|
47
|
+
});
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getCollections() {
|
|
52
|
+
const { data } = await axios.get(`${this.baseUrl}/api/collection`, {
|
|
53
|
+
headers: this.headers
|
|
54
|
+
});
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getDashboards() {
|
|
59
|
+
const { data } = await axios.get(`${this.baseUrl}/api/dashboard`, {
|
|
60
|
+
headers: this.headers
|
|
61
|
+
});
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getCollectionItems(collectionID) {
|
|
66
|
+
const { data } = await axios.get(`${this.baseUrl}/api/collection/${collectionID}/items`, {
|
|
67
|
+
headers: this.headers,
|
|
68
|
+
});
|
|
69
|
+
return data?.data ?? [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async createCollection({ name, parent_id = null, description = null }) {
|
|
73
|
+
const { data } = await axios.post(
|
|
74
|
+
`${this.baseUrl}/api/collection`,
|
|
75
|
+
{ name, parent_id, description },
|
|
76
|
+
{ headers: this.headers }
|
|
77
|
+
);
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async ensureCollection({ name, parent_id }) {
|
|
82
|
+
return this.createCollection({ name, parent_id });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getCard(cardID) {
|
|
86
|
+
const { data } = await axios.get(`${this.baseUrl}/api/card/${cardID}`, {
|
|
87
|
+
headers: this.headers
|
|
88
|
+
});
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getCards() {
|
|
93
|
+
const { data } = await axios.get(`${this.baseUrl}/api/card`, {
|
|
94
|
+
headers: this.headers
|
|
95
|
+
});
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async createCard(cardData) {
|
|
100
|
+
try {
|
|
101
|
+
const { data } = await axios.post(`${this.baseUrl}/api/card`, cardData, {
|
|
102
|
+
headers: this.headers
|
|
103
|
+
});
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.log(`error creating card`, err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------- Dashboards ----------
|
|
112
|
+
async getDashboard(dashboardID) {
|
|
113
|
+
const { data } = await axios.get(`${this.baseUrl}/api/dashboard/${dashboardID}`, {
|
|
114
|
+
headers: this.headers
|
|
115
|
+
});
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async createDashboard({ name, description = null, collection_id, parameters = [] }) {
|
|
120
|
+
const { data } = await axios.post(
|
|
121
|
+
`${this.baseUrl}/api/dashboard`,
|
|
122
|
+
{ name, description, collection_id, parameters, parameter_mappings: [] },
|
|
123
|
+
{ headers: this.headers }
|
|
124
|
+
);
|
|
125
|
+
return data; // { id, ... }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deepCopy = x => JSON.parse(JSON.stringify(x ?? null));
|
|
129
|
+
|
|
130
|
+
async getDatabases() {
|
|
131
|
+
const { data } = await axios.get(`${this.baseUrl}/api/database?include=tables`, {
|
|
132
|
+
headers: this.headers
|
|
133
|
+
});
|
|
134
|
+
return data.data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async mapEntities(targetInstance) {
|
|
138
|
+
try {
|
|
139
|
+
const sourceDatabases = await this.getDatabases();
|
|
140
|
+
const targetDatabases = await targetInstance.getDatabases();
|
|
141
|
+
for (const sd of sourceDatabases) {
|
|
142
|
+
for (const td of targetDatabases) {
|
|
143
|
+
if (sd.name !== td.name) continue;
|
|
144
|
+
mapperService.mapDatabaseId(sd.id, td.id);
|
|
145
|
+
const sourceTables = sd.tables;
|
|
146
|
+
const targetTables = td.tables;
|
|
147
|
+
|
|
148
|
+
for (const sT of sourceTables) {
|
|
149
|
+
for (const tT of targetTables) {
|
|
150
|
+
if (sT.name !== tT.name) continue;
|
|
151
|
+
mapperService.mapTableId(sT.id, tT.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sourceFields = await this.getFields(sd.id);
|
|
156
|
+
const targetFields = await targetInstance.getFields(td.id);
|
|
157
|
+
|
|
158
|
+
for (const sF of sourceFields) {
|
|
159
|
+
for (const tF of targetFields) {
|
|
160
|
+
if (tF.table_id == mapperService.getTableId(sF.table_id) && sF.name === tF.name)
|
|
161
|
+
mapperService.mapFieldId(sF.id, tF.id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const cards = await targetInstance.getCards();
|
|
168
|
+
const dashboards = await targetInstance.getDashboards();
|
|
169
|
+
|
|
170
|
+
for (const card of cards) mapperService.setCardByName(card.name, card);
|
|
171
|
+
for (const dashboard of dashboards) mapperService.setDashBoardByName(dashboard.name, dashboard);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.log("Error in mapping Entitites", error);
|
|
175
|
+
throw new Error("Error in mapping Entities");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getFields(dbId) {
|
|
180
|
+
const { data } = await axios.get(
|
|
181
|
+
`${this.baseUrl}/api/database/${dbId}/fields`,
|
|
182
|
+
{ headers: this.headers }
|
|
183
|
+
);
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async cloneCollection(sourceInstance, sourceCollectionId, collectionParentId, targetInstance) {
|
|
188
|
+
const srcCollection = await sourceInstance.getCollection(sourceCollectionId);
|
|
189
|
+
try {
|
|
190
|
+
console.log(`Creating the collection ${srcCollection.name}...`);
|
|
191
|
+
|
|
192
|
+
const targetCollections = await targetInstance.listCollections();
|
|
193
|
+
const foundedCollection = targetCollections.find(tg => tg.name == srcCollection.name);
|
|
194
|
+
|
|
195
|
+
if (foundedCollection) {
|
|
196
|
+
this.parentIdsMapping.set(srcCollection.id, foundedCollection.id);
|
|
197
|
+
mapperService.mapCollectionId(sourceCollectionId, foundedCollection.id);
|
|
198
|
+
console.log(`${srcCollection.name} Already exists`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const targetCollection = await targetInstance.ensureCollection({
|
|
203
|
+
name: srcCollection.name,
|
|
204
|
+
parent_id: this.parentIdsMapping.get(collectionParentId),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.parentIdsMapping.set(srcCollection.id, targetCollection.id);
|
|
208
|
+
|
|
209
|
+
mapperService.mapCollectionId(sourceCollectionId, targetCollection.id);
|
|
210
|
+
|
|
211
|
+
console.log(`${srcCollection.name} created`);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.log(err);
|
|
215
|
+
console.log(`Error while creating collection ${srcCollection.name}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async cloneRemainingDependentItems(sourceInstance, targetInstance) {
|
|
220
|
+
try {
|
|
221
|
+
const cards = await this.getCards();
|
|
222
|
+
let nonDependentCards = cards.filter((card) => !card.source_card_id);
|
|
223
|
+
let dependeCards = cards.filter((card) => card.source_card_id);
|
|
224
|
+
|
|
225
|
+
await this.cloneCardsWhichAreDependent(nonDependentCards, targetInstance);
|
|
226
|
+
await this.cloneCardsWhichAreDependent(dependeCards, targetInstance);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.log(err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async cloneDashboards(sourceInstance, sourceCollectionId, targetInstance) {
|
|
235
|
+
const collectionItems = await this.getCollectionItems(sourceCollectionId);
|
|
236
|
+
|
|
237
|
+
const targetCollectionId = mapperService.getCollectionId(sourceCollectionId);
|
|
238
|
+
|
|
239
|
+
const items = collectionItems.filter((item) => item.model == 'dashboard');
|
|
240
|
+
for (const item of items) {
|
|
241
|
+
const srcDashboard = await sourceInstance.getDashboard(item.id);
|
|
242
|
+
|
|
243
|
+
if (!srcDashboard) {
|
|
244
|
+
console.warn(`${srcDashboard.id} was not found in existing metabase instance`);
|
|
245
|
+
continue;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const targetDashboardPayload = {
|
|
249
|
+
description: srcDashboard.description,
|
|
250
|
+
collection_position: srcDashboard.collection_position,
|
|
251
|
+
enable_embedding: false,
|
|
252
|
+
collection_id: targetCollectionId,
|
|
253
|
+
name: srcDashboard.name,
|
|
254
|
+
width: srcDashboard.width,
|
|
255
|
+
caveats: srcDashboard.caveats,
|
|
256
|
+
can_restore: srcDashboard.can_restore,
|
|
257
|
+
parameters: this.deepCopy(srcDashboard.parameters),
|
|
258
|
+
auto_apply_filters: !!srcDashboard.auto_apply_filters
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
let createdDashboard;
|
|
262
|
+
const existingTargetDashboard = mapperService.getDashBoardByName(srcDashboard.name);
|
|
263
|
+
if (existingTargetDashboard && existingTargetDashboard.collection_id == mapperService.getCollectionId(srcDashboard.collection_id)) {
|
|
264
|
+
createdDashboard = await targetInstance.updateDashBoard(existingTargetDashboard.id, targetDashboardPayload);
|
|
265
|
+
}
|
|
266
|
+
else createdDashboard = await targetInstance.createDashboard(targetDashboardPayload);
|
|
267
|
+
|
|
268
|
+
const tabsPayload = [];
|
|
269
|
+
let tabId = -1;
|
|
270
|
+
const tabIdMapping = new Map();
|
|
271
|
+
for (const tab of srcDashboard?.tabs) {
|
|
272
|
+
tabsPayload.push({
|
|
273
|
+
id: tabId,
|
|
274
|
+
dashboard_id: createdDashboard.id,
|
|
275
|
+
name: tab.name,
|
|
276
|
+
position: tab.position,
|
|
277
|
+
});
|
|
278
|
+
tabIdMapping.set(tab.id, tabId--);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dashCardsPayloads = [];
|
|
282
|
+
let tempid = -1;
|
|
283
|
+
for (const srcDashcard of srcDashboard?.dashcards) {
|
|
284
|
+
const newCardId = mapperService.getCardId(srcDashcard.card_id);
|
|
285
|
+
|
|
286
|
+
if (!newCardId) {
|
|
287
|
+
console.warn(`${createdDashboard.name} doesn't have card id for target instance so skipping this dashboard cloning..`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const destinationCard = await targetInstance.getCard(newCardId);
|
|
292
|
+
destinationCard.dashboard_id = createdDashboard.id;
|
|
293
|
+
await targetInstance.updateCard(newCardId, destinationCard);
|
|
294
|
+
|
|
295
|
+
const dashCardPaylod = {
|
|
296
|
+
id: tempid--,
|
|
297
|
+
size_x: srcDashcard.size_x,
|
|
298
|
+
size_y: srcDashcard.size_y,
|
|
299
|
+
dashboard_tab_id: tabIdMapping.get(srcDashcard.dashboard_tab_id),
|
|
300
|
+
row: srcDashcard.row,
|
|
301
|
+
col: srcDashcard.col,
|
|
302
|
+
visualization_settings: this.deepCopy(srcDashcard.visualization_settings),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (srcDashcard.card_id) {
|
|
306
|
+
dashCardPaylod.card_id = newCardId;
|
|
307
|
+
dashCardPaylod.parameter_mappings = srcDashcard.parameter_mappings.map((pm) => { const copy = this.deepCopy(pm); copy.card_id = newCardId; return copy; });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (srcDashcard.card) {
|
|
311
|
+
dashCardPaylod.card = destinationCard
|
|
312
|
+
}
|
|
313
|
+
dashCardsPayloads.push(dashCardPaylod);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
createdDashboard.dashcards = dashCardsPayloads;
|
|
317
|
+
createdDashboard.tabs = tabsPayload;
|
|
318
|
+
|
|
319
|
+
await targetInstance.updateDashBoard(createdDashboard.id, createdDashboard);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async cloneCardsWhichAreDependent(items, targetInstance) {
|
|
324
|
+
for (const item of items) {
|
|
325
|
+
if (mapperService.getCardId(item.id)) continue;
|
|
326
|
+
const srcCard = await this.getCard(item.id);
|
|
327
|
+
const targetCollectionId = mapperService.getCollectionId(srcCard.collection_id);
|
|
328
|
+
|
|
329
|
+
if (srcCard.collection_id && !targetCollectionId) {
|
|
330
|
+
// console.warn(`${srcCard.name} does not have collection id for target instance so skipping the cloning.`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log(`cloning the card ${srcCard.name}`);
|
|
335
|
+
await this.copyThisCard(srcCard, targetInstance, targetCollectionId);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async copyThisCard(srcCard, targetInstance, targetCollectionId) {
|
|
340
|
+
const cardPayload = {
|
|
341
|
+
name: srcCard.name,
|
|
342
|
+
description: srcCard.description ?? null,
|
|
343
|
+
collection_id: targetCollectionId,
|
|
344
|
+
display: srcCard.display,
|
|
345
|
+
visualization_settings: this.deepCopy(srcCard.visualization_settings) || {},
|
|
346
|
+
parameter_mappings: this.deepCopy(srcCard.parameter_mappings),
|
|
347
|
+
dataset_query: this.deepCopy(srcCard.dataset_query) || {},
|
|
348
|
+
type: srcCard.type || null,
|
|
349
|
+
cache_ttl: srcCard.cache_ttl ?? null,
|
|
350
|
+
archived: false,
|
|
351
|
+
collection_position: srcCard?.collection_position || null,
|
|
352
|
+
};
|
|
353
|
+
if (srcCard.source_card_id) {
|
|
354
|
+
const targetSourceCardId = mapperService.getCardId(srcCard.source_card_id);
|
|
355
|
+
if (!targetSourceCardId) {
|
|
356
|
+
console.warn(`${srcCard.name} does not have source_card_id for target instance to skipping the cloning`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
cardPayload.source_card_id = targetSourceCardId;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
cardPayload.dataset_query = CardService.CloningTheDatasetQuery(srcCard.dataset_query);
|
|
364
|
+
|
|
365
|
+
const existingCardWithSameName = mapperService.getCardByName(srcCard.name);
|
|
366
|
+
if (existingCardWithSameName &&
|
|
367
|
+
(
|
|
368
|
+
mapperService.getCollectionId(srcCard.collection_id) == existingCardWithSameName.collection_id
|
|
369
|
+
|| (srcCard.collection_id == null && existingCardWithSameName.collection_id == null)
|
|
370
|
+
)){
|
|
371
|
+
await targetInstance.updateCard(existingCardWithSameName.id, cardPayload);
|
|
372
|
+
mapperService.mapCardId(srcCard.id, existingCardWithSameName.id);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const targetCard = await targetInstance.createCard(cardPayload);
|
|
377
|
+
mapperService.mapCardId(srcCard.id, targetCard.id);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async updateDashBoard(dashboardId, payload) {
|
|
381
|
+
const { data } = await axios.put(
|
|
382
|
+
`${this.baseUrl}/api/dashboard/${dashboardId}`,
|
|
383
|
+
payload,
|
|
384
|
+
{ headers: this.headers }
|
|
385
|
+
);
|
|
386
|
+
return data;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async updateCard(cardId, data) {
|
|
390
|
+
await axios.put(`${this.baseUrl}/api/card/${cardId}`,
|
|
391
|
+
data,
|
|
392
|
+
{ headers: this.headers }
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// async deleteduplicateCards() {
|
|
398
|
+
// const cards = await this.getCards();
|
|
399
|
+
// const cardsToDelete = cards.filter((c) => c.created_at.includes('2025-12-10'));
|
|
400
|
+
|
|
401
|
+
// let count = cardsToDelete.length - 1;
|
|
402
|
+
|
|
403
|
+
// while (count >= 0) {
|
|
404
|
+
// console.log(cardsToDelete[count].name);
|
|
405
|
+
// await this.deleteCard(cardsToDelete[count].id);
|
|
406
|
+
// count--;
|
|
407
|
+
// }
|
|
408
|
+
// }
|
|
409
|
+
|
|
410
|
+
// async deleteCard(cardId) {
|
|
411
|
+
// await axios.delete(`${this.baseUrl}/api/card/${cardId}`, {
|
|
412
|
+
// headers: this.headers
|
|
413
|
+
// });
|
|
414
|
+
// }
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = MetabaseService;
|