This commit is contained in:
Brage 2023-11-18 00:28:31 +01:00
parent 04bfa594cd
commit 843acfaa89
12 changed files with 618 additions and 19 deletions

View File

@ -28,15 +28,6 @@ function App() {
}); });
const testButton = () => {
client?.publish({
"destination": "/app/items",
"body": "Potato"
})
}
useEffect(() => { useEffect(() => {
// Kjør din funksjon her når komponenten lastes inn for første gang // Kjør din funksjon her når komponenten lastes inn for første gang
// Sjekk om cursor er null // Sjekk om cursor er null
@ -56,16 +47,26 @@ function App() {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<BrowserRouter> <Box sx={{
<Box sx={{ marginTop: "70px", minHeight: "50vh", width: "100%", display: "block", overflow: "hidden" }}> height: 70,
<button onClick={testButton}>Click me</button> backgroundColor: theme.palette.action.selected
<Routes> }}>
<Route path='/files' element={<ExplorePage />} /> </Box>
<Route path='/' element={<LaunchPage />} /> <Box sx={{
</Routes> display: "block",
</Box> maxHeight: window.screen.height - 70,
<Footer /> height: window.screen.height - 70,
</BrowserRouter> width: "100vw",
maxWidth: "100vw"
}}>
<BrowserRouter>
<Routes>
<Route path='/files' element={<ExplorePage />} />
<Route path='/' element={<LaunchPage />} />
</Routes>
<Footer />
</BrowserRouter>
</Box>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@ -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 (
<>
<Typography variant='body1'>
{day}.{month}.{year} {hours}.{minutes}
</Typography>
</>
)
}

View File

@ -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<T> {
(accessor: string, data: T): JSX.Element | null
}
type NullableTableRowActionEvents<T> = TableRowActionEvents<T> | null;
export interface TableRowActionEvents<T> {
click: (row: T) => void;
doubleClick: (row: T) => void;
contextMenu: (row: T) => void;
}
export default function SimpleTable<T>({ items, columns, customizer, onRowClickedEvent }: { items: Array<T>, columns: Array<TablePropetyConfig>, customizer?: TableCellCustomizer<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
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<string>('');
const [selectedRow, setSelectedRow] = useState<T | null>(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 (
<Box sx={{
display: "flex",
flexDirection: "column", // Bruk column-fleksretning
height: "100%",
overflow: "hidden"
}}>
<TableContainer sx={{
flex: 1,
overflowY: "auto",
position: "relative", // Legg til denne linjen for å justere layout
maxHeight: "100%" // Legg til denne linjen for å begrense høyden
}}>
<Table>
<TableHead sx={{
position: "sticky",
top: 0,
backgroundColor: muiTheme.palette.background.paper,
}}>
<TableRow>
{columns.map((column) => (
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
<Box display="flex">
{orderBy === column.accessor ?
(order === "asc" ? (<IconArrowDown />) : (<IconArrowUp />)) : (
<IconArrowDown sx={{ color: "transparent" }} />
)
}
<Typography>{column.label}</Typography>
</Box>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody sx={{
overflowY: "scroll"
}}>
{sortedData?.map((row: T, rowIndex: number) => (
<TableRow key={rowIndex}
onClick={() => tableRowSingleClicked(row)}
onDoubleClick={() => tableRowDoubleClicked(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>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
)
}

View File

@ -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<ExplorerItem> = (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 <FolderIcon sx={{ margin: 1 }} />
} else {
return <Typography>{data[accessor]}</Typography>
}
}
default: return null;
}
};
const columns: Array<TablePropetyConfig> = [
{ 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<string> = path?.split(/\\|\//).map((value: string, index: number) => value.replaceAll(":", "")) ?? [];
const segments = parts.map((name: string, index: number) => {
return (
<Box key={index} sx={{
display: "flex",
flexDirection: "row",
alignItems: "center"
}}>
<Button sx={{
borderRadius: 5
}} onClick={() => navigateTo(getPartFor(path!, index))}>
<Typography>{name}</Typography>
</Button>
{index < parts.length - 1 && <IconForward fontSize="small" />}
</Box>
)
});
console.log(parts)
return (
<Box display="flex">
{segments}
</Box>
)
}
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<ExplorerItem> = {
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<ExplorerCursor>("/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 (
<Box display="block">
<Box sx={{
height: 50,
width: "100%",
maxHeight: "100%",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
backgroundColor: muiTheme.palette.background.paper
}}>
<Box sx={{
display: "flex",
}}>
<Button onClick={onHomeClick} sx={{
borderRadius: 5
}}>
<IconHome />
</Button>
<Box sx={{
borderRadius: 5,
backgroundColor: muiTheme.palette.divider
}}>
{getSegmentedNaviagatablePath(navigateTo, cursor?.path)}
</Box>
</Box>
</Box>
<Box sx={{
display: "block",
height: "calc(100% - 120px)",
overflow: "hidden",
position: "absolute",
width: "100%"
}}>
<SimpleTable items={cursor?.items ?? []} columns={columns} customizer={createTableCell} onRowClickedEvent={onItemSelectedEvent} />
</Box>
</Box>
)
}

View File

@ -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<TablePropetyConfig> = [
{ 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 (
<Box display="block">
<Box sx={{
height: 50,
width: "100%",
maxHeight: "100%",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
backgroundColor: muiTheme.palette.background.paper
}}>
<Box sx={{
display: "flex",
}}>
<Button onClick={onRefresh} sx={{
borderRadius: 5
}}>
<IconRefresh />
</Button>
</Box>
</Box>
<Box sx={{
display: "block",
height: "calc(100% - 120px)",
overflow: "hidden",
position: "absolute",
width: "100%"
}}>
<SimpleTable items={simpleList.items} columns={columns} />
</Box>
</Box>
)
}

View File

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

View File

@ -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<ExplorerItem>
}
const initialState: ExplorerState = {
name: null,
path: null,
items: []
}
const composedSlice = createSlice({
name: "Explorer",
initialState,
reducers: {
updateItems(state, action: PayloadAction<ExplorerCursor>) {
state.items = action.payload.items;
state.name = action.payload.name;
state.path = action.payload.path
},
}
})
export const { updateItems } = composedSlice.actions;
export default composedSlice.reducer;

View File

@ -0,0 +1,22 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
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

@ -6,6 +6,8 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow: hidden; overflow: hidden;
max-height: 100vh;
height: 100vh;
} }
code { code {

16
UI/web/src/theme.d.ts vendored Normal file
View File

@ -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;
}

23
UI/web/src/theme.ts Normal file
View File

@ -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;

74
UI/web/src/types.d.ts vendored Normal file
View File

@ -0,0 +1,74 @@
import { type } from "os";
interface ExplorerCursor {
name: string
path: string
items: Array<ExplorerItem>
}
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<string>
}
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;
}