minor adjustments

This commit is contained in:
bskjon 2025-03-22 15:38:34 +01:00
parent e663c743ab
commit 749194f9e1
14 changed files with 295 additions and 110 deletions

View File

@ -1,10 +1,20 @@
package no.iktdev.mediaprocessing.ui.socket package no.iktdev.mediaprocessing.ui.socket
import no.iktdev.eventi.data.referenceId
import no.iktdev.eventi.database.toEpochSeconds
import no.iktdev.eventi.database.withDirtyRead
import no.iktdev.eventi.database.withTransaction import no.iktdev.eventi.database.withTransaction
import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.ProcessType
import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.shared.common.contract.dto.OperationEvents
import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo
import no.iktdev.mediaprocessing.shared.common.database.cal.toEvent
import no.iktdev.mediaprocessing.shared.common.database.cal.toTask import no.iktdev.mediaprocessing.shared.common.database.cal.toTask
import no.iktdev.mediaprocessing.shared.common.database.tables.events
import no.iktdev.mediaprocessing.shared.common.database.tables.tasks import no.iktdev.mediaprocessing.shared.common.database.tables.tasks
import no.iktdev.mediaprocessing.shared.common.task.Task import no.iktdev.mediaprocessing.shared.common.task.Task
import no.iktdev.mediaprocessing.shared.common.task.TaskType
import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService
import no.iktdev.mediaprocessing.ui.eventDatabase import no.iktdev.mediaprocessing.ui.eventDatabase
import no.iktdev.mediaprocessing.ui.socket.a2a.ProcesserListenerService import no.iktdev.mediaprocessing.ui.socket.a2a.ProcesserListenerService
@ -13,25 +23,33 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File
@Service @Service
class ProcesserTasksTopic( class ProcesserTasksTopic(
@Autowired a2AProcesserService: ProcesserListenerService, @Autowired a2AProcesserService: ProcesserListenerService,
@Autowired private val webSocketMonitoringService: WebSocketMonitoringService, @Autowired private val webSocketMonitoringService: WebSocketMonitoringService,
@Autowired override var template: SimpMessagingTemplate?, @Autowired private val message: SimpMessagingTemplate?,
): SocketListener(template) { ): SocketListener(message) {
final val a2a = object : ProcesserListenerService.A2AProcesserListener { final val a2a = object : ProcesserListenerService.A2AProcesserListener {
override fun onExtractProgress(info: ProcesserEventInfo) { override fun onExtractProgress(info: ProcesserEventInfo) {
message?.convertAndSend("/topic/processer/extract/progress", info)
pullAllTasks()
} }
override fun onEncodeProgress(info: ProcesserEventInfo) { override fun onEncodeProgress(info: ProcesserEventInfo) {
message?.convertAndSend("/topic/processer/encode/progress", info)
pullAllTasks()
} }
override fun onEncodeAssigned() { override fun onEncodeAssigned() {
pullAllTasks()
} }
override fun onExtractAssigned() { override fun onExtractAssigned() {
pullAllTasks()
} }
} }
@ -39,18 +57,96 @@ class ProcesserTasksTopic(
a2AProcesserService.attachListener(a2a) a2AProcesserService.attachListener(a2a)
} }
data class TaskGroup( enum class Status {
Skipped,
Awaiting, // Waiting for tasks to be created
NeedsApproval,
Pending,
InProgress,
Completed,
Failed,
}
data class ContentEventState(
val referenceId: String, val referenceId: String,
val tasks: List<Task> val title: String,
) val encode: Status = Status.Skipped,
val extract: Status = Status.Skipped,
val convert: Status = Status.Skipped,
val created: Long
) {}
@MessageMapping("/tasks/all") @MessageMapping("/tasks/all")
fun pullAllTasks() { fun pullAllTaskss() {
val states = update()
template?.convertAndSend("/topic/tasks/all", states)
}
fun getOperationState(tasks: List<Task>, hasOperation: Boolean, canStart: Boolean): Status {
if (!hasOperation) return Status.Skipped
if (tasks.isEmpty()) return Status.Awaiting
if (!canStart) return Status.NeedsApproval
if (tasks.any { it.consumed }) {
return Status.Completed
}
if (tasks.any { it.claimed }) {
return Status.InProgress
}
if (tasks.any{ it.status == "ERROR"}) {
return Status.Failed
}
return Status.Pending
}
fun update(): MutableList<ContentEventState> {
val eventStates: MutableList<ContentEventState> = mutableListOf()
val tasks = pullAllTasks()
val availableEvents = pullAllEvents()
for ((referenceId, events) in availableEvents) {
val startEvent = events.findFirstEventOf<MediaProcessStartEvent>() ?: continue
val startData = startEvent.data ?: continue
val title = events.findFirstEventOf<BaseInfoEvent>()?.data?.sanitizedName ?: startData.file.let { File(it).nameWithoutExtension }
val canStart = if (startData.type == ProcessType.FLOW) true else {
events.findEventsOf<PermitWorkCreationEvent>().isNotEmpty()
}
val tasksCreated = tasks[referenceId]
val encode = tasksCreated?.filter { it.task == TaskType.Encode } ?: emptyList()
val extract = tasksCreated?.filter { it.task == TaskType.Extract } ?: emptyList()
val convert = tasksCreated?.filter { it.task == TaskType.Convert } ?: emptyList()
eventStates.add(ContentEventState(
title = title,
referenceId = referenceId,
encode = getOperationState(encode, startData.operations.contains(OperationEvents.ENCODE), canStart),
extract = getOperationState(extract, startData.operations.contains(OperationEvents.EXTRACT), canStart),
convert = getOperationState(convert, startData.operations.contains(OperationEvents.CONVERT), canStart),
created = startEvent.metadata.created.toEpochSeconds() * 1000L
))
}
return eventStates
}
fun pullAllTasks(): Map<String, List<Task>> {
val result = withTransaction(eventDatabase.database) { val result = withTransaction(eventDatabase.database) {
tasks.selectAll().toTask() tasks.selectAll().toTask()
.groupBy { it.referenceId }.map { g -> TaskGroup(g.key, g.value) } .groupBy { it.referenceId }
} ?: emptyList() } ?: emptyMap()
template?.convertAndSend("/topic/tasks/all", result) return result
}
fun pullAllEvents(): Map<String, List<Event>> {
val result = withDirtyRead(eventDatabase.database) {
events.selectAll().toEvent()
.groupBy { it.referenceId() }
} ?: emptyMap()
return result
} }
} }

View File

@ -68,7 +68,7 @@ class UnprocessedFilesTopic(
FileInfo( FileInfo(
it[files.baseName], it[files.baseName],
it[files.fileName], it[files.fileName],
it[files.checksum] it[files.checksum],
) )
} }
unprocessedFiles = found unprocessedFiles = found

View File

@ -15,7 +15,6 @@ import org.springframework.stereotype.Service
@Service @Service
class ProcesserListenerService( class ProcesserListenerService(
@Autowired private val webSocketMonitoringService: WebSocketMonitoringService, @Autowired private val webSocketMonitoringService: WebSocketMonitoringService,
@Autowired private val message: SimpMessagingTemplate?,
) { ) {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val listeners: MutableList<A2AProcesserListener> = mutableListOf() private val listeners: MutableList<A2AProcesserListener> = mutableListOf()
@ -50,9 +49,11 @@ class ProcesserListenerService(
private val encodeProcessMessage = object : SocketMessageHandler() { private val encodeProcessMessage = object : SocketMessageHandler() {
override fun onMessage(socketMessage: String) { override fun onMessage(socketMessage: String) {
super.onMessage(socketMessage) super.onMessage(socketMessage)
message?.convertAndSend("/topic/processer/encode/progress", socketMessage)
val response = gson.fromJson(socketMessage, ProcesserEventInfo::class.java) val response = gson.fromJson(socketMessage, ProcesserEventInfo::class.java)
if (webSocketMonitoringService.anyListening()) { listeners.forEach { listener ->
run {
listener.onEncodeProgress(response)
}
} }
} }
} }
@ -61,11 +62,14 @@ class ProcesserListenerService(
private val extractProcessFrameHandler = object : SocketMessageHandler() { private val extractProcessFrameHandler = object : SocketMessageHandler() {
override fun onMessage(socketMessage: String) { override fun onMessage(socketMessage: String) {
super.onMessage(socketMessage) super.onMessage(socketMessage)
message?.convertAndSend("/topic/processer/extract/progress", socketMessage)
if (webSocketMonitoringService.anyListening()) { if (webSocketMonitoringService.anyListening()) {
} }
//val stringPayload = (if (payload is ByteArray) String(payload) else payload as String) val response = gson.fromJson(socketMessage, ProcesserEventInfo::class.java)
//val response = gson.fromJson(stringPayload, ProcesserEventInfo::class.java) listeners.forEach { listener ->
run {
listener.onEncodeProgress(response)
}
}
} }
} }

View File

@ -574,13 +574,13 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.22.6", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
"integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.22.5", "@babel/template": "^7.26.9",
"@babel/traverse": "^7.22.6", "@babel/types": "^7.26.10"
"@babel/types": "^7.22.5"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -2085,9 +2085,10 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.25.0", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -2133,9 +2134,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.9", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
@ -18009,11 +18010,12 @@
} }
}, },
"node_modules/use-sync-external-store": { "node_modules/use-sync-external-store": {
"version": "1.2.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/utf-8-validate": { "node_modules/utf-8-validate": {
@ -19510,13 +19512,12 @@
} }
}, },
"@babel/helpers": { "@babel/helpers": {
"version": "7.22.6", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
"integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"requires": { "requires": {
"@babel/template": "^7.22.5", "@babel/template": "^7.26.9",
"@babel/traverse": "^7.22.6", "@babel/types": "^7.26.10"
"@babel/types": "^7.22.5"
} }
}, },
"@babel/parser": { "@babel/parser": {
@ -20481,9 +20482,9 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.25.0", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"requires": { "requires": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -20520,9 +20521,9 @@
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.26.9", "version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"requires": { "requires": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9" "@babel/helper-validator-identifier": "^7.25.9"
@ -31684,9 +31685,9 @@
} }
}, },
"use-sync-external-store": { "use-sync-external-store": {
"version": "1.2.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"requires": {} "requires": {}
}, },
"utf-8-validate": { "utf-8-validate": {

View File

@ -12,7 +12,6 @@ import { updateItems } from './app/store/composed-slice';
import ExplorePage from './app/page/ExplorePage'; import ExplorePage from './app/page/ExplorePage';
import { ThemeProvider } from '@mui/material'; import { ThemeProvider } from '@mui/material';
import theme from './theme'; import theme from './theme';
import { simpleEventsUpdate } from './app/store/kafka-items-flat-slice';
import { EventDataObject, SimpleEventDataObject } from './types'; import { EventDataObject, SimpleEventDataObject } from './types';
import EventsChainPage from './app/page/EventsChainPage'; import EventsChainPage from './app/page/EventsChainPage';
import UnprocessedFilesPage from './app/page/UnprocessedFilesPage'; import UnprocessedFilesPage from './app/page/UnprocessedFilesPage';
@ -22,6 +21,14 @@ import QueueIcon from '@mui/icons-material/Queue';
import AppsIcon from '@mui/icons-material/Apps'; import AppsIcon from '@mui/icons-material/Apps';
import ConstructionIcon from '@mui/icons-material/Construction'; import ConstructionIcon from '@mui/icons-material/Construction';
import ProcesserTasksPage from './app/page/ProcesserTasksPage'; import ProcesserTasksPage from './app/page/ProcesserTasksPage';
import DashboardIcon from '@mui/icons-material/Dashboard';
import GraphicEqIcon from '@mui/icons-material/GraphicEq';
import HomeRepairServiceIcon from '@mui/icons-material/HomeRepairService';
import InboxIcon from '@mui/icons-material/Inbox';
import InputIcon from '@mui/icons-material/Input';
import NotStartedIcon from '@mui/icons-material/NotStarted';
import EventsPage from './app/page/EventsPage';
import TableChartIcon from '@mui/icons-material/TableChart';
function App() { function App() {
const client = useStompClient(); const client = useStompClient();
@ -31,9 +38,6 @@ function App() {
dispatch(updateItems(response)) dispatch(updateItems(response))
}); });
useWsSubscription<Array<SimpleEventDataObject>>("/topic/event/flat", (response) => {
dispatch(simpleEventsUpdate(response))
});
useEffect(() => { useEffect(() => {
@ -71,7 +75,12 @@ function App() {
}}> }}>
<AppsIcon /> <AppsIcon />
</IconButton> </IconButton>
<IconButton onClick={() => window.location.href = "/events"} sx={{ <IconButton onClick={() => window.location.href = "/processer"} sx={{
...iconHeight
}}>
<GraphicEqIcon />
</IconButton>
<IconButton onClick={() => window.location.href = "/eventsflow"} sx={{
...iconHeight ...iconHeight
}}> }}>
<AccountTreeIcon /> <AccountTreeIcon />
@ -89,7 +98,7 @@ function App() {
<IconButton onClick={() => window.location.href = "/tasks"} sx={{ <IconButton onClick={() => window.location.href = "/tasks"} sx={{
...iconHeight ...iconHeight
}}> }}>
<ConstructionIcon /> <TableChartIcon />
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ <Box sx={{
@ -102,9 +111,10 @@ function App() {
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path='/tasks' element={<ProcesserTasksPage />} /> <Route path='/tasks' element={<ProcesserTasksPage />} />
<Route path='/processer' element={<EventsPage />} />
<Route path='/unprocessed' element={<UnprocessedFilesPage />} /> <Route path='/unprocessed' element={<UnprocessedFilesPage />} />
<Route path='/files' element={<ExplorePage />} /> <Route path='/files' element={<ExplorePage />} />
<Route path='/events' element={<EventsChainPage />} /> <Route path='/eventsflow' element={<EventsChainPage />} />
<Route path='/' element={<LaunchPage />} /> <Route path='/' element={<LaunchPage />} />
</Routes> </Routes>
<Footer /> <Footer />

View File

@ -10,16 +10,31 @@ export interface ExpandableItem<T> {
} }
export type ExpandableRender<T> = (item: T) => ExpandableItem<T> | null; export type ExpandableRender<T> = (item: T) => ExpandableItem<T> | null;
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 default function ExpandableTable<T>({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent }: { items: Array<T>, columns: Array<TablePropetyConfig>, cellCustomizer?: TableCellCustomizer<T>, expandableRender: ExpandableRender<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
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 [selectedRow, setSelectedRow] = useState<T | null>(null); const [selectedRow, setSelectedRow] = useState<T | null>(null);
const tableRowSingleClicked = (row: T | null) => { const tableRowSingleClicked = (row: T | null) => {
if (row != null && 'rowId' in row) {
setExpandedRowIds(prev => {
const newExpandedRows = new Set(prev);
if (newExpandedRows.has(row.rowId)) {
newExpandedRows.delete(row.rowId);
} else {
newExpandedRows.add(row.rowId);
}
return newExpandedRows;
})
}
if (row === selectedRow) { if (row === selectedRow) {
setSelectedRow(null); setSelectedRow(null);
} else { } else {
@ -28,6 +43,7 @@ export default function ExpandableTable<T>({ items, columns, cellCustomizer: cus
onRowClickedEvent.click(row); onRowClickedEvent.click(row);
} }
} }
} }
const tableRowDoubleClicked = (row: T | null) => { const tableRowDoubleClicked = (row: T | null) => {
setSelectedRow(row); setSelectedRow(row);
@ -127,7 +143,7 @@ export default function ExpandableTable<T>({ items, columns, cellCustomizer: cus
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
{(selectedRow == row) ? {(expandedRowIds.has(row.rowId)) ?
(<TableRow key={rowIndex + "_1"}> (<TableRow key={rowIndex + "_1"}>
<TableCell colSpan={columns.length}> <TableCell colSpan={columns.length}>
{ {

View File

@ -59,12 +59,10 @@ export default function EventsChainPage() {
useEffect(() => { useEffect(() => {
if (Object.keys(cursor).length === 0) { client?.publish({
client?.publish({ destination: "/app/chained/all"
destination: "/app/chained/all" });
}); }, [client, dispatch]);
}
}, [cursor, client, dispatch]);
const onRefresh = () => { const onRefresh = () => {
client?.publish({ client?.publish({

View File

@ -16,6 +16,7 @@ import ContextMenu, { ContextMenuActionEvent, ContextMenuItem } from '../feature
import { canConvert, canEncode, canExtract } from '../../fileUtil'; import { canConvert, canEncode, canExtract } from '../../fileUtil';
import SimpleTable from '../features/table/sortableTable'; import SimpleTable from '../features/table/sortableTable';
import TagIcon from '@mui/icons-material/Tag'; import TagIcon from '@mui/icons-material/Tag';
import { CoordinatorOperationRequest } from '../features/types';
const createTableCell: TableCellCustomizer<ExplorerItem> = (accessor, data) => { const createTableCell: TableCellCustomizer<ExplorerItem> = (accessor, data) => {
@ -195,13 +196,6 @@ function getContextMenuFileActionMenuItems(row: ExplorerItem | null): ContextMen
return items; return items;
} }
interface ExplorerOperationRequest {
destination: string;
file: string;
source: string;
mode: "FLOW" | "MANUAL";
}
export default function ExplorePage() { export default function ExplorePage() {
const muiTheme = useTheme(); const muiTheme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -254,7 +248,7 @@ export default function ExplorePage() {
file: value.path, file: value.path,
source: `Web UI @ ${window.location.href}`, source: `Web UI @ ${window.location.href}`,
mode: "FLOW" mode: "FLOW"
} as ExplorerOperationRequest } as CoordinatorOperationRequest
} }
case 1: { case 1: {
return { return {
@ -262,7 +256,7 @@ export default function ExplorePage() {
file: value.path, file: value.path,
source: `Web UI @ ${window.location.href}`, source: `Web UI @ ${window.location.href}`,
mode: "FLOW" mode: "FLOW"
} as ExplorerOperationRequest } as CoordinatorOperationRequest
} }
case 2: { case 2: {
return { return {
@ -270,7 +264,7 @@ export default function ExplorePage() {
file: value.path, file: value.path,
source: `Web UI @ ${window.location.href}`, source: `Web UI @ ${window.location.href}`,
mode: "FLOW" mode: "FLOW"
} as ExplorerOperationRequest } as CoordinatorOperationRequest
} }
case 3: { case 3: {
return { return {
@ -278,7 +272,7 @@ export default function ExplorePage() {
file: value.path, file: value.path,
source: `Web UI @ ${window.location.href}`, source: `Web UI @ ${window.location.href}`,
mode: "FLOW" mode: "FLOW"
} as ExplorerOperationRequest } as CoordinatorOperationRequest
} }
default: { default: {
return null; return null;

View File

@ -22,15 +22,14 @@ export default function LaunchPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const muiTheme = useTheme(); const muiTheme = useTheme();
const client = useStompClient(); const client = useStompClient();
const simpleList = useSelector((state: RootState) => state.kafkaComposedFlat) /*useEffect(() => {
useEffect(() => {
if (simpleList.items.filter((item) => item.encodingTimeLeft !== null).length > 0) { if (simpleList.items.filter((item) => item.encodingTimeLeft !== null).length > 0) {
columns.push({ columns.push({
label: "Completion", label: "Completion",
accessor: "encodingTimeLeft" accessor: "encodingTimeLeft"
}) })
} }
}, [simpleList, dispatch]) }, [simpleList, dispatch])*/
const onRefresh = () => { const onRefresh = () => {
client?.publish({ client?.publish({
@ -82,7 +81,6 @@ export default function LaunchPage() {
position: "absolute", position: "absolute",
width: "100%" width: "100%"
}}> }}>
<SimpleTable items={simpleList.items} columns={columns} />
</Box> </Box>
</Box> </Box>

View File

@ -3,13 +3,16 @@ import { useDispatch, useSelector } from "react-redux";
import { useStompClient } from "react-stomp-hooks"; import { useStompClient } from "react-stomp-hooks";
import { Box, Button, Grid, TextField, Typography, useTheme } from '@mui/material'; import { Box, Button, Grid, TextField, Typography, useTheme } from '@mui/material';
import { ExplorerItem } from "../../types"; import { ExplorerItem } from "../../types";
import { ContextMenuItem } from "../features/ContextMenu"; import ContextMenu, { ContextMenuActionEvent, ContextMenuItem } from "../features/ContextMenu";
import { RootState } from "../store"; import { RootState } from "../store";
import { useWsSubscription } from "../ws/subscriptions"; import { useWsSubscription } from "../ws/subscriptions";
import { FileInfo, FileInfoGroup, IncomingUnprocessedFiles, update, } from "../store/unprocessed-files-slice"; import { FileInfo, FileInfoGroup, IncomingUnprocessedFiles, update, } from "../store/unprocessed-files-slice";
import SimpleTable from "../features/table/sortableTable"; import SimpleTable from "../features/table/sortableTable";
import { TablePropetyConfig } from "../features/table/table"; import { TablePropetyConfig, TableRowActionEvents } from "../features/table/table";
import MultiListSortedTable from "../features/table/multiListSortedTable"; import MultiListSortedTable from "../features/table/multiListSortedTable";
import { setContextMenuVisible, setContextMenuPosition } from "../store/context-menu-slice";
import { canConvert, canEncode, canExtract } from "../../fileUtil";
import { CoordinatorOperationRequest } from "../features/types";
const columns: Array<TablePropetyConfig> = [ const columns: Array<TablePropetyConfig> = [
@ -24,7 +27,7 @@ export default function UnprocessedFilesPage() {
const files = useSelector((state: RootState) => state.unprocessedFiles); const files = useSelector((state: RootState) => state.unprocessedFiles);
const [tableItems, setTableItems] = useState<Array<FileInfoGroup>>([]); const [tableItems, setTableItems] = useState<Array<FileInfoGroup>>([]);
const [selectedRow, setSelectedRow] = useState<ExplorerItem|null>(null); const [selectedRow, setSelectedRow] = useState<FileInfo|null>(null);
const [actionableItems, setActionableItems] = useState<Array<ContextMenuItem>>([]); const [actionableItems, setActionableItems] = useState<Array<ContextMenuItem>>([]);
useWsSubscription<IncomingUnprocessedFiles>("/topic/files/unprocessed", (response) => { useWsSubscription<IncomingUnprocessedFiles>("/topic/files/unprocessed", (response) => {
@ -32,6 +35,87 @@ export default function UnprocessedFilesPage() {
}); });
const onItemSelectedEvent: TableRowActionEvents<FileInfo> = {
contextMenu: (row: FileInfo, x: number, y: number) => {
dispatch(setContextMenuVisible(true));
dispatch(setContextMenuPosition({ x: x, y: y }));
setActionableItems(getContextMenuFileActionMenuItems(row));
},
click: function (row: FileInfo): void {
},
doubleClick: function (row: FileInfo): void {
}
};
function getContextMenuFileActionMenuItems(row: FileInfo | null): ContextMenuItem[] {
const items: Array<ContextMenuItem> = [
{
actionIndex: 0,
icon: null,
text: "All available"
},
{
actionIndex: 1,
icon: null,
text: "Encode"
},
{
actionIndex: 2,
icon: null,
text: "Extract"
}
];
return items;
}
const onContextMenuItemClickedEvent: ContextMenuActionEvent<FileInfo> = {
selected: function (actionIndex: number | null, value: FileInfo | null): void {
if (!value) {
return;
}
const payload = (() => {
switch(actionIndex) {
case 0: {
return {
destination: "request/all",
file: value.fileName,
source: `Web UI @ ${window.location.href}`,
mode: "FLOW"
} as CoordinatorOperationRequest
}
case 1: {
return {
destination: "request/encode",
file: value.fileName,
source: `Web UI @ ${window.location.href}`,
mode: "FLOW"
} as CoordinatorOperationRequest
}
case 2: {
return {
destination: "request/extract",
file: value.fileName,
source: `Web UI @ ${window.location.href}`,
mode: "FLOW"
} as CoordinatorOperationRequest
}
default: {
return null;
}
}
})();
if (payload) {
client?.publish({
destination: "/app/"+payload.destination,
body: JSON.stringify(payload)
})
}
}
}
const pullData = () => { const pullData = () => {
client?.publish({ client?.publish({
destination: "/app/files/unprocessed" destination: "/app/files/unprocessed"
@ -99,9 +183,10 @@ export default function UnprocessedFilesPage() {
position: "absolute", position: "absolute",
width: "100%" width: "100%"
}}> }}>
<MultiListSortedTable items={tableItems ?? []} columns={columns} /> <MultiListSortedTable items={tableItems ?? []} columns={columns} onRowClickedEvent={onItemSelectedEvent} />
</Box> </Box>
</Box> </Box>
<ContextMenu row={selectedRow} actionItems={actionableItems} onContextMenuItemClicked={onContextMenuItemClickedEvent} />
</> </>
) )

View File

@ -1,24 +1,23 @@
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import composedSlice from './store/composed-slice'; import composedSlice from './store/composed-slice';
import explorerSlice from './store/explorer-slice'; import explorerSlice from './store/explorer-slice';
import kafkaItemsFlatSlice from './store/kafka-items-flat-slice';
import contextMenuSlice from './store/context-menu-slice'; import contextMenuSlice from './store/context-menu-slice';
import persistentEventsSlice from './store/persistent-events-slice'; import persistentEventsSlice from './store/persistent-events-slice';
import chainedEventsSlice from './store/chained-events-slice'; import chainedEventsSlice from './store/chained-events-slice';
import unprocessedFilesSlice from './store/unprocessed-files-slice'; import unprocessedFilesSlice from './store/unprocessed-files-slice';
import tasksSlice from './store/tasks-slice'; import tasksSlice from './store/tasks-slice';
import workSlice from './store/work-slice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
composed: composedSlice, composed: composedSlice,
explorer: explorerSlice, explorer: explorerSlice,
kafkaComposedFlat: kafkaItemsFlatSlice,
contextMenu: contextMenuSlice, contextMenu: contextMenuSlice,
persistentEvents: persistentEventsSlice, persistentEvents: persistentEventsSlice,
chained: chainedEventsSlice, chained: chainedEventsSlice,
unprocessedFiles: unprocessedFilesSlice, unprocessedFiles: unprocessedFilesSlice,
tasks: tasksSlice, tasks: tasksSlice,
work: workSlice
}, },
}); });

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit" import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { ExpandableTableItem } from "../features/table/expandableTable";
export interface EventGroup { export interface EventGroup extends ExpandableTableItem {
referenceId: string, referenceId: string,
created: number, created: number,
fileName: string|null, fileName: string|null,

View File

@ -1,23 +0,0 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { SimpleEventDataObject } from "../../types"
interface ComposedState {
items: Array<SimpleEventDataObject>
}
const initialState: ComposedState = {
items: []
}
const kafkaComposedFlat = createSlice({
name: "Composed",
initialState,
reducers: {
simpleEventsUpdate(state, action: PayloadAction<Array<SimpleEventDataObject>>) {
state.items = action.payload
}
}
})
export const { simpleEventsUpdate } = kafkaComposedFlat.actions;
export default kafkaComposedFlat.reducer;

View File

@ -5,7 +5,9 @@ import no.iktdev.eventi.data.eventId
import no.iktdev.eventi.data.referenceId import no.iktdev.eventi.data.referenceId
import no.iktdev.eventi.data.toJson import no.iktdev.eventi.data.toJson
import no.iktdev.eventi.database.* import no.iktdev.eventi.database.*
import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.Event import no.iktdev.mediaprocessing.shared.common.contract.data.Event
import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent
import no.iktdev.mediaprocessing.shared.common.database.tables.events import no.iktdev.mediaprocessing.shared.common.database.tables.events
import no.iktdev.mediaprocessing.shared.common.database.tables.tasks import no.iktdev.mediaprocessing.shared.common.database.tables.tasks
import no.iktdev.mediaprocessing.shared.common.task.Task import no.iktdev.mediaprocessing.shared.common.task.Task
@ -196,3 +198,7 @@ fun Query?.toTask(): List<Task> {
val res = this?.mapNotNull { dz.deserializeTask(it) } ?: emptyList() val res = this?.mapNotNull { dz.deserializeTask(it) } ?: emptyList()
return res return res
} }
fun Query?.toEvent(): List<Event> {
return this?.mapNotNull { it[events.data].jsonToEvent(it[events.event]) } ?: emptyList()
}