Skip to content

Commit

Permalink
Merge pull request #119 from cptKNJO/ui
Browse files Browse the repository at this point in the history
UI
  • Loading branch information
uroni authored Jan 14, 2025
2 parents 40c1731 + 63fb6f2 commit 6d58a63
Show file tree
Hide file tree
Showing 10 changed files with 1,312 additions and 775 deletions.
2 changes: 1 addition & 1 deletion urbackupserver/www2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"@fluentui/react-charting": "^5.23.11",
"@fluentui/react-components": "^9.49.2",
"@fluentui/react-components": "^9.57.0",
"@fluentui/react-experiments": "^8.14.152",
"@fluentui/react-icons": "^2.0.239",
"@lingui/core": "^4.10.1",
Expand Down
1,753 changes: 982 additions & 771 deletions urbackupserver/www2/pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions urbackupserver/www2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ClientBackupsTable } from "./features/backups/ClientBackupsTable";
import { BackupsTable } from "./features/backups/BackupsTable";
import { BackupContentTable } from "./features/backups/BackupContentTable";
import BackupErrorPage from "./features/backups/BackupsError";
import { StatisticsPage } from "./pages/Statistics";
import "./css/global.css";

const initialDark =
Expand All @@ -36,6 +37,7 @@ export enum Pages {
Status = "status",
Activities = "activities",
Backups = "backups",
Statistics = "statistics",
Login = "login",
About = "about",
}
Expand Down Expand Up @@ -142,6 +144,15 @@ export const router = createHashRouter([
},
],
},
{
path: `/${Pages.Statistics}`,
element: <StatisticsPage />,
loader: async () => {
state.pageAfterLogin = Pages.Statistics;
await jumpToLoginPageIfNeccessary();
return null;
},
},
]);

function getSessionFromLocalStorage(): string {
Expand Down
9 changes: 7 additions & 2 deletions urbackupserver/www2/src/api/urbackupserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,13 @@ class UrBackupServer {

// Get data for usage graph showing storage usage over time
// scale: "d" for daily, "m" for monthly, "y" for yearly
getUsageGraphData = async (scale: "d"|"m"|"y") => {
const resp = await this.fetchData({"scale": scale}, "usagegraph");
getUsageGraphData = async (scale: "d"|"m"|"y", clientId?: string) => {
const params = {
scale,
clientid: clientId ?? ""
}

const resp = await this.fetchData(params, "usagegraph");
if(typeof resp.data == "undefined")
throw new ResponseParseError("No data found in response");

Expand Down
1 change: 1 addition & 0 deletions urbackupserver/www2/src/components/NavSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const NavSidebar = () => {
<Tab value={Pages.Status}>Status</Tab>
<Tab value={Pages.Activities}>Activities</Tab>
<Tab value={Pages.Backups}>Backups</Tab>
<Tab value={Pages.Statistics}>Statistics</Tab>
</TabList>
);
};
Expand Down
17 changes: 17 additions & 0 deletions urbackupserver/www2/src/css/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@

/* App variables */
--gutter: var(--spacingM);

/*
tokens color variables to apply to charting callout components
rendered out of FluentProvider
*/
--colorNeutralForeground1: #242424;
--colorNeutralCardBackground: #fafafa;

--colorTooltipBackground: var(--colorNeutralCardBackground);
--colorTooltipForeground: var(--colorNeutralForeground1);
}

@media (prefers-color-scheme: dark) {
:root {
--colorNeutralForeground1: #ffffff;
--colorNeutralCardBackground: #333333;
}
}

@layer composition {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
useComboboxFilter,
ComboboxProps,
Combobox,
} from "@fluentui/react-components";
import { useId, useState } from "react";
import { ClientInfo } from "../../api/urbackupserver";

export function SelectStorageUsageClient({
clients,
onSelect,
}: {
clients: ClientInfo[];
onSelect: (value?: string) => void;
}) {
const options = [
{
children: "All clients",
value: "all",
},
...clients.map((client) => ({
children: client.name,
value: String(client.id),
})),
];

const comboId = useId();

const [query, setQuery] = useState<string>(options[0].children);
const comboBoxChildren = useComboboxFilter(query, options, {
optionToText: (d) => d.children,
noOptionsMessage: `No results matched "${query}"`,
});

const onOptionSelect: ComboboxProps["onOptionSelect"] = (_, data) => {
const text = data.optionValue;
const selectedClient = clients.find((client) => String(client.id) === text);

if (!selectedClient) {
onSelect();
setQuery(options[0].children);
return;
}

onSelect(String(selectedClient.id));
setQuery(selectedClient.name);
};

return (
<div className="cluster">
<label id={comboId}>Select client</label>
<Combobox
aria-labelledby={comboId}
onOptionSelect={onOptionSelect}
onChange={(ev) => setQuery(ev.target.value)}
onOpenChange={(_, data) => {
if (data.open) {
setQuery("");
}
}}
value={query}
>
{comboBoxChildren}
</Combobox>
</div>
);
}
182 changes: 182 additions & 0 deletions urbackupserver/www2/src/features/statistics/StorageUsage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { useState } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import {
IChartProps,
ILineChartStyles,
LineChart,
} from "@fluentui/react-charting";
import { Tab, TabList, tokens } from "@fluentui/react-components";

import { urbackupServer } from "../../App";
import { SelectStorageUsageClient } from "./SelectStorageUsageClient";

const DURATIONS = {
d: "Day",
m: "Month",
y: "Year",
} as const;

const lineChartStyles: ILineChartStyles = {
root: {
overflow: "hidden",
line: {
strokeWidth: "1px",
},
},
chartWrapper: {
overflow: "hidden",
},
axisTitle: {
fill: "currentColor",
color: "currentColor",
},
yAxis: {
text: {
fill: "currentColor",
},
line: {
stroke: "currentColor",
},
},
xAxis: {
text: {
fill: "currentColor",
},
},
calloutContentRoot: {
background: "inherit",
},
calloutBlockContainer: {
borderLeft: "0",
margin: 0,
padding: 0,
color: "currentColor",
},
calloutContentX: {
color: "currentColor",
},
calloutlegendText: {
display: "none",
},
};

export function StorageUsage({
width,
height,
}: {
width: number;
height: number;
}) {
const [duration, setDuration] =
useState<Parameters<typeof urbackupServer.getUsageGraphData>[0]>("d");

const [selectedClientId, setSelectedClientId] = useState<
string | undefined
>();

const storageUsageResult = useSuspenseQuery({
queryKey: ["usage-graph", duration, selectedClientId],
queryFn: () => urbackupServer.getUsageGraphData(duration, selectedClientId),
});

const clientsResult = useSuspenseQuery({
queryKey: ["clients"],
queryFn: () => urbackupServer.getClients(),
});

const clients = clientsResult.data;

const storageUsage = storageUsageResult.data!;

const limits = {
min: Math.min(...storageUsage.map((d) => d.data)),
max: Math.max(...storageUsage.map((d) => d.data)),
};

// const width = 550;
// const height = 300;

const data: IChartProps = {
chartTitle: "Storage Usage",
lineChartData: [
{
legend: "Storage Usage",
data: storageUsage.map((d) => ({
x: new Date(d.xlabel),
y: d.data,
xAxisCalloutData: new Intl.DateTimeFormat("en-US", {
dateStyle: "long",
}).format(Date.parse(d.xlabel)),
yAxisCalloutData: `${d.data.toFixed(0)} MB`,
})),
color: tokens.colorBrandBackground,
},
],
};

return (
<div className="flow">
<div
className="cluster"
style={
{
"--cluster-horizontal-alignment": "space-between",
} as React.CSSProperties
}
>
<div>
<SelectStorageUsageClient
clients={clients}
onSelect={(id) => setSelectedClientId(id)}
/>
</div>
<TabList
size="small"
defaultSelectedValue={duration}
selectTabOnFocus={true}
onTabSelect={(_, data) => {
setDuration(data.value as typeof duration);
}}
>
<Tab value="d">{DURATIONS.d}</Tab>
<Tab value="m">{DURATIONS.m}</Tab>
<Tab value="y">{DURATIONS.y}</Tab>
</TabList>
</div>
<div
style={{
height: `${height}px`,
overflow: "hidden",
}}
>
<LineChart
key={selectedClientId ? `${selectedClientId}-${duration}` : duration}
culture={window.navigator.language}
enablePerfOptimization={true}
data={data}
hideLegend
yMinValue={limits.min}
yMaxValue={limits.max}
width={width}
height={height}
yAxisTitle="Storage Usage (MB)"
xAxisTitle={DURATIONS[duration]}
{...(duration === "y" && {
customDateTimeFormatter: (d) => d.getFullYear().toString(),
// Restrict tick values to the number of xLabels returned
tickValues: data.lineChartData?.[0].data.map((d) => d.x as Date),
})}
calloutProps={{
styles: {
calloutMain: {
background: "var(--colorTooltipBackground)",
color: "var(--colorTooltipForeground)",
},
},
}}
styles={lineChartStyles}
/>
</div>
</div>
);
}
1 change: 0 additions & 1 deletion urbackupserver/www2/src/features/status/DownloadClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Combobox,
ComboboxProps,
makeStyles,
OptionOnSelectData,
Popover,
PopoverSurface,
PopoverTrigger,
Expand Down
44 changes: 44 additions & 0 deletions urbackupserver/www2/src/pages/Statistics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Suspense, useEffect, useRef, useState } from "react";
import { Spinner } from "@fluentui/react-components";

import { StorageUsage } from "../features/statistics/StorageUsage";

export const StatisticsPage = () => {
const ref = useRef<HTMLDivElement | null>(null);

const [width, setWidth] = useState(0);

useEffect(() => {
function handleResize() {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setWidth(rect.width);
}
}

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
};
}, []);

return (
<Suspense fallback={<Spinner />}>
<div className="flow">
<h3>Statistics</h3>
<div
className="flow"
ref={ref}
style={{
overflow: "hidden",
width: "clamp(500px, 84vw, 1200px)",
}}
>
<h4>Storage Usage</h4>
<StorageUsage width={width / 2} height={300} />
</div>
</div>
</Suspense>
);
};

0 comments on commit 6d58a63

Please sign in to comment.