@mindline/sync 1.0.36 → 1.0.38

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,7 +1,7 @@
1
1
  //index.ts - published interface - AAD implementations, facade to Mindline Config API
2
2
  import * as signalR from "@microsoft/signalr"
3
3
  import { IPublicClientApplication, AuthenticationResult } from "@azure/msal-browser"
4
- import { deserializeArray } from 'class-transformer';
4
+ import { deserializeArray, instanceToPlain, ClassTransformOptions } from 'class-transformer';
5
5
  import { adminDelete, adminPost, adminsGet, configDelete, configsGet, configPost, configPut, graphConfig, initPost, readerPost, tenantPut, tenantPost, tenantDelete, tenantsGet, workspacesGet } from './hybridspa';
6
6
  import { version } from './package.json';
7
7
  import users from "./users.json";
@@ -9,6 +9,8 @@ import tenants from "./tenants.json";
9
9
  import configs from "./configs.json";
10
10
  import workspaces from "./workspaces.json";
11
11
  import tasksData from "./tasks";
12
+ import syncmilestones from './syncmilestones';
13
+ import { log } from "console";
12
14
  const FILTER_FIELD = "workspaceIDs";
13
15
  // called by unit tests
14
16
  export function sum(a: number, b: number): number {
@@ -42,7 +44,7 @@ export class User {
42
44
  this.oid = "";
43
45
  this.name = "";
44
46
  this.mail = "";
45
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
47
+ this.authority = "";
46
48
  this.tid = "";
47
49
  this.companyName = "";
48
50
  this.companyDomain = "";
@@ -87,12 +89,20 @@ export class Tenant {
87
89
  this.tenantType = "aad";
88
90
  this.permissionType = "notassigned";
89
91
  this.onboarded = "false";
90
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
92
+ this.authority = "";
91
93
  this.readServicePrincipal = "";
92
94
  this.writeServicePrincipal = "";
93
95
  this.workspaceIDs = "";
94
96
  }
95
97
  }
98
+ function getGraphEndpoint(authority: string): string {
99
+ switch (authority) {
100
+ case graphConfig.authorityWW: return "https://graph.microsoft.com/";
101
+ case graphConfig.authorityUS: return "https://graph.microsoft.us/";
102
+ case graphConfig.authorityCN: return "https://microsoftgraph.chinacloudapi.cn/";
103
+ default: debugger; return "";
104
+ }
105
+ }
96
106
  export enum TenantConfigType {
97
107
  source = 1,
98
108
  target = 2,
@@ -103,18 +113,24 @@ export class TenantConfigInfo {
103
113
  tid: string;
104
114
  sourceGroupId: string;
105
115
  sourceGroupName: string;
116
+ targetGroupId: string;
117
+ targetGroupName: string;
106
118
  configurationTenantType: TenantConfigTypeStrings;
107
119
  deltaToken: string;
108
- filesWritten: number;
120
+ usersWritten: number;
109
121
  configId: string;
122
+ batchId: string;
110
123
  constructor() {
111
124
  this.tid = "";
112
125
  this.sourceGroupId = "";
113
126
  this.sourceGroupName = "";
127
+ this.targetGroupId = "";
128
+ this.targetGroupName = "";
114
129
  this.configurationTenantType = "source";
115
130
  this.deltaToken = "";
116
- this.filesWritten = 0;
131
+ this.usersWritten = 0;
117
132
  this.configId = "";
133
+ this.batchId = "";
118
134
  }
119
135
  }
120
136
  export class Config {
@@ -426,8 +442,196 @@ export class Task {
426
442
  };
427
443
  }
428
444
  // class corresponding to an execution of a Config - a *TenantNode* for each source tenant, each with a *TenantNode* array of target tenants
445
+ export class Milestone {
446
+ Run: number;
447
+ Start: Date;
448
+ startDisplay: string;
449
+ POST: Date;
450
+ postDisplay: string;
451
+ Read: Date;
452
+ readDisplay: string;
453
+ Write: Date;
454
+ writeDisplay: string;
455
+ Duration: Date;
456
+ durationDisplay: string;
457
+ constructor(run: number) {
458
+ this.Run = run;
459
+ this.start("");
460
+ this.POST = null;
461
+ this.postDisplay = "";
462
+ this.Read = null;
463
+ this.readDisplay = "";
464
+ this.Write = null;
465
+ this.writeDisplay = "";
466
+ this.Duration = null;
467
+ this.durationDisplay = "";
468
+ }
469
+ start(start: string): void {
470
+ start == "" ? this.Start = new Date() : this.Start = new Date(start);
471
+ this.startDisplay = `${this.Start.getMinutes().toString().padStart(2, "0")}:${this.Start.getSeconds().toString().padStart(2, "0")}`;
472
+ }
473
+ post(post: string): void {
474
+ post == "" ? this.POST = new Date() : this.POST = new Date(post);
475
+ this.postDisplay = `${this.POST.getMinutes().toString().padStart(2, "0")}:${this.POST.getSeconds().toString().padStart(2, "0")}`;
476
+ }
477
+ read(read: string): void {
478
+ read == "" ? this.Read = new Date() : this.Read = new Date(read);
479
+ this.readDisplay = `${this.Read.getMinutes().toString().padStart(2, "0")}:${this.Read.getSeconds().toString().padStart(2, "0")}`;
480
+ }
481
+ write(write: string): void {
482
+ write == "" ? this.Write = new Date() : this.Write = new Date(write);
483
+ this.writeDisplay = `${this.Write.getMinutes().toString().padStart(2, "0")}:${this.Write.getSeconds().toString().padStart(2, "0")}`;
484
+ this.Duration = new Date(this.Write.getTime() - this.Start.getTime());
485
+ this.durationDisplay = `${this.Duration.getMinutes().toString().padStart(2, "0")}:${this.Duration.getSeconds().toString().padStart(2, "0")}`;
486
+ }
487
+ }
488
+ export class MilestoneArray {
489
+ milestones: Milestone[];
490
+ constructor(bClearLocalStorage: boolean) {
491
+ this.init(bClearLocalStorage);
492
+ }
493
+ init(bClearLocalStorage: boolean): void {
494
+ // read from localstorage by default
495
+ if (storageAvailable("localStorage")) {
496
+ let result = localStorage.getItem("syncmilestones");
497
+ if (result != null && typeof result === "string" && result !== "") {
498
+ let milestonesString: string = result;
499
+ let milestones: Object [] = JSON.parse(milestonesString);
500
+ if (milestones.length !== 0) {
501
+ if (bClearLocalStorage) {
502
+ localStorage.removeItem("syncmilestones");
503
+ }
504
+ else {
505
+ this.#initFromObjects(milestones);
506
+ return;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ // if storage unavailable or we were just asked to clear, read from default syncmilestone file
512
+ this.#initFromObjects(syncmilestones);
513
+ }
514
+ save(): void {
515
+ let milestonesString: string = JSON.stringify(this.milestones);
516
+ if (storageAvailable("localStorage")) {
517
+ localStorage.setItem("syncmilestones", milestonesString);
518
+ }
519
+ }
520
+ // milestone tracking during a sync
521
+ start(setMilestones): void {
522
+ // we should always have a milestone array and a first milestone
523
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
524
+ let currentRun: number = Number(this.milestones[0].Run);
525
+ // create a new milestone and prepend to front of array
526
+ let newMilestone: Milestone = new Milestone(currentRun+1);
527
+ this.milestones.unshift(newMilestone);
528
+ // re-define milestone array to trigger render
529
+ this.milestones = this.milestones.map((ms: Milestone) => {
530
+ let newms = new Milestone(ms.Run);
531
+ newms.Start = ms.Start;
532
+ newms.startDisplay = ms.startDisplay;
533
+ newms.POST = ms.POST;
534
+ newms.postDisplay = ms.postDisplay;
535
+ newms.Read = ms.Read;
536
+ newms.readDisplay = ms.readDisplay;
537
+ newms.Write = ms.Write;
538
+ newms.writeDisplay = ms.writeDisplay;
539
+ newms.Duration = ms.Duration;
540
+ newms.durationDisplay = ms.durationDisplay;
541
+ return newms;
542
+ });
543
+ setMilestones(this.milestones);
544
+ console.log(`Start milestone: ${this.milestones[0].Run}:${this.milestones[0].Start}`);
545
+ }
546
+ unstart(setMilestones): void {
547
+ // we should always have a milestone array and a first milestone
548
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
549
+ let currentRun: number = Number(this.milestones[0].Run);
550
+ // remove first milestone from front of array
551
+ let removedMilestone: Milestone = this.milestones.shift();
552
+ // re-define milestone array to trigger render
553
+ this.milestones = this.milestones.map((ms: Milestone) => {
554
+ let newms = new Milestone(ms.Run);
555
+ newms.Start = ms.Start;
556
+ newms.startDisplay = ms.startDisplay;
557
+ newms.POST = ms.POST;
558
+ newms.postDisplay = ms.postDisplay;
559
+ newms.Read = ms.Read;
560
+ newms.readDisplay = ms.readDisplay;
561
+ newms.Write = ms.Write;
562
+ newms.writeDisplay = ms.writeDisplay;
563
+ newms.Duration = ms.Duration;
564
+ newms.durationDisplay = ms.durationDisplay;
565
+ return newms;
566
+ });
567
+ setMilestones(this.milestones);
568
+ console.log(`Unstart removed first milestone: ${removedMilestone.Run}:${removedMilestone.Start}`);
569
+ }
570
+ post(setMilestones): void {
571
+ // update the post value of the first milestone
572
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
573
+ this.milestones[0].post("");
574
+ setMilestones(this.milestones);
575
+ console.log(`POST milestone: ${this.milestones[0].Run}:${this.milestones[0].POST}`);
576
+ }
577
+ read(setMilestones): void {
578
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
579
+ this.milestones[0].read("");
580
+ setMilestones(this.milestones);
581
+ console.log(`Read milestone: ${this.milestones[0].Run}:${this.milestones[0].Read}`);
582
+ }
583
+ write(setMilestones): void {
584
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
585
+ this.milestones[0].write("");
586
+ // while we have >10 complete milestones, remove the last
587
+ while (this.milestones.length > 10) {
588
+ let removed: Milestone = this.milestones.pop();
589
+ console.log(`Removed milestone #${removed.Run}: ${removed.Start}`);
590
+ }
591
+ // save to localstorage
592
+ this.save();
593
+ // re-define milestone array to trigger render
594
+ this.milestones = this.milestones.map((ms: Milestone) => {
595
+ let newms = new Milestone(ms.Run);
596
+ newms.Start = ms.Start;
597
+ newms.startDisplay = ms.startDisplay;
598
+ newms.POST = ms.POST;
599
+ newms.postDisplay = ms.postDisplay;
600
+ newms.Read = ms.Read;
601
+ newms.readDisplay = ms.readDisplay;
602
+ newms.Write = ms.Write;
603
+ newms.writeDisplay = ms.writeDisplay;
604
+ newms.Duration = ms.Duration;
605
+ newms.durationDisplay = ms.durationDisplay;
606
+ return newms;
607
+ });
608
+ setMilestones(this.milestones);
609
+ }
610
+ #initFromObjects(milestones: Object[]): void {
611
+ if (milestones == null) {
612
+ this.milestones = new Array();
613
+ }
614
+ else {
615
+ this.milestones = milestones.map((milestone: Object) => {
616
+ let ms: Milestone = new Milestone(Number(milestone.Run));
617
+ ms.start(milestone.Start);
618
+ ms.post(milestone.POST);
619
+ ms.read(milestone.Read);
620
+ ms.write(milestone.Write);
621
+ return ms;
622
+ });
623
+ }
624
+ }
625
+ }
429
626
  export class BatchArray {
430
627
  tenantNodes: TenantNode[];
628
+ pb_startTS: number;
629
+ pb_progress: number;
630
+ pb_increment: number;
631
+ pb_idle: number;
632
+ pb_idleMax: number;
633
+ pb_timer;
634
+ milestoneArray: MilestoneArray;
431
635
  constructor(
432
636
  config: Config | null,
433
637
  syncPortalGlobalState: InitInfo | null,
@@ -435,30 +639,36 @@ export class BatchArray {
435
639
  ) {
436
640
  this.tenantNodes = new Array<TenantNode>();
437
641
  this.init(config, syncPortalGlobalState, bClearLocalStorage);
642
+ this.pb_startTS = 0;
643
+ this.pb_progress = 0;
644
+ this.pb_increment = 0;
645
+ this.pb_timer = 0;
646
+ this.pb_idle = 0;
647
+ this.pb_idleMax = 0;
648
+ this.milestoneArray = new MilestoneArray(false);
438
649
  }
439
650
  // populate tenantNodes based on config tenants
440
651
  init(
441
- config: Config | null,
652
+ config: Config | null | undefined,
442
653
  syncPortalGlobalState: InitInfo | null,
443
654
  bClearLocalStorage: boolean
444
655
  ): void {
445
656
  console.log(
446
- `Calling BatchArray::init(config: "${config ? config.name : "null"
447
- }", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
657
+ `Calling BatchArray::init(config: "${config ? config.name : "null"}", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
448
658
  );
449
- // first clear batch array
450
- this.tenantNodes.length = 0;
451
- // then clear localStorage if we have been asked to
659
+ // clear localStorage if we have been asked to
452
660
  if (bClearLocalStorage) {
453
- if (storageAvailable("localStorage"))
661
+ if (storageAvailable("localStorage")) {
454
662
  localStorage.removeItem(config.name);
663
+ this.milestoneArray.init(bClearLocalStorage);
664
+ }
455
665
  }
456
666
  // create BatchArray if passed Config and InitInfo
457
- if (
458
- config != null &&
667
+ if (config != null &&
459
668
  config.tenants != null &&
460
- syncPortalGlobalState != null
461
- ) {
669
+ syncPortalGlobalState != null) {
670
+ // clear batch array only if we have been passed something with which to replace it
671
+ this.tenantNodes.length = 0;
462
672
  // create a sourceTenantNode for each Source and SourceTarget
463
673
  config.tenants.map((tciPotentialSource: TenantConfigInfo) => {
464
674
  if (
@@ -471,7 +681,8 @@ export class BatchArray {
471
681
  if (sourceTenant != null) {
472
682
  let sourceTenantNode: TenantNode = new TenantNode(
473
683
  tciPotentialSource.tid,
474
- sourceTenant.name
684
+ sourceTenant.name,
685
+ tciPotentialSource.batchId
475
686
  );
476
687
  this.tenantNodes.push(sourceTenantNode);
477
688
  } else {
@@ -499,7 +710,8 @@ export class BatchArray {
499
710
  if (targetTenant != null) {
500
711
  let targetTenantNode: TenantNode = new TenantNode(
501
712
  tciPotentialTarget.tid,
502
- targetTenant.name
713
+ targetTenant.name,
714
+ tciPotentialTarget.batchId
503
715
  );
504
716
  sourceTenantNode.targets.push(targetTenantNode);
505
717
  sourceTenantNode.expanded = true;
@@ -514,160 +726,257 @@ export class BatchArray {
514
726
  }
515
727
  });
516
728
  });
517
- // then try localStorage to find any matching source tenant metrics
518
- if (storageAvailable("localStorage")) {
519
- let result = localStorage.getItem(config.name);
520
- if (result != null && typeof result === "string" && result !== "") {
521
- // TODO: retrieve any relevant stored statistics from localStorage
522
- // let batchArrayString: string = result;
523
- // let batchArray: BatchArray = JSON.parse(batchArrayString);
524
- // batchArray.batches.map((batch: Batch) => {
525
- // config.tenants.map((tciTarget: TenantConfigInfo) => {
526
- // if(tciTarget.tid !== batch.tid) {
527
- // let target: Target = new Target(tciTarget.tid);
528
- // batch.targets.push(target);
529
- // }
530
- // });
531
- // });
532
- }
533
- }
534
729
  }
535
730
  }
536
- /*
537
- monitorSyncProgress(): void {
538
- const connection = new signalR.HubConnectionBuilder()
539
- .withUrl("https://dev-signalrdispatcher-westus.azurewebsites.net/?statsId=df9c2e0a-f6fe-43bb-a155-d51f66dffe0e")
540
- .configureLogging(signalR.LogLevel.Information)
541
- .build();
542
-
543
- // when you get a message
544
- connection.on("newMessage", function (message) {
545
- console.log(message); // log the message
546
- const item = JSON.parse(message); //parse it into an object
547
- const statsarray = item.Stats; // get the array of statistics
548
- const statskeys = Object.keys(statsarray); // get the keys of the array
549
- const statsvalues = Object.values(statsarray); // get the values of the array
550
- let statistics = ""; // initialize statistics
551
- let total = 1;
552
- let currentR = 0;
553
- let currentW = 0;
554
- let currentD = 0;
555
- for (let j = 0; j < statskeys.length; j++) { // store the TotalCount and store as total
556
- if (statskeys[j].endsWith("TotalCount")) {
557
- total = statsvalues[j];
731
+ initializeProgressBar(setSyncProgress, setConfigSyncResult, setIdleText, setMilestones): void {
732
+ this.pb_startTS = Date.now();
733
+ this.pb_progress = 0;
734
+ this.pb_increment = 1;
735
+ this.pb_idle = 0;
736
+ this.pb_idleMax = 0;
737
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
738
+ this.pb_timer = setInterval(() => {
739
+ // if we go 20 seconds without a signalR message, finish the sync
740
+ this.pb_idle = this.pb_idle + 1;
741
+ this.pb_idleMax = Math.max(this.pb_idle, this.pb_idleMax);
742
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
743
+ if (this.pb_idle >= 20) {
744
+ clearInterval(this.pb_timer);
745
+ if (this.milestoneArray.milestones[0].Write == null) {
746
+ this.milestoneArray.write(setMilestones);
558
747
  }
559
- if (statskeys[j].endsWith("CurrentCount")) { // store the Reader value as currentR
560
- if (statskeys[j].startsWith("Reader")) {
561
- currentR = statsvalues[j];
748
+ setConfigSyncResult(`finished sync, no updates for ${this.pb_idle} seconds`);
749
+ }
750
+ // if we get to 100, stop the timer, let SignalR or countdown timer finish sync
751
+ if (this.pb_progress < 100) {
752
+ this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
753
+ setSyncProgress(this.pb_progress);
754
+ }
755
+ }, 1000);
756
+ this.milestoneArray.start(setMilestones);
757
+ }
758
+ uninitializeProgressBar(setSyncProgress, setConfigSyncResult, setIdleText, setMilestones): void {
759
+ this.pb_startTS = 0;
760
+ this.pb_progress = 0;
761
+ setSyncProgress(this.pb_progress);
762
+ setConfigSyncResult("sync failed to execute");
763
+ this.pb_increment = 0;
764
+ clearInterval(this.pb_timer);
765
+ this.pb_timer = 0;
766
+ this.pb_idle = 0;
767
+ this.pb_idleMax = 0;
768
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
769
+ this.milestoneArray.unstart(setMilestones);
770
+ }
771
+ initializeSignalR(
772
+ config: Config | null | undefined,
773
+ syncPortalGlobalState: InitInfo | null,
774
+ batchIdArray: Array<Object>,
775
+ setRefreshDeltaTrigger,
776
+ setReadersTotal,
777
+ setReadersCurrent,
778
+ setWritersTotal,
779
+ setWritersCurrent,
780
+ setMilestones,
781
+ setConfigSyncResult,
782
+ bClearLocalStorage: boolean
783
+ ): void {
784
+ // we have just completed a successful POST to startSync
785
+ this.milestoneArray.post(setMilestones);
786
+ setConfigSyncResult("started sync, waiting for updates...");
787
+ // re-initialize batch array with Configuration updated by the succcessful POST to startSync
788
+ this.init(config, syncPortalGlobalState, false);
789
+ // define newMessage handler that can access *this*
790
+ let handler = (message) => {
791
+ console.log(message);
792
+ let item = JSON.parse(message);
793
+ // reset the countdown timer every time we get a message
794
+ this.pb_idle = 0;
795
+ // find the associated tenant for this SignalR message
796
+ let matchingPair: Object = batchIdArray.find((o: Object) => o.BatchId == item.TargetID);
797
+ if (matchingPair == null) {
798
+ console.log(`Batch ${item.TargetID} not found in batchIdArray.`);
799
+ debugger;
800
+ return;
801
+ }
802
+ let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === matchingPair.SourceId);
803
+ if (tenantNode == null) { // null OR undefined
804
+ console.log(`Tenant ${matchingPair.SourceId} not found in BatchArray.`);
805
+ debugger;
806
+ return;
807
+ }
808
+ tenantNode.batchId = matchingPair.BatchId;
809
+ // process stats for this SignalR message batch
810
+ let statsarray = item.Stats; // get the array of statistics
811
+ let statskeys = Object.keys(statsarray); // get the keys of the array
812
+ let statsvalues = Object.values(statsarray); // get the values of the array
813
+ for (let j = 0; j < statskeys.length; j++) {
814
+ let bTotalCount = statskeys[j].endsWith("TotalCount");
815
+ let bCurrentCount = statskeys[j].endsWith("CurrentCount");
816
+ let bDeferredCount = statskeys[j].endsWith("DeferredCount");
817
+ if (statskeys[j].startsWith("Reader")) {
818
+ // parse tid from Reader key
819
+ let tidRegexp = /Reader\/TID:(.+)\/TotalCount/;
820
+ if (bCurrentCount) tidRegexp = /Reader\/TID:(.+)\/CurrentCount/;
821
+ if (bDeferredCount) tidRegexp = /Reader\/TID:(.+)\/DeferredCount/;
822
+ let matchTID = statskeys[j].match(tidRegexp);
823
+ if (matchTID == null) {
824
+ console.log(`tid not found in ${statskeys[j]}.`);
825
+ debugger;
826
+ return;
827
+ }
828
+ if (bTotalCount) {
829
+ tenantNode.total = Math.max(Number(statsvalues[j]), tenantNode.total);
830
+ console.log(`----- ${tenantNode.name} TID: ${tenantNode.tid} batchId: ${tenantNode.batchId}`);
831
+ console.log(`----- ${tenantNode.name} Total To Read: ${tenantNode.total}`);
562
832
  }
563
- if (statskeys[j].startsWith("Writer")) { // store the Writer value as currentW
564
- currentW = statsvalues[j];
833
+ else {
834
+ tenantNode.read = Math.max(Number(statsvalues[j]), tenantNode.read);
835
+ console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
565
836
  }
566
837
  }
567
- if (statskeys[j].endsWith("DeferredCount")) { // store the deferred count
568
- currentD = statsvalues[j];
838
+ if (statskeys[j].startsWith("Writer")) {
839
+ // parse tid from Writer key
840
+ let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
841
+ if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
842
+ if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
843
+ let matchTID = statskeys[j].match(tidRegexp);
844
+ if (matchTID == null) {
845
+ console.log(`tid not found in ${statskeys[j]}.`);
846
+ debugger;
847
+ return;
848
+ }
849
+ // this Writer node should exist precisely under the Reader for this SignalR message
850
+ let writerNode: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid === matchTID[1]);
851
+ if (writerNode == null) {
852
+ console.log(`Writer ${tenantNode.name} not found under Reader ${tenantNode.name}.`);
853
+ debugger;
854
+ return;
855
+ }
856
+ writerNode.batchId = matchingPair.BatchId;
857
+ if (bTotalCount) {
858
+ writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
859
+ console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
860
+ console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
861
+ }
862
+ else if (bCurrentCount) {
863
+ writerNode.written = Math.max(Number(statsvalues[j]), writerNode.written);
864
+ console.log(`----- ${writerNode.name} Total Written: ${writerNode.written}`);
865
+ }
866
+ else if (bDeferredCount) {
867
+ writerNode.deferred = Math.max(Number(statsvalues[j]), writerNode.deferred);
868
+ console.log(`----- ${writerNode.name} Total Deferred: ${writerNode.deferred}`);
869
+ }
870
+ else {
871
+ console.log(`unknown writer type`);
872
+ debugger;
873
+ return;
874
+ }
875
+ writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
569
876
  }
570
- statistics = statistics + statskeys[j] + "=" + statsvalues[j] + "<br />" //
571
877
  }
572
- updateProgress(total, currentR, currentW, currentD);
573
- myFunction(item.TargetID, statistics);
574
- });
575
-
576
- connection.start().catch(console.error);
577
-
578
- function myFunction(targetid, str) {
579
- const table = document.getElementById("the-table");
580
- let row = document.getElementById(targetid);
581
- if (row) {
582
- row.cells[1].innerHTML = "<code>" + str + "</ code>";
583
- } else {
584
- row = table.insertRow(1);
585
- row.id = targetid;
586
- let cell1 = row.insertCell(0);
587
- let cell2 = row.insertCell(1);
588
- let caption = (targetid === "00000000-0000-0000-0000-000000000000") ? "<B>Status of ServiceBus QUEUES</B>" : targetid;
589
- cell1.innerHTML = "<code>" + caption + "</ code>";
590
- cell2.innerHTML = "<code>" + str + "</ code>";
878
+ // update status based on all updates in this message
879
+ tenantNode.update(tenantNode.total, tenantNode.read, tenantNode.written, tenantNode.deferred);
880
+ // for each message, enumerate nodes to assess completion state
881
+ let bReadingComplete: boolean = true;
882
+ let bWritingComplete: boolean = true;
883
+ let bWritingStarted: boolean = false;
884
+ let readerTotal: number = 0;
885
+ let readerCurrent: number = 0;
886
+ let writerTotal: number = 0;
887
+ let writerCurrent: number = 0;
888
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
889
+ sourceTenantNode.targets.map((writerNode: TenantNode) => {
890
+ bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
891
+ bWritingStarted ||= (writerNode.total > 0 || writerNode.status != "not started");
892
+ writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
893
+ writerCurrent += writerNode.written;
894
+ });
895
+ bReadingComplete &&= (sourceTenantNode.status == "complete" || sourceTenantNode.status == "failed");
896
+ readerTotal += sourceTenantNode.total;
897
+ readerCurrent += sourceTenantNode.read;
898
+ });
899
+ // set linear gauge max and current values
900
+ setReadersTotal(readerTotal);
901
+ setReadersCurrent(readerCurrent);
902
+ setWritersTotal(Math.max(writerTotal, readerTotal));
903
+ setWritersCurrent(writerCurrent);
904
+ // because it is an important milestone, we always check if we have *just* completed reading
905
+ if (bReadingComplete && this.milestoneArray.milestones[0].Read == null) {
906
+ this.milestoneArray.read(setMilestones);
907
+ setConfigSyncResult("reading complete");
908
+ console.log(`Setting config sync result: "reading complete"`);
909
+ // trigger refresh delta tokens
910
+ setRefreshDeltaTrigger(true);
911
+ // change to % per second to complete in 7x as long as it took to get here
912
+ let readTS = Date.now();
913
+ let secsElapsed = (readTS - this.pb_startTS) / 1000;
914
+ let expectedPercentDone = 7;
915
+ let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
916
+ this.pb_increment = expectedPercentPerSecond;
917
+ console.log(`Setting increment: ${this.pb_increment}% per second`);
591
918
  }
592
- }
593
-
594
- function updateProgress(total, currentR, currentW, currentD) {
595
- updateRead(total, currentR);
596
- updateWrite(total, currentW, currentD);
597
- }
598
-
599
- function updateRead(total, current) {
600
- const element = document.getElementById("readBar");
601
- const width = Math.round(100 * current / total);
602
- element.style.width = width + '%';
603
- element.innerHTML = "R=" + width + '%';
604
- }
605
-
606
- function updateWrite(total, w, d) {
607
- const elementW = document.getElementById("writeBar");
608
- const widthW = Math.round(100 * w / total);
609
- elementW.style.width = widthW + '%';
610
- elementW.innerHTML = "W=" + widthW + '%';
611
-
612
- const elementD = document.getElementById("deferBar");
613
- let width = 0;
614
- let widthScaled = 0;
615
- if (d > 0) {
616
- width = Math.round(100 * d / total) + 1;
617
- if (width < 10) {
618
- widthScaled = width * 10;
619
- }
620
- else if (width < 20) {
621
- widthScaled = width * 5;
622
- }
623
- else {
624
- widthScaled = width;
625
- }
919
+ // with that out of the way, is writing complete?
920
+ if (bWritingComplete) {
921
+ this.milestoneArray.write(setMilestones);
922
+ setConfigSyncResult("sync complete");
923
+ console.log(`Setting config sync result: "complete"`);
924
+ this.pb_progress = 99;
626
925
  }
627
- if (widthScaled + widthW >= 99) {
628
- widthScaled = 99 - widthW;
926
+ // if not, has writing even started?
927
+ else if (bWritingStarted) {
928
+ setConfigSyncResult("writing in progress");
929
+ console.log(`Setting config sync result: "writing in progress"`);
629
930
  }
630
- elementD.style.width = widthScaled + '%';
631
- elementD.innerHTML = width + '%';
632
- document.getElementById("deferred").innerHTML = 'Deferred:' + width + '% (' + d + ' of ' + total + ')';
633
- // cycle through desired states for sources and targets
634
- if (this.tenantNodes != null) {
635
- this.tenantNodes.map((sourceTenantNode: TenantNode) => {
636
- if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
637
- else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
638
- else sourceTenantNode.update(0, 0, 0, 0);
639
- if (sourceTenantNode.targets != null) {
640
- sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
641
- if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
642
- else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
643
- else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
644
- else targetTenantNode.update(0, 0, 0, 0);
645
- });
646
- }
647
- });
931
+ // else, we must be reading (unless we already completed reading)
932
+ else if (this.milestoneArray.milestones[0].Read == null){
933
+ setConfigSyncResult("reading in progress");
934
+ console.log(`Setting config sync result: "reading in progress"`);
648
935
  }
649
936
  }
937
+ // start SignalR connection based on each batchId
938
+ batchIdArray.map((batchPair: Object) => {
939
+ const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${batchPair.BatchId}`;
940
+ console.log(`Creating SignalR Hub for TID: ${batchPair.SourceId} ${endpointUrl}`);
941
+ const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
942
+ .withUrl(endpointUrl)
943
+ .withAutomaticReconnect()
944
+ .configureLogging(signalR.LogLevel.Information)
945
+ .build();
946
+ // when you get a message, process the message
947
+ connection.on("newMessage", handler);
948
+ connection.onreconnecting(error => {
949
+ console.assert(connection.state === signalR.HubConnectionState.Reconnecting);
950
+ console.log(`Connection lost due to error "${error}". Reconnecting.`);
951
+ });
952
+ connection.onreconnected(connectionId => {
953
+ console.assert(connection.state === signalR.HubConnectionState.Connected);
954
+ console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
955
+ });
956
+ // restart when you get a close event
957
+ connection.onclose(async () => {
958
+ console.log(`Connection closing. Attempting restart.`);
959
+ await connection.start();
960
+ });
961
+ // start and display any caught exceptions in the console
962
+ connection.start().catch(console.error);
963
+ });
650
964
  }
651
- */
652
965
  // start a sync cycle
653
- startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): void {
966
+ async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): Promise<APIResult>
967
+ {
968
+ let result: APIResult = new APIResult();
654
969
  if (this.tenantNodes == null || this.tenantNodes.length == 0) {
655
970
  // we should not have an empty batch array for a test
656
971
  debugger;
972
+ result.result = false;
973
+ result.error = "startSync: invalid parameters";
974
+ result.status = 500;
975
+ return result;
657
976
  }
658
- // start SignalR connection
659
- const connection = new signalR.HubConnectionBuilder() // SignalR initialization
660
- .withUrl("https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=df9c2e0a-f6fe-43bb-a155-d51f66dffe0e", { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets } )
661
- .configureLogging(signalR.LogLevel.Information)
662
- .build();
663
- // when you get a message, log the message
664
- connection.on("newMessage", function (message) {
665
- console.log(message); // log the message
666
- });
667
- connection.start().catch(console.error);
668
977
  // execute post to reader endpoint
669
- readerPost(instance, authorizedUser, config);
670
- // refresh delta tokens for this Configuration (we read all Configurations at once)
978
+ result = await readerPost(instance, authorizedUser, config);
979
+ return result;
671
980
  }
672
981
  }
673
982
  export class TenantNode {
@@ -675,15 +984,17 @@ export class TenantNode {
675
984
  status: string;
676
985
  name: string;
677
986
  tid: string;
987
+ batchId: string;
678
988
  total: number;
679
989
  read: number;
680
990
  written: number;
681
991
  deferred: number;
682
992
  targets: TenantNode[];
683
- constructor(tid: string, name: string) {
993
+ constructor(tid: string, name: string, batchId: string) {
684
994
  this.expanded = false;
685
995
  this.name = name;
686
996
  this.tid = tid;
997
+ this.batchId = batchId;
687
998
  this.targets = new Array<TenantNode>();
688
999
  this.update(0, 0, 0, 0);
689
1000
  }
@@ -700,7 +1011,7 @@ export class TenantNode {
700
1011
  else if (this.written > 0) {
701
1012
  if (this.written + this.deferred < this.total) this.status = "in progress";
702
1013
  else if (this.written === this.total) this.status = "complete";
703
- else if (this.written + this.deferred === this.total) this.status = "failed";
1014
+ else if (this.written + this.deferred >= this.total) this.status = "failed";
704
1015
  }
705
1016
  }
706
1017
  }
@@ -777,7 +1088,7 @@ export function signIn(user: User, tasks: TaskArray): void {
777
1088
  tenantURL += "MicrosoftIdentity/Account/Challenge";
778
1089
  let url: URL = new URL(tenantURL);
779
1090
  url.searchParams.append("redirectUri", window.location.origin);
780
- url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
1091
+ url.searchParams.append("scope", "openid offline_access Directory.AccessAsUser.All CrossTenantInformation.ReadBasic.All");
781
1092
  url.searchParams.append("domainHint", "organizations");
782
1093
  if (user.oid !== "1") {
783
1094
  url.searchParams.append("loginHint", user.mail);
@@ -815,9 +1126,9 @@ export function signOut(user: User): void {
815
1126
  export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
816
1127
  if (debug) debugger;
817
1128
  // do we already have a valid tenant name? if so, nothing to add
818
- if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
1129
+ if (tenant.name != null && tenant.name !== "") return false;
819
1130
  // if needed, retrieve and cache access token
820
- if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
1131
+ if (loggedInUser.accessToken != null && loggedInUser.accessToken === "") {
821
1132
  console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
822
1133
  try {
823
1134
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
@@ -837,29 +1148,32 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
837
1148
  // make tenant endpoint call
838
1149
  try {
839
1150
  // create tenant info endpoint
840
- var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
1151
+ var tenantEndpoint = getGraphEndpoint(tenant.authority) + graphConfig.graphTenantByDomainPredicate;
841
1152
  tenantEndpoint += "(domainName='";
842
1153
  tenantEndpoint += tenant.domain;
843
1154
  tenantEndpoint += "')";
844
1155
  console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
845
1156
  let response = await fetch(tenantEndpoint, options);
846
- let data = await response.json();
847
- if (data) {
848
- if (typeof data.error !== "undefined") {
849
- console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
850
- return false;
1157
+ if (response.status == 200 && response.statusText == "OK") {
1158
+ let data = await response.json();
1159
+ if (data) {
1160
+ if (data.error != null) {
1161
+ debugger;
1162
+ console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
1163
+ return false;
1164
+ }
1165
+ else if (data.displayName != null && data.displayName !== "") {
1166
+ // set domain information on passed tenant
1167
+ tenant.tid = data.tenantId;
1168
+ tenant.name = data.displayName;
1169
+ console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
1170
+ return true; // success, need UX to re-render
1171
+ }
851
1172
  }
852
- else if (typeof data.displayName !== undefined && data.displayName !== "") {
853
- // set domain information on passed tenant
854
- tenant.tid = data.tenantId;
855
- tenant.name = data.displayName;
856
- console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
857
- return true; // success, need UX to re-render
1173
+ else {
1174
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
858
1175
  }
859
1176
  }
860
- else {
861
- console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
862
- }
863
1177
  }
864
1178
  catch (error: any) {
865
1179
  console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
@@ -871,9 +1185,9 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
871
1185
  export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
872
1186
  if (debug) debugger;
873
1187
  // do we already have a valid company name? if so, nothing to add, no need for UX to re-render
874
- if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
1188
+ if (user.companyName != "") return false;
875
1189
  // if needed, retrieve and cache access token
876
- if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
1190
+ if (user.accessToken === "") {
877
1191
  try {
878
1192
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
879
1193
  user.accessToken = response.accessToken; // cache access token
@@ -892,7 +1206,7 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
892
1206
  // make tenant endpoint call
893
1207
  try {
894
1208
  // create tenant info endpoint
895
- var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
1209
+ var tenantEndpoint = getGraphEndpoint(user.authority) + graphConfig.graphTenantByIdPredicate;
896
1210
  tenantEndpoint += "(tenantId='";
897
1211
  tenantEndpoint += user.tid;
898
1212
  tenantEndpoint += "')";
@@ -931,6 +1245,54 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
931
1245
  tasks.setTaskEnd("GET tenant details", new Date(), "failed");
932
1246
  return false; // failed, no need for UX to re-render
933
1247
  }
1248
+ //tenantUnauthenticatedLookup (from https://gettenantpartitionweb.azurewebsites.net/js/gettenantpartition.js)
1249
+ export async function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean> {
1250
+ if (debug) debugger;
1251
+ // do we already have a valid tenant ID? if so, nothing to add
1252
+ if (tenant.tid !== "") return false;
1253
+ // do we not have a valid domain? if so, nothing to lookup
1254
+ if (tenant.domain == "") return false;
1255
+ // prepare the 3 endpoints and corresponding regular expressions
1256
+ let endpoints: string[] = [graphConfig.authorityWW, graphConfig.authorityUS, graphConfig.authorityCN];
1257
+ let regexes: RegExp[] = [graphConfig.authorityWWRegex, graphConfig.authorityUSRegex, graphConfig.authorityCNRegex];
1258
+ // make unauthenticated well-known openid endpoint call(s)
1259
+ let response = null;
1260
+ try {
1261
+ for (let j = 0; j < 3; j++) {
1262
+ // create well-known openid endpoint
1263
+ var openidEndpoint = endpoints[j];
1264
+ openidEndpoint += tenant.domain;
1265
+ openidEndpoint += "/.well-known/openid-configuration";
1266
+ console.log("Attempting GET from openid well-known endpoint: ", openidEndpoint);
1267
+ response = await fetch(openidEndpoint);
1268
+ if (response.status == 200 && response.statusText == "OK") {
1269
+ let data = await response.json();
1270
+ if (data) {
1271
+ // store tenant ID and authority
1272
+ var tenantAuthEndpoint = data.authorization_endpoint;
1273
+ var authMatches = tenantAuthEndpoint.match(regexes[j]);
1274
+ tenant.tid = authMatches[2];
1275
+ tenant.authority = authMatches[1]; // USGov tenants are registered in WW with USGov authority values!
1276
+ console.log("Successful GET from openid well-known endpoint");
1277
+ return true; // success, need UX to re-render
1278
+ }
1279
+ else {
1280
+ console.log(`Failed JSON parse of openid well-known endpoint response ${openidEndpoint}.`);
1281
+ }
1282
+ }
1283
+ else {
1284
+ console.log(`Failed GET from ${openidEndpoint}.`);
1285
+ }
1286
+ }
1287
+ }
1288
+ catch (error: any) {
1289
+ console.log("Failed to GET from openid well-known endpoint: ", error);
1290
+ }
1291
+ if (tenant.tid == "" || tenant.authority == "") {
1292
+ console.log(`GET from openid well-known endpoint failed to find tenant: ${response ? response.statusText : "unknown"}`);
1293
+ }
1294
+ return false; // failed, no need for UX to re-render
1295
+ }
934
1296
  //usersGet - GET from AAD Users endpoint
935
1297
  export async function usersGet(tenant: Tenant): Promise<{ users: string[], error: string }> {
936
1298
  // need a read or write access token to get graph users
@@ -1011,23 +1373,45 @@ export async function configsRefresh(instance: IPublicClientApplication, authori
1011
1373
  export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult> {
1012
1374
  let result: APIResult = new APIResult();
1013
1375
  if (debug) debugger;
1014
- // get tenant name and domain from AAD
1015
- result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
1016
- // 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
1017
- if (result.result) {
1018
- tasks.setTaskStart("POST config init", new Date());
1019
- result = await initPost(instance, authorizedUser, user, debug);
1020
- tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1021
- }
1022
- // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
1023
- if (result.result) {
1024
- tasks.setTaskStart("GET workspaces", new Date());
1025
- result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
1026
- tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1027
- }
1028
- if (result.result) result.error = version;
1029
- else console.log("@mindline/sync package version: " + version);
1030
- return result;
1376
+ // lookup authority for this user (the lookup call does it based on domain, but TID works as well to find authority)
1377
+ let tenant: Tenant = new Tenant();
1378
+ tenant.domain = user.tid;
1379
+ let bResult: boolean = await tenantUnauthenticatedLookup(tenant, debug);
1380
+ if (bResult) {
1381
+ // success, we at least got authority as a new bit of information at this point
1382
+ user.authority = tenant.authority;
1383
+ // do we have a logged in user from the same authority as this newly proposed tenant?
1384
+ let loggedInUser: User | undefined = ii.us.find((u: User) => (u.session === "Sign Out" && u.authority === user.authority));
1385
+ if (loggedInUser != null) {
1386
+ // get tenant name and domain from AAD
1387
+ result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
1388
+ // 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
1389
+ if (result.result) {
1390
+ tasks.setTaskStart("POST config init", new Date());
1391
+ result = await initPost(instance, authorizedUser, user, debug);
1392
+ tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1393
+ }
1394
+ // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
1395
+ if (result.result) {
1396
+ tasks.setTaskStart("GET workspaces", new Date());
1397
+ result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
1398
+ tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1399
+ }
1400
+ if (result.result) result.error = version;
1401
+ else console.log("@mindline/sync package version: " + version);
1402
+ return result;
1403
+ }
1404
+ else {
1405
+ result.error = `${user.mail} insufficient privileges to lookup under authority: ${user.authority}.`;
1406
+ result.result = false;
1407
+ return result;
1408
+ }
1409
+ }
1410
+ else {
1411
+ result.error = `Failed to retrieve authority for user "${user.mail}" TID ${user.tid}.`;
1412
+ result.result = false;
1413
+ return result;
1414
+ }
1031
1415
  }
1032
1416
  export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
1033
1417
  return tenantPost(instance, authorizedUser, tenant, workspaceId);
@@ -1051,7 +1435,7 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1051
1435
  returnedAdmins.map((item) => {
1052
1436
  // are we already tracking this user?
1053
1437
  let user: User | null = null;
1054
- let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
1438
+ let usIndex = ii.us.findIndex((u) => (u.oid === item.userId || u.oid === item.email));
1055
1439
  if (usIndex === -1) {
1056
1440
  // start tracking
1057
1441
  let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
@@ -1070,13 +1454,13 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1070
1454
  user = ii.us.at(usIndex);
1071
1455
  }
1072
1456
  // refresh all the data available from the server
1073
- user.oid = item.userId;
1457
+ user.oid = item.userId ? item.userId : item.email;
1074
1458
  user.name = item.firstName;
1075
1459
  user.mail = item.email;
1076
1460
  user.tid = item.tenantId;
1077
1461
  // ensure this workspace tracks this user
1078
- let idx = workspace.associatedUsers.findIndex((u) => u === item.userId);
1079
- if (idx == -1) workspace.associatedUsers.push(item.userId);
1462
+ let idx = workspace.associatedUsers.findIndex((u) => u === user.oid);
1463
+ if (idx == -1) workspace.associatedUsers.push(user.oid);
1080
1464
  });
1081
1465
  }
1082
1466
  function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>) {
@@ -1091,7 +1475,7 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1091
1475
  // clear and overwrite dummy
1092
1476
  tenant = ii.ts.at(dummyIndex);
1093
1477
  } else {
1094
- // create and track new workspace
1478
+ // create and track new tenant
1095
1479
  tenant = new Tenant();
1096
1480
  ii.ts.push(tenant);
1097
1481
  }
@@ -1105,7 +1489,12 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1105
1489
  tenant.tenantType = item.type.toLowerCase(); // should now be strings
1106
1490
  tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
1107
1491
  tenant.onboarded = item.isOnboarded ? "true" : "false";
1108
- tenant.authority = item.authority;
1492
+
1493
+ // canonicalize authority when getting it from config backend
1494
+ const regex = /^(https:\/\/login.microsoftonline.(?:us|com)\/)organizations\/v2.0$/;
1495
+ const regexMatch = item.authority.match(regex);
1496
+ tenant.authority = regexMatch ? regexMatch[1] : item.authority;
1497
+
1109
1498
  tenant.readServicePrincipal = item.readServicePrincipal;
1110
1499
  tenant.writeServicePrincipal = item.writeServicePrincipal;
1111
1500
  // ensure this workspace tracks this tenant
@@ -1143,11 +1532,14 @@ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConf
1143
1532
  item.tenants.map((tci) => {
1144
1533
  let tenantConfigInfo = new TenantConfigInfo();
1145
1534
  tenantConfigInfo.tid = tci.tenantId;
1146
- tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
1147
- tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1535
+ tenantConfigInfo.sourceGroupId = tci.sourceGroupId ?? "";
1536
+ tenantConfigInfo.sourceGroupName = tci.sourceGroupName ?? "";
1537
+ tenantConfigInfo.targetGroupId = tci.targetGroupId ?? "";
1538
+ tenantConfigInfo.targetGroupName = tci.targetGroupName ?? "";
1148
1539
  tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1149
1540
  tenantConfigInfo.deltaToken = tci.deltaToken ?? "";
1150
1541
  tenantConfigInfo.configId = config!.id;
1542
+ tenantConfigInfo.batchId = tci.batchId ?? "";
1151
1543
  config!.tenants.push(tenantConfigInfo);
1152
1544
  });
1153
1545
  // ensure this workspace tracks this config