Progress
This commit is contained in:
parent
749194f9e1
commit
e02754f390
@ -93,10 +93,11 @@ class FfmpegProgressDecoder {
|
|||||||
hasReadContinue = true
|
hasReadContinue = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val results = Regex("Duration:\\s*([^,]+),").find(value)?.groupValues?.firstOrNull()
|
val results = Regex("Duration:\\s*([^,]+),").find(value)?.groupValues?.firstOrNull() ?: return
|
||||||
log.info { "Identified duration for estimation $results" }
|
log.info { "Identified duration for estimation $results" }
|
||||||
|
|
||||||
val parsedDuration = Regex("[0-9]+:[0-9]+:[0-9]+.[0-9]+").find(results.toString())?.value ?: return
|
val parsedDuration = Regex("[0-9]+:[0-9]+:[0-9]+.[0-9]+").find(results.toString())?.value ?: return
|
||||||
|
|
||||||
if (!parsedDurations.contains(parsedDuration)) {
|
if (!parsedDurations.contains(parsedDuration)) {
|
||||||
parsedDurations.add(parsedDuration)
|
parsedDurations.add(parsedDuration)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package no.iktdev.mediaprocessing.ui.socket
|
package no.iktdev.mediaprocessing.ui.socket
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
import no.iktdev.eventi.data.referenceId
|
import no.iktdev.eventi.data.referenceId
|
||||||
import no.iktdev.eventi.database.toEpochSeconds
|
import no.iktdev.eventi.database.toEpochSeconds
|
||||||
import no.iktdev.eventi.database.withDirtyRead
|
import no.iktdev.eventi.database.withDirtyRead
|
||||||
@ -32,24 +33,35 @@ class ProcesserTasksTopic(
|
|||||||
@Autowired private val message: SimpMessagingTemplate?,
|
@Autowired private val message: SimpMessagingTemplate?,
|
||||||
): SocketListener(message) {
|
): SocketListener(message) {
|
||||||
|
|
||||||
|
private var referenceIds: List<String> = emptyList()
|
||||||
|
|
||||||
final val a2a = object : ProcesserListenerService.A2AProcesserListener {
|
final val a2a = object : ProcesserListenerService.A2AProcesserListener {
|
||||||
override fun onExtractProgress(info: ProcesserEventInfo) {
|
override fun onExtractProgress(info: ProcesserEventInfo) {
|
||||||
|
if (referenceIds.none { it == info.referenceId }) {
|
||||||
|
updateTopicWithTasks()
|
||||||
|
}
|
||||||
|
log.info { "Forwarding extract progress ${Gson().toJson(info)}" }
|
||||||
message?.convertAndSend("/topic/processer/extract/progress", info)
|
message?.convertAndSend("/topic/processer/extract/progress", info)
|
||||||
pullAllTasks()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEncodeProgress(info: ProcesserEventInfo) {
|
override fun onEncodeProgress(info: ProcesserEventInfo) {
|
||||||
|
if (referenceIds.none { it == info.referenceId }) {
|
||||||
|
updateTopicWithTasks()
|
||||||
|
}
|
||||||
|
log.info { "Forwarding encode progress ${Gson().toJson(info)}" }
|
||||||
message?.convertAndSend("/topic/processer/encode/progress", info)
|
message?.convertAndSend("/topic/processer/encode/progress", info)
|
||||||
pullAllTasks()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEncodeAssigned() {
|
override fun onEncodeAssigned(task: Task) {
|
||||||
pullAllTasks()
|
if (referenceIds.none { it == task.referenceId }) {
|
||||||
|
updateTopicWithTasks()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExtractAssigned() {
|
override fun onExtractAssigned(task: Task) {
|
||||||
pullAllTasks()
|
if (referenceIds.none { it == task.referenceId }) {
|
||||||
|
updateTopicWithTasks()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +90,7 @@ class ProcesserTasksTopic(
|
|||||||
|
|
||||||
|
|
||||||
@MessageMapping("/tasks/all")
|
@MessageMapping("/tasks/all")
|
||||||
fun pullAllTaskss() {
|
fun updateTopicWithTasks() {
|
||||||
val states = update()
|
val states = update()
|
||||||
template?.convertAndSend("/topic/tasks/all", states)
|
template?.convertAndSend("/topic/tasks/all", states)
|
||||||
}
|
}
|
||||||
@ -107,7 +119,9 @@ class ProcesserTasksTopic(
|
|||||||
val eventStates: MutableList<ContentEventState> = mutableListOf()
|
val eventStates: MutableList<ContentEventState> = mutableListOf()
|
||||||
|
|
||||||
val tasks = pullAllTasks()
|
val tasks = pullAllTasks()
|
||||||
val availableEvents = pullAllEvents()
|
val availableEvents = pullAllEvents().also {
|
||||||
|
referenceIds = it.keys.toList()
|
||||||
|
}
|
||||||
|
|
||||||
for ((referenceId, events) in availableEvents) {
|
for ((referenceId, events) in availableEvents) {
|
||||||
val startEvent = events.findFirstEventOf<MediaProcessStartEvent>() ?: continue
|
val startEvent = events.findFirstEventOf<MediaProcessStartEvent>() ?: continue
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package no.iktdev.mediaprocessing.ui.socket.a2a
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo
|
import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo
|
||||||
|
import no.iktdev.mediaprocessing.shared.common.task.Task
|
||||||
import no.iktdev.mediaprocessing.ui.UIEnv
|
import no.iktdev.mediaprocessing.ui.UIEnv
|
||||||
import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService
|
import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService
|
||||||
import no.iktdev.mediaprocessing.ui.log
|
import no.iktdev.mediaprocessing.ui.log
|
||||||
@ -35,6 +36,8 @@ class ProcesserListenerService(
|
|||||||
|
|
||||||
socketClient?.subscribe("/topic/encode/progress", encodeProcessMessage)
|
socketClient?.subscribe("/topic/encode/progress", encodeProcessMessage)
|
||||||
socketClient?.subscribe("/topic/extract/progress", extractProcessFrameHandler)
|
socketClient?.subscribe("/topic/extract/progress", extractProcessFrameHandler)
|
||||||
|
socketClient?.subscribe("/topic/encode/assigned", encodeTaskAssignedMessage)
|
||||||
|
socketClient?.subscribe("/topic/extract/assigned", extractTaskAssignedMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +48,30 @@ class ProcesserListenerService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val encodeTaskAssignedMessage = object: SocketMessageHandler() {
|
||||||
|
override fun onMessage(socketMessage: String) {
|
||||||
|
super.onMessage(socketMessage)
|
||||||
|
val response = gson.fromJson(socketMessage, Task::class.java)
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
run {
|
||||||
|
listener.onEncodeAssigned(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val extractTaskAssignedMessage = object: SocketMessageHandler() {
|
||||||
|
override fun onMessage(socketMessage: String) {
|
||||||
|
super.onMessage(socketMessage)
|
||||||
|
val response = gson.fromJson(socketMessage, Task::class.java)
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
run {
|
||||||
|
listener.onExtractAssigned(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val encodeProcessMessage = object : SocketMessageHandler() {
|
private val encodeProcessMessage = object : SocketMessageHandler() {
|
||||||
override fun onMessage(socketMessage: String) {
|
override fun onMessage(socketMessage: String) {
|
||||||
@ -76,8 +103,8 @@ class ProcesserListenerService(
|
|||||||
interface A2AProcesserListener {
|
interface A2AProcesserListener {
|
||||||
fun onExtractProgress(info: ProcesserEventInfo)
|
fun onExtractProgress(info: ProcesserEventInfo)
|
||||||
fun onEncodeProgress(info: ProcesserEventInfo)
|
fun onEncodeProgress(info: ProcesserEventInfo)
|
||||||
fun onEncodeAssigned()
|
fun onEncodeAssigned(task: Task)
|
||||||
fun onExtractAssigned()
|
fun onExtractAssigned(task: Task)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
export default function ProgressbarWithLabel({indeterminateText, progress}: {indeterminateText?: string, progress?: number | undefined | null}) {
|
||||||
|
const variant = (progress) ? "determinate" : "indeterminate"
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ width: '100%', mr: 1 }}>
|
||||||
|
<LinearProgress variant={variant} value={progress ?? 0} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ minWidth: 35 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: 'text.secondary' }}>
|
||||||
|
{progress ? (`${Math.round(progress)}%`) :
|
||||||
|
indeterminateText}
|
||||||
|
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*export default function LinearWithValueLabel() {
|
||||||
|
const [progress, setProgress] = React.useState(10);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setProgress((prevProgress) => (prevProgress >= 100 ? 10 : prevProgress + 10));
|
||||||
|
}, 800);
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<ProgressbarWithLabel value={progress} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}*/
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Box, TableContainer, Table, TableHead, TableRow, TableCell, Typography, TableBody, useTheme } from "@mui/material";
|
import { Box, TableContainer, Table, TableHead, TableRow, TableCell, Typography, TableBody, useTheme } from "@mui/material";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table";
|
import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table";
|
||||||
import IconArrowUp from '@mui/icons-material/ArrowUpward';
|
import IconArrowUp from '@mui/icons-material/ArrowUpward';
|
||||||
import IconArrowDown from '@mui/icons-material/ArrowDownward';
|
import IconArrowDown from '@mui/icons-material/ArrowDownward';
|
||||||
@ -14,13 +14,19 @@ export interface ExpandableTableItem {
|
|||||||
rowId: string
|
rowId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpandableTable<T extends ExpandableTableItem>({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent }: { items: Array<T>, columns: Array<TablePropetyConfig>, cellCustomizer?: TableCellCustomizer<T>, expandableRender: ExpandableRender<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
|
export interface SortByAccessor {
|
||||||
|
accessor: string
|
||||||
|
order: 'asc' | 'desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExpandableTable<T extends ExpandableTableItem>({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent, defaultSort}: { items: Array<T>, columns: Array<TablePropetyConfig>, cellCustomizer?: TableCellCustomizer<T>, expandableRender: ExpandableRender<T>, onRowClickedEvent?: TableRowActionEvents<T>, defaultSort?: SortByAccessor }) {
|
||||||
const muiTheme = useTheme();
|
const muiTheme = useTheme();
|
||||||
|
|
||||||
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
||||||
const [orderBy, setOrderBy] = useState<string>('');
|
const [orderBy, setOrderBy] = useState<string>('');
|
||||||
const [expandedRowIds, setExpandedRowIds] = useState<Set<string>>(new Set());
|
const [expandedRowIds, setExpandedRowIds] = useState<Set<string>>(new Set());
|
||||||
const [selectedRow, setSelectedRow] = useState<T | null>(null);
|
const [selectedRow, setSelectedRow] = useState<T | null>(null);
|
||||||
|
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||||
|
|
||||||
const tableRowSingleClicked = (row: T | null) => {
|
const tableRowSingleClicked = (row: T | null) => {
|
||||||
if (row != null && 'rowId' in row) {
|
if (row != null && 'rowId' in row) {
|
||||||
@ -37,8 +43,10 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
|||||||
|
|
||||||
if (row === selectedRow) {
|
if (row === selectedRow) {
|
||||||
setSelectedRow(null);
|
setSelectedRow(null);
|
||||||
|
setSelectedRowId(null);
|
||||||
} else {
|
} else {
|
||||||
setSelectedRow(row);
|
setSelectedRow(row);
|
||||||
|
setSelectedRowId(row?.rowId ?? null);
|
||||||
if (row && onRowClickedEvent) {
|
if (row && onRowClickedEvent) {
|
||||||
onRowClickedEvent.click(row);
|
onRowClickedEvent.click(row);
|
||||||
}
|
}
|
||||||
@ -74,18 +82,36 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedData = items.slice().sort((a, b) => {
|
|
||||||
if (order === 'asc') {
|
const sortedData = useMemo(() => {
|
||||||
return compareValues(a, b, orderBy);
|
return items.slice().sort((a, b) => {
|
||||||
} else {
|
if (order === 'asc') {
|
||||||
return compareValues(b, a, orderBy);
|
return compareValues(a, b, orderBy);
|
||||||
}
|
} else {
|
||||||
});
|
return compareValues(b, a, orderBy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [items, order, orderBy]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleSort(columns[0].accessor)
|
handleSort(columns[0].accessor)
|
||||||
}, [])
|
if (defaultSort) {
|
||||||
|
setOrder(defaultSort.order);
|
||||||
|
setOrderBy(defaultSort.accessor);
|
||||||
|
}
|
||||||
|
}, [defaultSort])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRowId) {
|
||||||
|
const matchingRow = items.find((item) => item.rowId === selectedRowId);
|
||||||
|
if (matchingRow) {
|
||||||
|
setSelectedRow(matchingRow);
|
||||||
|
} else {
|
||||||
|
setSelectedRow(null); // Hvis raden ikke finnes lenger, fjern valg
|
||||||
|
setSelectedRowId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [items, selectedRowId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@ -106,7 +132,7 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
|||||||
top: 0,
|
top: 0,
|
||||||
backgroundColor: muiTheme.palette.background.paper,
|
backgroundColor: muiTheme.palette.background.paper,
|
||||||
}}>
|
}}>
|
||||||
<TableRow>
|
<TableRow key={`orderRow-${Math.random()}`}>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
|
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
@ -124,37 +150,34 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
|||||||
<TableBody sx={{
|
<TableBody sx={{
|
||||||
overflowY: "scroll"
|
overflowY: "scroll"
|
||||||
}}>
|
}}>
|
||||||
{sortedData?.map((row: T, rowIndex: number) => (
|
{sortedData?.map((row: T, rowIndex: number) => [
|
||||||
<>
|
<TableRow key={row.rowId}
|
||||||
<TableRow key={rowIndex}
|
onClick={() => tableRowSingleClicked(row)}
|
||||||
onClick={() => tableRowSingleClicked(row)}
|
onDoubleClick={() => tableRowDoubleClicked(row)}
|
||||||
onDoubleClick={() => tableRowDoubleClicked(row)}
|
onContextMenu={(e) => {
|
||||||
onContextMenu={(e) => {
|
tableRowContextMenu(e, row);
|
||||||
tableRowContextMenu(e, row);
|
tableRowSingleClicked(row);
|
||||||
tableRowSingleClicked(row);
|
}}
|
||||||
}}
|
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
|
||||||
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
|
>
|
||||||
>
|
{columns.map((column) => (
|
||||||
{columns.map((column) => (
|
<TableCell key={column.accessor}>
|
||||||
<TableCell key={column.accessor}>
|
{customizer && customizer(column.accessor, row) !== null
|
||||||
{customizer && customizer(column.accessor, row) !== null
|
? customizer(column.accessor, row)
|
||||||
? customizer(column.accessor, row)
|
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
||||||
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
</TableCell>
|
||||||
</TableCell>
|
))}
|
||||||
))}
|
</TableRow>,
|
||||||
</TableRow>
|
(expandedRowIds.has(row.rowId)) ?
|
||||||
{(expandedRowIds.has(row.rowId)) ?
|
(<TableRow key={row.rowId + "-expanded"}>
|
||||||
(<TableRow key={rowIndex + "_1"}>
|
<TableCell colSpan={columns.length}>
|
||||||
<TableCell colSpan={columns.length}>
|
{
|
||||||
{
|
expandableRender(row)?.expandElement
|
||||||
expandableRender(row)?.expandElement
|
}
|
||||||
}
|
</TableCell>
|
||||||
</TableCell>
|
</TableRow>): null
|
||||||
</TableRow>): null
|
|
||||||
}
|
|
||||||
|
|
||||||
</>
|
])}
|
||||||
))}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
6
apps/ui/web/src/app/features/types.ts
Normal file
6
apps/ui/web/src/app/features/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface CoordinatorOperationRequest {
|
||||||
|
destination: string;
|
||||||
|
file: string;
|
||||||
|
source: string;
|
||||||
|
mode: "FLOW" | "MANUAL";
|
||||||
|
}
|
||||||
52
apps/ui/web/src/app/features/util.ts
Normal file
52
apps/ui/web/src/app/features/util.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import ReactDOMServer from "react-dom/server";
|
||||||
|
|
||||||
|
export const useCenteredTree = (defaultTranslate = { x: 0, y: 0 }) => {
|
||||||
|
const [translate, setTranslate] = useState<{ x: number; y: number }>(defaultTranslate);
|
||||||
|
const [dimensions, setDimensions] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const containerRef = useCallback((containerElem: HTMLDivElement | null) => {
|
||||||
|
if (containerElem !== null) {
|
||||||
|
const { width, height } = containerElem.getBoundingClientRect();
|
||||||
|
setDimensions({ width, height });
|
||||||
|
setTranslate({ x: width / 3, y: height / 2 });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [dimensions, translate, containerRef] as const; // Merk: `as const` sikrer faste typer
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const extractSvgContent = (icon: JSX.Element | null): JSX.Element[] => {
|
||||||
|
if (icon === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const svgMarkup = ReactDOMServer.renderToString(icon);
|
||||||
|
const parsedSvg = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml');
|
||||||
|
|
||||||
|
// Hent viewBox-attributtet fra SVG
|
||||||
|
const svgElement = parsedSvg.documentElement;
|
||||||
|
const viewBox = svgElement.getAttribute('viewBox');
|
||||||
|
|
||||||
|
if (!viewBox) {
|
||||||
|
throw new Error('SVG mangler viewBox-attributt');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ekstraher minX, minY, width, height fra viewBox
|
||||||
|
const [minX, minY, width, height] = viewBox.split(' ').map(Number);
|
||||||
|
|
||||||
|
// Beregn offset for senteret og sett det som negativt transform
|
||||||
|
const offsetX = -(minX + width / 2);
|
||||||
|
const offsetY = -(minY + height / 2);
|
||||||
|
|
||||||
|
return Array.from(parsedSvg.documentElement.children).map((child, index) => {
|
||||||
|
const props = {
|
||||||
|
key: index,
|
||||||
|
transform: `translate(${offsetX}, ${offsetY})`, // Juster transform med negativt offset
|
||||||
|
...Array.from(child.attributes).reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(child.tagName, props, null);
|
||||||
|
});
|
||||||
|
};
|
||||||
4
apps/ui/web/src/app/page/EventsPage.css
Normal file
4
apps/ui/web/src/app/page/EventsPage.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.thicc-link {
|
||||||
|
stroke-width: 3;
|
||||||
|
stroke: black!important;
|
||||||
|
}
|
||||||
288
apps/ui/web/src/app/page/EventsPage.tsx
Normal file
288
apps/ui/web/src/app/page/EventsPage.tsx
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import Tree, { CustomNodeElementProps } from "react-d3-tree";
|
||||||
|
import { useWsSubscription } from "../ws/subscriptions"
|
||||||
|
import NotStartedIcon from '@mui/icons-material/NotStarted';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import React, { ReactNode, useCallback, useEffect, useState } from "react";
|
||||||
|
import { extractSvgContent, useCenteredTree } from "../features/util";
|
||||||
|
import './EventsPage.css';
|
||||||
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight';
|
||||||
|
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||||
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||||
|
import SubtitlesIcon from '@mui/icons-material/Subtitles';
|
||||||
|
import MovieIcon from '@mui/icons-material/Movie';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||||
|
import { client } from "stompjs";
|
||||||
|
import { useStompClient } from "react-stomp-hooks";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { ContentEventState, ProcesserEventInfo, Status, update, updateEncodeProgress, WorkStatus } from "../store/work-slice";
|
||||||
|
import ExpandableTable, { ExpandableItem } from "../features/table/expandableTable";
|
||||||
|
import { RootState } from "../store";
|
||||||
|
import { TableCellCustomizer, TablePropetyConfig } from "../features/table/table";
|
||||||
|
import SimpleTable from "../features/table/sortableTable";
|
||||||
|
import { UnixTimestamp } from "../features/UxTc";
|
||||||
|
import ProgressbarWithLabel from "../features/components/ProgressbarWithLabel";
|
||||||
|
import { LinearProgress, Typography } from "@mui/material";
|
||||||
|
import { stat } from "fs";
|
||||||
|
|
||||||
|
interface RawNodeDatum {
|
||||||
|
name: string;
|
||||||
|
attributes?: Record<string, string | number | boolean>;
|
||||||
|
children?: RawNodeDatum[];
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventNodeData {
|
||||||
|
statusIcon: JSX.Element | null
|
||||||
|
statusColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventNodeStatus(status: Status): EventNodeData {
|
||||||
|
const toData = (status: Status): EventNodeData => {
|
||||||
|
switch (status) {
|
||||||
|
case Status.NeedsApproval: {
|
||||||
|
return {
|
||||||
|
statusIcon: <NotStartedIcon />,
|
||||||
|
statusColor: "crimson"
|
||||||
|
} as EventNodeData
|
||||||
|
}
|
||||||
|
case Status.Completed: {
|
||||||
|
return {
|
||||||
|
statusIcon: <CheckIcon />,
|
||||||
|
statusColor: "forestgreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Status.Skipped: {
|
||||||
|
return {
|
||||||
|
statusIcon: <KeyboardDoubleArrowRightIcon />,
|
||||||
|
statusColor: "#313131"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Status.Awaiting: {
|
||||||
|
return {
|
||||||
|
statusIcon: <MoreHorizIcon />,
|
||||||
|
statusColor: "#313131"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Status.Pending: {
|
||||||
|
return {
|
||||||
|
statusIcon: <HourglassEmptyIcon />,
|
||||||
|
statusColor: "#ffa000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Status.InProgress: {
|
||||||
|
return {
|
||||||
|
statusIcon: <HourglassEmptyIcon />,
|
||||||
|
statusColor: "dodgerblue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Status.Failed: {
|
||||||
|
return {
|
||||||
|
statusIcon: <ClearIcon />,
|
||||||
|
statusColor: "crimson"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toData(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderCustomNodeElement(nodeData: CustomNodeElementProps): JSX.Element {
|
||||||
|
const attr = nodeData.nodeDatum.attributes;
|
||||||
|
let workIcon: JSX.Element | null = null
|
||||||
|
switch (attr?.type) {
|
||||||
|
case "encode": {
|
||||||
|
workIcon = <MovieIcon />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "extract": {
|
||||||
|
workIcon = <SubtitlesIcon />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "convert": {
|
||||||
|
workIcon = <AutoAwesomeMotionIcon />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const status = getEventNodeStatus((attr?.status as Status) ?? Status.Awaiting)
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<g>
|
||||||
|
<circle r="20" strokeWidth={3} fill={status.statusColor} />
|
||||||
|
<g transform="scale(1.5)" stroke="none" fill="white">
|
||||||
|
{extractSvgContent(status.statusIcon)}
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0, 45) scale(1)" stroke="none" fill="white">
|
||||||
|
{extractSvgContent(workIcon)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskOperationStatusToStatus = (status: WorkStatus): Status | undefined => {
|
||||||
|
switch (status) {
|
||||||
|
case WorkStatus.Completed:
|
||||||
|
return Status.Completed;
|
||||||
|
case WorkStatus.Failed:
|
||||||
|
return Status.Failed;
|
||||||
|
case WorkStatus.Pending:
|
||||||
|
return Status.Pending;
|
||||||
|
case WorkStatus.Started:
|
||||||
|
case WorkStatus.Working:
|
||||||
|
return Status.InProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const transformToSteps = (state: ContentEventState): RawNodeDatum => ({
|
||||||
|
name: state.referenceId,
|
||||||
|
attributes: {
|
||||||
|
type: "encode",
|
||||||
|
status: taskOperationStatusToStatus(state?.encodeWork?.status) ?? state.encode
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: state.referenceId,
|
||||||
|
attributes: {
|
||||||
|
type: "extract",
|
||||||
|
status: state.extract
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: state.referenceId,
|
||||||
|
attributes: {
|
||||||
|
type: "convert",
|
||||||
|
status: state.extract
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default function EventsPage() {
|
||||||
|
const client = useStompClient();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const events = useSelector((state: RootState) => state.work);
|
||||||
|
|
||||||
|
|
||||||
|
useWsSubscription<Array<ContentEventState>>("/topic/tasks/all", (response) => {
|
||||||
|
console.log(response)
|
||||||
|
dispatch(update(response))
|
||||||
|
});
|
||||||
|
|
||||||
|
useWsSubscription<ProcesserEventInfo>("/topic/processer/encode/progress", (response) => {
|
||||||
|
console.log(response)
|
||||||
|
dispatch(updateEncodeProgress(response))
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client?.publish({
|
||||||
|
destination: "/app/tasks/all"
|
||||||
|
})
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const createCellTable: TableCellCustomizer<ContentEventState> = (accessor, data) => {
|
||||||
|
switch (accessor) {
|
||||||
|
case "runners": {
|
||||||
|
return (<>
|
||||||
|
<div id="treeWrapper" style={{
|
||||||
|
...linkThicc,
|
||||||
|
width: '150px', height: '90px'
|
||||||
|
}} ref={containerRef}>
|
||||||
|
<Tree
|
||||||
|
dimensions={dimensions}
|
||||||
|
translate={{
|
||||||
|
x: 24,
|
||||||
|
y: 24
|
||||||
|
}}
|
||||||
|
data={transformToSteps(data)}
|
||||||
|
orientation="horizontal"
|
||||||
|
separation={{
|
||||||
|
nonSiblings: 1,
|
||||||
|
siblings: 1,
|
||||||
|
}}
|
||||||
|
nodeSize={{
|
||||||
|
x: 50,
|
||||||
|
y: 50
|
||||||
|
}}
|
||||||
|
draggable={false}
|
||||||
|
zoomable={false}
|
||||||
|
renderCustomNodeElement={renderCustomNodeElement}
|
||||||
|
pathClassFunc={() => 'thicc-link'}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>)
|
||||||
|
};
|
||||||
|
case "created": {
|
||||||
|
if (typeof data[accessor] === "number") {
|
||||||
|
return UnixTimestamp({ timestamp: data[accessor] });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderExpandableItem(item: ContentEventState): ExpandableItem<ContentEventState> | null {
|
||||||
|
const progress = item.encodeWork?.progress?.progress ?? undefined;
|
||||||
|
const showProgressbar = [WorkStatus.Pending, WorkStatus.Started, WorkStatus.Working, WorkStatus.Completed].includes(item?.encodeWork?.status)
|
||||||
|
|
||||||
|
const processer = item.encodeWork // events.encodeWork[item.referenceId];
|
||||||
|
const showIndeterminate = processer?.status in [WorkStatus.Pending, WorkStatus.Started] || processer?.progress?.progress <= 0
|
||||||
|
console.log({
|
||||||
|
type: "info",
|
||||||
|
processer: processer,
|
||||||
|
showIndeterminate: showIndeterminate,
|
||||||
|
showProgressbar: showProgressbar,
|
||||||
|
isWorking: item.encodeWork?.status == WorkStatus.Working
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
tag: item.referenceId,
|
||||||
|
expandElement: (() => {
|
||||||
|
//const data = transformEventGroups(item);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>{item.encodeWork?.progress?.timeLeft}</Typography>
|
||||||
|
{(showProgressbar) ?
|
||||||
|
<ProgressbarWithLabel indeterminateText={"Waiting"} progress={progress} /> : null
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Array<TablePropetyConfig> = [
|
||||||
|
{ label: "Title", accessor: "title" },
|
||||||
|
{ label: "Started", accessor: "created" },
|
||||||
|
{ label: "", accessor: "runners" },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const [dimensions, translate, containerRef] = useCenteredTree();
|
||||||
|
|
||||||
|
const linkThicc = {
|
||||||
|
strokeWith: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExpandableTable items={events.items} columns={columns} cellCustomizer={createCellTable} expandableRender={renderExpandableItem} defaultSort={{
|
||||||
|
order: 'desc',
|
||||||
|
accessor: "created"
|
||||||
|
}} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -63,10 +63,16 @@ function getPartFor(path: string, index: number): string | null {
|
|||||||
let parts: string[];
|
let parts: string[];
|
||||||
if (isWindowsPath(path)) {
|
if (isWindowsPath(path)) {
|
||||||
parts = [path.slice(0, 3), ...path.slice(3).split(separator)];
|
parts = [path.slice(0, 3), ...path.slice(3).split(separator)];
|
||||||
} else if (path.startsWith(separator)) {
|
|
||||||
parts = [separator, ...path.slice(1).split(separator)];
|
|
||||||
} else {
|
} else {
|
||||||
parts = path.split(separator);
|
if (path.length == 1 && index == 0) {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.startsWith(separator)) {
|
||||||
|
parts = [separator, ...path.slice(1).split(separator)];
|
||||||
|
} else {
|
||||||
|
parts = path.split(separator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < parts.length) {
|
if (index < parts.length) {
|
||||||
@ -74,7 +80,10 @@ function getPartFor(path: string, index: number): string | null {
|
|||||||
if (isWindowsPath(path) && index === 0) {
|
if (isWindowsPath(path) && index === 0) {
|
||||||
return parts[0];
|
return parts[0];
|
||||||
}
|
}
|
||||||
return parts.slice(0, index + 1).join(separator);
|
|
||||||
|
|
||||||
|
const returningPath = parts.slice(0, index + 1).join(separator).replace(/\/\//g, "/");
|
||||||
|
return returningPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -115,7 +124,7 @@ function getSegments(absolutePath: string): Array<Segment> {
|
|||||||
|
|
||||||
|
|
||||||
function getSegmentedNaviagatablePath(rootClick: () => void, navigateTo: (path: string | null) => void, path: string | null): JSX.Element {
|
function getSegmentedNaviagatablePath(rootClick: () => void, navigateTo: (path: string | null) => void, path: string | null): JSX.Element {
|
||||||
const segments = getSegments("/src/input/completed")
|
const segments = getSegments(path!)
|
||||||
|
|
||||||
const utElements = segments.map((segment: Segment, index: number) => {
|
const utElements = segments.map((segment: Segment, index: number) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
86
apps/ui/web/src/app/store/work-slice.ts
Normal file
86
apps/ui/web/src/app/store/work-slice.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||||
|
import { ExpandableTableItem } from "../features/table/expandableTable"
|
||||||
|
|
||||||
|
export enum WorkStatus {
|
||||||
|
Pending = "Pending",
|
||||||
|
Started = "Started",
|
||||||
|
Working = "Working",
|
||||||
|
Completed = "Completed",
|
||||||
|
Failed = "Failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcesserProgress {
|
||||||
|
progress: number
|
||||||
|
speed: string
|
||||||
|
timeWorkedOn: string
|
||||||
|
timeLeft: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcesserEventInfo {
|
||||||
|
referenceId: string
|
||||||
|
eventId: string
|
||||||
|
status: WorkStatus
|
||||||
|
progress: ProcesserProgress
|
||||||
|
inputFile: string
|
||||||
|
outputFiles: Array<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
Skipped = 'Skipped',
|
||||||
|
Awaiting = 'Awaiting',
|
||||||
|
NeedsApproval = 'NeedsApproval',
|
||||||
|
Pending = 'Pending',
|
||||||
|
InProgress = 'InProgress',
|
||||||
|
Completed = 'Completed',
|
||||||
|
Failed = "Failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface ContentEventState extends ExpandableTableItem {
|
||||||
|
referenceId: string
|
||||||
|
title: string
|
||||||
|
encode: Status
|
||||||
|
extract: Status
|
||||||
|
convert: Status
|
||||||
|
created: number
|
||||||
|
encodeWork: ProcesserEventInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentEventStateItems {
|
||||||
|
items: Array<ContentEventState>,
|
||||||
|
encodeWork: { [key: string]: ProcesserEventInfo }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ContentEventStateItems = {
|
||||||
|
items: [],
|
||||||
|
encodeWork: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workSlice = createSlice({
|
||||||
|
name: "Work",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
update(state, action: PayloadAction<Array<ContentEventState>>) {
|
||||||
|
state.items = action.payload.map(item => ({
|
||||||
|
...item,
|
||||||
|
rowId: item.referenceId, // Setter rowId lik referenceId
|
||||||
|
encodeWork: state.encodeWork[item.referenceId] ?? undefined // Reapply encodeWork hvis det finnes
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
updateEncodeProgress(state, action: PayloadAction<ProcesserEventInfo>) {
|
||||||
|
state.encodeWork[action.payload.referenceId] = action.payload;
|
||||||
|
state.items = state.items.map(item =>
|
||||||
|
item.referenceId === action.payload.referenceId
|
||||||
|
? { ...item, encodeWork: action.payload }
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { update, updateEncodeProgress } = workSlice.actions;
|
||||||
|
export default workSlice.reducer;
|
||||||
Loading…
Reference in New Issue
Block a user