Progress
This commit is contained in:
parent
749194f9e1
commit
e02754f390
@ -93,10 +93,11 @@ class FfmpegProgressDecoder {
|
||||
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" }
|
||||
|
||||
val parsedDuration = Regex("[0-9]+:[0-9]+:[0-9]+.[0-9]+").find(results.toString())?.value ?: return
|
||||
|
||||
if (!parsedDurations.contains(parsedDuration)) {
|
||||
parsedDurations.add(parsedDuration)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package no.iktdev.mediaprocessing.ui.socket
|
||||
|
||||
import com.google.gson.Gson
|
||||
import no.iktdev.eventi.data.referenceId
|
||||
import no.iktdev.eventi.database.toEpochSeconds
|
||||
import no.iktdev.eventi.database.withDirtyRead
|
||||
@ -32,24 +33,35 @@ class ProcesserTasksTopic(
|
||||
@Autowired private val message: SimpMessagingTemplate?,
|
||||
): SocketListener(message) {
|
||||
|
||||
private var referenceIds: List<String> = emptyList()
|
||||
|
||||
final val a2a = object : ProcesserListenerService.A2AProcesserListener {
|
||||
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)
|
||||
pullAllTasks()
|
||||
}
|
||||
|
||||
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)
|
||||
pullAllTasks()
|
||||
}
|
||||
|
||||
override fun onEncodeAssigned() {
|
||||
pullAllTasks()
|
||||
override fun onEncodeAssigned(task: Task) {
|
||||
if (referenceIds.none { it == task.referenceId }) {
|
||||
updateTopicWithTasks()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExtractAssigned() {
|
||||
pullAllTasks()
|
||||
override fun onExtractAssigned(task: Task) {
|
||||
if (referenceIds.none { it == task.referenceId }) {
|
||||
updateTopicWithTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +90,7 @@ class ProcesserTasksTopic(
|
||||
|
||||
|
||||
@MessageMapping("/tasks/all")
|
||||
fun pullAllTaskss() {
|
||||
fun updateTopicWithTasks() {
|
||||
val states = update()
|
||||
template?.convertAndSend("/topic/tasks/all", states)
|
||||
}
|
||||
@ -107,7 +119,9 @@ class ProcesserTasksTopic(
|
||||
val eventStates: MutableList<ContentEventState> = mutableListOf()
|
||||
|
||||
val tasks = pullAllTasks()
|
||||
val availableEvents = pullAllEvents()
|
||||
val availableEvents = pullAllEvents().also {
|
||||
referenceIds = it.keys.toList()
|
||||
}
|
||||
|
||||
for ((referenceId, events) in availableEvents) {
|
||||
val startEvent = events.findFirstEventOf<MediaProcessStartEvent>() ?: continue
|
||||
|
||||
@ -3,6 +3,7 @@ package no.iktdev.mediaprocessing.ui.socket.a2a
|
||||
import com.google.gson.Gson
|
||||
import mu.KotlinLogging
|
||||
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.WebSocketMonitoringService
|
||||
import no.iktdev.mediaprocessing.ui.log
|
||||
@ -35,6 +36,8 @@ class ProcesserListenerService(
|
||||
|
||||
socketClient?.subscribe("/topic/encode/progress", encodeProcessMessage)
|
||||
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() {
|
||||
override fun onMessage(socketMessage: String) {
|
||||
@ -76,8 +103,8 @@ class ProcesserListenerService(
|
||||
interface A2AProcesserListener {
|
||||
fun onExtractProgress(info: ProcesserEventInfo)
|
||||
fun onEncodeProgress(info: ProcesserEventInfo)
|
||||
fun onEncodeAssigned()
|
||||
fun onExtractAssigned()
|
||||
fun onEncodeAssigned(task: Task)
|
||||
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 { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table";
|
||||
import IconArrowUp from '@mui/icons-material/ArrowUpward';
|
||||
import IconArrowDown from '@mui/icons-material/ArrowDownward';
|
||||
@ -14,13 +14,19 @@ export interface ExpandableTableItem {
|
||||
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 [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
||||
const [orderBy, setOrderBy] = useState<string>('');
|
||||
const [expandedRowIds, setExpandedRowIds] = useState<Set<string>>(new Set());
|
||||
const [selectedRow, setSelectedRow] = useState<T | null>(null);
|
||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||
|
||||
const tableRowSingleClicked = (row: T | null) => {
|
||||
if (row != null && 'rowId' in row) {
|
||||
@ -37,8 +43,10 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
||||
|
||||
if (row === selectedRow) {
|
||||
setSelectedRow(null);
|
||||
setSelectedRowId(null);
|
||||
} else {
|
||||
setSelectedRow(row);
|
||||
setSelectedRowId(row?.rowId ?? null);
|
||||
if (row && onRowClickedEvent) {
|
||||
onRowClickedEvent.click(row);
|
||||
}
|
||||
@ -74,18 +82,36 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortedData = items.slice().sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return compareValues(a, b, orderBy);
|
||||
} else {
|
||||
return compareValues(b, a, orderBy);
|
||||
}
|
||||
});
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
return items.slice().sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return compareValues(a, b, orderBy);
|
||||
} else {
|
||||
return compareValues(b, a, orderBy);
|
||||
}
|
||||
});
|
||||
}, [items, order, orderBy]);
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<Box sx={{
|
||||
@ -106,7 +132,7 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
||||
top: 0,
|
||||
backgroundColor: muiTheme.palette.background.paper,
|
||||
}}>
|
||||
<TableRow>
|
||||
<TableRow key={`orderRow-${Math.random()}`}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
|
||||
<Box display="flex">
|
||||
@ -124,37 +150,34 @@ export default function ExpandableTable<T extends ExpandableTableItem>({ items,
|
||||
<TableBody sx={{
|
||||
overflowY: "scroll"
|
||||
}}>
|
||||
{sortedData?.map((row: T, rowIndex: number) => (
|
||||
<>
|
||||
<TableRow key={rowIndex}
|
||||
onClick={() => tableRowSingleClicked(row)}
|
||||
onDoubleClick={() => tableRowDoubleClicked(row)}
|
||||
onContextMenu={(e) => {
|
||||
tableRowContextMenu(e, row);
|
||||
tableRowSingleClicked(row);
|
||||
}}
|
||||
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.accessor}>
|
||||
{customizer && customizer(column.accessor, row) !== null
|
||||
? customizer(column.accessor, row)
|
||||
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
{(expandedRowIds.has(row.rowId)) ?
|
||||
(<TableRow key={rowIndex + "_1"}>
|
||||
<TableCell colSpan={columns.length}>
|
||||
{
|
||||
expandableRender(row)?.expandElement
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>): null
|
||||
}
|
||||
|
||||
</>
|
||||
))}
|
||||
{sortedData?.map((row: T, rowIndex: number) => [
|
||||
<TableRow key={row.rowId}
|
||||
onClick={() => tableRowSingleClicked(row)}
|
||||
onDoubleClick={() => tableRowDoubleClicked(row)}
|
||||
onContextMenu={(e) => {
|
||||
tableRowContextMenu(e, row);
|
||||
tableRowSingleClicked(row);
|
||||
}}
|
||||
style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.accessor}>
|
||||
{customizer && customizer(column.accessor, row) !== null
|
||||
? customizer(column.accessor, row)
|
||||
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>,
|
||||
(expandedRowIds.has(row.rowId)) ?
|
||||
(<TableRow key={row.rowId + "-expanded"}>
|
||||
<TableCell colSpan={columns.length}>
|
||||
{
|
||||
expandableRender(row)?.expandElement
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>): null
|
||||
|
||||
])}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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[];
|
||||
if (isWindowsPath(path)) {
|
||||
parts = [path.slice(0, 3), ...path.slice(3).split(separator)];
|
||||
} else if (path.startsWith(separator)) {
|
||||
parts = [separator, ...path.slice(1).split(separator)];
|
||||
} 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) {
|
||||
@ -74,7 +80,10 @@ function getPartFor(path: string, index: number): string | null {
|
||||
if (isWindowsPath(path) && index === 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;
|
||||
@ -115,7 +124,7 @@ function getSegments(absolutePath: string): Array<Segment> {
|
||||
|
||||
|
||||
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) => {
|
||||
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