@fluidframework/debugger 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.0

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.
Files changed (39) hide show
  1. package/.eslintrc.js +9 -12
  2. package/README.md +33 -33
  3. package/api-extractor.json +2 -2
  4. package/dist/fluidDebugger.d.ts.map +1 -1
  5. package/dist/fluidDebugger.js.map +1 -1
  6. package/dist/fluidDebuggerController.d.ts.map +1 -1
  7. package/dist/fluidDebuggerController.js +5 -2
  8. package/dist/fluidDebuggerController.js.map +1 -1
  9. package/dist/fluidDebuggerUi.d.ts.map +1 -1
  10. package/dist/fluidDebuggerUi.js +11 -5
  11. package/dist/fluidDebuggerUi.js.map +1 -1
  12. package/dist/messageSchema.js.map +1 -1
  13. package/dist/sanitize.js.map +1 -1
  14. package/dist/sanitizer.d.ts.map +1 -1
  15. package/dist/sanitizer.js +9 -5
  16. package/dist/sanitizer.js.map +1 -1
  17. package/lib/fluidDebugger.d.ts.map +1 -1
  18. package/lib/fluidDebugger.js.map +1 -1
  19. package/lib/fluidDebuggerController.d.ts.map +1 -1
  20. package/lib/fluidDebuggerController.js +5 -2
  21. package/lib/fluidDebuggerController.js.map +1 -1
  22. package/lib/fluidDebuggerUi.d.ts.map +1 -1
  23. package/lib/fluidDebuggerUi.js +11 -5
  24. package/lib/fluidDebuggerUi.js.map +1 -1
  25. package/lib/messageSchema.js.map +1 -1
  26. package/lib/sanitize.js.map +1 -1
  27. package/lib/sanitizer.d.ts.map +1 -1
  28. package/lib/sanitizer.js +9 -5
  29. package/lib/sanitizer.js.map +1 -1
  30. package/package.json +33 -32
  31. package/prettier.config.cjs +1 -1
  32. package/src/fluidDebugger.ts +30 -30
  33. package/src/fluidDebuggerController.ts +348 -322
  34. package/src/fluidDebuggerUi.ts +306 -281
  35. package/src/messageSchema.ts +367 -367
  36. package/src/sanitize.ts +29 -29
  37. package/src/sanitizer.ts +699 -638
  38. package/tsconfig.esnext.json +6 -6
  39. package/tsconfig.json +10 -15
@@ -7,98 +7,97 @@ import { assert } from "@fluidframework/common-utils";
7
7
  import { ISequencedDocumentMessage, IVersion } from "@fluidframework/protocol-definitions";
8
8
 
9
9
  export interface IDebuggerUI {
10
- /**
11
- * Version information is provided.
12
- * Expect updates (information about seq#, timestamp) through updateVersion() calls
13
- */
14
- addVersions(version: IVersion[]): void;
15
-
16
- /**
17
- * Call when new version is downloaded from storage
18
- * Expect multiple callbacks.
19
- */
20
- updateVersion(index: number, version: IVersion, seqNumber: number): void;
21
-
22
- /**
23
- * Called in response to successful onVersionSelection() or onSnapshotFileSelection() call
24
- * and provides extra information about selection.
25
- * It expected that UI layer would change its mode as result of this call, i.e. switch to
26
- * displaying op playback controls (if this is supported)
27
- * Note: There maybe no call to versionSelected() in response to onSnapshotFileSelection() call
28
- * if file does not exist, has wrong name of wrong format.
29
- * @param version - version, file name, or undefined if playing ops.
30
- */
31
- versionSelected(seqNumber: number, version: IVersion | string): void;
32
-
33
- /**
34
- * Called by controller in response to new ops being downloaded
35
- * Called with disable = true if there are no (currently) ops to play
36
- */
37
- disableNextOpButton(disable: boolean): void;
38
-
39
- /**
40
- * Called by controller when new ops arrive (or we are done playing previous batch)
41
- * Indicates next batch of ops that would be played when UI calls controller's onOpButtonClick()
42
- * Called with ops=[] when there are no ops to play.
43
- */
44
- updateNextOpText(ops: ISequencedDocumentMessage[]): void;
45
-
46
- /**
47
- * Called periodically when new versions are downloaded from server
48
- */
49
- updateVersionText(versionsLeft: number): void;
50
-
51
- /**
52
- * Called periodically to notify about last known op
53
- * @param lastKnownOp - seq number of last known op. -1 if can't play ops in this mode (load from file)
54
- * @param stillLoading - true if we did not reach yet the end of the stream
55
- */
56
- updateLastOpText(lastKnownOp: number, stillLoading: boolean): void;
10
+ /**
11
+ * Version information is provided.
12
+ * Expect updates (information about seq#, timestamp) through updateVersion() calls
13
+ */
14
+ addVersions(version: IVersion[]): void;
15
+
16
+ /**
17
+ * Call when new version is downloaded from storage
18
+ * Expect multiple callbacks.
19
+ */
20
+ updateVersion(index: number, version: IVersion, seqNumber: number): void;
21
+
22
+ /**
23
+ * Called in response to successful onVersionSelection() or onSnapshotFileSelection() call
24
+ * and provides extra information about selection.
25
+ * It expected that UI layer would change its mode as result of this call, i.e. switch to
26
+ * displaying op playback controls (if this is supported)
27
+ * Note: There maybe no call to versionSelected() in response to onSnapshotFileSelection() call
28
+ * if file does not exist, has wrong name of wrong format.
29
+ * @param version - version, file name, or undefined if playing ops.
30
+ */
31
+ versionSelected(seqNumber: number, version: IVersion | string): void;
32
+
33
+ /**
34
+ * Called by controller in response to new ops being downloaded
35
+ * Called with disable = true if there are no (currently) ops to play
36
+ */
37
+ disableNextOpButton(disable: boolean): void;
38
+
39
+ /**
40
+ * Called by controller when new ops arrive (or we are done playing previous batch)
41
+ * Indicates next batch of ops that would be played when UI calls controller's onOpButtonClick()
42
+ * Called with ops=[] when there are no ops to play.
43
+ */
44
+ updateNextOpText(ops: ISequencedDocumentMessage[]): void;
45
+
46
+ /**
47
+ * Called periodically when new versions are downloaded from server
48
+ */
49
+ updateVersionText(versionsLeft: number): void;
50
+
51
+ /**
52
+ * Called periodically to notify about last known op
53
+ * @param lastKnownOp - seq number of last known op. -1 if can't play ops in this mode (load from file)
54
+ * @param stillLoading - true if we did not reach yet the end of the stream
55
+ */
56
+ updateLastOpText(lastKnownOp: number, stillLoading: boolean): void;
57
57
  }
58
58
 
59
59
  export interface IDebuggerController {
60
- /**
61
- * Initialization. UI layers calls into controller to connect the two.
62
- * @param ui - UI layer
63
- */
64
- connectToUi(ui: IDebuggerUI);
65
-
66
- /**
67
- * Called by UI layer when debugger window is closed by user
68
- * If called before user makes selection of snapshot/file, original
69
- * document service is returned to loader (instead of debugger service) and normal document load continues.
70
- */
71
- onClose(): void;
72
-
73
- /**
74
- * UI Layer notifies about selection of version to continue.
75
- * On successful load, versionSelected() is called.
76
- * @param version - Snapshot version to start from.
77
- */
78
- onVersionSelection(version: IVersion): void;
79
-
80
- /**
81
- * UI Layer notifies about selection of version to continue.
82
- * On successful load, versionSelected() is called.
83
- * @param version - File to load snapshot from
84
- */
85
- onSnapshotFileSelection(file: File): void;
86
-
87
- /**
88
- * "next op" button is clicked in the UI
89
- * @param steps - number of ops to play.
90
- */
91
- onOpButtonClick(steps: number): void;
92
-
93
- /**
94
- * "Download ops" option is clicked in the UI. Returns JSON of the full opStream when available.
95
- * @param anonymize - anonymize the ops json using the sanitization tool
96
- */
97
- onDownloadOpsButtonClick(anonymize: boolean): Promise<string>;
60
+ /**
61
+ * Initialization. UI layers calls into controller to connect the two.
62
+ * @param ui - UI layer
63
+ */
64
+ connectToUi(ui: IDebuggerUI);
65
+
66
+ /**
67
+ * Called by UI layer when debugger window is closed by user
68
+ * If called before user makes selection of snapshot/file, original
69
+ * document service is returned to loader (instead of debugger service) and normal document load continues.
70
+ */
71
+ onClose(): void;
72
+
73
+ /**
74
+ * UI Layer notifies about selection of version to continue.
75
+ * On successful load, versionSelected() is called.
76
+ * @param version - Snapshot version to start from.
77
+ */
78
+ onVersionSelection(version: IVersion): void;
79
+
80
+ /**
81
+ * UI Layer notifies about selection of version to continue.
82
+ * On successful load, versionSelected() is called.
83
+ * @param version - File to load snapshot from
84
+ */
85
+ onSnapshotFileSelection(file: File): void;
86
+
87
+ /**
88
+ * "next op" button is clicked in the UI
89
+ * @param steps - number of ops to play.
90
+ */
91
+ onOpButtonClick(steps: number): void;
92
+
93
+ /**
94
+ * "Download ops" option is clicked in the UI. Returns JSON of the full opStream when available.
95
+ * @param anonymize - anonymize the ops json using the sanitization tool
96
+ */
97
+ onDownloadOpsButtonClick(anonymize: boolean): Promise<string>;
98
98
  }
99
99
 
100
- const debuggerWindowHtml =
101
- `<Title>Fluid Debugger</Title>
100
+ const debuggerWindowHtml = `<Title>Fluid Debugger</Title>
102
101
  <body>
103
102
  <h3>Fluid Debugger</h3>
104
103
  Please select snapshot or file to start with<br/>
@@ -116,8 +115,7 @@ Close debugger window to proceed to live document<br/><br/>
116
115
  <br/><br/><div id='versionText'></div>
117
116
  </body>`;
118
117
 
119
- const debuggerWindowHtml2 =
120
- `<Title>Fluid Debugger</Title>
118
+ const debuggerWindowHtml2 = `<Title>Fluid Debugger</Title>
121
119
  <body>
122
120
  <h3>Fluid Debugger</h3>
123
121
  <div id='versionText'></div>
@@ -135,196 +133,223 @@ Step to move: <input type='number' id='steps' value='1' min='1' style='width:50p
135
133
  </body>`;
136
134
 
137
135
  export class DebuggerUI {
138
- public static create(controller: IDebuggerController): DebuggerUI | null {
139
- if (
140
- typeof window !== "object" ||
141
- window === null ||
142
- typeof window.document !== "object" ||
143
- window.document == null) {
144
- console.log("Can't create debugger window - not running in browser!");
145
- return null;
146
- }
147
-
148
- const debuggerWindow = window.open(
149
- "",
150
- "",
151
- "width=400,height=400,resizable=yes,location=no,menubar=no,titlebar=no,status=no,toolbar=no");
152
- if (!debuggerWindow) {
153
- console.error("Can't create debugger window - please enable pop-up windows in your browser!");
154
- return null;
155
- }
156
-
157
- return new DebuggerUI(controller, debuggerWindow);
158
- }
159
-
160
- private static formatDate(date: number) {
161
- // Alternative - without timezone
162
- // new Date().toLocaleString('default', { timeZone: 'UTC'}));
163
- // new Date().toLocaleString('default', { year: 'numeric', month: 'short',
164
- // day: 'numeric', hour: '2-digit', minute: 'numeric', second: 'numeric' }));
165
- return new Date(date).toUTCString();
166
- }
167
-
168
- protected selector?: HTMLSelectElement;
169
- protected versionText: HTMLDivElement;
170
-
171
- protected buttonOps?: HTMLButtonElement;
172
- protected text1?: HTMLDivElement;
173
- protected text2?: HTMLDivElement;
174
- protected text3?: HTMLDivElement;
175
- protected lastOpText?: HTMLDivElement;
176
- protected wasVersionSelected = false;
177
- protected versions: IVersion[] = [];
178
-
179
- protected documentClosed = false;
180
-
181
- protected constructor(private readonly controller: IDebuggerController, private readonly debuggerWindow: Window) {
182
- const doc = this.debuggerWindow.document;
183
- doc.write(debuggerWindowHtml);
184
-
185
- window.addEventListener("beforeunload", (e) => {
186
- this.documentClosed = true;
187
- this.debuggerWindow.close();
188
- }, false);
189
-
190
- this.debuggerWindow.addEventListener("beforeunload", (e) => {
191
- if (!this.documentClosed) {
192
- this.controller.onClose();
193
- }
194
- }, false);
195
-
196
- this.selector = doc.getElementById("selector") as HTMLSelectElement;
197
-
198
- const buttonVers = doc.getElementById("buttonVers") as HTMLDivElement;
199
- buttonVers.onclick = () => {
200
- const index = this.selector!.selectedIndex;
201
- controller.onVersionSelection(this.versions[index]);
202
- };
203
-
204
- const fileSnapshot = doc.getElementById("file") as HTMLInputElement;
205
- fileSnapshot.addEventListener("change", () => {
206
- const files = fileSnapshot.files;
207
- if (files) {
208
- controller.onSnapshotFileSelection(files[0]);
209
- }
210
- }, false);
211
-
212
- const opDownloadButton = doc.getElementById("downloadOps") as HTMLElement;
213
- const anonymizeCheckbox = doc.getElementById("anonymize") as HTMLInputElement;
214
- this.attachDownloadOpsListener(opDownloadButton, anonymizeCheckbox);
215
-
216
- this.versionText = doc.getElementById("versionText") as HTMLDivElement;
217
- this.versionText.textContent = "Fetching snapshots, please wait...";
218
-
219
- controller.connectToUi(this);
220
- }
221
-
222
- private attachDownloadOpsListener(element: HTMLElement, anonymize: HTMLInputElement) {
223
- element.addEventListener("click", () => {
224
- this.controller.onDownloadOpsButtonClick(anonymize.checked).then((opJson) => {
225
- this.download("opStream.json", opJson);
226
- }).catch((error) => { console.log(`Error downloading ops: ${error}`); });
227
- });
228
- }
229
-
230
- public addVersions(versions: IVersion[]) {
231
- if (this.selector) {
232
- this.versions = versions;
233
- for (const version of versions) {
234
- const option = document.createElement("option");
235
- option.text = version.date !== undefined
236
- ? `id = ${version.id}, time = ${version.date}`
237
- : `id = ${version.id}`;
238
- this.selector.add(option);
239
- }
240
- }
241
- }
242
-
243
- public updateVersion(index: number, version: IVersion, seqNumber: number) {
244
- if (this.selector) {
245
- const option = this.selector[index] as HTMLOptionElement;
246
- option.text = `${option.text}, seq = ${seqNumber}`;
247
- this.selector[index] = option;
248
- }
249
- }
250
-
251
- public versionSelected(seqNumber: number, version: IVersion | string) {
252
- const text = typeof version === "string"
253
- ? `Playing ${version} file`
254
- : `Playing from ${version.id}, seq# ${seqNumber}`;
255
-
256
- this.wasVersionSelected = true;
257
- this.selector = undefined;
258
-
259
- const doc = this.debuggerWindow.document;
260
- doc.open();
261
- doc.write(debuggerWindowHtml2);
262
- doc.close();
263
-
264
- this.lastOpText = doc.getElementById("lastOp") as HTMLDivElement;
265
- this.text1 = doc.getElementById("text1") as HTMLDivElement;
266
- this.text2 = doc.getElementById("text2") as HTMLDivElement;
267
- this.text3 = doc.getElementById("text3") as HTMLDivElement;
268
-
269
- const steps = doc.getElementById("steps") as HTMLInputElement;
270
- this.buttonOps = doc.getElementById("buttonOps") as HTMLButtonElement;
271
- this.buttonOps.disabled = true;
272
- this.buttonOps.onclick = () => {
273
- this.controller.onOpButtonClick(Number(steps.value));
274
- };
275
-
276
- this.versionText = doc.getElementById("versionText") as HTMLDivElement;
277
- this.versionText.textContent = text;
278
-
279
- const opDownloadButton = doc.getElementById("downloadOps") as HTMLElement;
280
- const anonymizeCheckbox = doc.getElementById("anonymize") as HTMLInputElement;
281
- this.attachDownloadOpsListener(opDownloadButton, anonymizeCheckbox);
282
- }
283
-
284
- public disableNextOpButton(disable: boolean) {
285
- assert(!!this.buttonOps, 0x088 /* "Missing button ops button!" */);
286
- this.buttonOps.disabled = disable;
287
- }
288
-
289
- public updateNextOpText(ops: ISequencedDocumentMessage[]) {
290
- if (ops.length === 0) {
291
- this.text1!.textContent = "";
292
- this.text2!.textContent = "";
293
- this.text3!.textContent = "";
294
- } else {
295
- const op = ops[0];
296
- const seq = op.sequenceNumber;
297
- const date = DebuggerUI.formatDate(op.timestamp);
298
- this.text1!.textContent = `Next op seq#: ${seq}`;
299
- this.text2!.textContent = `Type: ${op.type}`;
300
- this.text3!.textContent = `${date}`;
301
- }
302
- }
303
-
304
- public updateVersionText(versionCount: number) {
305
- if (!this.wasVersionSelected) {
306
- const text = versionCount === 0 ? "" : `Fetching information about ${versionCount} snapshots...`;
307
- this.versionText.textContent = text;
308
- }
309
- }
310
-
311
- public updateLastOpText(lastKnownOp: number, stillLoading: boolean) {
312
- const text = stillLoading
313
- ? `Last op (still loading): ${lastKnownOp}`
314
- : `Document's last op seq#: ${lastKnownOp}`;
315
- this.lastOpText!.textContent = text;
316
- }
317
-
318
- private download(filename: string, data: string): void {
319
- const element = document.createElement("a");
320
- element.setAttribute("href", `data:text/plain;charset=utf-8,${ encodeURIComponent(data) }`);
321
- element.setAttribute("download", filename);
322
-
323
- element.style.display = "none";
324
- document.body.appendChild(element);
325
-
326
- element.click();
327
-
328
- document.body.removeChild(element);
329
- }
136
+ public static create(controller: IDebuggerController): DebuggerUI | null {
137
+ if (
138
+ typeof window !== "object" ||
139
+ window === null ||
140
+ typeof window.document !== "object" ||
141
+ window.document == null
142
+ ) {
143
+ console.log("Can't create debugger window - not running in browser!");
144
+ return null;
145
+ }
146
+
147
+ const debuggerWindow = window.open(
148
+ "",
149
+ "",
150
+ "width=400,height=400,resizable=yes,location=no,menubar=no,titlebar=no,status=no,toolbar=no",
151
+ );
152
+ if (!debuggerWindow) {
153
+ console.error(
154
+ "Can't create debugger window - please enable pop-up windows in your browser!",
155
+ );
156
+ return null;
157
+ }
158
+
159
+ return new DebuggerUI(controller, debuggerWindow);
160
+ }
161
+
162
+ private static formatDate(date: number) {
163
+ // Alternative - without timezone
164
+ // new Date().toLocaleString('default', { timeZone: 'UTC'}));
165
+ // new Date().toLocaleString('default', { year: 'numeric', month: 'short',
166
+ // day: 'numeric', hour: '2-digit', minute: 'numeric', second: 'numeric' }));
167
+ return new Date(date).toUTCString();
168
+ }
169
+
170
+ protected selector?: HTMLSelectElement;
171
+ protected versionText: HTMLDivElement;
172
+
173
+ protected buttonOps?: HTMLButtonElement;
174
+ protected text1?: HTMLDivElement;
175
+ protected text2?: HTMLDivElement;
176
+ protected text3?: HTMLDivElement;
177
+ protected lastOpText?: HTMLDivElement;
178
+ protected wasVersionSelected = false;
179
+ protected versions: IVersion[] = [];
180
+
181
+ protected documentClosed = false;
182
+
183
+ protected constructor(
184
+ private readonly controller: IDebuggerController,
185
+ private readonly debuggerWindow: Window,
186
+ ) {
187
+ const doc = this.debuggerWindow.document;
188
+ doc.write(debuggerWindowHtml);
189
+
190
+ window.addEventListener(
191
+ "beforeunload",
192
+ (e) => {
193
+ this.documentClosed = true;
194
+ this.debuggerWindow.close();
195
+ },
196
+ false,
197
+ );
198
+
199
+ this.debuggerWindow.addEventListener(
200
+ "beforeunload",
201
+ (e) => {
202
+ if (!this.documentClosed) {
203
+ this.controller.onClose();
204
+ }
205
+ },
206
+ false,
207
+ );
208
+
209
+ this.selector = doc.getElementById("selector") as HTMLSelectElement;
210
+
211
+ const buttonVers = doc.getElementById("buttonVers") as HTMLDivElement;
212
+ buttonVers.onclick = () => {
213
+ const index = this.selector!.selectedIndex;
214
+ controller.onVersionSelection(this.versions[index]);
215
+ };
216
+
217
+ const fileSnapshot = doc.getElementById("file") as HTMLInputElement;
218
+ fileSnapshot.addEventListener(
219
+ "change",
220
+ () => {
221
+ const files = fileSnapshot.files;
222
+ if (files) {
223
+ controller.onSnapshotFileSelection(files[0]);
224
+ }
225
+ },
226
+ false,
227
+ );
228
+
229
+ const opDownloadButton = doc.getElementById("downloadOps") as HTMLElement;
230
+ const anonymizeCheckbox = doc.getElementById("anonymize") as HTMLInputElement;
231
+ this.attachDownloadOpsListener(opDownloadButton, anonymizeCheckbox);
232
+
233
+ this.versionText = doc.getElementById("versionText") as HTMLDivElement;
234
+ this.versionText.textContent = "Fetching snapshots, please wait...";
235
+
236
+ controller.connectToUi(this);
237
+ }
238
+
239
+ private attachDownloadOpsListener(element: HTMLElement, anonymize: HTMLInputElement) {
240
+ element.addEventListener("click", () => {
241
+ this.controller
242
+ .onDownloadOpsButtonClick(anonymize.checked)
243
+ .then((opJson) => {
244
+ this.download("opStream.json", opJson);
245
+ })
246
+ .catch((error) => {
247
+ console.log(`Error downloading ops: ${error}`);
248
+ });
249
+ });
250
+ }
251
+
252
+ public addVersions(versions: IVersion[]) {
253
+ if (this.selector) {
254
+ this.versions = versions;
255
+ for (const version of versions) {
256
+ const option = document.createElement("option");
257
+ option.text =
258
+ version.date !== undefined
259
+ ? `id = ${version.id}, time = ${version.date}`
260
+ : `id = ${version.id}`;
261
+ this.selector.add(option);
262
+ }
263
+ }
264
+ }
265
+
266
+ public updateVersion(index: number, version: IVersion, seqNumber: number) {
267
+ if (this.selector) {
268
+ const option = this.selector[index] as HTMLOptionElement;
269
+ option.text = `${option.text}, seq = ${seqNumber}`;
270
+ this.selector[index] = option;
271
+ }
272
+ }
273
+
274
+ public versionSelected(seqNumber: number, version: IVersion | string) {
275
+ const text =
276
+ typeof version === "string"
277
+ ? `Playing ${version} file`
278
+ : `Playing from ${version.id}, seq# ${seqNumber}`;
279
+
280
+ this.wasVersionSelected = true;
281
+ this.selector = undefined;
282
+
283
+ const doc = this.debuggerWindow.document;
284
+ doc.open();
285
+ doc.write(debuggerWindowHtml2);
286
+ doc.close();
287
+
288
+ this.lastOpText = doc.getElementById("lastOp") as HTMLDivElement;
289
+ this.text1 = doc.getElementById("text1") as HTMLDivElement;
290
+ this.text2 = doc.getElementById("text2") as HTMLDivElement;
291
+ this.text3 = doc.getElementById("text3") as HTMLDivElement;
292
+
293
+ const steps = doc.getElementById("steps") as HTMLInputElement;
294
+ this.buttonOps = doc.getElementById("buttonOps") as HTMLButtonElement;
295
+ this.buttonOps.disabled = true;
296
+ this.buttonOps.onclick = () => {
297
+ this.controller.onOpButtonClick(Number(steps.value));
298
+ };
299
+
300
+ this.versionText = doc.getElementById("versionText") as HTMLDivElement;
301
+ this.versionText.textContent = text;
302
+
303
+ const opDownloadButton = doc.getElementById("downloadOps") as HTMLElement;
304
+ const anonymizeCheckbox = doc.getElementById("anonymize") as HTMLInputElement;
305
+ this.attachDownloadOpsListener(opDownloadButton, anonymizeCheckbox);
306
+ }
307
+
308
+ public disableNextOpButton(disable: boolean) {
309
+ assert(!!this.buttonOps, 0x088 /* "Missing button ops button!" */);
310
+ this.buttonOps.disabled = disable;
311
+ }
312
+
313
+ public updateNextOpText(ops: ISequencedDocumentMessage[]) {
314
+ if (ops.length === 0) {
315
+ this.text1!.textContent = "";
316
+ this.text2!.textContent = "";
317
+ this.text3!.textContent = "";
318
+ } else {
319
+ const op = ops[0];
320
+ const seq = op.sequenceNumber;
321
+ const date = DebuggerUI.formatDate(op.timestamp);
322
+ this.text1!.textContent = `Next op seq#: ${seq}`;
323
+ this.text2!.textContent = `Type: ${op.type}`;
324
+ this.text3!.textContent = `${date}`;
325
+ }
326
+ }
327
+
328
+ public updateVersionText(versionCount: number) {
329
+ if (!this.wasVersionSelected) {
330
+ const text =
331
+ versionCount === 0 ? "" : `Fetching information about ${versionCount} snapshots...`;
332
+ this.versionText.textContent = text;
333
+ }
334
+ }
335
+
336
+ public updateLastOpText(lastKnownOp: number, stillLoading: boolean) {
337
+ const text = stillLoading
338
+ ? `Last op (still loading): ${lastKnownOp}`
339
+ : `Document's last op seq#: ${lastKnownOp}`;
340
+ this.lastOpText!.textContent = text;
341
+ }
342
+
343
+ private download(filename: string, data: string): void {
344
+ const element = document.createElement("a");
345
+ element.setAttribute("href", `data:text/plain;charset=utf-8,${encodeURIComponent(data)}`);
346
+ element.setAttribute("download", filename);
347
+
348
+ element.style.display = "none";
349
+ document.body.appendChild(element);
350
+
351
+ element.click();
352
+
353
+ document.body.removeChild(element);
354
+ }
330
355
  }