@mindline/sync 1.0.37 → 1.0.39
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 -0
- package/.vs/slnx.sqlite +0 -0
- package/.vs/sync/FileContentIndex/33a3ee4b-9c43-4e0d-ae64-59dc9fd9c401.vsidx +0 -0
- package/.vs/sync/FileContentIndex/8f4f98c3-3d66-47b7-9f29-de7333e0279d.vsidx +0 -0
- package/.vs/sync/v17/.wsuo +0 -0
- package/hybridspa.ts +30 -18
- package/index.d.ts +59 -4
- package/index.ts +447 -176
- package/package.json +1 -1
- package/syncmilestones.json +23 -0
- package/.vs/sync/FileContentIndex/08e0b916-d40e-4be9-82e8-27d6a5dba7e0.vsidx +0 -0
package/.vs/slnx.sqlite
CHANGED
|
Binary file
|
package/.vs/sync/v17/.wsuo
CHANGED
|
Binary file
|
package/hybridspa.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
User,
|
|
4
4
|
Tenant,
|
|
5
5
|
Config,
|
|
6
|
+
TenantConfigInfo,
|
|
6
7
|
APIResult
|
|
7
8
|
} from "./index";
|
|
8
9
|
import {
|
|
@@ -26,6 +27,7 @@ export const graphConfig = {
|
|
|
26
27
|
"https://dev-configurationapi-westus.azurewebsites.net/api/v1/configurations",
|
|
27
28
|
initEndpoint:
|
|
28
29
|
"https://dev-configurationapi-westus.azurewebsites.net/api/v1/configuration/init",
|
|
30
|
+
readerStartSyncEndpoint: "https://dev-configurationapi-westus.azurewebsites.net/api/v1/startSync",
|
|
29
31
|
tenantEndpoint:
|
|
30
32
|
"https://dev-configurationapi-westus.azurewebsites.net/api/v1/tenant",
|
|
31
33
|
tenantsEndpoint:
|
|
@@ -47,9 +49,6 @@ export const graphConfig = {
|
|
|
47
49
|
authorityUSRegex: /^(https:\/\/login\.microsoftonline\.(?:us|com)\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
|
|
48
50
|
authorityCN: "https://login.partner.microsoftonline.cn/",
|
|
49
51
|
authorityCNRegex: /^(https:\/\/login\.partner\.microsoftonline\.cn\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
|
|
50
|
-
// reader endpoint to trigger sync start
|
|
51
|
-
readerStartSyncEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/startSync/",
|
|
52
|
-
readerApiEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/lookup/"
|
|
53
52
|
};
|
|
54
53
|
// helper functions
|
|
55
54
|
async function defineHeaders(
|
|
@@ -134,7 +133,7 @@ export async function adminDelete(
|
|
|
134
133
|
console.log("Attempting DELETE from /admin: " + url!.href);
|
|
135
134
|
let response = await fetch(url!.href, options);
|
|
136
135
|
if (response.status === 200 && response.statusText === "OK") {
|
|
137
|
-
console.log(`Successful DELETE from
|
|
136
|
+
console.log(`Successful DELETE from /admin: ${url!.href}`);
|
|
138
137
|
return result;
|
|
139
138
|
} else {
|
|
140
139
|
result.error = await processErrors(response);
|
|
@@ -335,13 +334,20 @@ export async function configPost(
|
|
|
335
334
|
"isEnabled": ${config.isEnabled},
|
|
336
335
|
"tenants": [`;
|
|
337
336
|
config.tenants.map((tci) => {
|
|
337
|
+
// be sure we send null and not "null" in body
|
|
338
|
+
let sourceGroupId: string = tci.sourceGroupId != "" ? `"${tci.sourceGroupId}"` : "null";
|
|
339
|
+
let sourceGroupName: string = tci.sourceGroupName != "" ? `"${tci.sourceGroupName}"` : "null";
|
|
340
|
+
let targetGroupId: string = tci.targetGroupId != "" ? `"${tci.targetGroupId}"` : "null";
|
|
341
|
+
let targetGroupName: string = tci.targetGroupName != "" ? `"${tci.targetGroupName}"` : "null";
|
|
338
342
|
// if last character is } we need a comma first
|
|
339
343
|
let needComma: boolean = configBody.slice(-1) === "}";
|
|
340
344
|
if (needComma) configBody += ",";
|
|
341
345
|
configBody += `{
|
|
342
346
|
"tenantId": "${tci.tid}",
|
|
343
|
-
"sourceGroupId":
|
|
344
|
-
"sourceGroupName":
|
|
347
|
+
"sourceGroupId": ${sourceGroupId},
|
|
348
|
+
"sourceGroupName": ${sourceGroupName},
|
|
349
|
+
"targetGroupId": ${targetGroupId},
|
|
350
|
+
"targetGroupName": ${targetGroupName},
|
|
345
351
|
"configurationTenantType": "${tci.configurationTenantType}"
|
|
346
352
|
}`;
|
|
347
353
|
});
|
|
@@ -404,15 +410,21 @@ export async function configPut(
|
|
|
404
410
|
"description": "${config.description}",
|
|
405
411
|
"isEnabled": ${config.isEnabled},
|
|
406
412
|
"tenants": [`;
|
|
407
|
-
config.tenants.map((tci) => {
|
|
413
|
+
config.tenants.map((tci: TenantConfigInfo) => {
|
|
408
414
|
// if last character is } we need a comma first
|
|
409
415
|
let needComma: boolean = configBody.slice(-1) === "}";
|
|
410
416
|
if (needComma) configBody += ",";
|
|
411
|
-
//
|
|
417
|
+
// be sure we send null and not "null" in body
|
|
418
|
+
let sourceGroupId: string = tci.sourceGroupId != "" ? `"${tci.sourceGroupId}"` : "null";
|
|
419
|
+
let sourceGroupName: string = tci.sourceGroupName != "" ? `"${tci.sourceGroupName}"` : "null";
|
|
420
|
+
let targetGroupId: string = tci.targetGroupId != "" ? `"${tci.targetGroupId}"` : "null";
|
|
421
|
+
let targetGroupName: string = tci.targetGroupName != "" ? `"${tci.targetGroupName}"` : "null";
|
|
412
422
|
configBody += `{
|
|
413
423
|
"tenantId": "${tci.tid}",
|
|
414
|
-
"sourceGroupId":
|
|
415
|
-
"sourceGroupName":
|
|
424
|
+
"sourceGroupId": ${sourceGroupId},
|
|
425
|
+
"sourceGroupName": ${sourceGroupName},
|
|
426
|
+
"targetGroupId": ${targetGroupId},
|
|
427
|
+
"targetGroupName": ${targetGroupName},
|
|
416
428
|
"configurationTenantType": "${tci.configurationTenantType}",
|
|
417
429
|
"deltaToken": "${tci.deltaToken}"
|
|
418
430
|
}`;
|
|
@@ -857,21 +869,21 @@ export async function readerPost(
|
|
|
857
869
|
result.status = 500;
|
|
858
870
|
return result;
|
|
859
871
|
}
|
|
860
|
-
// create reader endpoint
|
|
861
|
-
let readerEndpoint: string = graphConfig.readerStartSyncEndpoint
|
|
872
|
+
// create reader endpoint
|
|
873
|
+
let readerEndpoint: string = graphConfig.readerStartSyncEndpoint;
|
|
874
|
+
let url: URL = new URL(readerEndpoint);
|
|
875
|
+
url.searchParams.append("configurationId", config.id);
|
|
862
876
|
// create headers
|
|
863
877
|
const headers = await defineHeaders(instance, authorizedUser);
|
|
864
878
|
// make reader endpoint call
|
|
865
879
|
let options = { method: "POST", headers: headers };
|
|
866
880
|
try {
|
|
867
|
-
console.log("Attempting POST to /startSync: " +
|
|
868
|
-
let response = await fetch(
|
|
881
|
+
console.log("Attempting POST to /startSync: " + url.href);
|
|
882
|
+
let response = await fetch(url.href, options);
|
|
869
883
|
if (response.status === 200 && response.statusText === "OK") {
|
|
870
884
|
console.log(`Successful POST to /startSync: ${readerEndpoint}`);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
textResponse = textResponse;
|
|
874
|
-
|
|
885
|
+
let jsonResponse = await response.json();
|
|
886
|
+
result.array = JSON.parse(jsonResponse.PayloadStr);
|
|
875
887
|
return result;
|
|
876
888
|
} else {
|
|
877
889
|
result.error = await processErrors(response);
|
package/index.d.ts
CHANGED
|
@@ -65,11 +65,14 @@ declare module "@mindline/sync" {
|
|
|
65
65
|
export class TenantConfigInfo {
|
|
66
66
|
tid: string; // tenant identifier
|
|
67
67
|
sourceGroupId: string; // source group - we can configure source group for reading
|
|
68
|
-
sourceGroupName: string; //
|
|
68
|
+
sourceGroupName: string; //
|
|
69
|
+
targetGroupId: string; // target group - we can configure target group for writing
|
|
70
|
+
targetGroupName: string; //
|
|
69
71
|
configurationTenantType: TenantConfigTypeStrings;
|
|
70
72
|
deltaToken: string;
|
|
71
|
-
|
|
73
|
+
usersWritten: number;
|
|
72
74
|
configId: string;
|
|
75
|
+
batchId: string;
|
|
73
76
|
}
|
|
74
77
|
export class Config {
|
|
75
78
|
id: string;
|
|
@@ -134,12 +137,63 @@ declare module "@mindline/sync" {
|
|
|
134
137
|
setEnd(endDate: Date): void;
|
|
135
138
|
setStart(startDate: Date): void;
|
|
136
139
|
}
|
|
140
|
+
export class Milestone {
|
|
141
|
+
Run: number;
|
|
142
|
+
Start: Date;
|
|
143
|
+
startDisplay: string;
|
|
144
|
+
POST: Date;
|
|
145
|
+
postDisplay: string;
|
|
146
|
+
Read: Date;
|
|
147
|
+
readDisplay: string;
|
|
148
|
+
Write: Date;
|
|
149
|
+
writeDisplay: string;
|
|
150
|
+
Duration: Date;
|
|
151
|
+
durationDisplay: string;
|
|
152
|
+
constructor(run: number);
|
|
153
|
+
start(start: string): void;
|
|
154
|
+
post(post: string): void;
|
|
155
|
+
read(read: string): void;
|
|
156
|
+
write(write: string): void;
|
|
157
|
+
}
|
|
158
|
+
export class MilestoneArray {
|
|
159
|
+
milestones: Milestone[];
|
|
160
|
+
constructor(bClearLocalStorage: boolean);
|
|
161
|
+
init(bClearLocalStorage: boolean): void;
|
|
162
|
+
save(): void;
|
|
163
|
+
start(setMilestones: (milestones: Milestone[]) => void): void;
|
|
164
|
+
unstart(setMilestones: (milestones: Milestone[]) => void): void;
|
|
165
|
+
post(setMilestones: (milestones: Milestone[]) => void): void;
|
|
166
|
+
read(setMilestones: (milestones: Milestone[]) => void): void;
|
|
167
|
+
write(setMilestones: (milestones: Milestone[]) => void): void;
|
|
168
|
+
#initFromObjects(milestones: Object[]): void;
|
|
169
|
+
}
|
|
137
170
|
export class BatchArray {
|
|
138
171
|
tenantNodes: TenantNode[];
|
|
172
|
+
pb_startTS: number;
|
|
173
|
+
pb_progress: number;
|
|
174
|
+
pb_increment: number;
|
|
175
|
+
pb_idle: number;
|
|
176
|
+
pb_idleMax: number;
|
|
177
|
+
pb_timer: NodeJS.Timer;
|
|
178
|
+
milestoneArray: MilestoneArray;
|
|
139
179
|
constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
|
|
140
180
|
// populate tenantNodes based on config tenants
|
|
141
|
-
init(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
|
|
142
|
-
|
|
181
|
+
init(config: Config|null|undefined, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
|
|
182
|
+
initializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void;
|
|
183
|
+
uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void;
|
|
184
|
+
initializeSignalR(
|
|
185
|
+
config: Config | null | undefined,
|
|
186
|
+
syncPortalGlobalState: InitInfo | null,
|
|
187
|
+
batchIdArray: Array<Object>,
|
|
188
|
+
setRefreshDeltaTrigger: (trigger: boolean) => void,
|
|
189
|
+
setReadersTotal: (readersTotal: number) => void,
|
|
190
|
+
setReadersCurrent: (readersCurrent: number) => void,
|
|
191
|
+
setWritersTotal: (writersTotal: number) => void,
|
|
192
|
+
setWritersCurrent: (writersCurrent: number) => void,
|
|
193
|
+
setMilestones: (milestones: Milestone[]) => void,
|
|
194
|
+
setConfigSyncResult: (result: string) => void,
|
|
195
|
+
bClearLocalStorage: boolean): void;
|
|
196
|
+
startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): APIResult;
|
|
143
197
|
}
|
|
144
198
|
export class TenantNode {
|
|
145
199
|
expanded: boolean;
|
|
@@ -159,6 +213,7 @@ declare module "@mindline/sync" {
|
|
|
159
213
|
result: boolean;
|
|
160
214
|
status: number;
|
|
161
215
|
error: string;
|
|
216
|
+
array: Array<Object> | null;
|
|
162
217
|
constructor();
|
|
163
218
|
}
|
|
164
219
|
//
|
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,7 @@ 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';
|
|
12
13
|
import { log } from "console";
|
|
13
14
|
const FILTER_FIELD = "workspaceIDs";
|
|
14
15
|
// called by unit tests
|
|
@@ -112,18 +113,24 @@ export class TenantConfigInfo {
|
|
|
112
113
|
tid: string;
|
|
113
114
|
sourceGroupId: string;
|
|
114
115
|
sourceGroupName: string;
|
|
116
|
+
targetGroupId: string;
|
|
117
|
+
targetGroupName: string;
|
|
115
118
|
configurationTenantType: TenantConfigTypeStrings;
|
|
116
119
|
deltaToken: string;
|
|
117
|
-
|
|
120
|
+
usersWritten: number;
|
|
118
121
|
configId: string;
|
|
122
|
+
batchId: string;
|
|
119
123
|
constructor() {
|
|
120
124
|
this.tid = "";
|
|
121
125
|
this.sourceGroupId = "";
|
|
122
126
|
this.sourceGroupName = "";
|
|
127
|
+
this.targetGroupId = "";
|
|
128
|
+
this.targetGroupName = "";
|
|
123
129
|
this.configurationTenantType = "source";
|
|
124
130
|
this.deltaToken = "";
|
|
125
|
-
this.
|
|
131
|
+
this.usersWritten = 0;
|
|
126
132
|
this.configId = "";
|
|
133
|
+
this.batchId = "";
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
export class Config {
|
|
@@ -435,8 +442,196 @@ export class Task {
|
|
|
435
442
|
};
|
|
436
443
|
}
|
|
437
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: (milestones: Milestone[]) => void): 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: (milestones: Milestone[]) => void): 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: (milestones: Milestone[]) => void): 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: (milestones: Milestone[]) => void): 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: (milestones: Milestone[]) => void): 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
|
+
}
|
|
438
626
|
export class BatchArray {
|
|
439
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: NodeJS.Timer;
|
|
634
|
+
milestoneArray: MilestoneArray;
|
|
440
635
|
constructor(
|
|
441
636
|
config: Config | null,
|
|
442
637
|
syncPortalGlobalState: InitInfo | null,
|
|
@@ -444,30 +639,36 @@ export class BatchArray {
|
|
|
444
639
|
) {
|
|
445
640
|
this.tenantNodes = new Array<TenantNode>();
|
|
446
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 = null;
|
|
646
|
+
this.pb_idle = 0;
|
|
647
|
+
this.pb_idleMax = 0;
|
|
648
|
+
this.milestoneArray = new MilestoneArray(false);
|
|
447
649
|
}
|
|
448
650
|
// populate tenantNodes based on config tenants
|
|
449
651
|
init(
|
|
450
|
-
config: Config | null,
|
|
652
|
+
config: Config | null | undefined,
|
|
451
653
|
syncPortalGlobalState: InitInfo | null,
|
|
452
654
|
bClearLocalStorage: boolean
|
|
453
655
|
): void {
|
|
454
656
|
console.log(
|
|
455
|
-
`Calling BatchArray::init(config: "${config ? config.name : "null"
|
|
456
|
-
}", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
|
|
657
|
+
`Calling BatchArray::init(config: "${config ? config.name : "null"}", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
|
|
457
658
|
);
|
|
458
|
-
//
|
|
459
|
-
this.tenantNodes.length = 0;
|
|
460
|
-
// then clear localStorage if we have been asked to
|
|
659
|
+
// clear localStorage if we have been asked to
|
|
461
660
|
if (bClearLocalStorage) {
|
|
462
|
-
if (storageAvailable("localStorage"))
|
|
661
|
+
if (storageAvailable("localStorage")) {
|
|
463
662
|
localStorage.removeItem(config.name);
|
|
663
|
+
this.milestoneArray.init(bClearLocalStorage);
|
|
664
|
+
}
|
|
464
665
|
}
|
|
465
666
|
// create BatchArray if passed Config and InitInfo
|
|
466
|
-
if (
|
|
467
|
-
config != null &&
|
|
667
|
+
if (config != null &&
|
|
468
668
|
config.tenants != null &&
|
|
469
|
-
syncPortalGlobalState != null
|
|
470
|
-
|
|
669
|
+
syncPortalGlobalState != null) {
|
|
670
|
+
// clear batch array only if we have been passed something with which to replace it
|
|
671
|
+
this.tenantNodes.length = 0;
|
|
471
672
|
// create a sourceTenantNode for each Source and SourceTarget
|
|
472
673
|
config.tenants.map((tciPotentialSource: TenantConfigInfo) => {
|
|
473
674
|
if (
|
|
@@ -480,7 +681,8 @@ export class BatchArray {
|
|
|
480
681
|
if (sourceTenant != null) {
|
|
481
682
|
let sourceTenantNode: TenantNode = new TenantNode(
|
|
482
683
|
tciPotentialSource.tid,
|
|
483
|
-
sourceTenant.name
|
|
684
|
+
sourceTenant.name,
|
|
685
|
+
tciPotentialSource.batchId
|
|
484
686
|
);
|
|
485
687
|
this.tenantNodes.push(sourceTenantNode);
|
|
486
688
|
} else {
|
|
@@ -508,7 +710,8 @@ export class BatchArray {
|
|
|
508
710
|
if (targetTenant != null) {
|
|
509
711
|
let targetTenantNode: TenantNode = new TenantNode(
|
|
510
712
|
tciPotentialTarget.tid,
|
|
511
|
-
targetTenant.name
|
|
713
|
+
targetTenant.name,
|
|
714
|
+
tciPotentialTarget.batchId
|
|
512
715
|
);
|
|
513
716
|
sourceTenantNode.targets.push(targetTenantNode);
|
|
514
717
|
sourceTenantNode.expanded = true;
|
|
@@ -523,195 +726,255 @@ export class BatchArray {
|
|
|
523
726
|
}
|
|
524
727
|
});
|
|
525
728
|
});
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
initializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): 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
|
+
this.pb_timer = null;
|
|
746
|
+
if (this.milestoneArray.milestones[0].Write == null) {
|
|
747
|
+
this.milestoneArray.write(setMilestones);
|
|
541
748
|
}
|
|
749
|
+
setConfigSyncResult(`finished sync, no updates for ${this.pb_idle} seconds`);
|
|
542
750
|
}
|
|
543
|
-
|
|
751
|
+
// if we get to 100, stop the timer, let SignalR or countdown timer finish sync
|
|
752
|
+
if (this.pb_progress < 100) {
|
|
753
|
+
this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
|
|
754
|
+
setSyncProgress(this.pb_progress);
|
|
755
|
+
}
|
|
756
|
+
}, 1000);
|
|
757
|
+
this.milestoneArray.start(setMilestones);
|
|
544
758
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
759
|
+
uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
760
|
+
this.pb_startTS = 0;
|
|
761
|
+
this.pb_progress = 0;
|
|
762
|
+
setSyncProgress(this.pb_progress);
|
|
763
|
+
setConfigSyncResult("sync failed to execute");
|
|
764
|
+
this.pb_increment = 0;
|
|
765
|
+
clearInterval(this.pb_timer);
|
|
766
|
+
this.pb_timer = null;
|
|
767
|
+
this.pb_idle = 0;
|
|
768
|
+
this.pb_idleMax = 0;
|
|
769
|
+
setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
|
|
770
|
+
this.milestoneArray.unstart(setMilestones);
|
|
771
|
+
}
|
|
772
|
+
initializeSignalR(
|
|
773
|
+
config: Config | null | undefined,
|
|
774
|
+
syncPortalGlobalState: InitInfo | null,
|
|
775
|
+
batchIdArray: Array<Object>,
|
|
776
|
+
setRefreshDeltaTrigger: (trigger: boolean) => void,
|
|
777
|
+
setReadersTotal: (readersTotal: number) => void,
|
|
778
|
+
setReadersCurrent: (readersCurrent: number) => void,
|
|
779
|
+
setWritersTotal: (writersTotal: number) => void,
|
|
780
|
+
setWritersCurrent: (writersCurrent: number) => void ,
|
|
781
|
+
setMilestones: (milestones: Milestone[]) => void,
|
|
782
|
+
setConfigSyncResult: (result: string) => void,
|
|
783
|
+
bClearLocalStorage: boolean
|
|
784
|
+
): void {
|
|
785
|
+
// we have just completed a successful POST to startSync
|
|
786
|
+
this.milestoneArray.post(setMilestones);
|
|
787
|
+
setConfigSyncResult("started sync, waiting for updates...");
|
|
788
|
+
// re-initialize batch array with Configuration updated by the succcessful POST to startSync
|
|
789
|
+
this.init(config, syncPortalGlobalState, false);
|
|
556
790
|
// define newMessage handler that can access *this*
|
|
557
791
|
let handler = (message) => {
|
|
558
792
|
console.log(message);
|
|
559
793
|
let item = JSON.parse(message);
|
|
794
|
+
// reset the countdown timer every time we get a message
|
|
795
|
+
this.pb_idle = 0;
|
|
560
796
|
// find the associated tenant for this SignalR message
|
|
561
|
-
let
|
|
797
|
+
let matchingPair: Object = batchIdArray.find((o: Object) => o.BatchId == item.TargetID);
|
|
798
|
+
if (matchingPair == null) {
|
|
799
|
+
console.log(`Batch ${item.TargetID} not found in batchIdArray.`);
|
|
800
|
+
debugger;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === matchingPair.SourceId);
|
|
562
804
|
if (tenantNode == null) { // null OR undefined
|
|
563
|
-
console.log(
|
|
805
|
+
console.log(`Tenant ${matchingPair.SourceId} not found in BatchArray.`);
|
|
564
806
|
debugger;
|
|
565
807
|
return;
|
|
566
808
|
}
|
|
567
|
-
|
|
568
|
-
// process stats for this SignalR message
|
|
809
|
+
tenantNode.batchId = matchingPair.BatchId;
|
|
810
|
+
// process stats for this SignalR message batch
|
|
569
811
|
let statsarray = item.Stats; // get the array of statistics
|
|
570
812
|
let statskeys = Object.keys(statsarray); // get the keys of the array
|
|
571
813
|
let statsvalues = Object.values(statsarray); // get the values of the array
|
|
572
814
|
for (let j = 0; j < statskeys.length; j++) {
|
|
815
|
+
let bTotalCount = statskeys[j].endsWith("TotalCount");
|
|
816
|
+
let bCurrentCount = statskeys[j].endsWith("CurrentCount");
|
|
817
|
+
let bDeferredCount = statskeys[j].endsWith("DeferredCount");
|
|
573
818
|
if (statskeys[j].startsWith("Reader")) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (actualWriter == null) {
|
|
589
|
-
console.log(`could not find queued writer ${queuedWriter.tid}.`);
|
|
590
|
-
debugger;
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
actualWriter.update(queuedWriter.total, queuedWriter.read, queuedWriter.written, queuedWriter.deferred);
|
|
594
|
-
}
|
|
595
|
-
idx = this.tenantNodes.findIndex((t: TenantNode) => (t.name === "QUEUED WRITER" && t.batchId === matchBID[1]));
|
|
596
|
-
}
|
|
597
|
-
// update Read node
|
|
598
|
-
tenantNode.batchId = matchBID[1];
|
|
599
|
-
tenantNode.total = Number(statsvalues[j]);
|
|
600
|
-
console.log(`----- ${tenantNode.name} batchId: ${tenantNode.batchId}`);
|
|
601
|
-
console.log(`----- ${tenantNode.name} Total Read: ${tenantNode.total}`);
|
|
819
|
+
// parse tid from Reader key
|
|
820
|
+
let tidRegexp = /Reader\/TID:(.+)\/TotalCount/;
|
|
821
|
+
if (bCurrentCount) tidRegexp = /Reader\/TID:(.+)\/CurrentCount/;
|
|
822
|
+
if (bDeferredCount) tidRegexp = /Reader\/TID:(.+)\/DeferredCount/;
|
|
823
|
+
let matchTID = statskeys[j].match(tidRegexp);
|
|
824
|
+
if (matchTID == null) {
|
|
825
|
+
console.log(`tid not found in ${statskeys[j]}.`);
|
|
826
|
+
debugger;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (bTotalCount) {
|
|
830
|
+
tenantNode.total = Math.max(Number(statsvalues[j]), tenantNode.total);
|
|
831
|
+
console.log(`----- ${tenantNode.name} TID: ${tenantNode.tid} batchId: ${tenantNode.batchId}`);
|
|
832
|
+
console.log(`----- ${tenantNode.name} Total To Read: ${tenantNode.total}`);
|
|
602
833
|
}
|
|
603
|
-
|
|
604
|
-
tenantNode.read = Number(statsvalues[j]);
|
|
834
|
+
else {
|
|
835
|
+
tenantNode.read = Math.max(Number(statsvalues[j]), tenantNode.read);
|
|
605
836
|
console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
|
|
606
837
|
}
|
|
607
838
|
}
|
|
608
839
|
if (statskeys[j].startsWith("Writer")) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// find the Writer node for this tenant under the Reader node of a different tenant
|
|
619
|
-
let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
|
|
620
|
-
if (readerNode == null) {
|
|
621
|
-
// reader for this batch has not yet been encountered, queue this writer for processing when reader arrives
|
|
622
|
-
writerNode = new TenantNode(tenantNode.tid, "QUEUED WRITER");
|
|
623
|
-
writerNode.batchId = matchBID[1];
|
|
624
|
-
writerNode.total = Number(statsvalues[j]);
|
|
625
|
-
this.tenantNodes.push(writerNode);
|
|
626
|
-
console.log(`----- QUEUED batchId: ${writerNode.batchId}`);
|
|
627
|
-
console.log(`----- QUEUED Total To Write: ${writerNode.total}`);
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
if (readerNode.name == "QUEUED WRITER") {
|
|
631
|
-
// update queued node
|
|
632
|
-
writerNode = readerNode;
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
// reader is legit, find writer node under this reader node
|
|
636
|
-
writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
|
|
637
|
-
if (writerNode == null) {
|
|
638
|
-
console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
|
|
639
|
-
debugger;
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
writerNode.batchId = matchBID[1];
|
|
644
|
-
writerNode.total = Number(statsvalues[j]);
|
|
645
|
-
console.log(`----- ${writerNode.name} batchId: ${writerNode.batchId}`);
|
|
646
|
-
console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
|
|
647
|
-
}
|
|
840
|
+
// parse tid from Writer key
|
|
841
|
+
let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
|
|
842
|
+
if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
|
|
843
|
+
if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
|
|
844
|
+
let matchTID = statskeys[j].match(tidRegexp);
|
|
845
|
+
if (matchTID == null) {
|
|
846
|
+
console.log(`tid not found in ${statskeys[j]}.`);
|
|
847
|
+
debugger;
|
|
848
|
+
return;
|
|
648
849
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
debugger;
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
// find the Writer node for this tenant under the Reader node of a different tenant
|
|
659
|
-
let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
|
|
660
|
-
if (readerNode == null) {
|
|
661
|
-
// no reader or queued writer for this batch
|
|
662
|
-
console.log(`No reader or queued writer for batch ${matchBID[1]}.`);
|
|
663
|
-
debugger;
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (readerNode.name == "QUEUED WRITER") {
|
|
667
|
-
// update queued node
|
|
668
|
-
writerNode = readerNode;
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
// reader is legit, find writer node under this reader node
|
|
672
|
-
writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
|
|
673
|
-
if (writerNode == null) {
|
|
674
|
-
console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
|
|
675
|
-
debugger;
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
writerNode.batchId = matchBID[1];
|
|
680
|
-
writerNode.written = Number(statsvalues[j]);
|
|
681
|
-
console.log(`${writerNode.name} batchId: ${writerNode.batchId}`);
|
|
682
|
-
console.log(`${writerNode.name} Total Written: ${writerNode.written}`);
|
|
850
|
+
// this Writer node should exist precisely under the Reader for this SignalR message
|
|
851
|
+
let writerNode: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid === matchTID[1]);
|
|
852
|
+
if (writerNode == null) {
|
|
853
|
+
console.log(`Writer ${tenantNode.name} not found under Reader ${tenantNode.name}.`);
|
|
854
|
+
debugger;
|
|
855
|
+
return;
|
|
683
856
|
}
|
|
857
|
+
writerNode.batchId = matchingPair.BatchId;
|
|
858
|
+
if (bTotalCount) {
|
|
859
|
+
writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
|
|
860
|
+
console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
|
|
861
|
+
console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
|
|
862
|
+
}
|
|
863
|
+
else if (bCurrentCount) {
|
|
864
|
+
writerNode.written = Math.max(Number(statsvalues[j]), writerNode.written);
|
|
865
|
+
console.log(`----- ${writerNode.name} Total Written: ${writerNode.written}`);
|
|
866
|
+
}
|
|
867
|
+
else if (bDeferredCount) {
|
|
868
|
+
writerNode.deferred = Math.max(Number(statsvalues[j]), writerNode.deferred);
|
|
869
|
+
console.log(`----- ${writerNode.name} Total Deferred: ${writerNode.deferred}`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
console.log(`unknown writer type`);
|
|
873
|
+
debugger;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
|
|
684
877
|
}
|
|
685
878
|
}
|
|
879
|
+
// update status based on all updates in this message
|
|
686
880
|
tenantNode.update(tenantNode.total, tenantNode.read, tenantNode.written, tenantNode.deferred);
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
881
|
+
// for each message, enumerate nodes to assess completion state
|
|
882
|
+
let bReadingComplete: boolean = true;
|
|
883
|
+
let bWritingComplete: boolean = true;
|
|
884
|
+
let bWritingStarted: boolean = false;
|
|
885
|
+
let readerTotal: number = 0;
|
|
886
|
+
let readerCurrent: number = 0;
|
|
887
|
+
let writerTotal: number = 0;
|
|
888
|
+
let writerCurrent: number = 0;
|
|
889
|
+
this.tenantNodes.map((sourceTenantNode: TenantNode) => {
|
|
890
|
+
sourceTenantNode.targets.map((writerNode: TenantNode) => {
|
|
891
|
+
bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
|
|
892
|
+
bWritingStarted ||= (writerNode.total > 0 || writerNode.status != "not started");
|
|
893
|
+
writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
|
|
894
|
+
writerCurrent += writerNode.written;
|
|
895
|
+
});
|
|
896
|
+
bReadingComplete &&= (sourceTenantNode.status == "complete" || sourceTenantNode.status == "failed");
|
|
897
|
+
readerTotal += sourceTenantNode.total;
|
|
898
|
+
readerCurrent += sourceTenantNode.read;
|
|
899
|
+
});
|
|
900
|
+
// set linear gauge max and current values
|
|
901
|
+
setReadersTotal(readerTotal);
|
|
902
|
+
setReadersCurrent(readerCurrent);
|
|
903
|
+
setWritersTotal(Math.max(writerTotal, readerTotal));
|
|
904
|
+
setWritersCurrent(writerCurrent);
|
|
905
|
+
// because it is an important milestone, we always check if we have *just* completed reading
|
|
906
|
+
if (bReadingComplete && this.milestoneArray.milestones[0].Read == null) {
|
|
907
|
+
this.milestoneArray.read(setMilestones);
|
|
908
|
+
setConfigSyncResult("reading complete");
|
|
909
|
+
console.log(`Setting config sync result: "reading complete"`);
|
|
910
|
+
// trigger refresh delta tokens
|
|
911
|
+
setRefreshDeltaTrigger(true);
|
|
912
|
+
// change to % per second to complete in 7x as long as it took to get here
|
|
913
|
+
let readTS = Date.now();
|
|
914
|
+
let secsElapsed = (readTS - this.pb_startTS) / 1000;
|
|
915
|
+
let expectedPercentDone = 7;
|
|
916
|
+
let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
|
|
917
|
+
this.pb_increment = expectedPercentPerSecond;
|
|
918
|
+
console.log(`Setting increment: ${this.pb_increment}% per second`);
|
|
697
919
|
}
|
|
698
|
-
|
|
920
|
+
// with that out of the way, is writing complete?
|
|
921
|
+
if (bWritingComplete) {
|
|
922
|
+
this.milestoneArray.write(setMilestones);
|
|
923
|
+
setConfigSyncResult("sync complete");
|
|
924
|
+
console.log(`Setting config sync result: "complete"`);
|
|
925
|
+
this.pb_progress = 99;
|
|
926
|
+
}
|
|
927
|
+
// if not, has writing even started?
|
|
928
|
+
else if (bWritingStarted) {
|
|
929
|
+
setConfigSyncResult("writing in progress");
|
|
930
|
+
console.log(`Setting config sync result: "writing in progress"`);
|
|
931
|
+
}
|
|
932
|
+
// else, we must be reading (unless we already completed reading)
|
|
933
|
+
else if (this.milestoneArray.milestones[0].Read == null){
|
|
699
934
|
setConfigSyncResult("reading in progress");
|
|
700
935
|
console.log(`Setting config sync result: "reading in progress"`);
|
|
701
936
|
}
|
|
702
937
|
}
|
|
703
|
-
// start SignalR connection
|
|
704
|
-
|
|
705
|
-
const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${
|
|
938
|
+
// start SignalR connection based on each batchId
|
|
939
|
+
batchIdArray.map((batchPair: Object) => {
|
|
940
|
+
const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${batchPair.BatchId}`;
|
|
941
|
+
console.log(`Creating SignalR Hub for TID: ${batchPair.SourceId} ${endpointUrl}`);
|
|
706
942
|
const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
|
|
707
943
|
.withUrl(endpointUrl)
|
|
708
|
-
|
|
944
|
+
.withAutomaticReconnect()
|
|
709
945
|
.configureLogging(signalR.LogLevel.Information)
|
|
710
946
|
.build();
|
|
711
947
|
// when you get a message, process the message
|
|
712
948
|
connection.on("newMessage", handler);
|
|
949
|
+
connection.onreconnecting(error => {
|
|
950
|
+
console.assert(connection.state === signalR.HubConnectionState.Reconnecting);
|
|
951
|
+
console.log(`Connection lost due to error "${error}". Reconnecting.`);
|
|
952
|
+
});
|
|
953
|
+
connection.onreconnected(connectionId => {
|
|
954
|
+
console.assert(connection.state === signalR.HubConnectionState.Connected);
|
|
955
|
+
console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
|
|
956
|
+
});
|
|
957
|
+
// restart when you get a close event
|
|
958
|
+
connection.onclose(async () => {
|
|
959
|
+
console.log(`Connection closing. Attempting restart.`);
|
|
960
|
+
await connection.start();
|
|
961
|
+
});
|
|
962
|
+
// start and display any caught exceptions in the console
|
|
713
963
|
connection.start().catch(console.error);
|
|
714
964
|
});
|
|
965
|
+
}
|
|
966
|
+
// start a sync cycle
|
|
967
|
+
async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): Promise<APIResult>
|
|
968
|
+
{
|
|
969
|
+
let result: APIResult = new APIResult();
|
|
970
|
+
if (this.tenantNodes == null || this.tenantNodes.length == 0) {
|
|
971
|
+
// we should not have an empty batch array for a test
|
|
972
|
+
debugger;
|
|
973
|
+
result.result = false;
|
|
974
|
+
result.error = "startSync: invalid parameters";
|
|
975
|
+
result.status = 500;
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
715
978
|
// execute post to reader endpoint
|
|
716
979
|
result = await readerPost(instance, authorizedUser, config);
|
|
717
980
|
return result;
|
|
@@ -728,11 +991,11 @@ export class TenantNode {
|
|
|
728
991
|
written: number;
|
|
729
992
|
deferred: number;
|
|
730
993
|
targets: TenantNode[];
|
|
731
|
-
constructor(tid: string, name: string) {
|
|
994
|
+
constructor(tid: string, name: string, batchId: string) {
|
|
732
995
|
this.expanded = false;
|
|
733
996
|
this.name = name;
|
|
734
997
|
this.tid = tid;
|
|
735
|
-
this.batchId =
|
|
998
|
+
this.batchId = batchId;
|
|
736
999
|
this.targets = new Array<TenantNode>();
|
|
737
1000
|
this.update(0, 0, 0, 0);
|
|
738
1001
|
}
|
|
@@ -749,7 +1012,7 @@ export class TenantNode {
|
|
|
749
1012
|
else if (this.written > 0) {
|
|
750
1013
|
if (this.written + this.deferred < this.total) this.status = "in progress";
|
|
751
1014
|
else if (this.written === this.total) this.status = "complete";
|
|
752
|
-
else if (this.written + this.deferred
|
|
1015
|
+
else if (this.written + this.deferred >= this.total) this.status = "failed";
|
|
753
1016
|
}
|
|
754
1017
|
}
|
|
755
1018
|
}
|
|
@@ -826,7 +1089,7 @@ export function signIn(user: User, tasks: TaskArray): void {
|
|
|
826
1089
|
tenantURL += "MicrosoftIdentity/Account/Challenge";
|
|
827
1090
|
let url: URL = new URL(tenantURL);
|
|
828
1091
|
url.searchParams.append("redirectUri", window.location.origin);
|
|
829
|
-
url.searchParams.append("scope", "openid offline_access
|
|
1092
|
+
url.searchParams.append("scope", "openid offline_access Directory.AccessAsUser.All CrossTenantInformation.ReadBasic.All");
|
|
830
1093
|
url.searchParams.append("domainHint", "organizations");
|
|
831
1094
|
if (user.oid !== "1") {
|
|
832
1095
|
url.searchParams.append("loginHint", user.mail);
|
|
@@ -1173,7 +1436,7 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
|
|
|
1173
1436
|
returnedAdmins.map((item) => {
|
|
1174
1437
|
// are we already tracking this user?
|
|
1175
1438
|
let user: User | null = null;
|
|
1176
|
-
let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
|
|
1439
|
+
let usIndex = ii.us.findIndex((u) => (u.oid === item.userId || u.oid === item.email));
|
|
1177
1440
|
if (usIndex === -1) {
|
|
1178
1441
|
// start tracking
|
|
1179
1442
|
let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
|
|
@@ -1192,13 +1455,13 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
|
|
|
1192
1455
|
user = ii.us.at(usIndex);
|
|
1193
1456
|
}
|
|
1194
1457
|
// refresh all the data available from the server
|
|
1195
|
-
user.oid = item.userId;
|
|
1458
|
+
user.oid = item.userId ? item.userId : item.email;
|
|
1196
1459
|
user.name = item.firstName;
|
|
1197
1460
|
user.mail = item.email;
|
|
1198
1461
|
user.tid = item.tenantId;
|
|
1199
1462
|
// ensure this workspace tracks this user
|
|
1200
|
-
let idx = workspace.associatedUsers.findIndex((u) => u ===
|
|
1201
|
-
if (idx == -1) workspace.associatedUsers.push(
|
|
1463
|
+
let idx = workspace.associatedUsers.findIndex((u) => u === user.oid);
|
|
1464
|
+
if (idx == -1) workspace.associatedUsers.push(user.oid);
|
|
1202
1465
|
});
|
|
1203
1466
|
}
|
|
1204
1467
|
function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>) {
|
|
@@ -1227,7 +1490,12 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
|
|
|
1227
1490
|
tenant.tenantType = item.type.toLowerCase(); // should now be strings
|
|
1228
1491
|
tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
|
|
1229
1492
|
tenant.onboarded = item.isOnboarded ? "true" : "false";
|
|
1230
|
-
|
|
1493
|
+
|
|
1494
|
+
// canonicalize authority when getting it from config backend
|
|
1495
|
+
const regex = /^(https:\/\/login.microsoftonline.(?:us|com)\/)organizations\/v2.0$/;
|
|
1496
|
+
const regexMatch = item.authority.match(regex);
|
|
1497
|
+
tenant.authority = regexMatch ? regexMatch[1] : item.authority;
|
|
1498
|
+
|
|
1231
1499
|
tenant.readServicePrincipal = item.readServicePrincipal;
|
|
1232
1500
|
tenant.writeServicePrincipal = item.writeServicePrincipal;
|
|
1233
1501
|
// ensure this workspace tracks this tenant
|
|
@@ -1265,11 +1533,14 @@ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConf
|
|
|
1265
1533
|
item.tenants.map((tci) => {
|
|
1266
1534
|
let tenantConfigInfo = new TenantConfigInfo();
|
|
1267
1535
|
tenantConfigInfo.tid = tci.tenantId;
|
|
1268
|
-
tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
|
|
1269
|
-
tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
|
|
1536
|
+
tenantConfigInfo.sourceGroupId = tci.sourceGroupId ?? "";
|
|
1537
|
+
tenantConfigInfo.sourceGroupName = tci.sourceGroupName ?? "";
|
|
1538
|
+
tenantConfigInfo.targetGroupId = tci.targetGroupId ?? "";
|
|
1539
|
+
tenantConfigInfo.targetGroupName = tci.targetGroupName ?? "";
|
|
1270
1540
|
tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
|
|
1271
1541
|
tenantConfigInfo.deltaToken = tci.deltaToken ?? "";
|
|
1272
1542
|
tenantConfigInfo.configId = config!.id;
|
|
1543
|
+
tenantConfigInfo.batchId = tci.batchId ?? "";
|
|
1273
1544
|
config!.tenants.push(tenantConfigInfo);
|
|
1274
1545
|
});
|
|
1275
1546
|
// ensure this workspace tracks this config
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindline/sync",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.39",
|
|
5
5
|
"types": "index.d.ts",
|
|
6
6
|
"exports": "./index.ts",
|
|
7
7
|
"description": "sync is a node.js package encapsulating javscript classes required for configuring Mindline sync service.",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"Run": "3",
|
|
4
|
+
"Start": "2023-09-17T12:00:00.000-07:00",
|
|
5
|
+
"POST": "2023-09-17T12:00:09.500-07:00",
|
|
6
|
+
"Read": "2023-09-17T12:00:15.500-07:00",
|
|
7
|
+
"Write": "2023-09-17T12:01:30.000-07:00"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"Run": "2",
|
|
11
|
+
"Start": "2023-09-17T12:00:00.000-07:00",
|
|
12
|
+
"POST": "2023-09-17T12:00:09.500-07:00",
|
|
13
|
+
"Read": "2023-09-17T12:00:15.500-07:00",
|
|
14
|
+
"Write": "2023-09-17T12:01:30.000-07:00"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"Run": "1",
|
|
18
|
+
"Start": "2023-09-17T12:00:00.000-07:00",
|
|
19
|
+
"POST": "2023-09-17T12:00:09.500-07:00",
|
|
20
|
+
"Read": "2023-09-17T12:00:15.500-07:00",
|
|
21
|
+
"Write": "2023-09-17T12:01:30.000-07:00"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
Binary file
|