Merged PR 185: Implement Messages
This commit is contained in:
parent
8a7196eeac
commit
e94671d1d8
15 changed files with 242 additions and 15 deletions
|
@ -1,3 +1,3 @@
|
||||||
|
VITE_API_FETCH_URL=http://localhost:5000
|
||||||
VITE_API_FETCH_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
|
VITE_API_SET_CONTAINER_LIST_URL=http://localhost:5000/SetContainerList
|
||||||
VITE_API_POST_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/ApplicationState
|
VITE_API_GET_FEEDBACK_URL=http://localhost:5000/GetFeedback
|
|
@ -1,3 +1,3 @@
|
||||||
|
VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
|
||||||
VITE_API_FETCH_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
|
VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList
|
||||||
VITE_API_POST_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/ApplicationState
|
VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "*", "severity": "warn" }
|
||||||
|
]
|
||||||
|
}
|
63
public/workers/message_worker.js
Normal file
63
public/workers/message_worker.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
const DELAY_BEFORE_SEND = 200;
|
||||||
|
const queue = [];
|
||||||
|
let messagePacket = [];
|
||||||
|
onmessage = async(e) => {
|
||||||
|
let packetLength
|
||||||
|
|
||||||
|
const url = e.data.url;
|
||||||
|
const state = e.data.state;
|
||||||
|
const request = {
|
||||||
|
ApplicationState: state
|
||||||
|
};
|
||||||
|
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
|
||||||
|
queue.push(request);
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}),
|
||||||
|
body: dataParsed
|
||||||
|
})
|
||||||
|
.then((response) =>
|
||||||
|
response.json()
|
||||||
|
)
|
||||||
|
.then(async(json) => {
|
||||||
|
messagePacket.push.apply(messagePacket, json.messages);
|
||||||
|
queue.pop();
|
||||||
|
|
||||||
|
// The sleep allow the message packet to be filled by
|
||||||
|
// others requests before being sent as a single batch
|
||||||
|
// Reducing the wait time will reduce latency but increase error rate
|
||||||
|
let doLoop = true;
|
||||||
|
do {
|
||||||
|
packetLength = messagePacket.length;
|
||||||
|
await sleep(DELAY_BEFORE_SEND);
|
||||||
|
const newPacketLength = messagePacket.length;
|
||||||
|
doLoop = newPacketLength !== packetLength;
|
||||||
|
packetLength = newPacketLength;
|
||||||
|
} while (doLoop);
|
||||||
|
|
||||||
|
if (queue.length <= 0 && messagePacket.length > 0) {
|
||||||
|
console.debug(`[GetFeedback] Packet size before sent: ${messagePacket.length}`)
|
||||||
|
postMessage(messagePacket)
|
||||||
|
messagePacket.splice(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetCircularReplacerKeepDataStructure()
|
||||||
|
{
|
||||||
|
return (key, value) => {
|
||||||
|
if (key === 'parent') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export async function FetchConfiguration(): Promise<IConfiguration> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function SetContainerList(request: ISetContainerListRequest): Promise<ISetContainerListResponse> {
|
export async function SetContainerList(request: ISetContainerListRequest): Promise<ISetContainerListResponse> {
|
||||||
const url = import.meta.env.VITE_API_POST_URL;
|
const url = import.meta.env.VITE_API_SET_CONTAINER_LIST_URL;
|
||||||
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
|
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
|
||||||
// The test library cannot use the Fetch API
|
// The test library cannot use the Fetch API
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ClockIcon, CubeIcon, LinkIcon } from '@heroicons/react/outline';
|
import { ClockIcon, CubeIcon, LinkIcon, MailIcon } from '@heroicons/react/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { BarIcon } from './BarIcon';
|
import { BarIcon } from './BarIcon';
|
||||||
|
|
||||||
|
@ -7,9 +7,11 @@ interface IBarProps {
|
||||||
isSymbolsOpen: boolean
|
isSymbolsOpen: boolean
|
||||||
isElementsSidebarOpen: boolean
|
isElementsSidebarOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
|
isMessagesOpen: boolean
|
||||||
toggleSidebar: () => void
|
toggleSidebar: () => void
|
||||||
toggleSymbols: () => void
|
toggleSymbols: () => void
|
||||||
toggleTimeline: () => void
|
toggleTimeline: () => void
|
||||||
|
toggleMessages: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BAR_WIDTH = 64; // 4rem
|
export const BAR_WIDTH = 64; // 4rem
|
||||||
|
@ -35,6 +37,12 @@ export function Bar(props: IBarProps): JSX.Element {
|
||||||
onClick={() => props.toggleTimeline()}>
|
onClick={() => props.toggleTimeline()}>
|
||||||
<ClockIcon className='heroicon' />
|
<ClockIcon className='heroicon' />
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
|
<BarIcon
|
||||||
|
isActive={props.isMessagesOpen}
|
||||||
|
title='Messages'
|
||||||
|
onClick={() => props.toggleMessages()}>
|
||||||
|
<MailIcon className='heroicon' />
|
||||||
|
</BarIcon>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
100
src/Components/MessagesSidebar/MessagesSidebar.tsx
Normal file
100
src/Components/MessagesSidebar/MessagesSidebar.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { TrashIcon } from '@heroicons/react/outline';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import { MessageType } from '../../Enums/MessageType';
|
||||||
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
|
import { IMessage } from '../../Interfaces/IMessage';
|
||||||
|
|
||||||
|
interface IMessagesSidebarProps {
|
||||||
|
historyState: IHistoryState
|
||||||
|
isOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const myWorker = new Worker('workers/message_worker.js');
|
||||||
|
|
||||||
|
function UseWorker(
|
||||||
|
state: IHistoryState,
|
||||||
|
messages: IMessage[],
|
||||||
|
setMessages: React.Dispatch<React.SetStateAction<IMessage[]>>
|
||||||
|
): void {
|
||||||
|
React.useEffect(() => {
|
||||||
|
// use webworker for the stringify to avoid freezing
|
||||||
|
myWorker.postMessage({
|
||||||
|
state,
|
||||||
|
url: import.meta.env.VITE_API_GET_FEEDBACK_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
myWorker.onmessage = (event) => {
|
||||||
|
setMessages(messages.concat(event.data as IMessage[]));
|
||||||
|
};
|
||||||
|
}, [messages, setMessages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
|
||||||
|
const [messages, setMessages] = React.useState<IMessage[]>([]);
|
||||||
|
|
||||||
|
UseWorker(
|
||||||
|
props.historyState,
|
||||||
|
messages,
|
||||||
|
setMessages
|
||||||
|
);
|
||||||
|
|
||||||
|
function Row({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element {
|
||||||
|
const reversedIndex = (messages.length - 1) - index;
|
||||||
|
const message = messages[reversedIndex];
|
||||||
|
let classType = '';
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.Success:
|
||||||
|
classType = 'bg-green-400 hover:bg-green-400/60';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MessageType.Warning:
|
||||||
|
classType = 'bg-yellow-400 hover:bg-yellow-400/60';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MessageType.Error:
|
||||||
|
classType = 'bg-red-400 hover:bg-red-400/60';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (<p
|
||||||
|
key={`m-${reversedIndex}`}
|
||||||
|
className={`p-2 ${classType}`}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</p>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||||
|
return (
|
||||||
|
<div className={`fixed z-10 bg-slate-200
|
||||||
|
text-gray-700 transition-all h-full w-64
|
||||||
|
${isOpenClasses}`}>
|
||||||
|
<div className='bg-slate-100 sidebar-title flex place-content-between'>
|
||||||
|
Messages
|
||||||
|
<button
|
||||||
|
onClick={() => { setMessages([]); }}
|
||||||
|
className='h-6'
|
||||||
|
aria-label='Clear all messages'
|
||||||
|
title='Clear all messages'
|
||||||
|
>
|
||||||
|
<TrashIcon className='heroicon'></TrashIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
className='List md:text-xs font-bold'
|
||||||
|
itemCount={messages.length}
|
||||||
|
itemSize={65}
|
||||||
|
height={window.innerHeight}
|
||||||
|
width={256}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -12,6 +12,7 @@ import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||||
import { Symbols } from '../Symbols/Symbols';
|
import { Symbols } from '../Symbols/Symbols';
|
||||||
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
||||||
import { PropertyType } from '../../Enums/PropertyType';
|
import { PropertyType } from '../../Enums/PropertyType';
|
||||||
|
import { MessagesSidebar } from '../MessagesSidebar/MessagesSidebar';
|
||||||
|
|
||||||
interface IUIProps {
|
interface IUIProps {
|
||||||
selectedContainer: IContainerModel | undefined
|
selectedContainer: IContainerModel | undefined
|
||||||
|
@ -36,16 +37,19 @@ interface IUIProps {
|
||||||
|
|
||||||
function CloseOtherSidebars(
|
function CloseOtherSidebars(
|
||||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setIsMessagesOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
): void {
|
): void {
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
setIsSymbolsOpen(false);
|
setIsSymbolsOpen(false);
|
||||||
|
setIsMessagesOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UI(props: IUIProps): JSX.Element {
|
export function UI(props: IUIProps): JSX.Element {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||||
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||||
|
const [isMessagesOpen, setIsMessagesOpen] = React.useState(false);
|
||||||
|
|
||||||
let buttonRightOffsetClasses = 'right-12';
|
let buttonRightOffsetClasses = 'right-12';
|
||||||
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
|
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
|
||||||
|
@ -62,15 +66,20 @@ export function UI(props: IUIProps): JSX.Element {
|
||||||
isSymbolsOpen={isSymbolsOpen}
|
isSymbolsOpen={isSymbolsOpen}
|
||||||
isElementsSidebarOpen={isSidebarOpen}
|
isElementsSidebarOpen={isSidebarOpen}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
|
isMessagesOpen={isMessagesOpen}
|
||||||
toggleSidebar={() => {
|
toggleSidebar={() => {
|
||||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||||
setIsSidebarOpen(!isSidebarOpen);
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
} }
|
} }
|
||||||
toggleSymbols={() => {
|
toggleSymbols={() => {
|
||||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||||
setIsSymbolsOpen(!isSymbolsOpen);
|
setIsSymbolsOpen(!isSymbolsOpen);
|
||||||
} }
|
} }
|
||||||
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} />
|
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||||
|
toggleMessages={() => {
|
||||||
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||||
|
setIsMessagesOpen(!isMessagesOpen);
|
||||||
|
} }/>
|
||||||
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
componentOptions={props.availableContainers}
|
componentOptions={props.availableContainers}
|
||||||
|
@ -98,6 +107,10 @@ export function UI(props: IUIProps): JSX.Element {
|
||||||
onPropertyChange={props.onSymbolPropertyChange}
|
onPropertyChange={props.onSymbolPropertyChange}
|
||||||
selectSymbol={props.selectSymbol}
|
selectSymbol={props.selectSymbol}
|
||||||
/>
|
/>
|
||||||
|
<MessagesSidebar
|
||||||
|
historyState={props.current}
|
||||||
|
isOpen={isMessagesOpen}
|
||||||
|
/>
|
||||||
<History
|
<History
|
||||||
history={props.history}
|
history={props.history}
|
||||||
historyCurrentStep={props.historyCurrentStep}
|
historyCurrentStep={props.historyCurrentStep}
|
||||||
|
|
6
src/Enums/MessageType.ts
Normal file
6
src/Enums/MessageType.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export enum MessageType {
|
||||||
|
Normal,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
7
src/Interfaces/IGetFeedbackRequest.ts
Normal file
7
src/Interfaces/IGetFeedbackRequest.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import { IHistoryState } from './IHistoryState';
|
||||||
|
|
||||||
|
export interface IGetFeedbackRequest {
|
||||||
|
/** Current application state */
|
||||||
|
ApplicationState: IHistoryState
|
||||||
|
}
|
7
src/Interfaces/IGetFeedbackResponse.ts
Normal file
7
src/Interfaces/IGetFeedbackResponse.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
import { IMessage } from './IMessage';
|
||||||
|
|
||||||
|
export interface IGetFeedbackResponse {
|
||||||
|
messages: IMessage[]
|
||||||
|
}
|
6
src/Interfaces/IMessage.ts
Normal file
6
src/Interfaces/IMessage.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { MessageType } from '../Enums/MessageType';
|
||||||
|
|
||||||
|
export interface IMessage {
|
||||||
|
text: string
|
||||||
|
type: MessageType
|
||||||
|
}
|
3
src/vite-env.d.ts
vendored
3
src/vite-env.d.ts
vendored
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_FETCH_URL: string
|
readonly VITE_API_FETCH_URL: string
|
||||||
readonly VITE_API_POST_URL: string
|
readonly VITE_API_SET_CONTAINER_LIST_URL: string
|
||||||
|
readonly VITE_API_GET_FEEDBACK_URL: string
|
||||||
// more env variables...
|
// more env variables...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ serve({
|
||||||
let json;
|
let json;
|
||||||
if (url.pathname === '/GetSVGLayoutConfiguration') {
|
if (url.pathname === '/GetSVGLayoutConfiguration') {
|
||||||
json = GetSVGLayoutConfiguration();
|
json = GetSVGLayoutConfiguration();
|
||||||
} else if (url.pathname === '/ApplicationState') {
|
} else if (url.pathname === '/SetContainerList') {
|
||||||
const bodyParsed = await request.json();
|
const bodyParsed = await request.json();
|
||||||
console.log(bodyParsed);
|
console.log(bodyParsed);
|
||||||
switch (bodyParsed.Action) {
|
switch (bodyParsed.Action) {
|
||||||
|
@ -23,6 +23,17 @@ serve({
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else if (url.pathname === '/GetFeedback') {
|
||||||
|
const bodyParsed = await request.json();
|
||||||
|
console.log(bodyParsed);
|
||||||
|
json = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
text: `${new Date()}`,
|
||||||
|
type: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Return 404 rather than this
|
// TODO: Return 404 rather than this
|
||||||
json = GetSVGLayoutConfiguration();
|
json = GetSVGLayoutConfiguration();
|
||||||
|
|
|
@ -10,7 +10,7 @@ const requestListener = async(request, response) => {
|
||||||
response.setHeader('Content-Type', 'application/json');
|
response.setHeader('Content-Type', 'application/json');
|
||||||
const url = request.url;
|
const url = request.url;
|
||||||
let json;
|
let json;
|
||||||
if (url === '/ApplicationState') {
|
if (url === '/SetContainerList') {
|
||||||
const buffers = [];
|
const buffers = [];
|
||||||
for await (const chunk of request) {
|
for await (const chunk of request) {
|
||||||
buffers.push(chunk);
|
buffers.push(chunk);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue