Merged PR 185: Implement Messages

This commit is contained in:
Eric Nguyen 2022-09-16 10:03:41 +00:00
parent 8a7196eeac
commit e94671d1d8
15 changed files with 242 additions and 15 deletions

View file

@ -1,3 +1,3 @@
VITE_API_FETCH_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
VITE_API_POST_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/ApplicationState
VITE_API_FETCH_URL=http://localhost:5000
VITE_API_SET_CONTAINER_LIST_URL=http://localhost:5000/SetContainerList
VITE_API_GET_FEEDBACK_URL=http://localhost:5000/GetFeedback

View file

@ -1,3 +1,3 @@
VITE_API_FETCH_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
VITE_API_POST_URL=https://pc-003.techform-dom.local/SmartMenuiserieTemplate/Service.svc/ApplicationState
VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList
VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
]
}

View 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;
};
}

View file

@ -33,7 +33,7 @@ export async function FetchConfiguration(): Promise<IConfiguration> {
}
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());
// The test library cannot use the Fetch API
// @ts-expect-error

View file

@ -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 { BarIcon } from './BarIcon';
@ -7,9 +7,11 @@ interface IBarProps {
isSymbolsOpen: boolean
isElementsSidebarOpen: boolean
isHistoryOpen: boolean
isMessagesOpen: boolean
toggleSidebar: () => void
toggleSymbols: () => void
toggleTimeline: () => void
toggleMessages: () => void
}
export const BAR_WIDTH = 64; // 4rem
@ -35,6 +37,12 @@ export function Bar(props: IBarProps): JSX.Element {
onClick={() => props.toggleTimeline()}>
<ClockIcon className='heroicon' />
</BarIcon>
<BarIcon
isActive={props.isMessagesOpen}
title='Messages'
onClick={() => props.toggleMessages()}>
<MailIcon className='heroicon' />
</BarIcon>
</div>
);
}

View 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>
);
};

View file

@ -12,6 +12,7 @@ import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
import { PropertyType } from '../../Enums/PropertyType';
import { MessagesSidebar } from '../MessagesSidebar/MessagesSidebar';
interface IUIProps {
selectedContainer: IContainerModel | undefined
@ -36,16 +37,19 @@ interface IUIProps {
function CloseOtherSidebars(
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 {
setIsSidebarOpen(false);
setIsSymbolsOpen(false);
setIsMessagesOpen(false);
}
export function UI(props: IUIProps): JSX.Element {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
const [isMessagesOpen, setIsMessagesOpen] = React.useState(false);
let buttonRightOffsetClasses = 'right-12';
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
@ -62,15 +66,20 @@ export function UI(props: IUIProps): JSX.Element {
isSymbolsOpen={isSymbolsOpen}
isElementsSidebarOpen={isSidebarOpen}
isHistoryOpen={isHistoryOpen}
isMessagesOpen={isMessagesOpen}
toggleSidebar={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
setIsSidebarOpen(!isSidebarOpen);
} }
toggleSymbols={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
setIsSymbolsOpen(!isSymbolsOpen);
} }
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} />
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
toggleMessages={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
setIsMessagesOpen(!isMessagesOpen);
} }/>
<Sidebar
componentOptions={props.availableContainers}
@ -98,6 +107,10 @@ export function UI(props: IUIProps): JSX.Element {
onPropertyChange={props.onSymbolPropertyChange}
selectSymbol={props.selectSymbol}
/>
<MessagesSidebar
historyState={props.current}
isOpen={isMessagesOpen}
/>
<History
history={props.history}
historyCurrentStep={props.historyCurrentStep}

6
src/Enums/MessageType.ts Normal file
View file

@ -0,0 +1,6 @@
export enum MessageType {
Normal,
Success,
Warning,
Error
}

View file

@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { IHistoryState } from './IHistoryState';
export interface IGetFeedbackRequest {
/** Current application state */
ApplicationState: IHistoryState
}

View file

@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { IMessage } from './IMessage';
export interface IGetFeedbackResponse {
messages: IMessage[]
}

View file

@ -0,0 +1,6 @@
import { MessageType } from '../Enums/MessageType';
export interface IMessage {
text: string
type: MessageType
}

3
src/vite-env.d.ts vendored
View file

@ -2,7 +2,8 @@
interface ImportMetaEnv {
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...
}

View file

@ -10,7 +10,7 @@ serve({
let json;
if (url.pathname === '/GetSVGLayoutConfiguration') {
json = GetSVGLayoutConfiguration();
} else if (url.pathname === '/ApplicationState') {
} else if (url.pathname === '/SetContainerList') {
const bodyParsed = await request.json();
console.log(bodyParsed);
switch (bodyParsed.Action) {
@ -23,6 +23,17 @@ serve({
default:
break;
}
} else if (url.pathname === '/GetFeedback') {
const bodyParsed = await request.json();
console.log(bodyParsed);
json = {
messages: [
{
text: `${new Date()}`,
type: 3
}
]
}
} else {
// TODO: Return 404 rather than this
json = GetSVGLayoutConfiguration();

View file

@ -10,7 +10,7 @@ const requestListener = async(request, response) => {
response.setHeader('Content-Type', 'application/json');
const url = request.url;
let json;
if (url === '/ApplicationState') {
if (url === '/SetContainerList') {
const buffers = [];
for await (const chunk of request) {
buffers.push(chunk);