@adcops/autocore-react 3.0.14 → 3.0.15

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.
@@ -0,0 +1,38 @@
1
+ /** @file FileList
2
+ * FileList allows a user to view the contents of a DataStore in the autocore-server.
3
+ * Files can be downloaded, deleted and optionally uploaded (if enabled).
4
+ *
5
+ * The FileList requires the autocore-server to be the backend, as files are transferred
6
+ * using specific commands via websockets.
7
+ */
8
+ import React from 'react';
9
+ /**
10
+ * Defines properties for the file list.
11
+ */
12
+ type FileListProps = {
13
+ domain?: string;
14
+ enableUpload?: boolean;
15
+ };
16
+ /**
17
+ * `FileList` is a React functional component that displays a list of files retrieved from a specified domain
18
+ * in an autocore-server.
19
+ * It allows users to download and delete files. The component also supports file uploads, if enabled.
20
+ *
21
+ * The component uses the `EventEmitterContext` to make API calls to a backend to list, download, and delete files.
22
+ * It dynamically handles file operations based on the `domain` prop which determines the API endpoints for these actions.
23
+ *
24
+ * Props:
25
+ * - `domain` (string): The domain name assigned to the DATASTORE servelet containing the data.
26
+ * Default: "DATASTORE"
27
+ * - `enableUpload` (boolean): If true, enables an upload button allowing files to be uploaded to the datastore.
28
+ * Default: false
29
+ *
30
+ * Example Usage:
31
+ * ```tsx
32
+ * <FileList domain="MyDomain" enableUpload={true} />
33
+ * ```
34
+ *
35
+ * @param {FileListProps} props The properties passed to the component.
36
+ */
37
+ export declare const FileList: React.FC<FileListProps>;
38
+ export default FileList;
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useState,useContext,useEffect}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Toolbar}from"primereact/toolbar";import{Button}from"primereact/button";import{ConfirmPopup,confirmPopup}from"primereact/confirmpopup";import{MessageSeverity}from"primereact/api";import{EventEmitterContext}from"../core/EventEmitterContext";export const FileList=({domain:e="DATASTORE",enableUpload:t=!1})=>{const{invoke:a,dispatch:o}=useContext(EventEmitterContext),[r,i]=useState(),s=async()=>{try{let t=await a(e,"list_files",{}),o=[];for(let e=0;e<t.data.length;++e){const a=t.data[e];o.push({id:e+1,name:a})}i(o)}catch(e){o({topic:"autocore-react/alert/error",payload:{message:`Failed to upload file list: ${e}`,timeoutSec:7,severity:MessageSeverity.ERROR}})}},n=`${e} File Listing`,l=_jsx(React.Fragment,{children:_jsx("span",{style:{fontWeight:600},children:n})}),m=_jsxs(React.Fragment,{children:[t&&_jsx(Button,{icon:"pi pi-upload",className:"p-button-rounded p-mr-2","aria-label":"Upload",size:"small",rounded:!0,text:!0}),_jsx(Button,{icon:"pi pi-refresh",onClick:()=>{s()},className:"p-button-rounded p-mr-2","aria-label":"Refresh",size:"small",rounded:!0,text:!0})]}),c=(t,r)=>{confirmPopup({target:r.currentTarget,message:`Are you want to delete file ${t.name}?\nWARNING: This cannot be undone.`,icon:"pi pi-info-circle",defaultFocus:"reject",acceptClassName:"p-button-danger",accept:()=>(async t=>{try{await a(e,"delete_file",{file_name:t})}catch(e){o({topic:"autocore-react/alert/error",payload:{message:`Failed deleting file: ${e}`,timeoutSec:7,severity:MessageSeverity.ERROR}})}s()})(t.name)})};return useEffect((()=>(s(),()=>{})),[e,t]),_jsxs("div",{children:[_jsx(Toolbar,{start:l,end:m,style:{padding:"1mm"}}),_jsx(ConfirmPopup,{}),_jsxs(DataTable,{value:r,children:[_jsx(Column,{field:"name",header:"Name"}),_jsx(Column,{body:t=>_jsxs(_Fragment,{children:[_jsx(Button,{icon:"pi pi-download",onClick:()=>(async t=>{try{await a(e,"download_file",{file_name:t.name})}catch(e){o({topic:"autocore-react/alert/error",payload:{message:`Failed to downloading file: ${e}`,timeoutSec:7,severity:MessageSeverity.ERROR}})}})(t),className:"p-button-rounded p-button-success p-mr-2",style:{marginRight:"2mm"},size:"small"}),_jsx(Button,{icon:"pi pi-trash",onClick:e=>c(t,e),className:"p-button-rounded p-button-danger",size:"small"})]}),header:"Actions"})]})]})};export default FileList;
@@ -17,7 +17,17 @@ export declare class HubWebSocket extends HubBase {
17
17
  private socket;
18
18
  private requestId;
19
19
  private pendingRequests;
20
+ /**
21
+ * Constructor. Creates and attempts to make the Websocket connection.
22
+ */
20
23
  constructor();
24
+ /**
25
+ * Invoke a command in the remote webserver.
26
+ * @param domain The domain of the Servelet supplying the functionality.
27
+ * @param fname The name of the command to execute.
28
+ * @param payload The arguments of the command.
29
+ * @returns Promise<CommandMessageResult>
30
+ */
21
31
  invoke: (domain: string, fname: string, payload?: object) => Promise<CommandMessageResult>;
22
32
  handleUnsolicitedMessage: (msg: CommandMessage) => void;
23
33
  disconnect: () => void;
@@ -1 +1 @@
1
- import{HubBase}from"./HubBase";export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise(((i,r)=>{const n=++this.requestId;this.pendingRequests.set(n,{resolve:i,reject:r});let o={request_id:n,domain:e,fname:t,args:s,result:void 0};this.socket.send(JSON.stringify(o))}))}),Object.defineProperty(this,"handleUnsolicitedMessage",{enumerable:!0,configurable:!0,writable:!0,value:e=>{if("BROADCAST"===e.fname&&e.domain.length>=1&&void 0!==e.result&&null!==e.result&&void 0!==e.result.data&&null!==e.result.data){let t=`${e.domain}/${e.result.data.topic}`;this.publish(t,e.result.data)}}}),Object.defineProperty(this,"disconnect",{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.socket.close()}}),Object.defineProperty(this,"emit",{enumerable:!0,configurable:!0,writable:!0,value:(e,t)=>{this.socket.send(JSON.stringify({eventName:e,payload:t}))}});const e=window.location.hostname,t=window.location.port,s=`${"https:"===window.location.protocol?"wss://":"ws://"}${e}${t?":"+t:""}/ws/`;this.socket=new WebSocket(s);let i=this;this.socket.onopen=function(){i.publish("HUB/connected",!0)},this.socket.onmessage=e=>{const t=JSON.parse(e.data);if(t.request_id&&this.pendingRequests.has(t.request_id)){const{resolve:e,reject:s}=this.pendingRequests.get(t.request_id);t.result?.success?e(t.result):s(new Error(t.result?.error_message)),this.pendingRequests.delete(t.request_id)}else this.handleUnsolicitedMessage(t)},this.socket.onerror=e=>{},this.socket.onclose=()=>{}}}
1
+ import{HubBase}from"./HubBase";function b64toBlob(e,t="application/octet-stream",s=512){const n=atob(e),o=[];for(let e=0;e<n.length;e+=s){const t=n.slice(e,e+s),r=new Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);const i=new Uint8Array(r);o.push(i)}return new Blob(o,{type:t})}function downloadBlob(e,t){const s=window.URL.createObjectURL(e),n=document.createElement("a");n.href=s,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(s)}export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise(((n,o)=>{const r=++this.requestId;this.pendingRequests.set(r,{resolve:n,reject:o});let i={request_id:r,domain:e,fname:t,args:s,result:void 0};this.socket.send(JSON.stringify(i))}))}),Object.defineProperty(this,"handleUnsolicitedMessage",{enumerable:!0,configurable:!0,writable:!0,value:e=>{if("BROADCAST"===e.fname&&e.domain.length>=1&&void 0!==e.result&&null!==e.result&&void 0!==e.result.data&&null!==e.result.data){let t=`${e.domain}/${e.result.data.topic}`;this.publish(t,e.result.data)}}}),Object.defineProperty(this,"disconnect",{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.socket.close()}}),Object.defineProperty(this,"emit",{enumerable:!0,configurable:!0,writable:!0,value:(e,t)=>{this.socket.send(JSON.stringify({eventName:e,payload:t}))}});const e=window.location.hostname,t=window.location.port,s=`${"https:"===window.location.protocol?"wss://":"ws://"}${e}${t?":"+t:""}/ws/`;this.socket=new WebSocket(s);let n=this;this.socket.onopen=function(){n.publish("HUB/connected",!0)},this.socket.onmessage=e=>{const t=JSON.parse(e.data);if(t.request_id&&this.pendingRequests.has(t.request_id)){const{resolve:e,reject:s}=this.pendingRequests.get(t.request_id);if("FILE_DOWNLOAD"===t.fname&&void 0!==t.args.file_name&&null!==t.args.file_name&&t.args.file_name.length>0){let n=t.args.file_name.split("/");const o=n[n.length-1];if(t.result&&t.result.success){downloadBlob(b64toBlob(t.result.data),o),t.result.data=o,e(t.result)}else s(new Error(t.result?.error_message))}else t.result?.success?e(t.result):s(new Error(t.result?.error_message));this.pendingRequests.delete(t.request_id)}else this.handleUnsolicitedMessage(t)},this.socket.onerror=e=>{},this.socket.onclose=()=>{}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.0.14",
3
+ "version": "3.0.15",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,245 @@
1
+ /*
2
+ * Copyright (C) 2024 Automated Design Corp.. All Rights Reserved.
3
+ * Created Date: 2024-04-24 16:01:53
4
+ * -----
5
+ * Last Modified: 2024-04-25 06:40:11
6
+ * -----
7
+ *
8
+ */
9
+
10
+ /** @file FileList
11
+ * FileList allows a user to view the contents of a DataStore in the autocore-server.
12
+ * Files can be downloaded, deleted and optionally uploaded (if enabled).
13
+ *
14
+ * The FileList requires the autocore-server to be the backend, as files are transferred
15
+ * using specific commands via websockets.
16
+ */
17
+
18
+ import React, { useState, useContext, useEffect } from 'react';
19
+ import { DataTable } from 'primereact/datatable';
20
+ import { Column } from 'primereact/column';
21
+ import { Toolbar } from 'primereact/toolbar';
22
+ import { Button } from 'primereact/button';
23
+ import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
24
+ import { MessageSeverity } from 'primereact/api';
25
+
26
+ import { EventEmitterContext } from '../core/EventEmitterContext';
27
+
28
+ /**
29
+ * Defines properties for the file list.
30
+ */
31
+ type FileListProps = {
32
+ /// The domain name assigned to the DATASTORE servelet containing the data.
33
+ /// Default: DATASTORE
34
+ domain?: string,
35
+ /// Enable the upload button so that files can be uploaded to the datastore.
36
+ /// Default: false
37
+ enableUpload?: boolean
38
+ }
39
+
40
+ /**
41
+ * Defines the information for every row item in the file list.
42
+ */
43
+ type FileItem = {
44
+ id: number;
45
+ name: string;
46
+ };
47
+
48
+
49
+ /**
50
+ * `FileList` is a React functional component that displays a list of files retrieved from a specified domain
51
+ * in an autocore-server.
52
+ * It allows users to download and delete files. The component also supports file uploads, if enabled.
53
+ *
54
+ * The component uses the `EventEmitterContext` to make API calls to a backend to list, download, and delete files.
55
+ * It dynamically handles file operations based on the `domain` prop which determines the API endpoints for these actions.
56
+ *
57
+ * Props:
58
+ * - `domain` (string): The domain name assigned to the DATASTORE servelet containing the data.
59
+ * Default: "DATASTORE"
60
+ * - `enableUpload` (boolean): If true, enables an upload button allowing files to be uploaded to the datastore.
61
+ * Default: false
62
+ *
63
+ * Example Usage:
64
+ * ```tsx
65
+ * <FileList domain="MyDomain" enableUpload={true} />
66
+ * ```
67
+ *
68
+ * @param {FileListProps} props The properties passed to the component.
69
+ */
70
+ export const FileList: React.FC<FileListProps> = ({
71
+ domain = "DATASTORE",
72
+ enableUpload = false
73
+ }) => {
74
+
75
+ const { invoke, dispatch } = useContext(EventEmitterContext);
76
+
77
+ const [files, setFiles] = useState<FileItem[]>();
78
+
79
+ /**
80
+ * Retrieve a list of files from an autocore-server DataStoreServelet.
81
+ */
82
+ const listFiles = async () => {
83
+ try {
84
+ let res = await invoke(domain, "list_files", {});
85
+
86
+ let items = [];
87
+ for (let i = 0; i < res.data.length; ++i) {
88
+ const item = res.data[i];
89
+ items.push({
90
+ id: i + 1,
91
+ name: item
92
+ });
93
+ }
94
+
95
+ setFiles(items);
96
+ }
97
+ catch (error) {
98
+ console.error("Failed to upload file list: " + error);
99
+
100
+ dispatch({
101
+ topic: "autocore-react/alert/error",
102
+ payload: {
103
+ message: `Failed to upload file list: ${error}`,
104
+ timeoutSec: 7,
105
+ severity: MessageSeverity.ERROR
106
+ }
107
+ });
108
+
109
+ }
110
+
111
+
112
+ }
113
+
114
+ /**
115
+ * Handles when the download button is clicked on a list item.
116
+ * @param file The file item selected in the DataTable
117
+ */
118
+ const handleDownload = async (file: FileItem) => {
119
+ try {
120
+ await invoke(domain, "download_file", { file_name: file.name });
121
+ } catch (error) {
122
+ console.error("Failed downloading file: " + error);
123
+ dispatch({
124
+ topic: "autocore-react/alert/error",
125
+ payload: {
126
+ message: `Failed to downloading file: ${error}`,
127
+ timeoutSec: 7,
128
+ severity: MessageSeverity.ERROR
129
+ }
130
+ });
131
+ }
132
+
133
+ };
134
+
135
+ /**
136
+ * Sends the command to the autocore-server domain to delete the file.
137
+ * @param file_name Name of the file to delete.
138
+ */
139
+ const handleDelete = async (file_name: string) => {
140
+
141
+ try {
142
+ await invoke(domain, "delete_file", { file_name: file_name });
143
+ } catch (error) {
144
+ console.error("Failed deleting file: " + error);
145
+
146
+ dispatch({
147
+ topic: "autocore-react/alert/error",
148
+ payload: {
149
+ message: `Failed deleting file: ${error}`,
150
+ timeoutSec: 7,
151
+ severity: MessageSeverity.ERROR
152
+ }
153
+ });
154
+ }
155
+
156
+
157
+ listFiles();
158
+ };
159
+
160
+ const title = `${domain} File Listing`;
161
+ const toolbarStartContents = (
162
+ <React.Fragment>
163
+ <span style={{ fontWeight: 600 }}>{title}</span>
164
+ </React.Fragment>
165
+ );
166
+
167
+ const toolbarEndContents = (
168
+ <React.Fragment>
169
+ {enableUpload && (
170
+ <Button
171
+ icon="pi pi-upload"
172
+ className="p-button-rounded p-mr-2"
173
+ aria-label="Upload"
174
+ size="small"
175
+ rounded text
176
+ />
177
+
178
+ )}
179
+ <Button
180
+ icon="pi pi-refresh"
181
+ onClick={() => { listFiles() }}
182
+ className="p-button-rounded p-mr-2"
183
+ aria-label="Refresh"
184
+ size="small"
185
+ rounded text
186
+ />
187
+ </React.Fragment>
188
+ );
189
+
190
+
191
+ /**
192
+ * Confirm that the user really wants to delete.
193
+ * @param event
194
+ */
195
+ const confirmDelete = (file: FileItem, event: React.MouseEvent<HTMLButtonElement>) => {
196
+
197
+ confirmPopup({
198
+ target: event.currentTarget,
199
+ message: `Are you want to delete file ${file.name}?\nWARNING: This cannot be undone.`,
200
+ icon: 'pi pi-info-circle',
201
+ defaultFocus: 'reject',
202
+ acceptClassName: 'p-button-danger',
203
+ accept: () => handleDelete(file.name)
204
+ });
205
+ };
206
+
207
+ useEffect(() => {
208
+
209
+ listFiles();
210
+
211
+ return () => {
212
+ }
213
+ }, [domain, enableUpload]);
214
+
215
+ return (
216
+
217
+ <div>
218
+ <Toolbar start={toolbarStartContents} end={toolbarEndContents} style={{ padding: "1mm" }} />
219
+ <ConfirmPopup />
220
+ <DataTable value={files}>
221
+ <Column field="name" header="Name"></Column>
222
+
223
+ <Column body={(rowData: FileItem) => (
224
+ <>
225
+ <Button
226
+ icon="pi pi-download"
227
+ onClick={() => handleDownload(rowData)}
228
+ className="p-button-rounded p-button-success p-mr-2"
229
+ style={{ marginRight: "2mm" }}
230
+ size="small"
231
+ />
232
+ <Button
233
+ icon="pi pi-trash"
234
+ onClick={(e) => confirmDelete(rowData, e)}
235
+ className="p-button-rounded p-button-danger"
236
+ size="small"
237
+ />
238
+ </>
239
+ )} header="Actions"></Column>
240
+ </DataTable>
241
+ </div>
242
+ );
243
+ };
244
+
245
+ export default FileList;
@@ -2,7 +2,7 @@
2
2
  * Copyright (C) 2024 Automated Design Corp.. All Rights Reserved.
3
3
  * Created Date: 2024-04-17 09:13:07
4
4
  * -----
5
- * Last Modified: 2024-04-24 11:56:51
5
+ * Last Modified: 2024-04-24 15:43:21
6
6
  * -----
7
7
  *
8
8
  */
@@ -38,11 +38,95 @@ interface RequestRecord {
38
38
  }
39
39
 
40
40
 
41
+ /**
42
+ * Converts a Base64 encoded string into a Blob object.
43
+ *
44
+ * This function is useful for handling binary data encoded in Base64 format,
45
+ * especially when the data needs to be reconstructed into a binary format
46
+ * such as when downloading files that were transferred as Base64 strings over
47
+ * a network. The function slices the Base64 string, decodes it to binary,
48
+ * and constructs a Blob from the resulting byte arrays.
49
+ *
50
+ * @param {string} b64Data The base64 encoded string you want to convert to a Blob.
51
+ * @param {string} [contentType='application/octet-stream'] The MIME type of the Blob.
52
+ * This parameter is optional and defaults to 'application/octet-stream'.
53
+ * @param {number} [sliceSize=512] The size of each chunk used to slice the Base64 string.
54
+ * Smaller slice sizes can improve the function's handling of large Base64 strings.
55
+ * This parameter is optional and defaults to 512.
56
+ *
57
+ * @returns {Blob} A Blob object representing the decoded binary data.
58
+ */
59
+ function b64toBlob(
60
+ b64Data: string,
61
+ contentType: string = 'application/octet-stream',
62
+ sliceSize: number = 512
63
+ ): Blob {
64
+
65
+ const byteCharacters = atob(b64Data);
66
+ const byteArrays: Uint8Array[] = [];
67
+
68
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
69
+ const slice = byteCharacters.slice(offset, offset + sliceSize);
70
+
71
+ const byteNumbers: number[] = new Array(slice.length);
72
+ for (let i = 0; i < slice.length; i++) {
73
+ byteNumbers[i] = slice.charCodeAt(i);
74
+ }
75
+
76
+ const byteArray = new Uint8Array(byteNumbers);
77
+ byteArrays.push(byteArray);
78
+ }
79
+
80
+ const blob = new Blob(byteArrays, { type: contentType });
81
+ return blob;
82
+ }
83
+
84
+
85
+ /**
86
+ * Creates a temporary hyperlink in the document and programmatically triggers a download of the Blob provided.
87
+ *
88
+ * This function is useful for triggering downloads of binary data such as files, which are represented
89
+ * by Blob objects in the browser. The function creates a URL from the Blob, sets it as the href of a
90
+ * dynamically created anchor tag (`<a>`), sets the suggested filename, and then simulates a click on
91
+ * this link to start the download. After the download is triggered, the link is removed and the URL
92
+ * is revoked to free up memory.
93
+ *
94
+ * @param {Blob} blob The Blob object containing the data to be downloaded.
95
+ * @param {string} filename The filename to be used for the downloaded file.
96
+ */
97
+ function downloadBlob(blob: Blob, filename: string): void {
98
+ // Create a URL for the blob object
99
+ const url = window.URL.createObjectURL(blob);
100
+
101
+ // Create a temporary anchor tag (`<a>`) element
102
+ const a = document.createElement('a');
103
+ a.href = url; // Set the href to the blob URL
104
+ a.download = filename; // Suggest a filename for the downloaded file
105
+
106
+ // Append the anchor tag to the body of the document
107
+ document.body.appendChild(a);
108
+
109
+ // Programmatically trigger a click on the anchor tag
110
+ a.click();
111
+
112
+ // Remove the anchor tag from the document
113
+ document.body.removeChild(a);
114
+
115
+ // Revoke the blob URL to free up resources
116
+ window.URL.revokeObjectURL(url);
117
+ }
118
+
119
+
120
+
121
+
41
122
  export class HubWebSocket extends HubBase {
42
123
  private socket: WebSocket;
43
124
  private requestId = 0;
44
125
  private pendingRequests = new Map<number, RequestRecord>();
45
126
 
127
+ /**
128
+ * Constructor. Creates and attempts to make the Websocket connection.
129
+ */
46
130
  constructor() {
47
131
  super();
48
132
 
@@ -55,21 +139,60 @@ export class HubWebSocket extends HubBase {
55
139
 
56
140
 
57
141
  let self = this;
142
+
143
+ //
144
+ // Websocket connection established.
145
+ //
58
146
  this.socket.onopen = function() {
59
147
  console.log("WebSocket connection established.");
60
148
  self.publish("HUB/connected", true);
61
149
  //ws.send("Hello, server!"); // Send a message to the server
62
150
  };
63
151
 
64
-
152
+
153
+ // Message recevied via the websocket connection.
154
+ //
65
155
  this.socket.onmessage = (event) => {
66
156
  const data: CommandMessage = JSON.parse(event.data);
157
+
67
158
  if (data.request_id && this.pendingRequests.has(data.request_id)) {
159
+
68
160
  const { resolve, reject } = this.pendingRequests.get(data.request_id)!;
69
- if (!data.result?.success) {
70
- reject(new Error(data.result?.error_message));
71
- } else {
72
- resolve(data.result);
161
+
162
+ if (data.fname === "FILE_DOWNLOAD"
163
+ && data.args["file_name"] !== undefined && data.args["file_name"] !== null
164
+ && data.args["file_name"].length > 0
165
+ ) {
166
+
167
+ // The file name was supplied in the original request
168
+ let tokens = data.args["file_name"].split("/");
169
+
170
+ const filename = tokens[tokens.length - 1];
171
+ if (data.result && data.result.success) {
172
+ const blob = b64toBlob(data.result.data);
173
+ downloadBlob(blob, filename);
174
+
175
+ // Don't send the whole file through; that's crazy.
176
+ data.result.data = filename;
177
+
178
+ // Signal the function that initiated the download that all is well.
179
+ resolve(data.result);
180
+
181
+ } else {
182
+ // Signal the function that initiated the download that it failed.
183
+ reject(new Error(data.result?.error_message));
184
+ }
185
+
186
+ }
187
+ else {
188
+
189
+ if (!data.result?.success) {
190
+ // Return the result to the function that initiated the request.
191
+ reject(new Error(data.result?.error_message));
192
+ } else {
193
+ // Send the error to the function that intiated the request.
194
+ resolve(data.result);
195
+ }
73
196
  }
74
197
  this.pendingRequests.delete(data.request_id);
75
198
  } else {
@@ -77,15 +200,29 @@ export class HubWebSocket extends HubBase {
77
200
  }
78
201
  };
79
202
 
203
+ //
204
+ // Error occurred in the Websocket connection.
205
+ //
80
206
  this.socket.onerror = (error: Event) => {
81
207
  console.error('WebSocket error:', error);
82
208
  };
83
209
 
210
+ //
211
+ // The Websocket connection has closed.
212
+ //
84
213
  this.socket.onclose = () => {
85
214
  console.log('WebSocket connection closed.');
86
215
  };
87
216
  }
88
217
 
218
+
219
+ /**
220
+ * Invoke a command in the remote webserver.
221
+ * @param domain The domain of the Servelet supplying the functionality.
222
+ * @param fname The name of the command to execute.
223
+ * @param payload The arguments of the command.
224
+ * @returns Promise<CommandMessageResult>
225
+ */
89
226
  invoke = (domain : string, fname: string, payload?: object): Promise<CommandMessageResult> => {
90
227
 
91
228
  return new Promise((resolve, reject) => {