Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms committed Jun 26, 2024
1 parent 3214e28 commit 9cd4e03
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@framework/ModuleInstanceStatusController";

import { cloneDeep, filter, isEqual, keys } from "lodash";
import { v4 } from "uuid";

type StatusMessage = {
source: StatusSource;
Expand All @@ -15,9 +16,36 @@ type StatusMessage = {
datetimeMs: number;
};

export enum LogEntryType {
LOADING_DONE = "loading_done",
LOADING = "loading",
SUCCESS = "success",
MESSAGE = "message",
}

export type LogEntry = {
id: string;
datetimeMs: number;
} & (
| {
type: LogEntryType.LOADING;
}
| {
type: LogEntryType.LOADING_DONE;
}
| {
type: LogEntryType.SUCCESS;
}
| {
type: LogEntryType.MESSAGE;
message: StatusMessage;
repetitions?: number;
}
);

type StatusControllerState = {
hotMessageCache: StatusMessage[];
coldMessageCache: StatusMessage[];
log: LogEntry[];
loading: boolean;
viewDebugMessage: string;
settingsDebugMessage: string;
Expand All @@ -29,7 +57,7 @@ export class ModuleInstanceStatusControllerInternal implements ModuleInstanceSta
protected _stateCandidates: StatusControllerState;
protected _state: StatusControllerState = {
hotMessageCache: [],
coldMessageCache: [],
log: [],
loading: false,
viewDebugMessage: "",
settingsDebugMessage: "",
Expand All @@ -52,16 +80,95 @@ export class ModuleInstanceStatusControllerInternal implements ModuleInstanceSta
}

clearHotMessageCache(source: StatusSource): void {
this._stateCandidates.coldMessageCache.push(
...this._stateCandidates.hotMessageCache.filter((msg) => msg.source === source)
);
this._stateCandidates.hotMessageCache = this._stateCandidates.hotMessageCache.filter(
(msg) => msg.source !== source
);
}

private areMessagesEqual(msg1: StatusMessage, msg2: StatusMessage): boolean {
if (msg1.message !== msg2.message || msg1.type !== msg2.type) {
return false;
}

return true;
}

private transferHotMessagesToLog(): void {
const messagesToBeTransferred = this._stateCandidates.hotMessageCache;

if (this._stateCandidates.loading) {
return;
}
for (const [index, message] of messagesToBeTransferred.entries()) {
const potentialDuplicateMessage = this._stateCandidates.log[index];
if (
potentialDuplicateMessage &&
potentialDuplicateMessage.type === LogEntryType.MESSAGE &&
this.areMessagesEqual(potentialDuplicateMessage.message, message)
) {
potentialDuplicateMessage.repetitions = (potentialDuplicateMessage.repetitions || 1) + 1;
continue;
}
this._stateCandidates.log.unshift(
...messagesToBeTransferred.map((message) => ({
type: LogEntryType.MESSAGE,
message: message,
datetimeMs: Date.now(),
id: v4(),
}))
);
break;
}
}

private maybeLogSuccess() {
let storeSuccess = false;

if (this._stateCandidates.log.length > 0 && this._stateCandidates.log[0].type === LogEntryType.LOADING_DONE) {
storeSuccess = true;
}

if (!storeSuccess) {
return;
}

this._stateCandidates.log.unshift({
type: LogEntryType.SUCCESS,
datetimeMs: Date.now(),
id: v4(),
});
}

clearLog(): void {
this._state.log = [];
this.notifySubscribers("log");
}

setLoading(isLoading: boolean): void {
this._stateCandidates.loading = isLoading;
if (isLoading) {
if (this._stateCandidates.log.length === 0 || this._stateCandidates.log[0].type !== LogEntryType.LOADING) {
this._stateCandidates.log.unshift({
type: LogEntryType.LOADING,
datetimeMs: Date.now(),
id: v4(),
});
}
} else {
for (const [index, logEntry] of this._stateCandidates.log.entries()) {
if (logEntry.type === LogEntryType.LOADING_DONE) {
break;
}
if (logEntry.type === LogEntryType.LOADING) {
this._stateCandidates.log.splice(index, 0, {
type: LogEntryType.LOADING_DONE,
datetimeMs: Date.now(),
id: v4(),
});
break;
}
}
}
}

setDebugMessage(source: StatusSource, message: string): void {
Expand Down Expand Up @@ -89,6 +196,9 @@ export class ModuleInstanceStatusControllerInternal implements ModuleInstanceSta
}

reviseAndPublishState(): void {
this.transferHotMessagesToLog();
this.maybeLogSuccess();

const differentStateKeys = filter(keys(this._stateCandidates), (key: keyof StatusControllerState) => {
return !isEqual(this._state[key], this._stateCandidates[key]);
}) as (keyof StatusControllerState)[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect";
import { createPortal } from "@lib/utils/createPortal";
import { isDevMode } from "@lib/utils/devMode";
import { resolveClassNames } from "@lib/utils/resolveClassNames";
import { Close, Error, Input, Output, Warning } from "@mui/icons-material";
import { Close, Error, History, Input, Output, Warning } from "@mui/icons-material";

export type HeaderProps = {
moduleInstance: ModuleInstance<any, any, any, any>;
Expand All @@ -37,11 +37,9 @@ export const Header: React.FC<HeaderProps> = (props) => {
props.moduleInstance.getStatusController(),
"hotMessageCache"
);
const coldStatusMessages = useStatusControllerStateValue(
props.moduleInstance.getStatusController(),
"coldMessageCache"
);
const log = useStatusControllerStateValue(props.moduleInstance.getStatusController(), "log");
const [, setRightDrawerContent] = useGuiState(props.guiMessageBroker, GuiState.RightDrawerContent);
const [, setActiveModuleInstanceId] = useGuiState(props.guiMessageBroker, GuiState.ActiveModuleInstanceId);
const [rightSettingsPanelWidth, setRightSettingsPanelWidth] = useGuiState(
props.guiMessageBroker,
GuiState.RightSettingsPanelWidthInPercent
Expand Down Expand Up @@ -103,7 +101,10 @@ export const Header: React.FC<HeaderProps> = (props) => {
setRightSettingsPanelWidth(15);
}

setActiveModuleInstanceId(props.moduleInstance.getId());
setRightDrawerContent(RightDrawerContent.ModuleInstanceLog);

setStatusMessagesVisible(false);
}

function makeStatusIndicator(): React.ReactNode {
Expand Down Expand Up @@ -274,11 +275,13 @@ export const Header: React.FC<HeaderProps> = (props) => {
}}
>
{makeHotStatusMessages()}
{coldStatusMessages.length > 0 && (
{log.length > 0 && (
<>
<div className="bg-gray-300 h-0.5 w-full my-2" />
<div className="bg-gray-300 h-0.5 w-full my-1" />
<Button variant="text" onPointerDown={handleColdStatusMessagesClick} className="w-full">
Show {coldStatusMessages.length} older message{coldStatusMessages.length > 1 && "s"}
<>
<History fontSize="inherit" /> Show complete log
</>
</Button>
</>
)}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/framework/internal/components/Drawer/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type DrawerProps = {
filterPlaceholder?: string;
onFilterChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClose?: () => void;
actions?: React.ReactNode;
headerChildren?: React.ReactNode;
children: React.ReactNode;
};
Expand All @@ -21,12 +22,11 @@ export const Drawer: React.FC<DrawerProps> = (props) => {
<div className="flex justify-center items-center p-2 bg-slate-100 h-10">
{props.icon && React.cloneElement(props.icon, { fontSize: "small", className: "mr-2" })}
<span className="font-bold flex-grow p-0 text-sm">{props.title}</span>
{props.actions}
{props.onClose && (
<Close
fontSize="small"
className="hover:text-slate-500 cursor-pointer mr-2"
onClick={props.onClose}
/>
<div className="hover:text-slate-500 cursor-pointer mr-2" onClick={props.onClose}>
<Close fontSize="inherit" />
</div>
)}
</div>
<div className="flex-grow flex flex-col">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { GuiState, RightDrawerContent, useGuiValue } from "@framework/GuiMessage
import { ModuleInstance } from "@framework/ModuleInstance";
import { StatusMessageType } from "@framework/ModuleInstanceStatusController";
import { Workbench } from "@framework/Workbench";
import { useStatusControllerStateValue } from "@framework/internal/ModuleInstanceStatusControllerInternal";
import {
LogEntry,
LogEntryType,
useStatusControllerStateValue,
} from "@framework/internal/ModuleInstanceStatusControllerInternal";
import { Drawer } from "@framework/internal/components/Drawer";
import { timestampUtcMsToCompactIsoString } from "@framework/utils/timestampUtils";
import { Error, History, Warning } from "@mui/icons-material";
import { CheckCircle, ClearAll, CloudDone, CloudDownload, Error, History, Warning } from "@mui/icons-material";

export type ModuleInstanceLogProps = {
workbench: Workbench;
Expand All @@ -24,15 +27,52 @@ export function ModuleInstanceLog(props: ModuleInstanceLogProps): React.ReactNod
props.onClose();
}

function handleClearAll() {
if (moduleInstance) {
moduleInstance.getStatusController().clearLog();
}
}

function makeTitle() {
if (!moduleInstance) {
return "Module log";
}

return `Log for ${moduleInstance.getModule().getName()}`;
}

function makeActions() {
if (!moduleInstance) {
return null;
}

return (
<div
className="hover:text-slate-500 cursor-pointer mr-2"
title="Clear all messages"
onClick={handleClearAll}
>
<ClearAll fontSize="inherit" />
</div>
);
}

return (
<Drawer
title="Module instance log"
title={makeTitle()}
icon={<History />}
visible={drawerContent === RightDrawerContent.ModuleInstanceLog}
onClose={handleClose}
actions={makeActions()}
>
<div className="flex flex-col p-2 gap-4 overflow-y-auto">
{moduleInstance ? <LogList moduleInstance={moduleInstance} /> : <>No module selected.</>}
<div className="h-full flex flex-col p-2 gap-1 overflow-y-auto">
{moduleInstance ? (
<LogList moduleInstance={moduleInstance} />
) : (
<div className="w-full h-full flex flex-col items-center justify-center text-gray-400">
No module selected
</div>
)}
</div>
</Drawer>
);
Expand All @@ -43,26 +83,66 @@ type LogListProps = {
};

function LogList(props: LogListProps): React.ReactNode {
const coldStatusMessages = useStatusControllerStateValue(
props.moduleInstance.getStatusController(),
"coldMessageCache"
);
const log = useStatusControllerStateValue(props.moduleInstance.getStatusController(), "log");

if (log.length === 0) {
return (
<div className="w-full h-full flex flex-col items-center justify-center text-gray-400">No log entries</div>
);
}

return (
<>
{coldStatusMessages.map((entry, i) => (
<div key={`${entry.message}-${i}`} className="flex items-center gap-2">
{entry.type === StatusMessageType.Error && <Error fontSize="small" color="error" />}
{entry.type === StatusMessageType.Warning && <Warning fontSize="small" color="warning" />}
<span className="text-sm text-gray-500">{timestampUtcMsToCompactIsoString(entry.datetimeMs)}</span>
<span
className="ml-2 overflow-hidden text-ellipsis min-w-0 whitespace-nowrap"
title={entry.message}
>
{entry.message}
</span>
</div>
{log.map((entry) => (
<LogEntryComponent key={entry.id} logEntry={entry} />
))}
</>
);
}

type LogEntryProps = {
logEntry: LogEntry;
};

function LogEntryComponent(props: LogEntryProps): React.ReactNode {
let icon = <CloudDownload fontSize="inherit" className="text-gray-600" />;
let message = "Loading...";
if (props.logEntry.type === LogEntryType.MESSAGE) {
if (props.logEntry.message?.type === StatusMessageType.Error) {
icon = <Error fontSize="inherit" className="text-red-600" />;
} else if (props.logEntry.message?.type === StatusMessageType.Warning) {
icon = <Warning fontSize="inherit" className="text-orange-600" />;
}
message = props.logEntry.message?.message ?? "";
} else if (props.logEntry.type === LogEntryType.SUCCESS) {
icon = <CheckCircle fontSize="inherit" className="text-green-600" />;
message = "Loading successful";
} else if (props.logEntry.type === LogEntryType.LOADING_DONE) {
icon = <CloudDone fontSize="inherit" className="text-blue-600" />;
message = "Loading done";
}

return (
<div className="py-1 flex gap-3 items-center hover:bg-blue-100 p-2">
{icon}
<div className="flex flex-col gap-0.5">
<span className="text-sm text-gray-500">
{convertDatetimeMsToHumanReadableString(props.logEntry.datetimeMs)}
</span>
<span title={message}>{message}</span>
</div>
</div>
);
}

function convertDatetimeMsToHumanReadableString(datetimeMs: number): string {
const date = new Date(datetimeMs);
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();

return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
}
Loading

0 comments on commit 9cd4e03

Please sign in to comment.