Skip to content

Commit 307b9cc

Browse files
committed
#12 Supporting app builder migrations
Note: doesn't yet update the url link for the new app id
1 parent 267e398 commit 307b9cc

File tree

4 files changed

+160
-125
lines changed

4 files changed

+160
-125
lines changed

src/DataClient.ts

Lines changed: 105 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export abstract class DataClient {
4040

4141
abstract invalidateCache();
4242

43-
abstract createApplication(app: IApplication, blob: Blob): Promise<string | number>;
43+
abstract createApplication(app: IApplication, blob?: Blob): Promise<string | number>;
4444
abstract createManagedObject(managedObject: IManagedObject): Promise<string | number>;
4545
abstract createSimulator(simulatorConfig: Partial<ISimulatorConfig>): Promise<{ simulatorId: string, deviceIds: (string | number)[]}>;
4646
abstract createSmartRule(smartRuleConfig: ISmartRuleConfig): Promise<string|number>;
@@ -57,7 +57,7 @@ export abstract class DataClient {
5757
}
5858
): Promise<void>;
5959

60-
abstract updateApplication(app: IApplication, blob: Blob): Promise<string | number>;
60+
abstract updateApplication(app: IApplication, blob?: Blob): Promise<string | number>;
6161
abstract updateBinary(binary: IManagedObject, blob: Blob): Promise<string | number>;
6262
abstract updateManagedObject(managedObject: IManagedObject): Promise<string | number>;
6363
abstract updateSimulator(simulatorConfig: Partial<ISimulatorConfig>): Promise<{ simulatorId: string, deviceIds: (string | number)[]}>;
@@ -111,100 +111,112 @@ export abstract class DataClient {
111111
}
112112

113113
async findLinkedDashboardsFromApplication(app: IApplication & { id: string | number; binary: IManagedObject }, onDownloadProgress?: (progress: number) => any): Promise<IManagedObject[]> {
114-
const blob = await this.getApplicationBlob(app, onDownloadProgress);
115-
let zip;
116-
try {
117-
zip = await JSZip.loadAsync(blob);
118-
} catch(e) {
119-
console.debug('Not a zip file');
120-
return [];
121-
}
114+
// App builder apps include a list of dashboards in a custom field "applicationBuilder.dashboards"
115+
if ((app as any).applicationBuilder) {
116+
const dashboardIds = ((app as any).applicationBuilder.dashboards || []).map(dashboard => dashboard.id);
117+
const dashboardManagedObjects = await this.getDashboards();
118+
return dashboardManagedObjects.filter(dashboard => dashboardIds.includes(dashboard.id));
119+
// Other apps might have a zip file containing some js and html files that indicate which dashboards to use
120+
} else {
121+
const blob = await this.getApplicationBlob(app, onDownloadProgress);
122+
123+
// If there's no blob then the app has no binary
124+
if (blob == undefined) return [];
125+
126+
let zip;
127+
try {
128+
zip = await JSZip.loadAsync(blob);
129+
} catch (e) {
130+
console.debug('Not a zip file');
131+
return [];
132+
}
133+
134+
const files = zip.file(/.*\.(js|html)/);
135+
136+
const names = [];
137+
const partialNames = [];
138+
const ids = [];
139+
140+
await Promise.all(
141+
files.map(async (file) => {
142+
const text = await file.async('text');
143+
let matches;
144+
const nameRegex = /<c8y-dashboard-gridstack[^>]+?name=(["'])((\w|-)+?)\1.*?>/g;
145+
// noinspection JSAssignmentUsedAsCondition
146+
while (matches = nameRegex.exec(text)) {
147+
names.push(matches[2]);
148+
}
122149

123-
const files = zip.file(/.*\.(js|html)/);
124-
125-
const names = [];
126-
const partialNames = [];
127-
const ids = [];
128-
129-
await Promise.all(
130-
files.map(async (file) => {
131-
const text = await file.async('text');
132-
let matches;
133-
const nameRegex = /<c8y-dashboard-gridstack[^>]+?name=(["'])((\w|-)+?)\1.*?>/g;
134-
// noinspection JSAssignmentUsedAsCondition
135-
while (matches = nameRegex.exec(text)) {
136-
names.push(matches[2]);
137-
}
138-
139-
const nameCtrlRegex = /dashboard[-_]?[nN]ame=(["'])((\w|-)+?)\1/g;
140-
// noinspection JSAssignmentUsedAsCondition
141-
while (matches = nameCtrlRegex.exec(text)) {
142-
partialNames.push(matches[2] + '*');
143-
}
144-
145-
const nameJsRegex = /c8y_Dashboard!name!((\w|-)+)/g;
146-
// noinspection JSAssignmentUsedAsCondition
147-
while (matches = nameJsRegex.exec(text)) {
148-
partialNames.push(matches[1] + '*');
149-
}
150-
151-
// If the dashboard name is some combinations of a string and some templates, eg: 'mydashboard-{{vm.dashboardNumber}}' we might be able to guess all of the allGroups associated with an app by finding all 'mydashboard-*'
152-
const partialNameRegex = /<c8y-dashboard-gridstack[^>]+?name=(["'])(.+?)\1.*?>/g;
153-
// noinspection JSAssignmentUsedAsCondition
154-
while (matches = partialNameRegex.exec(text)) {
155-
if (!nameRegex.test(matches[0])) {
156-
const partialName = matches[2]
157-
.trim()
158-
.replace(/{{.*?}}/g, '*'); // replace angularjs template string with wildcard matcher
159-
// If the whole expression starts with '::' then it's a single run angular expression so ignore it (we can't find a dashboard from that)
160-
// If the expression has enough (3 or more) non-special characters then we'll assume it might be unique enough to find a dashboard
161-
if (!partialName.startsWith('::') && partialName.replace(/[-*_.:]/g, '').length >= 3) {
162-
partialNames.push(partialName);
150+
const nameCtrlRegex = /dashboard[-_]?[nN]ame=(["'])((\w|-)+?)\1/g;
151+
// noinspection JSAssignmentUsedAsCondition
152+
while (matches = nameCtrlRegex.exec(text)) {
153+
partialNames.push(matches[2] + '*');
154+
}
155+
156+
const nameJsRegex = /c8y_Dashboard!name!((\w|-)+)/g;
157+
// noinspection JSAssignmentUsedAsCondition
158+
while (matches = nameJsRegex.exec(text)) {
159+
partialNames.push(matches[1] + '*');
160+
}
161+
162+
// If the dashboard name is some combinations of a string and some templates, eg: 'mydashboard-{{vm.dashboardNumber}}' we might be able to guess all of the allGroups associated with an app by finding all 'mydashboard-*'
163+
const partialNameRegex = /<c8y-dashboard-gridstack[^>]+?name=(["'])(.+?)\1.*?>/g;
164+
// noinspection JSAssignmentUsedAsCondition
165+
while (matches = partialNameRegex.exec(text)) {
166+
if (!nameRegex.test(matches[0])) {
167+
const partialName = matches[2]
168+
.trim()
169+
.replace(/{{.*?}}/g, '*'); // replace angularjs template string with wildcard matcher
170+
// If the whole expression starts with '::' then it's a single run angular expression so ignore it (we can't find a dashboard from that)
171+
// If the expression has enough (3 or more) non-special characters then we'll assume it might be unique enough to find a dashboard
172+
if (!partialName.startsWith('::') && partialName.replace(/[-*_.:]/g, '').length >= 3) {
173+
partialNames.push(partialName);
174+
}
163175
}
164176
}
165-
}
166-
const idRegex = /<c8y-dashboard-gridstack[^>]+?id=(["'])((\w|-)+?)\1.*?>/g;
167-
// noinspection JSAssignmentUsedAsCondition
168-
while (matches = idRegex.exec(text)) {
169-
ids.push(matches[2]);
170-
}
171-
}));
172-
173-
const dashboardManagedObjects = await this.getDashboards();
174-
175-
return _.uniqBy([
176-
..._.flatMap(names, name => {
177-
const matchingManagedObject = dashboardManagedObjects.find(mo => mo[`c8y_Dashboard!name!${name}`] !== undefined);
178-
if (matchingManagedObject) {
179-
console.log('Found dashboard with name', name, matchingManagedObject);
180-
return [matchingManagedObject];
181-
} else {
182-
console.error('Unable to find dashboard with name', name);
183-
return [];
184-
}
185-
}),
186-
..._.flatMap(ids, id => {
187-
const matchingManagedObject = dashboardManagedObjects.find(mo => mo.id.toString() === id.toString());
188-
if (matchingManagedObject) {
189-
console.log('Found dashboard with id', id, matchingManagedObject);
190-
return [matchingManagedObject];
191-
} else {
192-
console.error('Unable to find dashboard with id', id);
193-
return [];
194-
}
195-
}),
196-
..._.flatMap(partialNames, wildCardName => {
197-
const wildCardRegex = RegExp('^c8y_Dashboard!name!' + wildCardName.split('*').map(expressionPart => _.escapeRegExp(expressionPart)).join('.*') + '$');
198-
const matchingManagedObjects = dashboardManagedObjects.filter(mo => Object.keys(mo).find(key => wildCardRegex.test(key)) !== undefined);
199-
if (matchingManagedObjects.length > 0) {
200-
console.log('Found allGroups with wild-card name', wildCardName, matchingManagedObjects);
201-
return matchingManagedObjects;
202-
} else {
203-
console.error('Unable to find dashboard with wild-card name', wildCardName);
204-
return [];
205-
}
206-
}),
207-
], dashboard => dashboard.id);
177+
const idRegex = /<c8y-dashboard-gridstack[^>]+?id=(["'])((\w|-)+?)\1.*?>/g;
178+
// noinspection JSAssignmentUsedAsCondition
179+
while (matches = idRegex.exec(text)) {
180+
ids.push(matches[2]);
181+
}
182+
}));
183+
184+
const dashboardManagedObjects = await this.getDashboards();
185+
186+
return _.uniqBy([
187+
..._.flatMap(names, name => {
188+
const matchingManagedObject = dashboardManagedObjects.find(mo => mo[`c8y_Dashboard!name!${name}`] !== undefined);
189+
if (matchingManagedObject) {
190+
console.log('Found dashboard with name', name, matchingManagedObject);
191+
return [matchingManagedObject];
192+
} else {
193+
console.error('Unable to find dashboard with name', name);
194+
return [];
195+
}
196+
}),
197+
..._.flatMap(ids, id => {
198+
const matchingManagedObject = dashboardManagedObjects.find(mo => mo.id.toString() === id.toString());
199+
if (matchingManagedObject) {
200+
console.log('Found dashboard with id', id, matchingManagedObject);
201+
return [matchingManagedObject];
202+
} else {
203+
console.error('Unable to find dashboard with id', id);
204+
return [];
205+
}
206+
}),
207+
..._.flatMap(partialNames, wildCardName => {
208+
const wildCardRegex = RegExp('^c8y_Dashboard!name!' + wildCardName.split('*').map(expressionPart => _.escapeRegExp(expressionPart)).join('.*') + '$');
209+
const matchingManagedObjects = dashboardManagedObjects.filter(mo => Object.keys(mo).find(key => wildCardRegex.test(key)) !== undefined);
210+
if (matchingManagedObjects.length > 0) {
211+
console.log('Found allGroups with wild-card name', wildCardName, matchingManagedObjects);
212+
return matchingManagedObjects;
213+
} else {
214+
console.error('Unable to find dashboard with wild-card name', wildCardName);
215+
return [];
216+
}
217+
}),
218+
], dashboard => dashboard.id);
219+
}
208220
};
209221

210222
async findLinkedFromDashboard(dashboard: IManagedObject):

src/FileDataClient.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,25 +92,30 @@ export class FileDataClient extends DataClient {
9292
}
9393
}
9494

95-
async createApplication(app: IApplication, blob: Blob): Promise<string | number> {
95+
async createApplication(app: IApplication, blob?: Blob): Promise<string | number> {
9696
app = _.cloneDeep(app);
9797
app.id = this.getNextManagedObjectId();
9898
const json = await this.exportJsonFormat;
99-
const binaryId = this.getNextManagedObjectId();
100-
app.activeVersionId = binaryId;
99+
if (blob != undefined) {
100+
app.activeVersionId = this.getNextManagedObjectId();
101+
}
101102
json.applications.push(app);
102-
json.managedObjects.push({
103-
owner: "file",
104-
name: `${app.name}.zip`,
105-
id: binaryId,
106-
c8y_applications_storage: "",
107-
c8y_IsBinary: "",
108-
length: 305,
109-
c8y_application_context_path: "file/unknown",
110-
contentType: "application/x-zip-compressed"
111-
} as any);
103+
if (blob != undefined) {
104+
json.managedObjects.push({
105+
owner: "file",
106+
name: `${app.name}.zip`,
107+
id: app.activeVersionId,
108+
c8y_applications_storage: "",
109+
c8y_IsBinary: "",
110+
length: 305,
111+
c8y_application_context_path: "file/unknown",
112+
contentType: "application/x-zip-compressed"
113+
} as any);
114+
}
112115
const zip = (await JSZip.loadAsync(this.file));
113-
zip.file(`binaries/${binaryId}.zip`, blob);
116+
if (blob != undefined) {
117+
zip.file(`binaries/${app.activeVersionId}.zip`, blob);
118+
}
114119
zip.file('data.json', JSON.stringify(await this.exportJsonFormat, undefined, 2));
115120
this.file = await zip.generateAsync({type: 'blob'});
116121
return app.id;

src/HttpDataClient.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ export class HttpDataClient extends DataClient {
8383
}
8484

8585
getApplicationBlob(app: (IApplication & {binary: IManagedObject}), onProgress?: (progress: number) => any) {
86+
if (app.binary == undefined) {
87+
return undefined;
88+
}
8689
return this.getBinaryBlob(app.binary, onProgress);
8790
}
8891

89-
async createApplication(app: IApplication, blob: Blob): Promise<string | number> {
92+
async createApplication(app: IApplication, blob?: Blob): Promise<string | number> {
9093
// Create the app
9194
const newApp = (await this.client.application.create(app)).data;
9295

@@ -182,20 +185,22 @@ export class HttpDataClient extends DataClient {
182185
};
183186
}
184187

185-
async updateApplication(app: IApplication, blob: Blob): Promise<string | number> {
186-
// Create the binary
187-
const fd = new FormData();
188-
fd.append('file', blob, `${app.name.replace(/\s/g, '-').replace(/[^a-zA-Z0-9\-]/g, '') || 'application'}.zip`);
189-
190-
app.activeVersionId = (await (await this.client.core.fetch(`/application/applications/${app.id}/binaries`, {
191-
method: 'POST',
192-
body: fd,
193-
headers: {
194-
Accept: 'application/json'
195-
}
196-
})).json()).id;
188+
async updateApplication(app: IApplication & {applicationBuilder?: any}, blob?: Blob): Promise<string | number> {
189+
if (blob != undefined) {
190+
// Create the binary
191+
const fd = new FormData();
192+
fd.append('file', blob, `${app.name.replace(/\s/g, '-').replace(/[^a-zA-Z0-9\-]/g, '') || 'application'}.zip`);
193+
194+
app.activeVersionId = (await (await this.client.core.fetch(`/application/applications/${app.id}/binaries`, {
195+
method: 'POST',
196+
body: fd,
197+
headers: {
198+
Accept: 'application/json'
199+
}
200+
})).json()).id;
201+
}
197202

198-
await this.client.application.update({id: app.id, activeVersionId: app.activeVersionId});
203+
await this.client.application.update({id: app.id, activeVersionId: app.activeVersionId, applicationBuilder: app.applicationBuilder} as IApplication);
199204

200205
return app.id
201206
}

src/migrate/migrate.component.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,11 @@ export class MigrateComponent {
366366
await Promise.all(this.appMigrations.map(async (appMigration) => {
367367
const blob = await sourceClient.getApplicationBlob(appMigration.application);
368368
if (appMigration.updateExisting) {
369-
return destinationClient.updateApplication(appMigration.updateExisting, blob);
369+
const app = MigrateComponent.appMigrationToApp(appMigration, oldIdsToNewIdsMap);
370+
app.id = appMigration.updateExisting.id;
371+
return destinationClient.updateApplication(app, blob);
370372
} else {
371-
return destinationClient.createApplication(MigrateComponent.appMigrationToApp(appMigration), blob);
373+
return destinationClient.createApplication(MigrateComponent.appMigrationToApp(appMigration, oldIdsToNewIdsMap), blob);
372374
}
373375
}));
374376

@@ -395,8 +397,8 @@ export class MigrateComponent {
395397
}
396398
}
397399

398-
static appMigrationToApp(appMigration: ApplicationMigration): IApplication {
399-
const result: IApplication = {};
400+
static appMigrationToApp(appMigration: ApplicationMigration, oldIdsToNewIds: Map<string, string|number>): IApplication {
401+
const result: IApplication & {applicationBuilder?: any} = {};
400402

401403
// Blacklist certain fields
402404
function isBlacklistedKey(key) {
@@ -431,6 +433,17 @@ export class MigrateComponent {
431433
_.set(result, path, _.get(appMigration.application, path));
432434
});
433435

436+
// Update application builder dashboard ids
437+
if (result.applicationBuilder && result.applicationBuilder.dashboards) {
438+
result.applicationBuilder.dashboards.forEach(dashboard => {
439+
if (oldIdsToNewIds.has(dashboard.id.toString())) {
440+
dashboard.id = oldIdsToNewIds.get(dashboard.id.toString());
441+
} else {
442+
// TODO: add to warning
443+
}
444+
})
445+
}
446+
434447
// Update the application with the user provided changes...
435448
result.contextPath = appMigration.newContextPath;
436449
result.name = appMigration.newName;

0 commit comments

Comments
 (0)