From e02754f3902439ae48bde4ceb48200b400acc991 Mon Sep 17 00:00:00 2001 From: bskjon Date: Wed, 26 Mar 2025 23:17:35 +0100 Subject: [PATCH] Progress --- .../ffmpeg/progress/FfmpegProgressDecoder.kt | 3 +- .../ui/socket/ProcesserTasksTopic.kt | 30 +- .../ui/socket/a2a/ProcesserListenerService.kt | 31 +- .../components/ProgressbarWithLabel.tsx | 43 +++ .../app/features/table/expandableTable.tsx | 107 ++++--- apps/ui/web/src/app/features/types.ts | 6 + apps/ui/web/src/app/features/util.ts | 52 ++++ apps/ui/web/src/app/page/EventsPage.css | 4 + apps/ui/web/src/app/page/EventsPage.tsx | 288 ++++++++++++++++++ apps/ui/web/src/app/page/ExplorePage.tsx | 19 +- apps/ui/web/src/app/store/work-slice.ts | 86 ++++++ 11 files changed, 611 insertions(+), 58 deletions(-) create mode 100644 apps/ui/web/src/app/features/components/ProgressbarWithLabel.tsx create mode 100644 apps/ui/web/src/app/features/types.ts create mode 100644 apps/ui/web/src/app/features/util.ts create mode 100644 apps/ui/web/src/app/page/EventsPage.css create mode 100644 apps/ui/web/src/app/page/EventsPage.tsx create mode 100644 apps/ui/web/src/app/store/work-slice.ts diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoder.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoder.kt index 23a5b46f..7a17e439 100644 --- a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoder.kt +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoder.kt @@ -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) } diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt index c875673c..1e295de3 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt @@ -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 = 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 = mutableListOf() val tasks = pullAllTasks() - val availableEvents = pullAllEvents() + val availableEvents = pullAllEvents().also { + referenceIds = it.keys.toList() + } for ((referenceId, events) in availableEvents) { val startEvent = events.findFirstEventOf() ?: continue diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt index 30691703..77bbb4c5 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt @@ -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) } } \ No newline at end of file diff --git a/apps/ui/web/src/app/features/components/ProgressbarWithLabel.tsx b/apps/ui/web/src/app/features/components/ProgressbarWithLabel.tsx new file mode 100644 index 00000000..9e1e002f --- /dev/null +++ b/apps/ui/web/src/app/features/components/ProgressbarWithLabel.tsx @@ -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 ( + + + + + + + {progress ? (`${Math.round(progress)}%`) : + indeterminateText} + + + + + ); +} + +/*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 ( + + + + ); +}*/ \ No newline at end of file diff --git a/apps/ui/web/src/app/features/table/expandableTable.tsx b/apps/ui/web/src/app/features/table/expandableTable.tsx index 7e2b1357..6d0bbf91 100644 --- a/apps/ui/web/src/app/features/table/expandableTable.tsx +++ b/apps/ui/web/src/app/features/table/expandableTable.tsx @@ -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({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent }: { items: Array, columns: Array, cellCustomizer?: TableCellCustomizer, expandableRender: ExpandableRender, onRowClickedEvent?: TableRowActionEvents }) { +export interface SortByAccessor { + accessor: string + order: 'asc' | 'desc' +} + +export default function ExpandableTable({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent, defaultSort}: { items: Array, columns: Array, cellCustomizer?: TableCellCustomizer, expandableRender: ExpandableRender, onRowClickedEvent?: TableRowActionEvents, defaultSort?: SortByAccessor }) { const muiTheme = useTheme(); const [order, setOrder] = useState<'asc' | 'desc'>('asc'); const [orderBy, setOrderBy] = useState(''); const [expandedRowIds, setExpandedRowIds] = useState>(new Set()); const [selectedRow, setSelectedRow] = useState(null); + const [selectedRowId, setSelectedRowId] = useState(null); const tableRowSingleClicked = (row: T | null) => { if (row != null && 'rowId' in row) { @@ -37,8 +43,10 @@ export default function ExpandableTable({ 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({ 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 ( ({ items, top: 0, backgroundColor: muiTheme.palette.background.paper, }}> - + {columns.map((column) => ( handleSort(column.accessor)} sx={{ cursor: "pointer" }}> @@ -124,37 +150,34 @@ export default function ExpandableTable({ items, - {sortedData?.map((row: T, rowIndex: number) => ( - <> - 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) => ( - - {customizer && customizer(column.accessor, row) !== null - ? customizer(column.accessor, row) - : {(row as any)[column.accessor]}} - - ))} - - {(expandedRowIds.has(row.rowId)) ? - ( - - { - expandableRender(row)?.expandElement - } - - ): null - } - - - ))} + {sortedData?.map((row: T, rowIndex: number) => [ + 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) => ( + + {customizer && customizer(column.accessor, row) !== null + ? customizer(column.accessor, row) + : {(row as any)[column.accessor]}} + + ))} + , + (expandedRowIds.has(row.rowId)) ? + ( + + { + expandableRender(row)?.expandElement + } + + ): null + + ])} diff --git a/apps/ui/web/src/app/features/types.ts b/apps/ui/web/src/app/features/types.ts new file mode 100644 index 00000000..b2a68fbd --- /dev/null +++ b/apps/ui/web/src/app/features/types.ts @@ -0,0 +1,6 @@ +export interface CoordinatorOperationRequest { + destination: string; + file: string; + source: string; + mode: "FLOW" | "MANUAL"; + } \ No newline at end of file diff --git a/apps/ui/web/src/app/features/util.ts b/apps/ui/web/src/app/features/util.ts new file mode 100644 index 00000000..cd9684ed --- /dev/null +++ b/apps/ui/web/src/app/features/util.ts @@ -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); + }); +}; \ No newline at end of file diff --git a/apps/ui/web/src/app/page/EventsPage.css b/apps/ui/web/src/app/page/EventsPage.css new file mode 100644 index 00000000..f22b0ae2 --- /dev/null +++ b/apps/ui/web/src/app/page/EventsPage.css @@ -0,0 +1,4 @@ +.thicc-link { + stroke-width: 3; + stroke: black!important; +} \ No newline at end of file diff --git a/apps/ui/web/src/app/page/EventsPage.tsx b/apps/ui/web/src/app/page/EventsPage.tsx new file mode 100644 index 00000000..bd61205a --- /dev/null +++ b/apps/ui/web/src/app/page/EventsPage.tsx @@ -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; + 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: , + statusColor: "crimson" + } as EventNodeData + } + case Status.Completed: { + return { + statusIcon: , + statusColor: "forestgreen" + } + } + case Status.Skipped: { + return { + statusIcon: , + statusColor: "#313131" + } + } + case Status.Awaiting: { + return { + statusIcon: , + statusColor: "#313131" + } + } + case Status.Pending: { + return { + statusIcon: , + statusColor: "#ffa000" + } + } + case Status.InProgress: { + return { + statusIcon: , + statusColor: "dodgerblue" + } + } + case Status.Failed: { + return { + statusIcon: , + 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 = + break; + } + case "extract": { + workIcon = + break; + } + case "convert": { + workIcon = + break; + } + } + + + + const status = getEventNodeStatus((attr?.status as Status) ?? Status.Awaiting) + + return (<> + + + + {extractSvgContent(status.statusIcon)} + + + {extractSvgContent(workIcon)} + + + + + ); +} + +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>("/topic/tasks/all", (response) => { + console.log(response) + dispatch(update(response)) + }); + + useWsSubscription("/topic/processer/encode/progress", (response) => { + console.log(response) + dispatch(updateEncodeProgress(response)) + }) + + useEffect(() => { + client?.publish({ + destination: "/app/tasks/all" + }) + }, [client]); + + const createCellTable: TableCellCustomizer = (accessor, data) => { + switch (accessor) { + case "runners": { + return (<> +
+ 'thicc-link'} + + /> +
+ ) + }; + case "created": { + if (typeof data[accessor] === "number") { + return UnixTimestamp({ timestamp: data[accessor] }); + } + return null; + } + default: return null; + } + }; + + function renderExpandableItem(item: ContentEventState): ExpandableItem | 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 ( + <> + {item.encodeWork?.progress?.timeLeft} + {(showProgressbar) ? + : null + } + + ); + })() + }; + } + + const columns: Array = [ + { label: "Title", accessor: "title" }, + { label: "Started", accessor: "created" }, + { label: "", accessor: "runners" }, + ]; + + + const [dimensions, translate, containerRef] = useCenteredTree(); + + const linkThicc = { + strokeWith: 5 + } + + return ( + <> + + + ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/page/ExplorePage.tsx b/apps/ui/web/src/app/page/ExplorePage.tsx index a34c76d2..4451d226 100644 --- a/apps/ui/web/src/app/page/ExplorePage.tsx +++ b/apps/ui/web/src/app/page/ExplorePage.tsx @@ -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 { 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 ( diff --git a/apps/ui/web/src/app/store/work-slice.ts b/apps/ui/web/src/app/store/work-slice.ts new file mode 100644 index 00000000..4064b564 --- /dev/null +++ b/apps/ui/web/src/app/store/work-slice.ts @@ -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 +} + + +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, + encodeWork: { [key: string]: ProcesserEventInfo } + +} + +const initialState: ContentEventStateItems = { + items: [], + encodeWork: {} +} + +const workSlice = createSlice({ + name: "Work", + initialState, + reducers: { + update(state, action: PayloadAction>) { + 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) { + 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; \ No newline at end of file