diff --git a/UI/web/src/App.tsx b/UI/web/src/App.tsx
index 473e391c..27d66022 100644
--- a/UI/web/src/App.tsx
+++ b/UI/web/src/App.tsx
@@ -28,15 +28,6 @@ function App() {
});
- const testButton = () => {
- client?.publish({
- "destination": "/app/items",
- "body": "Potato"
- })
- }
-
-
-
useEffect(() => {
// Kjør din funksjon her når komponenten lastes inn for første gang
// Sjekk om cursor er null
@@ -56,16 +47,26 @@ function App() {
return (
-
-
-
-
- } />
- } />
-
-
-
-
+
+
+
+
+
+ } />
+ } />
+
+
+
+
);
}
diff --git a/UI/web/src/app/features/UxTc.tsx b/UI/web/src/app/features/UxTc.tsx
new file mode 100644
index 00000000..6ec870db
--- /dev/null
+++ b/UI/web/src/app/features/UxTc.tsx
@@ -0,0 +1,22 @@
+import { Typography } from "@mui/material";
+
+
+export function UnixTimestamp({ timestamp }: { timestamp?: number }) {
+ if (!timestamp) {
+ return null;
+ }
+
+ const date = new Date(timestamp);
+ const day = date.getDate();
+ const month = date.toLocaleString('default', { month: 'short' });
+ const year = date.getFullYear();
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return (
+ <>
+
+ {day}.{month}.{year} {hours}.{minutes}
+
+ >
+ )
+}
diff --git a/UI/web/src/app/features/table.tsx b/UI/web/src/app/features/table.tsx
new file mode 100644
index 00000000..88c52f7c
--- /dev/null
+++ b/UI/web/src/app/features/table.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { RootState } from "../store";
+import IconArrowUp from '@mui/icons-material/ArrowUpward';
+import IconArrowDown from '@mui/icons-material/ArrowDownward';
+import { Table, TableHead, TableRow, TableCell, TableBody, Typography, Box, useTheme, TableContainer } from "@mui/material";
+
+export interface TablePropetyConfig {
+ label: string
+ accessor: string
+}
+
+export interface TableCellCustomizer {
+ (accessor: string, data: T): JSX.Element | null
+}
+
+type NullableTableRowActionEvents = TableRowActionEvents | null;
+export interface TableRowActionEvents {
+ click: (row: T) => void;
+ doubleClick: (row: T) => void;
+ contextMenu: (row: T) => void;
+}
+
+
+export default function SimpleTable({ items, columns, customizer, onRowClickedEvent }: { items: Array, columns: Array, customizer?: TableCellCustomizer, onRowClickedEvent?: TableRowActionEvents }) {
+ const muiTheme = useTheme();
+
+ const [contextMenuVisible, setContextMenuVisible] = useState(false);
+ const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 });
+
+ const [order, setOrder] = useState<'asc' | 'desc'>('asc');
+ const [orderBy, setOrderBy] = useState('');
+ const [selectedRow, setSelectedRow] = useState(null);
+
+ const tableRowSingleClicked = (row: T | null) => {
+ setSelectedRow(row);
+ if (row && onRowClickedEvent) {
+ onRowClickedEvent.click(row);
+ }
+ }
+ const tableRowDoubleClicked = (row: T | null) => {
+ setSelectedRow(row);
+ if (row && onRowClickedEvent) {
+ onRowClickedEvent.doubleClick(row);
+ }
+ }
+
+ const handleSort = (property: string) => {
+ const isAsc = orderBy === property && order === 'asc';
+ setOrder(isAsc ? 'desc' : 'asc');
+ setOrderBy(property);
+ };
+
+ const compareValues = (a: any, b: any, orderBy: string) => {
+ if (typeof a[orderBy] === 'string') {
+ return a[orderBy].localeCompare(b[orderBy]);
+ } else if (typeof a[orderBy] === 'number') {
+ return a[orderBy] - b[orderBy];
+ }
+ return 0;
+ };
+
+ const sortedData = items.slice().sort((a, b) => {
+ if (order === 'asc') {
+ return compareValues(a, b, orderBy);
+ } else {
+ return compareValues(b, a, orderBy);
+ }
+ });
+
+ useEffect(() => {
+ handleSort(columns[0].accessor)
+ }, [])
+
+
+ return (
+
+
+
+
+
+ {columns.map((column) => (
+ handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
+
+ {orderBy === column.accessor ?
+ (order === "asc" ? () : ()) : (
+
+ )
+ }
+ {column.label}
+
+
+ ))}
+
+
+
+ {sortedData?.map((row: T, rowIndex: number) => (
+ tableRowSingleClicked(row)}
+ onDoubleClick={() => tableRowDoubleClicked(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]}}
+
+ ))}
+
+ ))}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/UI/web/src/app/page/ExplorePage.tsx b/UI/web/src/app/page/ExplorePage.tsx
new file mode 100644
index 00000000..91b2bb27
--- /dev/null
+++ b/UI/web/src/app/page/ExplorePage.tsx
@@ -0,0 +1,180 @@
+import { useEffect } from 'react';
+import { UnixTimestamp } from '../features/UxTc';
+import { Box, Button, Typography, useTheme } from '@mui/material';
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../store';
+import SimpleTable, { TableCellCustomizer, TablePropetyConfig, TableRowActionEvents } from '../features/table';
+import { useStompClient } from 'react-stomp-hooks';
+import { useWsSubscription } from '../ws/subscriptions';
+import { updateItems } from '../store/explorer-slice';
+import FolderIcon from '@mui/icons-material/Folder';
+import IconForward from '@mui/icons-material/ArrowForwardIosRounded';
+import IconHome from '@mui/icons-material/Home';
+import { ExplorerItem, ExplorerCursor, ExplorerItemType } from '../../types';
+
+
+const createTableCell: TableCellCustomizer = (accessor, data) => {
+ switch (accessor) {
+ case "created": {
+ if (typeof data[accessor] === "number") {
+ const timestampObject = { timestamp: data[accessor] as number }; // Opprett et objekt med riktig struktur
+ return UnixTimestamp(timestampObject);
+ } else {
+ return null;
+ }
+ }
+ case "extension": {
+ if (data[accessor] === null) {
+ return
+ } else {
+ return {data[accessor]}
+ }
+ }
+ default: return null;
+ }
+};
+
+const columns: Array = [
+ { label: "Name", accessor: "name" },
+ { label: "Format", accessor: "extension" },
+ { label: "Created", accessor: "created" },
+];
+
+
+
+function getPartFor(path: string, index: number): string | null {
+ if (path.match(/\//)) {
+ return path.split(/\//, index + 1).join("/");
+ } else if (path.match(/\\/)) {
+ return path.split(/\\/, index + 1).join("\\");
+ }
+ return null;
+}
+
+function getSegmentedNaviagatablePath(navigateTo: (path: string | null) => void, path: string | null): JSX.Element {
+ console.log(path);
+ const parts: Array = path?.split(/\\|\//).map((value: string, index: number) => value.replaceAll(":", "")) ?? [];
+ const segments = parts.map((name: string, index: number) => {
+ return (
+
+
+ {index < parts.length - 1 && }
+
+ )
+ });
+
+
+ console.log(parts)
+ return (
+
+ {segments}
+
+ )
+}
+
+
+export default function ExplorePage() {
+ const muiTheme = useTheme();
+ const dispatch = useDispatch();
+ const client = useStompClient();
+ const cursor = useSelector((state: RootState) => state.explorer)
+
+ const navigateTo = (path: string | null) => {
+ console.log(path)
+ if (path) {
+ client?.publish({
+ destination: "/app/explorer/navigate",
+ body: path
+ })
+ }
+ }
+
+ const onItemSelectedEvent: TableRowActionEvents = {
+ click: (row: ExplorerItem) => null,
+ doubleClick: (row: ExplorerItem) => {
+ console.log(row);
+ if (row.type === "FOLDER") {
+ navigateTo(row.path);
+ }
+ },
+ contextMenu: (row: ExplorerItem) => null
+ }
+
+ const onHomeClick = () => {
+ client?.publish({
+ destination: "/app/explorer/home"
+ })
+ }
+
+ useWsSubscription("/topic/explorer/go", (response) => {
+ dispatch(updateItems(response))
+ });
+
+
+ useEffect(() => {
+ if (cursor)
+
+
+ // Kjør din funksjon her når komponenten lastes inn for første gang
+ // Sjekk om cursor er null
+ if (cursor.path === null && client !== null) {
+ console.log(cursor)
+ // Kjør din funksjon her når cursor er null og client ikke er null
+ client?.publish({
+ destination: "/app/explorer/home"
+ });
+
+ // Alternativt, du kan dispatche en Redux handling her
+ // dispatch(fetchDataAction()); // Eksempel på å dispatche en handling
+ }
+ }, [cursor, client, dispatch]);
+
+
+ return (
+
+
+
+
+
+ {getSegmentedNaviagatablePath(navigateTo, cursor?.path)}
+
+
+
+
+
+
+
+ )
+}
diff --git a/UI/web/src/app/page/LaunchPage.tsx b/UI/web/src/app/page/LaunchPage.tsx
new file mode 100644
index 00000000..6c29b10d
--- /dev/null
+++ b/UI/web/src/app/page/LaunchPage.tsx
@@ -0,0 +1,75 @@
+import { useDispatch, useSelector } from "react-redux";
+import SimpleTable, { TableCellCustomizer, TablePropetyConfig } from "../features/table"
+import { RootState } from "../store";
+import { useEffect } from "react";
+import { useStompClient } from "react-stomp-hooks";
+import { Box, Button, useTheme } from "@mui/material";
+import IconRefresh from '@mui/icons-material/Refresh'
+
+
+const columns: Array = [
+ { label: "Title", accessor: "givenTitle" },
+ { label: "Type", accessor: "determinedType" },
+ { label: "Collection", accessor: "givenCollection" },
+ { label: "Encoded", accessor: "eventEncoded" }
+];
+
+
+
+export default function LaunchPage() {
+ const dispatch = useDispatch();
+ const muiTheme = useTheme();
+ const client = useStompClient();
+ const simpleList = useSelector((state: RootState) => state.kafkaComposedFlat)
+ useEffect(() => {
+ if (simpleList.items.filter((item) => item.encodingTimeLeft !== null).length > 0) {
+ columns.push({
+ label: "Completion",
+ accessor: "encodingTimeLeft"
+ })
+ }
+ }, [simpleList, dispatch])
+
+ const onRefresh = () => {
+ client?.publish({
+ "destination": "/app/items",
+ "body": "Potato"
+ })
+ }
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/UI/web/src/app/store/composed-slice.ts b/UI/web/src/app/store/composed-slice.ts
new file mode 100644
index 00000000..67c29d6f
--- /dev/null
+++ b/UI/web/src/app/store/composed-slice.ts
@@ -0,0 +1,22 @@
+import { PayloadAction, createSlice } from "@reduxjs/toolkit"
+
+interface ComposedState {
+ items: Array
+}
+
+const initialState: ComposedState = {
+ items: []
+}
+
+const composedSlice = createSlice({
+ name: "Composed",
+ initialState,
+ reducers: {
+ updateItems(state, action: PayloadAction>) {
+ state.items = action.payload
+ }
+ }
+})
+
+export const { updateItems } = composedSlice.actions;
+export default composedSlice.reducer;
\ No newline at end of file
diff --git a/UI/web/src/app/store/explorer-slice.ts b/UI/web/src/app/store/explorer-slice.ts
new file mode 100644
index 00000000..768fc728
--- /dev/null
+++ b/UI/web/src/app/store/explorer-slice.ts
@@ -0,0 +1,29 @@
+import { PayloadAction, createSlice } from "@reduxjs/toolkit"
+import { ExplorerItem, ExplorerCursor } from "../../types"
+
+interface ExplorerState {
+ name: string | null
+ path: string | null
+ items: Array
+}
+
+const initialState: ExplorerState = {
+ name: null,
+ path: null,
+ items: []
+}
+
+const composedSlice = createSlice({
+ name: "Explorer",
+ initialState,
+ reducers: {
+ updateItems(state, action: PayloadAction) {
+ state.items = action.payload.items;
+ state.name = action.payload.name;
+ state.path = action.payload.path
+ },
+ }
+})
+
+export const { updateItems } = composedSlice.actions;
+export default composedSlice.reducer;
\ No newline at end of file
diff --git a/UI/web/src/app/store/kafka-items-flat-slice.ts b/UI/web/src/app/store/kafka-items-flat-slice.ts
new file mode 100644
index 00000000..c32b3fd9
--- /dev/null
+++ b/UI/web/src/app/store/kafka-items-flat-slice.ts
@@ -0,0 +1,22 @@
+import { PayloadAction, createSlice } from "@reduxjs/toolkit"
+
+interface ComposedState {
+ items: Array
+}
+
+const initialState: ComposedState = {
+ items: []
+}
+
+const kafkaComposedFlat = createSlice({
+ name: "Composed",
+ initialState,
+ reducers: {
+ simpleEventsUpdate(state, action: PayloadAction>) {
+ state.items = action.payload
+ }
+ }
+})
+
+export const { simpleEventsUpdate } = kafkaComposedFlat.actions;
+export default kafkaComposedFlat.reducer;
\ No newline at end of file
diff --git a/UI/web/src/index.css b/UI/web/src/index.css
index ef3dd44f..440b1c54 100644
--- a/UI/web/src/index.css
+++ b/UI/web/src/index.css
@@ -6,6 +6,8 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
+ max-height: 100vh;
+ height: 100vh;
}
code {
diff --git a/UI/web/src/theme.d.ts b/UI/web/src/theme.d.ts
new file mode 100644
index 00000000..7c3600fb
--- /dev/null
+++ b/UI/web/src/theme.d.ts
@@ -0,0 +1,16 @@
+import { Theme, ThemeOptions } from '@mui/material/styles';
+
+declare module '@mui/material/styles' {
+ interface CustomTheme extends Theme {
+ status: {
+ danger: string;
+ };
+ }
+ // allow configuration using `createTheme`
+ interface CustomThemeOptions extends ThemeOptions {
+ status?: {
+ danger?: string;
+ };
+ }
+ export function createTheme(options?: CustomThemeOptions): CustomTheme;
+}
\ No newline at end of file
diff --git a/UI/web/src/theme.ts b/UI/web/src/theme.ts
new file mode 100644
index 00000000..a98c46ce
--- /dev/null
+++ b/UI/web/src/theme.ts
@@ -0,0 +1,23 @@
+import { createTheme } from '@mui/material/styles';
+
+export const theme = createTheme({
+ palette: {
+ mode: "dark",
+ primary: {
+ main: '#8800da',
+ },
+ secondary: {
+ main: '#004dbb',
+ },
+ background: {
+ default: '#181818',
+ paper: '#080808',
+ },
+ divider: '#000000',
+ action: {
+ selected: "rgb(67 0 107)"
+ }
+ },
+});
+
+export default theme;
\ No newline at end of file
diff --git a/UI/web/src/types.d.ts b/UI/web/src/types.d.ts
new file mode 100644
index 00000000..1afe602b
--- /dev/null
+++ b/UI/web/src/types.d.ts
@@ -0,0 +1,74 @@
+import { type } from "os";
+
+
+interface ExplorerCursor {
+ name: string
+ path: string
+ items: Array
+}
+
+
+type ExplorerItemType = "FILE" | "FOLDER";
+
+interface ExplorerItem {
+ path: string;
+ name: string;
+ extension: string|null;
+ created: number,
+ type: ExplorerItemType
+}
+
+
+interface EventDataObject {
+ id: string;
+ details: Details
+ encode: Encode
+ io: IO
+ events: Array
+}
+
+interface Details {
+ title: string;
+ file: string;
+ sanitizedName: string
+ collection: string|null
+}
+
+interface Metadata {
+ source: string
+}
+
+interface Encode {
+ state: string;
+ progress: number = 0
+}
+
+interface IO {
+ inputFile: string;
+ outputFile: string;
+}
+
+
+enum SimpleEventDataState {
+ NA,
+ QUEUED,
+ STARTED,
+ ENDED,
+ FAILED,
+ }
+
+ interface SimpleEventDataObject {
+ id: string;
+ name?: string | null;
+ path?: string | null;
+ givenTitle?: string | null;
+ givenSanitizedName?: string | null;
+ givenCollection?: string | null;
+ determinedType?: string | null;
+ eventEncoded: SimpleEventDataState;
+ eventExtracted: SimpleEventDataState;
+ eventConverted: SimpleEventDataState;
+ eventCollected: SimpleEventDataState;
+ encodingProgress?: number | null;
+ encodingTimeLeft?: number | null;
+ }
\ No newline at end of file