@mindline/sync 1.0.28 → 1.0.30

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/index.ts CHANGED
@@ -1,15 +1,14 @@
1
- //index.js
2
-
1
+ //index.ts - published interface - AAD implementations, facade to Mindline Config API
2
+ import { IPublicClientApplication, AuthenticationResult } from "@azure/msal-browser"
3
3
  import { deserializeArray } from 'class-transformer';
4
- import { IPublicClientApplication } from '@azure/msal-browser';
5
- import { getTenantInfo } from './hybridspa';
4
+ import { adminDelete, adminPost, adminsGet, configDelete, configsGet, configPost, configPut, graphConfig, initPost, tenantPut, tenantPost, tenantDelete, tenantsGet, workspacesGet} from './hybridspa';
5
+ import { version } from './package.json';
6
6
  import users from "./users.json";
7
- import targets from "./targets.json";
7
+ import tenants from "./tenants.json";
8
8
  import configs from "./configs.json";
9
9
  import workspaces from "./workspaces.json";
10
-
10
+ import tasksData from "./tasks";
11
11
  const FILTER_FIELD = "workspaceIDs";
12
-
13
12
  // called by unit tests
14
13
  export function sum(a: number, b: number): number {
15
14
  return a + b;
@@ -17,8 +16,12 @@ export function sum(a: number, b: number): number {
17
16
  export function helloNpm() : string {
18
17
  return "hello NPM";
19
18
  }
20
-
21
- class User {
19
+ export class Group {
20
+ id: string;
21
+ displayName: string;
22
+ description: string;
23
+ }
24
+ export class User {
22
25
  oid: string;
23
26
  name: string;
24
27
  mail: string;
@@ -28,30 +31,50 @@ class User {
28
31
  companyDomain: string;
29
32
  associatedWorkspaces: string[];
30
33
  workspaceIDs: string;
31
- session: string;
32
- spacode: string;
33
- accessToken: string;
34
+ session: string; // button text
35
+ spacode: string; // to get front end access token
36
+ accessToken: string; // front end access token
37
+ loginHint: string; // to help sign out without prompt
38
+ scopes: string[]; // to detect if incremental consent has happened
39
+ authTS: Date; // timestamp user was authenticated
34
40
  constructor() {
35
41
  this.oid = "";
36
42
  this.name = "";
37
43
  this.mail = "";
38
- this.authority = "";
44
+ this.authority = "https://login.microsoftonline.com/organizations/v2.0";
39
45
  this.tid = "";
40
46
  this.companyName = "";
41
47
  this.companyDomain = "";
42
48
  this.associatedWorkspaces = new Array();
43
49
  this.workspaceIDs = "";
44
- this.session = "";
50
+ this.session = "Sign In";
45
51
  this.spacode = "";
46
52
  this.accessToken = "";
53
+ this.loginHint = "";
54
+ this.scopes = new Array();
55
+ this.authTS = new Date(0);
47
56
  }
48
57
  }
49
-
50
- class Target {
58
+ export enum TenantType {
59
+ invalid = 0,
60
+ aad = 1,
61
+ ad = 2,
62
+ googleworkspace = 3
63
+ }
64
+ type TenantTypeStrings = keyof typeof TenantType;
65
+ export enum TenantPermissionType {
66
+ read = 1,
67
+ write = 2,
68
+ notassigned = 3
69
+ }
70
+ type TenantPermissionTypeStrings = keyof typeof TenantPermissionType;
71
+ export class Tenant {
51
72
  tid: string;
52
73
  name: string;
53
74
  domain: string;
54
- type: string;
75
+ tenantType: TenantTypeStrings;
76
+ permissionType: TenantPermissionTypeStrings;
77
+ onboarded: string;
55
78
  authority: string;
56
79
  readServicePrincipal: string;
57
80
  writeServicePrincipal: string;
@@ -60,65 +83,142 @@ class Target {
60
83
  this.tid = "";
61
84
  this.name = "";
62
85
  this.domain = "";
63
- this.type = "";
64
- this.authority = "";
86
+ this.tenantType = "aad";
87
+ this.permissionType = "notassigned";
88
+ this.onboarded = "false";
89
+ this.authority = "https://login.microsoftonline.com/organizations/v2.0";
65
90
  this.readServicePrincipal = "";
66
91
  this.writeServicePrincipal = "";
67
92
  this.workspaceIDs = "";
68
93
  }
69
94
  }
70
-
71
- class TargetConfigInfo {
95
+ export enum TenantConfigType {
96
+ source = 1,
97
+ target = 2,
98
+ sourcetarget = 3
99
+ }
100
+ export type TenantConfigTypeStrings = keyof typeof TenantConfigType;
101
+ export class TenantConfigInfo {
72
102
  tid: string;
73
- sourceGroups: string[];
74
- targetGroup: string;
103
+ sourceGroupId: string;
104
+ sourceGroupName: string;
105
+ configurationTenantType: TenantConfigTypeStrings;
106
+ constructor(){
107
+ this.tid = "";
108
+ this.sourceGroupId = "";
109
+ this.sourceGroupName = "";
110
+ this.configurationTenantType = "source";
111
+ }
75
112
  }
76
-
77
- class Config {
113
+ export class Config {
78
114
  id: string;
115
+ workspaceId: string;
79
116
  name: string;
80
117
  description: string;
81
- targetConfigs: TargetConfigInfo[];
82
- enabled: boolean;
118
+ tenants: TenantConfigInfo[];
119
+ isEnabled: boolean;
83
120
  workspaceIDs: string;
84
121
  constructor(){
85
122
  this.id = "";
86
123
  this.name = "";
87
124
  this.description = "";
88
- this.targetConfigs = new Array();
89
- this.enabled = false;
125
+ this.tenants = new Array();
126
+ this.isEnabled = false;
90
127
  this.workspaceIDs = "";
91
128
  }
92
129
  }
93
-
94
- class Workspace {
130
+ export class Workspace {
95
131
  id: string;
96
132
  name: string;
97
133
  associatedUsers: string[];
98
- associatedTargets: string[];
134
+ associatedTenants: string[];
99
135
  associatedConfigs: string[];
100
136
  constructor(){
101
137
  this.id = "";
102
138
  this.name = "";
103
139
  this.associatedUsers = new Array();
104
- this.associatedTargets = new Array();
140
+ this.associatedTenants = new Array();
105
141
  this.associatedConfigs = new Array();
106
142
  }
107
143
  }
108
-
144
+ // check for localStorage availability
145
+ function storageAvailable(type) {
146
+ let storage;
147
+ try {
148
+ storage = window[type];
149
+ const x = "__storage_test__";
150
+ storage.setItem(x, x);
151
+ storage.removeItem(x);
152
+ return true;
153
+ } catch (e) {
154
+ return (
155
+ e instanceof DOMException &&
156
+ // everything except Firefox
157
+ (e.code === 22 ||
158
+ // Firefox
159
+ e.code === 1014 ||
160
+ // test name field too, because code might not be present
161
+ // everything except Firefox
162
+ e.name === "QuotaExceededError" ||
163
+ // Firefox
164
+ e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
165
+ // acknowledge QuotaExceededError only if there's something already stored
166
+ storage &&
167
+ storage.length !== 0
168
+ );
169
+ }
170
+ }
109
171
  export class InitInfo {
110
172
  us: User[];
111
- ts: Target[];
173
+ ts: Tenant[];
112
174
  cs: Config[];
113
175
  ws: Workspace[];
114
- constructor(){
115
- this.us = new Array();
116
- this.ts = new Array();
117
- this.cs = new Array();
118
- this.ws = new Array();
176
+ constructor(bClearLocalStorage: boolean) {
177
+ this.init(bClearLocalStorage);
178
+ }
179
+ // get initial data from localStorage or file
180
+ init(bClearLocalStorage: boolean): void {
181
+ console.log(`Calling InitInfo::init(bClearLocalStorage: ${bClearLocalStorage?"true":"false"})`);
182
+ // if we have a non-zero value stored, read it from localStorage
183
+ if (storageAvailable("localStorage")) {
184
+ let result = localStorage.getItem("InitInfo");
185
+ if (result != null && typeof result === "string" && result !== "") {
186
+ let initInfoString: string = result;
187
+ let iiReadFromLocalStorage: InitInfo = JSON.parse(initInfoString);
188
+ if (iiReadFromLocalStorage.us.length !== 0) {
189
+ if(bClearLocalStorage) { localStorage.removeItem("InitInfo"); }
190
+ else{
191
+ this.#initFromObjects(iiReadFromLocalStorage);
192
+ return;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // if storage unavailable or we were just asked to clear, read from default files to enable usable UI
198
+ var usersString = JSON.stringify(users);
199
+ var tenantsString = JSON.stringify(tenants);
200
+ var configsString = JSON.stringify(configs);
201
+ var workspacesString = JSON.stringify(workspaces);
202
+ try {
203
+ this.us = deserializeArray(User, usersString);
204
+ this.ts = deserializeArray(Tenant, tenantsString);
205
+ this.cs = deserializeArray(Config, configsString);
206
+ this.ws = deserializeArray(Workspace, workspacesString);
207
+ this.tagWithWorkspaces();
208
+ } catch (e) {
209
+ debugger;
210
+ }
211
+ }
212
+ save(): void{
213
+ let initInfoString: string = JSON.stringify(this);
214
+ localStorage.setItem("InitInfo", initInfoString);
119
215
  }
120
216
  tagWithWorkspaces(): boolean {
121
- // for each Workspace tag associated User, Target, Config with Workspace.id
217
+ // first clear everyone's workspaceIDs
218
+ this.us.map((item) => item.workspaceIDs = "");
219
+ this.ts.map((item) => item.workspaceIDs = "");
220
+ this.cs.map((item) => item.workspaceIDs = "");
221
+ // for each workspace tag WorkspaceIDs of associated Users, Tenants, Configs
122
222
  for (let workspace of this.ws) {
123
223
  // find matching Users to tag with this workspace
124
224
  for (let userID of workspace.associatedUsers) {
@@ -133,15 +233,15 @@ export class InitInfo {
133
233
  return false;
134
234
  }
135
235
  }
136
- // find matching Targets to tag with this workspace
137
- for (let targetID of workspace.associatedTargets) {
138
- let target = this.ts.find(
139
- (currentTarget) => currentTarget.tid === targetID
236
+ // find matching Tenants to tag with this workspace
237
+ for (let tenantID of workspace.associatedTenants) {
238
+ let tenant = this.ts.find(
239
+ (currentTenant) => currentTenant.tid === tenantID
140
240
  );
141
- if (target !== undefined) {
142
- // we found the target
143
- target[FILTER_FIELD] += workspace.id;
144
- target[FILTER_FIELD] += " ";
241
+ if (tenant !== undefined) {
242
+ // we found the tenant
243
+ tenant[FILTER_FIELD] += workspace.id;
244
+ tenant[FILTER_FIELD] += " ";
145
245
  } else {
146
246
  // we should not have InitInfo missing Workspace components
147
247
  debugger;
@@ -166,99 +266,805 @@ export class InitInfo {
166
266
  }
167
267
  return true;
168
268
  }
269
+ #initFromObjects(ii: InitInfo) : void {
270
+ // user array is the only one that has a Date that must be re-created on every read
271
+ if(typeof ii.us === "undefined") this.us = new Array();
272
+ else this.us = ii.us.map((user: User) => { user.authTS = new Date(user.authTS); return user } );
273
+ if(typeof ii.ts === "undefined") this.ts = new Array();
274
+ else this.ts = ii.ts;
275
+ if(typeof ii.cs === "undefined") this.cs = new Array();
276
+ else this.cs = ii.cs;
277
+ if(typeof ii.ws === "undefined") this.ws = new Array();
278
+ else this.ws = ii.ws;
279
+ }
169
280
  }
170
-
171
- // get hardcoded data from JSON
172
- function DummyInit(ii: InitInfo)
173
- {
174
- var usersString = JSON.stringify(users);
175
- var targetsString = JSON.stringify(targets);
176
- var configsString = JSON.stringify(configs);
177
- var workspacesString = JSON.stringify(workspaces);
281
+ export type TaskType = "initialization" |
282
+ "authenticate user" |
283
+ "reload React" |
284
+ "GET tenant details" |
285
+ "POST config init" |
286
+ "GET workspaces" |
287
+ "onboard tenant" |
288
+ "create 2nd tenant" |
289
+ "invite 2nd admin" |
290
+ "onboard 2nd tenant" |
291
+ "create config";
292
+ export class TaskArray {
293
+ tasks: Task[];
294
+ constructor(bClearLocalStorage: boolean) {
295
+ this.tasks = [ new Task() ];
296
+ this.init(bClearLocalStorage);
297
+ }
298
+ // get initial data from localStorage or file
299
+ init(bClearLocalStorage: boolean): void {
300
+ console.log(`Calling TaskArray::init(bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`);
301
+ // first clear task array
302
+ this.tasks.length = 0;
303
+ // then clear localStorage if we have been asked to
304
+ if (bClearLocalStorage) {
305
+ if (storageAvailable("localStorage")) localStorage.removeItem("Tasks");
306
+ }
307
+ // then try localStorage
308
+ if (storageAvailable("localStorage")) {
309
+ let result = localStorage.getItem("Tasks");
310
+ if (result != null && typeof result === "string" && result !== "") {
311
+ // properly create Tasks and Dates from retrieved string
312
+ let tasksString: string = result;
313
+ let taskArray: TaskArray = JSON.parse(tasksString);
314
+ this.tasks = this.#initTasksFromObjects(taskArray.tasks);
315
+ let l = this.tasks.length;
316
+ if (l !== 0) return;
317
+ }
318
+ }
319
+ // if here, there was nothing in localStorage, use initialization file
320
+ this.tasks = this.#initTasksFromObjects(tasksData);
321
+ }
322
+ // set start time for a task
323
+ setTaskStart(taskType: TaskType, startDate: Date): void {
324
+ let task: Task | undefined = this.#findTask(taskType);
325
+ if (task != undefined && task != null) {
326
+ task.setStart(startDate);
327
+ task.status = "in progress";
328
+ this.#save();
329
+ }
330
+ else {
331
+ debugger;
332
+ }
333
+ }
334
+ // set end time for a task
335
+ setTaskEnd(taskType: TaskType, endDate: Date, status: string): void {
336
+ let task: Task | undefined = this.#findTask(taskType);
337
+ if (task != undefined && task != null) {
338
+ task.setEnd(endDate);
339
+ task.status = status;
340
+ this.#save();
341
+ }
342
+ else {
343
+ debugger;
344
+ }
345
+ }
346
+ //
347
+ // private
348
+ //
349
+ #findTask(taskType: TaskType): Task | undefined {
350
+ let task: Task | undefined = this.tasks.find(t => t.task == taskType);
351
+ if (task == undefined || task == null) {
352
+ for(task of this.tasks){
353
+ if(task.subtasks != undefined && task.subtasks != null){
354
+ task = task.subtasks.find(t => t.task == taskType);
355
+ if(task != undefined && task != null) break;
356
+ }
357
+ }
358
+ }
359
+ return task;
360
+ }
361
+ #initTasksFromObjects(tasks: Task[]): Task[]{
362
+ return tasks.map((t: Task) => {
363
+ let newTask: Task = new Task();
364
+ newTask.id = t.id;
365
+ newTask.task = t.task;
366
+ newTask.setStart(new Date(t.start));
367
+ newTask.setEnd(new Date(t.end));
368
+ newTask.expected = t.expected;
369
+ newTask.status = t.status;
370
+ newTask.expanded = t.expanded;
371
+ if(typeof t.subtasks !== "undefined" && t.subtasks != null){
372
+ newTask.subtasks = t.subtasks.map((st: Task) => {
373
+ let newSubtask: Task = new Task();
374
+ newSubtask.id = st.id;
375
+ newSubtask.task = st.task;
376
+ newSubtask.setStart(new Date(st.start))
377
+ newSubtask.setEnd(new Date(st.end));
378
+ newSubtask.expected = st.expected;
379
+ newSubtask.status = st.status;
380
+ newSubtask.expanded = st.expanded;
381
+ return newSubtask;
382
+ } )
383
+ }
384
+ return newTask;
385
+ } );
386
+ }
387
+ #save(): void{
388
+ let taskArrayString: string = JSON.stringify(this);
389
+ if (storageAvailable("localStorage")) {
390
+ localStorage.setItem("Tasks", taskArrayString);
391
+ }
392
+ }
393
+ }
394
+ export class Task {
395
+ id: number;
396
+ task: string;
397
+ start: Date;
398
+ startDisplay: string;
399
+ end: Date;
400
+ endDisplay: string;
401
+ elapsedDisplay: string;
402
+ expected: number;
403
+ status: string;
404
+ expanded: boolean;
405
+ subtasks: Task[];
406
+ setEnd(endDate: Date): void{
407
+ this.end = endDate;
408
+ this.endDisplay = `${this.end.getMinutes().toString().padStart(2, "0")}:${this.end.getSeconds().toString().padStart(2, "0")}`;
409
+ let minuteAdjustment: number = 0;
410
+ let elapsedSeconds: number = this.end.getSeconds() - this.start.getSeconds();
411
+ if (elapsedSeconds < 0) { elapsedSeconds += 60; minuteAdjustment = -1; }
412
+ let elapsedMinutes: number = this.end.getMinutes() - this.start.getMinutes() + minuteAdjustment;
413
+ if (elapsedMinutes < 0) elapsedMinutes += 60;
414
+ this.elapsedDisplay = `${elapsedMinutes.toString().padStart(2, "0")}:${elapsedSeconds.toString().padStart(2, "0")}`;
415
+ };
416
+ setStart(startDate: Date): void{
417
+ this.start = startDate;
418
+ this.startDisplay = `${this.start.getMinutes().toString().padStart(2, "0")}:${this.start.getSeconds().toString().padStart(2, "0")}`;
419
+ };
420
+ }
421
+ // class corresponding to an execution of a Config - a *TenantNode* for each source tenant, each with a *TenantNode* array of target tenants
422
+ export class BatchArray {
423
+ tenantNodes: TenantNode[];
424
+ constructor(
425
+ config: Config | null,
426
+ syncPortalGlobalState: InitInfo | null,
427
+ bClearLocalStorage: boolean
428
+ ) {
429
+ this.tenantNodes = new Array<TenantNode>();
430
+ this.init(config, syncPortalGlobalState, bClearLocalStorage);
431
+ }
432
+ // populate tenantNodes based on config tenants
433
+ init(
434
+ config: Config | null,
435
+ syncPortalGlobalState: InitInfo | null,
436
+ bClearLocalStorage: boolean
437
+ ) : void {
438
+ console.log(
439
+ `Calling BatchArray::init(config: "${
440
+ config ? config.name : "null"
441
+ }", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
442
+ );
443
+ // first clear batch array
444
+ this.tenantNodes.length = 0;
445
+ // then clear localStorage if we have been asked to
446
+ if (bClearLocalStorage) {
447
+ if (storageAvailable("localStorage"))
448
+ localStorage.removeItem(config.name);
449
+ }
450
+ // create BatchArray if passed Config and InitInfo
451
+ if (
452
+ config != null &&
453
+ config.tenants != null &&
454
+ syncPortalGlobalState != null
455
+ ) {
456
+ // create a sourceTenantNode for each Source and SourceTarget
457
+ config.tenants.map((tciPotentialSource: TenantConfigInfo) => {
458
+ if (
459
+ tciPotentialSource.configurationTenantType === "source" ||
460
+ tciPotentialSource.configurationTenantType === "sourcetarget"
461
+ ) {
462
+ let sourceTenant = syncPortalGlobalState.ts.find(
463
+ (t) => t.tid === tciPotentialSource.tid
464
+ );
465
+ if (sourceTenant != null) {
466
+ let sourceTenantNode: TenantNode = new TenantNode(
467
+ tciPotentialSource.tid,
468
+ sourceTenant.name
469
+ );
470
+ this.tenantNodes.push(sourceTenantNode);
471
+ } else {
472
+ console.log(
473
+ `Error: no tenant found for config source tenant ${config.name}`
474
+ );
475
+ debugger;
476
+ return;
477
+ }
478
+ }
479
+ });
480
+ // create targetTenantNodes for each non-matching Target and SourceTarget
481
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
482
+ config.tenants.map((tciPotentialTarget: TenantConfigInfo) => {
483
+ // is this a valid target?
484
+ if (
485
+ tciPotentialTarget.configurationTenantType === "target" ||
486
+ tciPotentialTarget.configurationTenantType === "sourcetarget"
487
+ ) {
488
+ // is this a valid target that does not match this source?
489
+ if (tciPotentialTarget.tid !== sourceTenantNode.tid) {
490
+ let targetTenant = syncPortalGlobalState.ts.find(
491
+ (t) => t.tid === tciPotentialTarget.tid
492
+ );
493
+ if (targetTenant != null) {
494
+ let targetTenantNode: TenantNode = new TenantNode(
495
+ tciPotentialTarget.tid,
496
+ targetTenant.name
497
+ );
498
+ sourceTenantNode.targets.push(targetTenantNode);
499
+ sourceTenantNode.expanded = true;
500
+ } else {
501
+ console.log(
502
+ `Error: no tenant found for config target tenant ${config.name}`
503
+ );
504
+ debugger;
505
+ return;
506
+ }
507
+ }
508
+ }
509
+ });
510
+ });
511
+ // then try localStorage to find any matching source tenant metrics
512
+ if (storageAvailable("localStorage")) {
513
+ let result = localStorage.getItem(config.name);
514
+ if (result != null && typeof result === "string" && result !== "") {
515
+ // TODO: retrieve any relevant stored statistics from localStorage
516
+ // let batchArrayString: string = result;
517
+ // let batchArray: BatchArray = JSON.parse(batchArrayString);
518
+ // batchArray.batches.map((batch: Batch) => {
519
+ // config.tenants.map((tciTarget: TenantConfigInfo) => {
520
+ // if(tciTarget.tid !== batch.tid) {
521
+ // let target: Target = new Target(tciTarget.tid);
522
+ // batch.targets.push(target);
523
+ // }
524
+ // });
525
+ // });
526
+ }
527
+ }
528
+ }
529
+ }
530
+ // cycle through test state machine
531
+ test() : void {
532
+ if (this.tenantNodes == null || this.tenantNodes.length == 0) {
533
+ // we should not have an empty batch array for a test
534
+ debugger;
535
+ }
536
+ // cycle through desired states for sources and targets
537
+ if (this.tenantNodes != null) {
538
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
539
+ if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
540
+ else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
541
+ else sourceTenantNode.update(0, 0, 0, 0);
542
+ if (sourceTenantNode.targets != null) {
543
+ sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
544
+ if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
545
+ else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
546
+ else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
547
+ else targetTenantNode.update(0, 0, 0, 0);
548
+ });
549
+ }
550
+ });
551
+ }
552
+ }
553
+ }
554
+ export class TenantNode {
555
+ expanded: boolean;
556
+ status: string;
557
+ name: string;
558
+ tid: string;
559
+ total: number;
560
+ read: number;
561
+ written: number;
562
+ deferred: number;
563
+ targets: TenantNode[];
564
+ constructor(tid: string, name: string) {
565
+ this.expanded = false;
566
+ this.name = name;
567
+ this.tid = tid;
568
+ this.targets = new Array<TenantNode>();
569
+ this.update(0, 0, 0, 0);
570
+ }
571
+ update(total: number, read: number, written: number, deferred: number): void {
572
+ this.total = total;
573
+ this.read = read;
574
+ this.written = written;
575
+ this.deferred = deferred;
576
+ if(this.read === 0 && this.written === 0) this.status = "not started";
577
+ if(this.read > 0) {
578
+ if(this.read < this.total) this.status = "in progress";
579
+ else if(this.read === this.total) this.status = "complete";
580
+ }
581
+ else if(this.written > 0) {
582
+ if(this.written + this.deferred < this.total) this.status = "in progress";
583
+ else if(this.written === this.total) this.status = "complete";
584
+ else if(this.written + this.deferred === this.total) this.status = "failed";
585
+ }
586
+ }
587
+ }
588
+ export class APIResult {
589
+ result: boolean;
590
+ status: number;
591
+ error: string;
592
+ array: Array<Object> | null;
593
+ constructor() { this.result = true; this.status = 200; this.error = ""; this.array = null; }
594
+ }
595
+ //
596
+ // Azure AD Graph API
597
+ //
598
+ //groupGet - GET /groups/{id}
599
+ export async function groupGet(tenant: Tenant, groupid: string): Promise<{group: string, error: string}> {
600
+ // need a read or write access token to get graph users
601
+ let accessToken: string = "";
602
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
603
+ accessToken = tenant.readServicePrincipal;
604
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
605
+ accessToken = tenant.writeServicePrincipal;
606
+ if(accessToken === "") return { group: "", error: "no access token specified" };
607
+ // prepare Authorization headers as part of options
608
+ const headers = new Headers();
609
+ const bearer = `Bearer ${accessToken}`;
610
+ headers.append("Authorization", bearer);
611
+ let options = { method: "GET", headers: headers };
612
+ // make /groups endpoint call
178
613
  try {
179
- ii.us = deserializeArray(User, usersString);
180
- ii.ts = deserializeArray(Target, targetsString);
181
- ii.cs = deserializeArray(Config, configsString);
182
- ii.ws = deserializeArray(Workspace, workspacesString);
183
- if(!ii.tagWithWorkspaces()) return false;
184
- } catch (e) {
185
- debugger;
186
- return false;
187
- }
188
- return true;
189
- }
190
- // retrieve Workspace(s), User(s), Target(s), Config(s) given logged in user
191
- // three InitGet scenarios
192
- // scenario 1: empty user array, read defaults
193
- // scenario 2: user array with dummy user at end, return without doing anything
194
- // scenario 3: user array with non-dummy user at end, call back end Init
195
- // (1) TODO: Azure AD: lookup companyDomain and companyName
196
- // look up tenant name
197
- // look up tenant domain
198
- // TODO: Mindline: post complete user to init endpoint
199
- // create and push partial tenant at end of target array
200
- // post user and partial tenant to InitInfo
201
- // Return: success return value, need to query for associations
202
- // (2) Sync NPM: ii.ws: once InitInfo completes, retrieve workspaces for the new user
203
- // TODO: Mindline: retrieve associated workspaces
204
- // Returns: associated workspaces
205
- // (3) Sync NPM:
206
- // TODO: Mindline: retrieve associated admins, targets for each workspace
207
- // ii.us / ii.ts / ii.cs: query components of each associated workspaces
208
- // Returns: users, targets, configs for each workspace
209
- export function InitGet(ii: InitInfo, instance: IPublicClientApplication, debug: boolean): boolean
210
- {
211
- // if empty user array, retrieve dummy data from JSON to populate UI
212
- let l = ii.us.length;
213
- if(l === 0) return DummyInit(ii);
214
-
215
- // if last user is a dummy user, then we have nothing
216
- let user:User = ii.us[l-1];
217
- if(user.oid === "1") return true;
218
-
219
- // we have a real user: remove existing dummy user, target, config, workspace
220
- let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
221
- if(dummyIndex!==-1) ii.us.splice(dummyIndex, 1);
222
- dummyIndex = ii.ts.findIndex((u) => u.tid === "1");
223
- if(dummyIndex!==-1) ii.ts.splice(dummyIndex, 1);
224
- dummyIndex = ii.cs.findIndex((u) => u.id === "1");
225
- if(dummyIndex!==-1) ii.cs.splice(dummyIndex, 1);
226
- dummyIndex = ii.ws.findIndex((u) => u.id === "1");
227
- if(dummyIndex!==-1) ii.ws.splice(dummyIndex, 1);
228
-
229
- // why would instance be null here? investigate!
230
- if(instance===null) {
231
- debugger;
232
- return false;
233
- }
234
-
235
- // valid user, query AAD for associated company name and domain
236
- if(debug) debugger;
237
- getTenantInfo(user, instance);
238
- return true;
239
- }
240
-
241
- function AddTarget(): boolean
242
- {
243
- return true;
614
+ let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/${groupid}`;
615
+ let response = await fetch(groupsEndpoint, options);
616
+ let data = await response.json();
617
+ if(typeof data.error !== "undefined"){
618
+ return { group: "", error: `${data.error.code}: ${data.error.message}` };
619
+ }
620
+ return { group: data.value, error: `` };
621
+ }
622
+ catch(error: any) {
623
+ console.log(error);
624
+ return { group: "", error: `Exception: ${error}` };
625
+ }
626
+ }
627
+ //groupsGet - GET /groups
628
+ export async function groupsGet(tenant: Tenant, groupSearchString: string): Promise<{groups: Group[], error: string}> {
629
+ // need a read or write access token to get graph users
630
+ let accessToken: string = "";
631
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
632
+ accessToken = tenant.readServicePrincipal;
633
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
634
+ accessToken = tenant.writeServicePrincipal;
635
+ if(accessToken === "") return { groups: [], error: "no access token specified" };
636
+ // prepare Authorization headers as part of options
637
+ const headers = new Headers();
638
+ const bearer = `Bearer ${accessToken}`;
639
+ headers.append("Authorization", bearer);
640
+ let options = { method: "GET", headers: headers };
641
+ // make /groups endpoint call
642
+ try {
643
+ let groupsEndpoint = `${graphConfig.graphGroupsEndpoint}/?$filter=startsWith(displayName, '${groupSearchString}')`;
644
+ let response = await fetch(groupsEndpoint, options);
645
+ let data = await response.json();
646
+ if(typeof data.error !== "undefined"){
647
+ return { groups: [], error: `${data.error.code}: ${data.error.message}` };
648
+ }
649
+ return { groups: data.value, error: `` };
650
+ }
651
+ catch(error: any) {
652
+ console.log(error);
653
+ return { group: "", error: `Exception: ${error}` };
654
+ }
244
655
  }
245
-
246
- function CompleteTarget(): boolean
656
+ export function signIn(user: User, tasks: TaskArray): void {
657
+ let tenantURL: string = window.location.href;
658
+ tenantURL += "MicrosoftIdentity/Account/Challenge";
659
+ let url: URL = new URL(tenantURL);
660
+ url.searchParams.append("redirectUri", window.location.origin);
661
+ url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
662
+ url.searchParams.append("domainHint", "organizations");
663
+ if (user.oid !== "1"){
664
+ url.searchParams.append("loginHint", user.mail);
665
+ }
666
+ tasks.setTaskStart("initialization", new Date());
667
+ tasks.setTaskStart("authenticate user", new Date());
668
+ window.location.assign(url.href);
669
+ }
670
+ export function signInIncrementally(user: User, scope: string): void {
671
+ if (user.oid == "1") return;
672
+ let tenantURL: string = window.location.href;
673
+ tenantURL += "MicrosoftIdentity/Account/Challenge";
674
+ let url: URL = new URL(tenantURL);
675
+ url.searchParams.append("redirectUri", window.location.origin);
676
+ let scopes = scope;
677
+ url.searchParams.append("scope", scopes);
678
+ url.searchParams.append("domainHint", "organizations");
679
+ url.searchParams.append("loginHint", user.mail);
680
+ window.location.assign(url.href);
681
+ }
682
+ export function signOut(user: User): void {
683
+ if (user.oid == "1") return;
684
+ // these lines provide more callbacks during logout
685
+ //let tenantURL: string = window.location.href;
686
+ //tenantURL += "MicrosoftIdentity/Account/SignOut";
687
+ // this line takes advantage of our saved loginHint to logout right away, but requires additional cleanup logic
688
+ // https://aaddevsup.azurewebsites.net/2022/03/how-to-logout-of-an-oauth2-application-without-getting-prompted-to-select-a-user/
689
+ let tenantURL: string = "https://login.microsoftonline.com/common/oauth2/logout";
690
+ let url: URL = new URL(tenantURL);
691
+ url.searchParams.append("post_logout_redirect_uri", window.location.origin);
692
+ url.searchParams.append("logout_hint", user.loginHint);
693
+ window.location.assign(url.href);
694
+ }
695
+ //tenantRelationshipsGetByDomain - query AAD for associated company name and id
696
+ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
697
+ if (debug) debugger;
698
+ // do we already have a valid tenant name? if so, nothing to add
699
+ if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
700
+ // if needed, retrieve and cache access token
701
+ if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
702
+ console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
703
+ try {
704
+ let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
705
+ loggedInUser.accessToken = response.accessToken; // cache access token on the user
706
+ console.log("Front end token acquired: " + loggedInUser.accessToken.slice(0,20));
707
+ }
708
+ catch(error: any) {
709
+ console.log("Front end token failure: " + error);
710
+ return false; // failed to get access token, no need to re-render
711
+ }
712
+ }
713
+ // prepare Authorization headers as part of options
714
+ const headers = new Headers();
715
+ const bearer = `Bearer ${loggedInUser.accessToken}`;
716
+ headers.append("Authorization", bearer);
717
+ let options = { method: "GET", headers: headers };
718
+ // make tenant endpoint call
719
+ try {
720
+ // create tenant info endpoint
721
+ var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
722
+ tenantEndpoint += "(domainName='";
723
+ tenantEndpoint += tenant.domain;
724
+ tenantEndpoint += "')";
725
+ console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
726
+ let response = await fetch(tenantEndpoint, options);
727
+ let data = await response.json();
728
+ if(data) {
729
+ if(typeof data.error !== "undefined") {
730
+ console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
731
+ return false;
732
+ }
733
+ else if (typeof data.displayName !== undefined && data.displayName !== "") {
734
+ // set domain information on passed tenant
735
+ tenant.tid = data.tenantId;
736
+ tenant.name = data.displayName;
737
+ console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
738
+ return true; // success, need UX to re-render
739
+ }
740
+ }
741
+ else{
742
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
743
+ }
744
+ }
745
+ catch(error: any) {
746
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
747
+ return false; // failed, no need for UX to re-render
748
+ }
749
+ return false; // failed, no need for UX to re-render
750
+ }
751
+ //tenantRelationshipsGetById - query AAD for associated company name and domain
752
+ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
753
+ if (debug) debugger;
754
+ // do we already have a valid company name? if so, nothing to add, no need for UX to re-render
755
+ if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
756
+ // if needed, retrieve and cache access token
757
+ if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
758
+ try {
759
+ let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
760
+ user.accessToken = response.accessToken; // cache access token
761
+ console.log("Front end token acquired: " + user.accessToken.slice(0,20));
762
+ }
763
+ catch(error: any) {
764
+ console.log("Front end token failure: " + error);
765
+ return false; // failed to get access token, no need to re-render
766
+ }
767
+ }
768
+ // prepare Authorization headers as part of options
769
+ const headers = new Headers();
770
+ const bearer = `Bearer ${user.accessToken}`;
771
+ headers.append("Authorization", bearer);
772
+ let options = { method: "GET", headers: headers };
773
+ // make tenant endpoint call
774
+ try {
775
+ // create tenant info endpoint
776
+ var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
777
+ tenantEndpoint += "(tenantId='";
778
+ tenantEndpoint += user.tid;
779
+ tenantEndpoint += "')";
780
+ // track time of tenant details query
781
+ tasks.setTaskStart("GET tenant details", new Date());
782
+ console.log("Attempting GET from /findTenantInformationByTenantId:", tenantEndpoint);
783
+ let response = await fetch(tenantEndpoint, options);
784
+ let data = await response.json();
785
+ if(data && typeof data.displayName !== undefined && data.displayName !== "") {
786
+ // set domain information on user
787
+ user.companyName = data.displayName;
788
+ user.companyDomain = data.defaultDomainName;
789
+ // set domain information on tenant
790
+ let tenant: Tenant | undefined = ii.ts.find((t) => t.tid === user.tid);
791
+ if(tenant !== undefined){
792
+ tenant.name = data.displayName;
793
+ tenant.domain = data.defaultDomainName;
794
+ }
795
+ else{
796
+ console.log("tenantRelationshipsGetById: missing associated tenant for logged in user.");
797
+ debugger;
798
+ }
799
+ console.log("Successful GET from /findTenantInformationByTenantId: ", data.displayName);
800
+ tasks.setTaskEnd("GET tenant details", new Date(), "complete");
801
+ return true; // success, need UX to re-render
802
+ }
803
+ else{
804
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
805
+ }
806
+ }
807
+ catch(error: any) {
808
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
809
+ tasks.setTaskEnd("GET tenant details", new Date(), "failed");
810
+ return false; // failed, no need for UX to re-render
811
+ }
812
+ tasks.setTaskEnd("GET tenant details", new Date(), "failed");
813
+ return false; // failed, no need for UX to re-render
814
+ }
815
+ //usersGet - GET from AAD Users endpoint
816
+ export async function usersGet(tenant: Tenant): Promise<{users: string[], error: string}> {
817
+ // need a read or write access token to get graph users
818
+ let accessToken: string = "";
819
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.read])
820
+ accessToken = tenant.readServicePrincipal;
821
+ if(tenant.permissionType === TenantPermissionType[TenantPermissionType.write])
822
+ accessToken = tenant.writeServicePrincipal;
823
+ if(accessToken === "") return { users: [], error: "no access token specified" };
824
+ // prepare Authorization headers as part of options
825
+ const headers = new Headers();
826
+ const bearer = `Bearer ${accessToken}`;
827
+ headers.append("Authorization", bearer);
828
+ let options = { method: "GET", headers: headers };
829
+ // make /users endpoint call
830
+ try {
831
+ let response = await fetch(graphConfig.graphUsersEndpoint, options);
832
+ let data = await response.json();
833
+ if(typeof data.error !== "undefined"){
834
+ return { users: [], error: `${data.error.code}: ${data.error.message}` };
835
+ }
836
+ let users = new Array<User>();
837
+ for (let user of data.value) {
838
+ users.push(user.mail);
839
+ }
840
+ return { users: users, error: `` };
841
+ }
842
+ catch(error: any) {
843
+ console.log(error);
844
+ return { users: [], error: `Exception: ${error}` };
845
+ }
846
+ }
847
+ //
848
+ // Mindline Config API
849
+ //
850
+ export async function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
851
+ let result: APIResult = new APIResult();
852
+ if (config.id === "1") {
853
+ result = await configPost(instance, authorizedUser, config, workspaceId, debug);
854
+ }
855
+ else {
856
+ result = await configPut(instance, authorizedUser, config, debug);
857
+ }
858
+ return result;
859
+ }
860
+ export async function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
861
+ return configDelete(instance, authorizedUser, config, workspaceId, debug);
862
+ }
863
+ // retrieve Workspace(s), User(s), Tenant(s), Config(s) given newly logged in user
864
+ export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult>
247
865
  {
248
- return true;
866
+ let result: APIResult = new APIResult();
867
+ if (debug) debugger;
868
+ // get tenant name and domain from AAD
869
+ result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
870
+ // if this is the first time, we have just gotten tenant info, then we must POST user and not-yet-onboarded tenant to back end
871
+ if (result.result) {
872
+ tasks.setTaskStart("POST config init", new Date());
873
+ result = await initPost(instance, authorizedUser, user, debug);
874
+ tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
875
+ }
876
+ // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
877
+ if (result.result) {
878
+ tasks.setTaskStart("GET workspaces", new Date());
879
+ result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
880
+ tasks.setTaskEnd("GET workspaces", new Date(), result ? "complete" : "failed");
881
+ }
882
+ if(result.result) result.error = version;
883
+ return result;
884
+ }
885
+ export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
886
+ return tenantPost(instance, authorizedUser, tenant, workspaceId);
887
+ }
888
+ export async function tenantComplete(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, debug: boolean): Promise<APIResult> {
889
+ return tenantPut(instance, authorizedUser, tenant, debug);
890
+ }
891
+ export async function tenantRemove(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string, debug: boolean): Promise<APIResult> {
892
+ return tenantDelete(instance, authorizedUser, tenant, workspaceId, debug);
893
+ }
894
+ export async function userAdd(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
895
+ return adminPost(instance, authorizedUser, user, workspaceId);
249
896
  }
250
-
251
- function AddUser(): boolean
897
+ export async function userRemove(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): Promise<APIResult> {
898
+ return adminDelete(instance, authorizedUser, user, workspaceId);
899
+ }
900
+ //
901
+ // Mindline Config API internal helper functions
902
+ //
903
+ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmins: Array<Object>)
252
904
  {
253
- return true;
905
+ returnedAdmins.map((item) => {
906
+ // are we already tracking this user?
907
+ let user: User|null = null;
908
+ let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
909
+ if(usIndex===-1) {
910
+ // start tracking
911
+ let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
912
+ if(dummyIndex!==-1) {
913
+ // clear and overwrite dummy
914
+ user = ii.us.at(dummyIndex);
915
+ user.associatedWorkspaces.length = 0;
916
+ }
917
+ else {
918
+ // create and track new user
919
+ user = new User();
920
+ ii.us.push(user);
921
+ }
922
+ } else {
923
+ // already tracking this user
924
+ user = ii.us.at(usIndex);
925
+ }
926
+ // refresh all the data available from the server
927
+ user.oid = item.userId;
928
+ user.name = item.firstName;
929
+ user.mail = item.email;
930
+ user.tid = item.tenantId;
931
+ // ensure this workspace tracks this user
932
+ let idx = workspace.associatedUsers.findIndex((u) => u === item.userId);
933
+ if(idx == -1) workspace.associatedUsers.push(item.userId);
934
+ });
254
935
  }
255
-
256
- function CompleteUser(): boolean
936
+ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>)
257
937
  {
258
- return true;
938
+ returnedTenants.map((item) => {
939
+ // are we already tracking this tenant?
940
+ let tenant: Tenant|null = null;
941
+ let tsIndex = ii.ts.findIndex((t) => t.tid === item.tenantId);
942
+ if (tsIndex === -1) {
943
+ // start tracking
944
+ let dummyIndex = ii.ts.findIndex((t) => t.tid === "1");
945
+ if (dummyIndex !== -1) {
946
+ // clear and overwrite dummy
947
+ tenant = ii.ts.at(dummyIndex);
948
+ } else {
949
+ // create and track new workspace
950
+ tenant = new Tenant();
951
+ ii.ts.push(tenant);
952
+ }
953
+ } else {
954
+ // already tracking this tenant
955
+ tenant = ii.ts.at(tsIndex);
956
+ }
957
+ tenant.tid = item.tenantId;
958
+ tenant.name = item.name;
959
+ tenant.domain = item.domain;
960
+ tenant.tenantType = item.type.toLowerCase(); // should now be strings
961
+ tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
962
+ tenant.onboarded = item.isOnboarded ? "true" : "false";
963
+ tenant.authority = item.authority;
964
+ tenant.readServicePrincipal = item.readServicePrincipal;
965
+ tenant.writeServicePrincipal = item.writeServicePrincipal;
966
+ // ensure this workspace tracks this tenant
967
+ let idx = workspace.associatedTenants.findIndex((t) => t === item.tenantId);
968
+ if (idx == -1) workspace.associatedTenants.push(item.tenantId);
969
+ });
259
970
  }
260
-
261
- function CreateConfig(): boolean
971
+ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConfigs: Array<Object>)
262
972
  {
263
- return true;
973
+ // process returned configs
974
+ returnedConfigs.map((item) => {
975
+ // are we already tracking this config?
976
+ let config: Config | null = null;
977
+ let csIndex = ii.cs.findIndex((c) => c.id === item.id);
978
+ if (csIndex === -1) {
979
+ // start tracking
980
+ let dummyIndex = ii.cs.findIndex((c) => c.id === "1");
981
+ if (dummyIndex !== -1) {
982
+ // clear and overwrite dummy
983
+ config = ii.cs.at(dummyIndex);
984
+ } else {
985
+ // create and track new workspace
986
+ config = new Config();
987
+ ii.cs.push(config);
988
+ }
989
+ } else {
990
+ // already tracking this config
991
+ config = ii.cs.at(csIndex);
992
+ }
993
+ config!.id = item.id;
994
+ config!.name = item.name;
995
+ config!.description = item.description;
996
+ config!.isEnabled = item.isEnabled;
997
+ // create TenantConfigInfo array
998
+ config!.tenants.length = 0;
999
+ item.tenants.map((tci) => {
1000
+ let tenantConfigInfo = new TenantConfigInfo();
1001
+ tenantConfigInfo.tid = tci.tenantId;
1002
+ tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
1003
+ tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1004
+ tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1005
+ config!.tenants.push(tenantConfigInfo);
1006
+ });
1007
+ // ensure this workspace tracks this config
1008
+ let idx = workspace.associatedConfigs.findIndex((c) => c === item.id);
1009
+ if (idx == -1) workspace.associatedConfigs.push(item.id);
1010
+ });
1011
+ }
1012
+ async function workspaceInfoGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, debug: boolean): Promise<APIResult> {
1013
+ let result: APIResult = new APIResult();
1014
+ if (debug) debugger;
1015
+ try {
1016
+ result = await workspacesGet(instance, authorizedUser, user, debug);
1017
+ if (result.result) {
1018
+ for (let o of result.array!) {
1019
+ // are we already tracking this workspace?
1020
+ let workspace: Workspace = null;
1021
+ let wsIndex = ii.ws.findIndex((w) => w.id === o.id);
1022
+ if (wsIndex === -1) {
1023
+ // start tracking
1024
+ let dummyIndex = ii.ws.findIndex((w) => w.id === "1");
1025
+ if(dummyIndex !== -1) {
1026
+ // clear and overwrite dummy
1027
+ workspace = ii.ws.at(dummyIndex);
1028
+ }
1029
+ else {
1030
+ // create and track new workspace
1031
+ workspace = new Workspace();
1032
+ ii.ws.push(workspace);
1033
+ }
1034
+ } else {
1035
+ // already tracking this workspace
1036
+ workspace = ii.ws.at(wsIndex);
1037
+ }
1038
+ // clear associations as we are about to reset
1039
+ workspace.associatedUsers.length = 0;
1040
+ workspace.associatedTenants.length = 0;
1041
+ workspace.associatedConfigs.length = 0;
1042
+ workspace.id = o.id;
1043
+ workspace.name = o.name;
1044
+ // parallel GET admins, tenants, configs associated with this workspace
1045
+ let adminsPromise: Promise<APIResult> = adminsGet(instance, authorizedUser, workspace.id, debug);
1046
+ let tenantsPromise: Promise<APIResult> = tenantsGet(instance, authorizedUser, workspace.id, debug);
1047
+ let configsPromise: Promise<APIResult> = configsGet(instance, authorizedUser, workspace.id, debug);
1048
+ // wait for all to finish, return on any failure
1049
+ let [adminsResult, tenantsResult, configsResult] = await Promise.all([adminsPromise, tenantsPromise, configsPromise]);
1050
+ if(!adminsResult.result) return adminsResult;
1051
+ if(!tenantsResult.result) return tenantsResult;
1052
+ if(!configsResult.result) return configsResult;
1053
+ // process returned workspace components
1054
+ processReturnedAdmins(workspace, ii, adminsResult.array!);
1055
+ processReturnedTenants(workspace, ii, tenantsResult.array!);
1056
+ processReturnedConfigs(workspace, ii, configsResult.array!);
1057
+ // tag components with workspaceIDs
1058
+ ii.tagWithWorkspaces();
1059
+ }
1060
+ return result;
1061
+ }
1062
+ }
1063
+ catch (error: any) {
1064
+ console.log(error.message);
1065
+ result.error = error.message;
1066
+ }
1067
+ result.result = false;
1068
+ result.status = 500;
1069
+ return result;
264
1070
  }