Skip to content

Commit

Permalink
Merge pull request #112 from cptKNJO/ui
Browse files Browse the repository at this point in the history
UI
  • Loading branch information
uroni authored Nov 3, 2024
2 parents e388ab4 + 01e14f5 commit ea6df31
Show file tree
Hide file tree
Showing 10 changed files with 884 additions and 47 deletions.
1 change: 1 addition & 0 deletions urbackupserver/www2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test": "vitest"
},
"dependencies": {
"@fluentui/react-charting": "^5.23.11",
"@fluentui/react-components": "^9.49.2",
"@fluentui/react-experiments": "^8.14.152",
"@fluentui/react-icons": "^2.0.239",
Expand Down
478 changes: 478 additions & 0 deletions urbackupserver/www2/pnpm-lock.yaml

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions urbackupserver/www2/src/features/activities/ACTIONS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const ACTIONS = {
NONE: "None",
INCR_FILE: "Incremental file backup",
FULL_FILE: "Full file backup",
INCR_IMAGE: "Incremental image backup",
FULL_IMAGE: "Full image backup",
RESUME_INCR_FILE: "Resumed incremental file backup",
RESUME_FULL_FILE: "Resumed full file backup",
RESTORE_FILE: "File restore",
RESTORE_IMAGE: "Image restore",
UPDATE: "Client update",
CHECK_INTEGRITY: "Checking database integrity",
BACKUP_DATABASE: "Backing up database",
RECALCULATE_STATISTICS: "Recalculating statistics",
NIGHTLY_CLEANUP: "Nightly clean-up",
EMERGENCY_CLEANUP: "Emergency clean-up",
STORAGE_MIGRATION: "Storage migration",

// Delete actions
DEL_INCR_FILE: "Deleting incremental file backup",
DEL_FULL_FILE: "Deleting full file backup",
DEL_INCR_IMAGE: "Deleting incremental image backup",
DEL_FULL_IMAGE: "Deleting full image backup",
} as const;

export const NUMBERED_ACTIONS_MAP = new Map([
[0, ACTIONS.NONE],
[1, ACTIONS.INCR_FILE],
[2, ACTIONS.FULL_FILE],
[3, ACTIONS.INCR_IMAGE],
[4, ACTIONS.FULL_IMAGE],
[5, ACTIONS.RESUME_INCR_FILE],
[6, ACTIONS.RESUME_FULL_FILE],
[8, ACTIONS.RESTORE_FILE],
[9, ACTIONS.RESTORE_IMAGE],
[10, ACTIONS.UPDATE],
[11, ACTIONS.CHECK_INTEGRITY],
[12, ACTIONS.BACKUP_DATABASE],
[13, ACTIONS.RECALCULATE_STATISTICS],
[14, ACTIONS.NIGHTLY_CLEANUP],
[15, ACTIONS.EMERGENCY_CLEANUP],
[16, ACTIONS.STORAGE_MIGRATION],
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { tokens, Button, makeStyles } from "@fluentui/react-components";
import { OpenRegular } from "@fluentui/react-icons";
import { useMutation } from "@tanstack/react-query";

import type { ProcessItem } from "../../api/urbackupserver";
import { urbackupServer } from "../../App";

function useStopProcessMutation() {
return useMutation({
mutationFn: ({
clientId,
processId,
}: {
clientId: number;
processId: number;
}) => urbackupServer.stopProcess(clientId, processId, false),
});
}

const useStyles = makeStyles({
root: {
display: "flex",
gap: tokens.spacingHorizontalXS,
},
stopButton: {
minWidth: 0,
},
});

export function OngoingActivitiesActions({
process,
}: {
process: ProcessItem;
}) {
const stopProcessMutatiion = useStopProcessMutation();

const classes = useStyles();

return (
<div className={classes.root}>
{process.can_stop_backup && (
<Button
size="small"
className={classes.stopButton}
onClick={() =>
stopProcessMutatiion.mutate({
clientId: process.clientid,
processId: process.id,
})
}
>
Stop
</Button>
)}
{process.can_show_backup_log && (
<Button size="small" icon={<OpenRegular />}>
Show Log
</Button>
)}
</div>
);
}
209 changes: 209 additions & 0 deletions urbackupserver/www2/src/features/activities/OngoingActivitiesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
DataGrid,
DataGridHeader,
DataGridRow,
DataGridHeaderCell,
DataGridBody,
DataGridCell,
TableCellLayout,
TableColumnDefinition,
createTableColumn,
TableColumnId,
DataGridCellFocusMode,
Field,
ProgressBar,
tokens,
Caption1,
Text,
} from "@fluentui/react-components";

import type { ProcessItem } from "../../api/urbackupserver";
import { format_size, formatDuration } from "../../utils/format";
import { NUMBERED_ACTIONS_MAP } from "./ACTIONS";
import { OngoingActivitiesActions } from "./OngoingActivitiesActions";
import { ProcessSpeedChart } from "./ProcessSpeedChart";

const styles: Record<string, React.CSSProperties> = {
progressField: {
width: "100%",
},
progressWrapper: {
width: "100%",
paddingBlock: tokens.spacingVerticalM,
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalXS,
},
};

export const columns: TableColumnDefinition<ProcessItem>[] = [
createTableColumn<ProcessItem>({
columnId: "name",
renderHeaderCell: () => {
return "Computer name";
},
renderCell: (item) => {
return <TableCellLayout>{item.name}</TableCellLayout>;
},
}),
createTableColumn<ProcessItem>({
columnId: "action",
renderHeaderCell: () => {
return "Action";
},
renderCell: (item) => {
return (
<TableCellLayout>
{NUMBERED_ACTIONS_MAP.get(item.action)}
</TableCellLayout>
);
},
}),
createTableColumn<ProcessItem>({
columnId: "details",
renderHeaderCell: () => {
return "Details";
},
renderCell: (item) => {
if (["0", ""].includes(item.details)) {
return "-";
}

return (
<TableCellLayout>
<Caption1 block>Volume</Caption1>
<Text>{item.details}</Text>
</TableCellLayout>
);
},
}),
createTableColumn<ProcessItem>({
columnId: "progress",
renderHeaderCell: () => {
return "Progress";
},
renderCell: (item) => {
if (item.pcdone < 0) {
return (
<Field
validationMessage="Indexing"
validationState="none"
style={styles.progressField}
>
<ProgressBar />
</Field>
);
}

return (
<div style={styles.progressWrapper}>
<Field
validationMessage={`${item.pcdone}%`}
validationState="none"
style={styles.progressField}
>
<ProgressBar max={100} value={item.pcdone} />
</Field>
<Caption1>
{format_size(item.done_bytes)} / {format_size(item.total_bytes)}
</Caption1>
</div>
);
},
}),
createTableColumn<ProcessItem>({
columnId: "eta",
renderHeaderCell: () => {
return "ETA";
},
renderCell: (item) => {
if (item.pcdone < 0 || item.pcdone === 100) {
return "-";
}

return (
<TableCellLayout>{formatDuration(item.eta_ms / 1000)}</TableCellLayout>
);
},
}),
createTableColumn<ProcessItem>({
columnId: "speed",
renderHeaderCell: () => {
return "Speed";
},
renderCell: ProcessSpeedChart,
}),
createTableColumn<ProcessItem>({
columnId: "queue",
renderHeaderCell: () => {
return <TableCellLayout>Files in Queue</TableCellLayout>;
},
renderCell: (item) => {
return <TableCellLayout>{String(item.queue)}</TableCellLayout>;
},
}),
createTableColumn<ProcessItem>({
columnId: "actions",
renderHeaderCell: () => {
return "Actions";
},
renderCell: (item) => {
return <OngoingActivitiesActions process={item} />;
},
}),
];

const getCellFocusMode = (columnId: TableColumnId): DataGridCellFocusMode => {
switch (columnId) {
case "actions":
return "group";
default:
return "cell";
}
};

export function OngoingActivitiesTable({ data }: { data: ProcessItem[] }) {
if (data.length === 0) {
return <span>No activities</span>;
}

return (
<DataGrid items={data} getRowId={(item) => item.id} columns={columns}>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell, columnId }) => (
<DataGridHeaderCell style={getNarrowColumnStyles(columnId)}>
{renderHeaderCell()}
</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<ProcessItem>>
{({ item }) => (
<DataGridRow<ProcessItem> key={item.id}>
{({ renderCell, columnId }) => (
<DataGridCell
focusMode={getCellFocusMode(columnId)}
style={getNarrowColumnStyles(columnId)}
>
{renderCell(item)}
</DataGridCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
);
}

/**
* Style some columns to take up less space.
*/
function getNarrowColumnStyles(columnId: TableColumnId) {
const stringId = columnId.toString();

return {
flexGrow: ["queue", "eta"].includes(stringId) ? "0" : "1",
flexBasis: ["queue", "eta"].includes(stringId) ? "12ch" : "0",
};
}
36 changes: 36 additions & 0 deletions urbackupserver/www2/src/features/activities/ProcessSpeedChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type IChartProps, Sparkline } from "@fluentui/react-charting";
import { tokens } from "@fluentui/react-components";

import type { ProcessItem } from "../../api/urbackupserver";
import { format_speed_bpms_to_bps } from "../../utils/format";

const sparklineStyles = {
valueText: {
fill: tokens.colorNeutralForeground1,
},
};

export function ProcessSpeedChart(process: ProcessItem) {
if (process.speed_bpms === 0 && process.past_speed_bpms.length === 0) {
return "-";
}

const legend =
process.speed_bpms > 0 ? format_speed_bpms_to_bps(process.speed_bpms) : "";

const data: IChartProps = {
chartTitle: "Speed chart",
lineChartData: [
{
legend,
color: tokens.colorBrandBackground,
data: process.past_speed_bpms.map((d, i) => ({
x: i + 1,
y: d,
})),
},
],
};

return <Sparkline data={data} showLegend styles={sparklineStyles} />;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "vitest";

import { ACTIONS, getActionFromLastAct } from "./getActionFromLastAct";
import { getActionFromLastAct } from "./getActionFromLastAct";
import { ACTIONS } from "./ACTIONS";

const testCases: {
lastact: Parameters<typeof getActionFromLastAct>[0];
Expand Down
Loading

0 comments on commit ea6df31

Please sign in to comment.