@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/.vs/VSWorkspaceState.json +1 -1
- package/.vs/slnx.sqlite +0 -0
- package/.vs/sync/FileContentIndex/33a3ee4b-9c43-4e0d-ae64-59dc9fd9c401.vsidx +0 -0
- package/.vs/sync/FileContentIndex/4199b40e-a51a-4aab-a20d-788df6fd8a20.vsidx +0 -0
- package/.vs/sync/FileContentIndex/48c8495d-1d23-4a29-98d1-5d8fb1fc34fd.vsidx +0 -0
- package/.vs/sync/v17/.wsuo +0 -0
- package/hybridspa.ts +42 -35
- package/index.d.ts +62 -5
- package/index.ts +592 -200
- package/package.json +4 -1
- package/syncmilestones.json +23 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node.json +9 -0
- package/.vs/sync/FileContentIndex/65b0b1c9-0b62-4457-8808-4e1317638277.vsidx +0 -0
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 = "
|
|
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 = "
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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].
|
|
568
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
let
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
1129
|
+
if (tenant.name != null && tenant.name !== "") return false;
|
|
819
1130
|
// if needed, retrieve and cache access token
|
|
820
|
-
if (
|
|
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.
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
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
|
|
853
|
-
|
|
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 (
|
|
1188
|
+
if (user.companyName != "") return false;
|
|
875
1189
|
// if needed, retrieve and cache access token
|
|
876
|
-
if (
|
|
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.
|
|
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
|
-
//
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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 ===
|
|
1079
|
-
if (idx == -1) workspace.associatedUsers.push(
|
|
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
|
|
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
|
-
|
|
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
|