-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #112 from cptKNJO/ui
UI
- Loading branch information
Showing
10 changed files
with
884 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
]); |
62 changes: 62 additions & 0 deletions
62
urbackupserver/www2/src/features/activities/OngoingActivitiesActions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
209
urbackupserver/www2/src/features/activities/OngoingActivitiesTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
36
urbackupserver/www2/src/features/activities/ProcessSpeedChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
} |
3 changes: 2 additions & 1 deletion
3
urbackupserver/www2/src/features/activities/getActionFromLastAct.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.