@adcops/autocore-react 3.0.13 → 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.
- package/dist/components/FileList.d.ts +38 -0
- package/dist/components/FileList.js +1 -0
- package/dist/core/EventEmitterContext.d.ts +2 -1
- package/dist/core/EventEmitterContext.js +1 -1
- package/dist/hub/HubWebSocket.d.ts +10 -0
- package/dist/hub/HubWebSocket.js +1 -1
- package/package.json +1 -1
- package/src/components/FileList.tsx +245 -0
- package/src/core/EventEmitterContext.tsx +9 -3
- package/src/hub/HubWebSocket.ts +143 -6
|
@@ -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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
2
|
import { Hub } from "../hub";
|
|
3
|
+
import { CommandMessageResult } from "../hub/CommandMessage";
|
|
3
4
|
export { Hub };
|
|
4
5
|
/**
|
|
5
6
|
* Represents an event subsription.
|
|
@@ -76,7 +77,7 @@ export interface EventEmitterContextType {
|
|
|
76
77
|
* Invoke/send a message to the back end.
|
|
77
78
|
* This does NOT get published to the front end.
|
|
78
79
|
*/
|
|
79
|
-
invoke(domain: string, fname: string, payload?: object): Promise<
|
|
80
|
+
invoke(domain: string, fname: string, payload?: object): Promise<CommandMessageResult>;
|
|
80
81
|
/**
|
|
81
82
|
* Subscribe to events identified by the topic.
|
|
82
83
|
* @param topic The subscription topic.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx}from"react/jsx-runtime";import{createContext,useState,useMemo}from"react";import{createHub,Hub}from"../hub";export{Hub};let globalSubscriptionId=1;export const EventEmitterContext=createContext({state:{subscriptions:{},nextSubscriptionId:1},dispatch:()=>{},subscribe:()=>0,invoke:async(t,s,e)=>Promise.resolve({}),unsubscribe:t=>{},hub:null,getSubscriptions:()=>[]});export const EventEmitterProvider=({children:t})=>{const[s,e]=useState({subscriptions:{},nextSubscriptionId:1}),i=useMemo((()=>createHub()),[]),r=t=>{const{topic:s,payload:i}=t;e((t=>(t.subscriptions[s]?.forEach((t=>t.callback(i))),{...t,eventData:i})))},o=(t,s)=>{globalSubscriptionId+=1;const i=globalSubscriptionId;return e((e=>({...e,subscriptions:{...e.subscriptions,[t]:[...e.subscriptions[t]||[],{id:i,callback:s}]},nextSubscriptionId:globalSubscriptionId+1}))),i},n=t=>{e((s=>{const e={...s.subscriptions};for(const s in e)e.hasOwnProperty(s)&&(e[s]=e[s].filter((s=>s.id!==t)),0===e[s].length&&delete e[s]);return{...s,subscriptions:e}}))},c=t=>t?s.subscriptions[t]||[]:s.subscriptions,u=useMemo((()=>({state:s,dispatch:r,subscribe:o,unsubscribe:n,invoke:i.invoke,hub:i,getSubscriptions:c})),[s,i]);return i.setContext(u),_jsx(EventEmitterContext.Provider,{value:u,children:t})};
|
|
1
|
+
import{jsx as _jsx}from"react/jsx-runtime";import{createContext,useState,useMemo}from"react";import{createHub,Hub}from"../hub";export{Hub};let globalSubscriptionId=1;export const EventEmitterContext=createContext({state:{subscriptions:{},nextSubscriptionId:1},dispatch:()=>{},subscribe:()=>0,invoke:async(t,s,e)=>Promise.resolve({data:{},success:!1,error_message:""}),unsubscribe:t=>{},hub:null,getSubscriptions:()=>[]});export const EventEmitterProvider=({children:t})=>{const[s,e]=useState({subscriptions:{},nextSubscriptionId:1}),i=useMemo((()=>createHub()),[]),r=t=>{const{topic:s,payload:i}=t;e((t=>(t.subscriptions[s]?.forEach((t=>t.callback(i))),{...t,eventData:i})))},o=(t,s)=>{globalSubscriptionId+=1;const i=globalSubscriptionId;return e((e=>({...e,subscriptions:{...e.subscriptions,[t]:[...e.subscriptions[t]||[],{id:i,callback:s}]},nextSubscriptionId:globalSubscriptionId+1}))),i},n=t=>{e((s=>{const e={...s.subscriptions};for(const s in e)e.hasOwnProperty(s)&&(e[s]=e[s].filter((s=>s.id!==t)),0===e[s].length&&delete e[s]);return{...s,subscriptions:e}}))},c=t=>t?s.subscriptions[t]||[]:s.subscriptions,u=useMemo((()=>({state:s,dispatch:r,subscribe:o,unsubscribe:n,invoke:i.invoke,hub:i,getSubscriptions:c})),[s,i]);return i.setContext(u),_jsx(EventEmitterContext.Provider,{value:u,children:t})};
|
|
@@ -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;
|
package/dist/hub/HubWebSocket.js
CHANGED
|
@@ -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(((
|
|
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
|
@@ -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-01-17 11:45:10
|
|
4
4
|
* -----
|
|
5
|
-
* Last Modified: 2024-04-
|
|
5
|
+
* Last Modified: 2024-04-24 12:11:16
|
|
6
6
|
* Modified By: ADC
|
|
7
7
|
* -----
|
|
8
8
|
*
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import React, { createContext, ReactNode, useState, useMemo } from 'react';
|
|
13
13
|
import {createHub, Hub} from "../hub";
|
|
14
|
+
import {CommandMessageResult} from "../hub/CommandMessage";
|
|
14
15
|
|
|
15
16
|
export {Hub};
|
|
16
17
|
|
|
@@ -103,7 +104,7 @@ export interface EventEmitterContextType {
|
|
|
103
104
|
* Invoke/send a message to the back end.
|
|
104
105
|
* This does NOT get published to the front end.
|
|
105
106
|
*/
|
|
106
|
-
invoke( domain: string, fname: string, payload? : object) : Promise<
|
|
107
|
+
invoke( domain: string, fname: string, payload? : object) : Promise<CommandMessageResult>;
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Subscribe to events identified by the topic.
|
|
@@ -268,9 +269,14 @@ export const EventEmitterContext = createContext<EventEmitterContextType>({
|
|
|
268
269
|
domain;
|
|
269
270
|
fname;
|
|
270
271
|
payload;
|
|
272
|
+
let ret :CommandMessageResult = {
|
|
273
|
+
data: {},
|
|
274
|
+
success: false,
|
|
275
|
+
error_message : ""
|
|
276
|
+
};
|
|
271
277
|
// Placeholder for invoke logic
|
|
272
278
|
// Implement the logic to send a message to the backend and return a promise
|
|
273
|
-
return Promise.resolve(
|
|
279
|
+
return Promise.resolve(ret); // Example placeholder, replace with actual implementation
|
|
274
280
|
},
|
|
275
281
|
unsubscribe: (subscriptionId: number) => { subscriptionId;}, // Placeholder for unsubscription logic
|
|
276
282
|
hub: null,
|
package/src/hub/HubWebSocket.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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) => {
|