Initial commit
This commit is contained in:
40
frontend/views/dashboard/ts/calibration.ts
Normal file
40
frontend/views/dashboard/ts/calibration.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Main } from './main';
|
||||
|
||||
export class Calibration {
|
||||
private _Main: Main;
|
||||
|
||||
container: HTMLDivElement = document.querySelector('.ntsh_calibration');
|
||||
image: HTMLImageElement = this.container.querySelector('img');
|
||||
fullscreenButton: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_calibration-fullscreen'
|
||||
);
|
||||
|
||||
constructor(Main: Main) {
|
||||
this._Main = Main;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private fetchClock: NodeJS.Timeout;
|
||||
startFetchClock() {
|
||||
this.image.src = `/calibrationImage?t=${Date.now()}`;
|
||||
this.fetchClock = setInterval(() => {
|
||||
this.image.src = `/calibrationImage?t=${Date.now()}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
stopFetchClock() {
|
||||
clearInterval(this.fetchClock);
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
this._Main.TabController.registerListener('calibration', (visible) => {
|
||||
if (visible) this.startFetchClock();
|
||||
else this.stopFetchClock();
|
||||
});
|
||||
|
||||
this.fullscreenButton.addEventListener('click', () => {
|
||||
this.image.requestFullscreen();
|
||||
});
|
||||
}
|
||||
}
|
||||
192
frontend/views/dashboard/ts/dashboard.camerarunner.ts
Normal file
192
frontend/views/dashboard/ts/dashboard.camerarunner.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { MorphFeature } from 'morphux';
|
||||
import { Main } from './main';
|
||||
import {
|
||||
formatUptime,
|
||||
ServiceState,
|
||||
setStatusState,
|
||||
StatusType,
|
||||
} from './utils';
|
||||
|
||||
const CELCIUS = 'ºC';
|
||||
|
||||
export class DashboardCameraRunner {
|
||||
private _Main: Main;
|
||||
|
||||
container: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-camerarunner'
|
||||
);
|
||||
|
||||
connectionStatus: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-connectionstatus'
|
||||
);
|
||||
connectionInfo: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-connectioninfo'
|
||||
);
|
||||
rebootButton: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-reboot'
|
||||
);
|
||||
|
||||
processStatus: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-processstatus'
|
||||
);
|
||||
processInfo: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-processinfo'
|
||||
);
|
||||
restartButton: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-restart'
|
||||
);
|
||||
|
||||
uptimeInfo: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-uptime'
|
||||
);
|
||||
|
||||
errorContainer: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-error'
|
||||
);
|
||||
errorText: HTMLDivElement = this.container.querySelector(
|
||||
'.ntsh_dashboard-camerarunner-errortext'
|
||||
);
|
||||
|
||||
constructor(Main: Main) {
|
||||
this._Main = Main;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
updateState(state: CameraRunnerStatus) {
|
||||
// ----------- Connection -----------
|
||||
setStatusState(
|
||||
this.connectionStatus,
|
||||
{
|
||||
CONNECTING: 'yellow',
|
||||
CONNECTED: 'green',
|
||||
DISCONNECTED: 'red',
|
||||
FAILED: 'red',
|
||||
}[state.state] as StatusType
|
||||
);
|
||||
this.connectionInfo.innerText = state.message ?? '';
|
||||
|
||||
// ----------- Process -----------
|
||||
if (state.state != 'CONNECTED') {
|
||||
state.processStatus.state = 'STOPPED';
|
||||
state.processStatus.message = 'Not connected';
|
||||
state.processStatus.startTime = -1;
|
||||
|
||||
this.restartButton.style.display = 'none';
|
||||
this.rebootButton.style.display = 'none';
|
||||
} else {
|
||||
this.rebootButton.style.display = 'flex';
|
||||
|
||||
if (state.processStatus.state == 'RUNNING')
|
||||
this.restartButton.style.display = 'flex';
|
||||
else this.restartButton.style.display = 'none';
|
||||
}
|
||||
|
||||
setStatusState(
|
||||
this.processStatus,
|
||||
{
|
||||
RUNNING: 'green',
|
||||
STOPPED: 'gray',
|
||||
STARTING: 'yellow',
|
||||
PROBLEM: 'red',
|
||||
}[state.processStatus.state] as StatusType
|
||||
);
|
||||
this.processInfo.innerText = state.processStatus.message ?? '';
|
||||
|
||||
// ----------- Uptime -----------
|
||||
const uptimeSeconds =
|
||||
state.processStatus.startTime == -1
|
||||
? -1
|
||||
: (Date.now() - state.processStatus.startTime) / 1000;
|
||||
this.uptimeInfo.innerText = formatUptime(uptimeSeconds);
|
||||
|
||||
// ----------- Error -----------
|
||||
const errors: string[] = [];
|
||||
if ((state?.error ?? '').trim().length > 0) errors.push(state.error);
|
||||
if ((state?.processStatus?.error ?? '').trim().length > 0)
|
||||
errors.push(state.processStatus.error);
|
||||
if (errors.length > 0) {
|
||||
this.errorText.innerText = errors.join('\n');
|
||||
this.errorContainer.style.display = 'block';
|
||||
} else {
|
||||
this.errorContainer.style.display = 'none';
|
||||
this.errorText.innerText = '';
|
||||
}
|
||||
|
||||
this._Main.Logs.setCameraLogs(state.processStatus.output.current);
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this._Main.socket.on(
|
||||
'cameraRunnerState',
|
||||
(state: CameraRunnerStatus) => {
|
||||
this.updateState(state);
|
||||
}
|
||||
);
|
||||
|
||||
this.restartButton.addEventListener('click', async () => {
|
||||
this.executeCommand(
|
||||
'restart',
|
||||
'Are you sure you want to restart the Camera Runner process?'
|
||||
);
|
||||
});
|
||||
|
||||
this.rebootButton.addEventListener('click', async () => {
|
||||
this.executeCommand(
|
||||
'reboot',
|
||||
'Are you sure you want to reboot the Camera Runner machine?'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async executeCommand(command: string, message: string) {
|
||||
const confirmed = await MorphFeature.Confirm({
|
||||
title: 'Are you sure?',
|
||||
message,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
MorphFeature.Loader({
|
||||
active: true,
|
||||
message: `Requesting Camera Runner ${command}...`,
|
||||
});
|
||||
this._Main.socket.emit(
|
||||
'cameraRunner',
|
||||
command,
|
||||
(response: { succeed: boolean; message?: string }) => {
|
||||
MorphFeature.Loader({ active: false });
|
||||
|
||||
if (!response.succeed)
|
||||
return MorphFeature.Alert({
|
||||
title: 'Error',
|
||||
message: response.message,
|
||||
});
|
||||
|
||||
MorphFeature.Notification({
|
||||
level: 'success',
|
||||
message: `Camera Runner is ${command}ing...`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface CameraRunnerStatus {
|
||||
state: ServiceState;
|
||||
message?: string;
|
||||
error?: string;
|
||||
|
||||
processStatus: ProcessStatus;
|
||||
}
|
||||
|
||||
export type ProcessStatusState = 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
|
||||
|
||||
interface ProcessStatusSimple {
|
||||
state: ProcessStatusState;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
interface ProcessStatus extends ProcessStatusSimple {
|
||||
startTime: number;
|
||||
output: { current: string[]; last: string[] };
|
||||
}
|
||||
353
frontend/views/dashboard/ts/dashboard.unity.ts
Normal file
353
frontend/views/dashboard/ts/dashboard.unity.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { ce, MorphComponent, MorphFeature } from 'morphux';
|
||||
import { Main } from './main';
|
||||
import {
|
||||
createProgress,
|
||||
formatUptime,
|
||||
ServiceState,
|
||||
setProgressState,
|
||||
setStatusState,
|
||||
StatusType,
|
||||
} from './utils';
|
||||
|
||||
export class DashboardUnity {
|
||||
private _Main: Main;
|
||||
|
||||
container: HTMLDivElement = document.querySelector('.ntsh_dashboard-unity');
|
||||
|
||||
processStatus: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-processstatus'
|
||||
);
|
||||
processInfo: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-processinfo'
|
||||
);
|
||||
restartButton: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-restart'
|
||||
);
|
||||
|
||||
uptimeInfo: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-uptime'
|
||||
);
|
||||
|
||||
webSocketStatus: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-websocketstatus'
|
||||
);
|
||||
webSocketInfo: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-websocketinfo'
|
||||
);
|
||||
|
||||
zedStreamStatus: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-zedstreamstatus'
|
||||
);
|
||||
zedStreamInfo: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-zedstreaminfo'
|
||||
);
|
||||
|
||||
zedStreamFps: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-zedstreamfps'
|
||||
);
|
||||
zedStreamPath: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-zedstreampath'
|
||||
);
|
||||
|
||||
timelineWatching: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-timeline-watching'
|
||||
);
|
||||
timelineStanding: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-timeline-standing'
|
||||
);
|
||||
timelineProgress: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-timeline-progress'
|
||||
);
|
||||
|
||||
parametersTable: HTMLTableElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-parameters'
|
||||
);
|
||||
|
||||
errorContainer: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-error'
|
||||
);
|
||||
errorText: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_dashboard-unity-errortext'
|
||||
);
|
||||
|
||||
constructor(Main: Main) {
|
||||
this._Main = Main;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private runnerError: string;
|
||||
updateRunnerState(state: UnityRunnerStatus) {
|
||||
// ----------- Process -----------
|
||||
if (state.state != 'RUNNING') {
|
||||
state.startTime = -1;
|
||||
|
||||
this.restartButton.style.display = 'none';
|
||||
} else {
|
||||
this.restartButton.style.display = 'flex';
|
||||
}
|
||||
|
||||
setStatusState(
|
||||
this.processStatus,
|
||||
{
|
||||
RUNNING: 'green',
|
||||
STOPPED: 'red',
|
||||
STARTING: 'yellow',
|
||||
PROBLEM: 'red',
|
||||
}[state.state] as StatusType
|
||||
);
|
||||
this.processInfo.innerText = state.message ?? '';
|
||||
|
||||
// ----------- Uptime -----------
|
||||
const uptimeSeconds =
|
||||
state.startTime == -1 ? -1 : (Date.now() - state.startTime) / 1000;
|
||||
this.uptimeInfo.innerText = formatUptime(uptimeSeconds);
|
||||
|
||||
// ----------- Error -----------
|
||||
if ((state?.error ?? '').trim().length > 0)
|
||||
this.runnerError = state.error;
|
||||
else this.runnerError = null;
|
||||
this.updateError();
|
||||
|
||||
this._Main.Logs.setUnityLogs(state.output.current);
|
||||
}
|
||||
|
||||
private webSocketError: string;
|
||||
updateWebSocketState(state: UnityWebSocketStatus) {
|
||||
// ----------- WebSocket -----------
|
||||
setStatusState(
|
||||
this.webSocketStatus,
|
||||
{
|
||||
CONNECTING: 'yellow',
|
||||
CONNECTED: 'green',
|
||||
DISCONNECTED: 'gray',
|
||||
FAILED: 'red',
|
||||
}[state.state] as StatusType
|
||||
);
|
||||
this.webSocketInfo.innerText = state.message ?? '';
|
||||
|
||||
// ----------- ZED Stream -----------
|
||||
setStatusState(
|
||||
this.zedStreamStatus,
|
||||
state.parameters.zedReady ? 'green' : 'red'
|
||||
);
|
||||
this.zedStreamInfo.innerText = state.parameters.zedReady
|
||||
? `Connected to ${state.parameters.zedPath}`
|
||||
: 'Not ready';
|
||||
this.zedStreamFps.innerText =
|
||||
state.parameters.zedFPS == '-' ? '' : state.parameters.zedFPS;
|
||||
|
||||
// ----------- Timeline -----------
|
||||
this.timelineWatching.innerText = state.parameters.timelineWatching
|
||||
? 'Yes'
|
||||
: 'No';
|
||||
this.timelineStanding.innerText = state.parameters.timelineStanding
|
||||
? 'Yes'
|
||||
: 'No';
|
||||
setProgressState(
|
||||
this.timelineProgress,
|
||||
state.parameters.timelineProgress
|
||||
);
|
||||
|
||||
// ----------- Parameters -----------
|
||||
this.renderParameterSliders(state.parameters.parameters);
|
||||
|
||||
// ----------- Error -----------
|
||||
if ((state?.error ?? '').trim().length > 0)
|
||||
this.webSocketError = state.error;
|
||||
else this.webSocketError = null;
|
||||
this.updateError();
|
||||
}
|
||||
|
||||
private renderParameterSliders(parameters: UnityParameters['parameters']) {
|
||||
if (parameters.length === 0) return;
|
||||
|
||||
const existingSliders = this.parametersTable.querySelectorAll(
|
||||
'.ntsh_dashboard-unity-parameter-row'
|
||||
);
|
||||
|
||||
if (existingSliders.length !== parameters.length) {
|
||||
this.parametersTable.innerHTML = '';
|
||||
|
||||
parameters.forEach((param) => {
|
||||
const row = ce('tr', 'ntsh_dashboard-unity-parameter-row');
|
||||
|
||||
const nameCell = ce('td');
|
||||
nameCell.appendChild(
|
||||
ce('div', 'mux_text', null, param.sliderName)
|
||||
);
|
||||
row.appendChild(nameCell);
|
||||
|
||||
const progressCell = ce('td', 'no-service');
|
||||
progressCell.appendChild(createProgress(param.outputValue));
|
||||
row.appendChild(progressCell);
|
||||
|
||||
const sliderCell = ce('td', 'only-service');
|
||||
const sliderProgress = createProgress(param.outputValue);
|
||||
const sliderValue: HTMLDivElement =
|
||||
sliderProgress.querySelector('.ntsh_progress-value');
|
||||
sliderValue.classList.add('mux_resizer');
|
||||
sliderCell.appendChild(sliderProgress);
|
||||
|
||||
const resizer = new MorphComponent.Resizer({
|
||||
existingContainer: sliderValue,
|
||||
direction: 'right',
|
||||
relative: true,
|
||||
min: 0,
|
||||
max: () => sliderProgress.clientWidth,
|
||||
});
|
||||
let lastPercentage: number = -1;
|
||||
resizer.on('resized', (size) => {
|
||||
const percentage =
|
||||
Math.round((size / sliderProgress.clientWidth) * 100) /
|
||||
100;
|
||||
if (percentage === lastPercentage) return;
|
||||
lastPercentage = percentage;
|
||||
|
||||
this._Main.socket.emit(
|
||||
'unityWebSocket',
|
||||
'parameterValue',
|
||||
param.sliderIndex,
|
||||
percentage
|
||||
);
|
||||
setProgressState(sliderProgress, percentage);
|
||||
});
|
||||
|
||||
row.appendChild(sliderCell);
|
||||
|
||||
this.parametersTable.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
existingSliders.forEach((row, index) => {
|
||||
const value = parameters[index].outputValue;
|
||||
|
||||
const progressElement: HTMLDivElement = row.querySelector(
|
||||
'.no-service .ntsh_progress'
|
||||
);
|
||||
setProgressState(progressElement, value);
|
||||
|
||||
const sliderElement: HTMLDivElement = row.querySelector(
|
||||
'.only-service .ntsh_progress'
|
||||
);
|
||||
if (sliderElement.querySelector('.mux_resizer-moving') == null)
|
||||
setProgressState(sliderElement, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateError() {
|
||||
const errors: string[] = [];
|
||||
if (this.runnerError != null) errors.push(this.runnerError);
|
||||
if (this.webSocketError != null) errors.push(this.webSocketError);
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.errorText.innerText = errors.join('\n');
|
||||
this.errorContainer.style.display = 'block';
|
||||
} else {
|
||||
this.errorContainer.style.display = 'none';
|
||||
this.errorText.innerText = '';
|
||||
}
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this._Main.socket.on('unityRunnerState', (state: UnityRunnerStatus) =>
|
||||
this.updateRunnerState(state)
|
||||
);
|
||||
this._Main.socket.on(
|
||||
'unityWebSocketState',
|
||||
(state: UnityWebSocketStatus) => this.updateWebSocketState(state)
|
||||
);
|
||||
|
||||
this.restartButton.addEventListener('click', async () => {
|
||||
this.executeCommand(
|
||||
'restart',
|
||||
'Are you sure you want to restart the Unity Runner process?'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async executeCommand(command: string, message: string) {
|
||||
const confirmed = await MorphFeature.Confirm({
|
||||
title: 'Are you sure?',
|
||||
message,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
MorphFeature.Loader({
|
||||
active: true,
|
||||
message: `Requesting Unity Runner ${command}...`,
|
||||
});
|
||||
this._Main.socket.emit(
|
||||
'unityRunner',
|
||||
command,
|
||||
(response: { succeed: boolean; message?: string }) => {
|
||||
MorphFeature.Loader({ active: false });
|
||||
|
||||
if (!response.succeed)
|
||||
return MorphFeature.Alert({
|
||||
title: 'Error',
|
||||
message: response.message,
|
||||
});
|
||||
|
||||
MorphFeature.Notification({
|
||||
level: 'success',
|
||||
message: `Unity Runner is ${command}ing...`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface UnityRunnerStatus {
|
||||
state: 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
|
||||
message?: string;
|
||||
error?: string;
|
||||
|
||||
startTime: number;
|
||||
output: { current: string[]; last: string[] };
|
||||
}
|
||||
|
||||
interface UnityWebSocketStatus {
|
||||
state: ServiceState;
|
||||
message?: string;
|
||||
error?: string;
|
||||
|
||||
parameters: UnityParameters;
|
||||
}
|
||||
|
||||
interface UnityParameters {
|
||||
timelineWatching: boolean;
|
||||
timelineStanding: boolean;
|
||||
timelineProgress: number;
|
||||
zedPath: string;
|
||||
zedReady: boolean;
|
||||
zedFPS: string;
|
||||
parameters: UnitySocketMessageHeartbeat['heartbeat']['dataSliders'];
|
||||
}
|
||||
|
||||
interface UnitySocketMessageBase {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
}
|
||||
interface UnitySocketMessageHeartbeat extends UnitySocketMessageBase {
|
||||
type: 'heartbeat_data';
|
||||
heartbeat: {
|
||||
dataSliders: {
|
||||
sliderIndex: number;
|
||||
sliderName: string;
|
||||
outputValue: number;
|
||||
}[];
|
||||
dataTimeline: {
|
||||
isStanding: boolean;
|
||||
isWatching: boolean;
|
||||
timelineProgress: number;
|
||||
};
|
||||
zedCamera: {
|
||||
cameraFPS: string;
|
||||
isZedReady: boolean;
|
||||
streamInputIP: string;
|
||||
streamInputPort: number;
|
||||
zedGrabError: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
96
frontend/views/dashboard/ts/logsHandler.ts
Normal file
96
frontend/views/dashboard/ts/logsHandler.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ce } from 'morphux';
|
||||
import { delay } from './utils';
|
||||
import { Main } from './main';
|
||||
import { AnsiUp } from 'ansi_up';
|
||||
|
||||
export class LogsHandler {
|
||||
private _Main: Main;
|
||||
|
||||
ansiUp = new AnsiUp();
|
||||
|
||||
cameraLogsContainer: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_tab[tabid="cameralogs"] .ntsh_logs'
|
||||
);
|
||||
unityLogsContainer: HTMLDivElement = document.querySelector(
|
||||
'.ntsh_tab[tabid="unitylogs"] .ntsh_logs'
|
||||
);
|
||||
|
||||
constructor(Main: Main) {
|
||||
this._Main = Main;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
setCameraLogs(logs: string[]) {
|
||||
this.applyLogs(this.cameraLogsContainer, logs);
|
||||
}
|
||||
|
||||
setUnityLogs(logs: string[]) {
|
||||
this.applyLogs(this.unityLogsContainer, logs);
|
||||
}
|
||||
|
||||
private firstTime: Set<HTMLDivElement> = new Set();
|
||||
|
||||
private async applyLogs(container: HTMLDivElement, logs: string[]) {
|
||||
let logCount = container.querySelectorAll(
|
||||
'.ntsh_log.ntsh_log-normal'
|
||||
).length;
|
||||
if (logCount === logs.length) return;
|
||||
if (logCount > logs.length) logCount = 0;
|
||||
|
||||
const isAtBottom =
|
||||
!this.firstTime.has(container) ||
|
||||
container.scrollTop + container.clientHeight >=
|
||||
container.scrollHeight;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
logs.forEach((log, i) => {
|
||||
const element = ce(
|
||||
'div',
|
||||
['ntsh_log', 'ntsh_log-normal'],
|
||||
null,
|
||||
null,
|
||||
this.ansiUp.ansi_to_html(log)
|
||||
);
|
||||
|
||||
if (this.firstTime.has(container) && i > logCount - 1) {
|
||||
element.style.display = 'none';
|
||||
const spawnDelay = (i - logCount) * 10;
|
||||
setTimeout(() => {
|
||||
element.style.display = 'block';
|
||||
if (isAtBottom)
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, spawnDelay);
|
||||
}
|
||||
|
||||
container.appendChild(element);
|
||||
if (isAtBottom) container.scrollTop = container.scrollHeight;
|
||||
});
|
||||
|
||||
if (!this.firstTime.has(container)) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.firstTime.add(container);
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
this._Main.TabController.registerListener('cameralogs', (visible) => {
|
||||
if (!visible) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.cameraLogsContainer.scrollTop =
|
||||
this.cameraLogsContainer.scrollHeight;
|
||||
}, 10);
|
||||
});
|
||||
|
||||
this._Main.TabController.registerListener('unitylogs', (visible) => {
|
||||
if (!visible) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.unityLogsContainer.scrollTop =
|
||||
this.unityLogsContainer.scrollHeight;
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
37
frontend/views/dashboard/ts/main.ts
Normal file
37
frontend/views/dashboard/ts/main.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { io } from 'socket.io-client';
|
||||
import { MenuBar } from './menuBar';
|
||||
import { TabController } from './tabController';
|
||||
import { MorphFeature } from 'morphux';
|
||||
import { DashboardCameraRunner } from './dashboard.camerarunner';
|
||||
import { LogsHandler } from './logsHandler';
|
||||
import { Calibration } from './calibration';
|
||||
import { DashboardUnity } from './dashboard.unity';
|
||||
|
||||
const socket = io('/');
|
||||
export class Main {
|
||||
socket = socket;
|
||||
|
||||
MenuBar = new MenuBar(this);
|
||||
TabController = new TabController(this);
|
||||
|
||||
Logs = new LogsHandler(this);
|
||||
Calibration = new Calibration(this);
|
||||
|
||||
DashboardCameraRunner = new DashboardCameraRunner(this);
|
||||
DashboardUnityRunner = new DashboardUnity(this);
|
||||
|
||||
constructor() {
|
||||
if (window.location.search.includes('debug')) {
|
||||
(window as any).SN = this;
|
||||
console.log('Debug mode enabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MorphFeature.Loader({ active: true, message: 'Connecting to server...' });
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
MorphFeature.Loader({ active: false });
|
||||
});
|
||||
|
||||
const _Main = new Main();
|
||||
142
frontend/views/dashboard/ts/menuBar.ts
Normal file
142
frontend/views/dashboard/ts/menuBar.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { MorphComponent, MorphFeature } from 'morphux';
|
||||
import { Main } from './main';
|
||||
import { ComponentMenuBar } from 'morphux/dist/Components/MenuBar/Component.MenuBar';
|
||||
|
||||
export class MenuBar {
|
||||
private _Main: Main;
|
||||
|
||||
container: HTMLDivElement = document.querySelector('.ntsh_menubar');
|
||||
|
||||
menubar: ComponentMenuBar;
|
||||
|
||||
constructor(main: Main) {
|
||||
this._Main = main;
|
||||
|
||||
this.build();
|
||||
|
||||
setTimeout(() => {
|
||||
if (localStorage?.getItem('serviceMode') === 'true')
|
||||
this.toggleServiceMode(true, true);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
build() {
|
||||
this.menubar = new ComponentMenuBar({
|
||||
left: [
|
||||
{
|
||||
type: 'image',
|
||||
url: '/img/morphix_logo_white.png',
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
text: 'Dashboard',
|
||||
materialIcon: 'dashboard',
|
||||
uniqueIdentifier: 'dashboard',
|
||||
selected: true,
|
||||
|
||||
click: () => this._Main.TabController.showTab('dashboard'),
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
text: 'Calibration',
|
||||
uniqueIdentifier: 'calibration',
|
||||
materialIcon: 'crop_free',
|
||||
|
||||
click: () =>
|
||||
this._Main.TabController.showTab('calibration'),
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
text: 'Unity Logs',
|
||||
uniqueIdentifier: 'unitylogs',
|
||||
materialIcon: 'deployed_code',
|
||||
|
||||
click: () => this._Main.TabController.showTab('unitylogs'),
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
text: 'Camera Logs',
|
||||
uniqueIdentifier: 'cameralogs',
|
||||
materialIcon: 'photo_camera',
|
||||
|
||||
click: () => this._Main.TabController.showTab('cameralogs'),
|
||||
},
|
||||
],
|
||||
|
||||
right: [
|
||||
{
|
||||
type: 'normal',
|
||||
text: document.body.classList.contains('ntsh_service')
|
||||
? 'Exit Service'
|
||||
: 'Service Mode',
|
||||
uniqueIdentifier: 'serviceMode',
|
||||
materialIcon: document.body.classList.contains(
|
||||
'ntsh_service'
|
||||
)
|
||||
? 'logout'
|
||||
: 'engineering',
|
||||
|
||||
click: async () => {
|
||||
const mobileMenu: HTMLDivElement =
|
||||
document.querySelector('.mux_mobilemenu');
|
||||
mobileMenu?.click();
|
||||
|
||||
this.toggleServiceMode();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
this.container.innerHTML = '';
|
||||
this.container.appendChild(this.menubar.container);
|
||||
}
|
||||
|
||||
async toggleServiceMode(
|
||||
mode?: boolean,
|
||||
skipPin?: boolean
|
||||
): Promise<boolean> {
|
||||
const newMode =
|
||||
mode ?? !document.body.classList.contains('ntsh_service');
|
||||
|
||||
if (newMode) {
|
||||
if (skipPin !== true) {
|
||||
const servicePin: string = await MorphFeature.Prompt({
|
||||
title: 'Service Mode',
|
||||
message: 'Enter the service PIN:',
|
||||
type: 'number',
|
||||
canBeEmpty: false,
|
||||
placeholder: '****',
|
||||
});
|
||||
if (servicePin !== '4252') {
|
||||
MorphFeature.Alert({
|
||||
title: 'Error',
|
||||
message: 'Incorrect PIN provided.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
document.body.classList.add('ntsh_service');
|
||||
|
||||
localStorage.setItem('serviceMode', 'true');
|
||||
|
||||
MorphFeature.Notification({
|
||||
level: 'success',
|
||||
message: 'Service mode activated.',
|
||||
});
|
||||
} else {
|
||||
document.body.classList.remove('ntsh_service');
|
||||
|
||||
this._Main.TabController.showTab('dashboard');
|
||||
|
||||
localStorage.setItem('serviceMode', 'false');
|
||||
|
||||
MorphFeature.Notification({
|
||||
level: 'success',
|
||||
message: 'Service mode deactivated.',
|
||||
});
|
||||
}
|
||||
|
||||
this.build();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
88
frontend/views/dashboard/ts/tabController.ts
Normal file
88
frontend/views/dashboard/ts/tabController.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { MorphFeature } from 'morphux';
|
||||
import { Main } from './main';
|
||||
|
||||
export type Tabs = 'dashboard' | 'calibration' | 'cameralogs' | 'unitylogs';
|
||||
export class TabController {
|
||||
private _Main: Main;
|
||||
|
||||
container: HTMLDivElement = document.querySelector('.ntsh_tabs');
|
||||
|
||||
currentTabId: Tabs;
|
||||
|
||||
constructor(main: Main) {
|
||||
this._Main = main;
|
||||
|
||||
this.registerNavigationListener();
|
||||
}
|
||||
|
||||
private tabListeners: Map<Tabs, (visible: boolean) => void> = new Map();
|
||||
registerListener(tabId: Tabs, callback: (visible: boolean) => void) {
|
||||
if (this.tabListeners.has(tabId))
|
||||
throw new Error(
|
||||
`Listener for tab id ${tabId} has already been registered!`
|
||||
);
|
||||
|
||||
this.tabListeners.set(tabId, callback);
|
||||
callback(this.currentTabId == tabId);
|
||||
}
|
||||
|
||||
async showTab(tabId: Tabs) {
|
||||
if (this.currentTabId == tabId) return;
|
||||
|
||||
if (
|
||||
tabId !== 'dashboard' &&
|
||||
!document.body.classList.contains('ntsh_service')
|
||||
) {
|
||||
const confirmed = await MorphFeature.Confirm({
|
||||
title: 'Service Mode Required',
|
||||
message: `You need to be in service mode to access tab ${tabId}, switch to Service Mode now?`,
|
||||
});
|
||||
if (!confirmed) return this.showTab('dashboard');
|
||||
|
||||
const succeed = await this._Main.MenuBar.toggleServiceMode(true);
|
||||
if (!succeed) return this.showTab('dashboard');
|
||||
}
|
||||
|
||||
this._Main.MenuBar.menubar.setSelected(tabId);
|
||||
|
||||
if (this.tabListeners.has(this.currentTabId))
|
||||
this.tabListeners.get(this.currentTabId)(false);
|
||||
if (this.tabListeners.has(tabId)) this.tabListeners.get(tabId)(true);
|
||||
this.currentTabId = tabId;
|
||||
|
||||
this.container
|
||||
.querySelectorAll('.ntsh_tab')
|
||||
.forEach((tab) => tab.classList.remove('ntsh_tab-visible'));
|
||||
|
||||
this.container
|
||||
.querySelector(`.ntsh_tab[tabid="${tabId}"]`)
|
||||
.classList.add('ntsh_tab-visible');
|
||||
|
||||
window.history.pushState(
|
||||
{ tab: tabId },
|
||||
'',
|
||||
`/${tabId}${window.location.search}`
|
||||
);
|
||||
|
||||
var tabName = {
|
||||
dashboard: 'Dashboard',
|
||||
calibration: 'Calibration',
|
||||
cameralogs: 'Camera Logs',
|
||||
unitylogs: 'Unity Logs',
|
||||
}[tabId];
|
||||
document.title = `NTSH Control - ${tabName}`;
|
||||
}
|
||||
|
||||
private registerNavigationListener() {
|
||||
window.addEventListener('popstate', (event) => {
|
||||
const state = event.state;
|
||||
if (state && state.tab) this.showTab(state.tab);
|
||||
});
|
||||
|
||||
var startTab = window.location.pathname
|
||||
.replace('/', '')
|
||||
.split('/')
|
||||
.shift() as Tabs;
|
||||
this.showTab(startTab.length > 0 ? startTab : 'dashboard');
|
||||
}
|
||||
}
|
||||
81
frontend/views/dashboard/ts/utils.ts
Normal file
81
frontend/views/dashboard/ts/utils.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ce } from 'morphux';
|
||||
|
||||
/**
|
||||
* A linear interpolation helper function.
|
||||
* @param a The start value.
|
||||
* @param b The end value.
|
||||
* @param t The interpolation factor (0 to 1).
|
||||
* @returns The interpolated value.
|
||||
*/
|
||||
export const lerp = (a: number, b: number, t: number): number =>
|
||||
a + (b - a) * t;
|
||||
|
||||
export const setStatusState = (
|
||||
statusElement: HTMLDivElement,
|
||||
state: StatusType
|
||||
) => {
|
||||
statusElement.classList.remove(
|
||||
'ntsh_status-green',
|
||||
'ntsh_status-yellow',
|
||||
'ntsh_status-red',
|
||||
'ntsh_status-gray'
|
||||
);
|
||||
statusElement.classList.add(`ntsh_status-${state}`);
|
||||
};
|
||||
export type StatusType = 'green' | 'yellow' | 'red' | 'gray';
|
||||
|
||||
export const setProgressState = (
|
||||
progressElement: HTMLDivElement,
|
||||
percentage: number
|
||||
) => {
|
||||
const value: HTMLDivElement = progressElement.querySelector(
|
||||
'.ntsh_progress-value'
|
||||
);
|
||||
value.style.width = `${percentage * 100}%`;
|
||||
|
||||
const label: HTMLDivElement = progressElement.querySelector(
|
||||
'.ntsh_progress-label'
|
||||
);
|
||||
label.innerText = `${Math.round(percentage * 100)}%`;
|
||||
};
|
||||
|
||||
export const createProgress = (value: number) => {
|
||||
const progress = ce('div', 'ntsh_progress');
|
||||
progress.appendChild(ce('div', 'ntsh_progress-value'));
|
||||
progress.appendChild(ce('div', 'ntsh_progress-label'));
|
||||
setProgressState(progress, value);
|
||||
return progress;
|
||||
};
|
||||
export function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export type ServiceState =
|
||||
| 'CONNECTING'
|
||||
| 'CONNECTED'
|
||||
| 'DISCONNECTED'
|
||||
| 'FAILED';
|
||||
|
||||
export function formatUptime(seconds: number): string {
|
||||
if (seconds < 0) return '';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
seconds %= 86400;
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
seconds %= 3600;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
seconds = Math.floor(seconds % 60);
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
|
||||
parts.push(
|
||||
`${hours.toString().padStart(2, '0')}:${minutes
|
||||
.toString()
|
||||
.padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export function delay(duration: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, duration));
|
||||
}
|
||||
Reference in New Issue
Block a user