diff --git a/src/components/Meter.tsx b/src/components/Meter.tsx index 29bb613251..5a72224eb3 100644 --- a/src/components/Meter.tsx +++ b/src/components/Meter.tsx @@ -1,17 +1,36 @@ import { FC } from "react"; +import classnames from "classnames"; interface Props { percentage: number; + secondaryPercentage?: number; text: string; } -const Meter: FC = ({ percentage, text }: Props) => { +const Meter: FC = ({ + percentage, + secondaryPercentage = 0, + text, +}: Props) => { return ( <>
-
+
0, + })} + /> + {secondaryPercentage ? ( +
+ ) : null} +
+
+ {text}
-
{text}
); }; diff --git a/src/pages/instances/InstanceDisk.tsx b/src/pages/instances/InstanceDisk.tsx new file mode 100644 index 0000000000..f1772a66c7 --- /dev/null +++ b/src/pages/instances/InstanceDisk.tsx @@ -0,0 +1,48 @@ +import { FC } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "util/queryKeys"; +import { fetchMetrics } from "api/metrics"; +import { humanFileSize } from "util/helpers"; +import { getInstanceMetrics } from "util/metricSelectors"; +import Meter from "components/Meter"; +import type { LxdInstance } from "types/instance"; +import { useAuth } from "context/auth"; + +interface Props { + instance: LxdInstance; +} + +const InstanceUsageDisk: FC = ({ instance }) => { + const { isRestricted } = useAuth(); + + const { data: metrics = [] } = useQuery({ + queryKey: [queryKeys.metrics], + queryFn: fetchMetrics, + refetchInterval: 15 * 1000, // 15 seconds + enabled: !isRestricted, + }); + + const instanceMetrics = getInstanceMetrics(metrics, instance); + + return instanceMetrics.disk ? ( +
+ +
+ ) : ( + "" + ); +}; + +export default InstanceUsageDisk; diff --git a/src/pages/instances/InstanceList.tsx b/src/pages/instances/InstanceList.tsx index e66eab9782..8c04a49f90 100644 --- a/src/pages/instances/InstanceList.tsx +++ b/src/pages/instances/InstanceList.tsx @@ -46,6 +46,8 @@ import { SNAPSHOTS, STATUS, TYPE, + MEMORY, + DISK, } from "util/instanceTable"; import { getInstanceName } from "util/operations"; import ScrollableTable from "components/ScrollableTable"; @@ -61,10 +63,12 @@ import InstanceDetailPanel from "./InstanceDetailPanel"; import { useSmallScreen } from "context/useSmallScreen"; import { useSettings } from "context/useSettings"; import { isClusteredServer } from "util/settings"; +import InstanceUsageMemory from "pages/instances/InstanceUsageMemory"; +import InstanceUsageDisk from "pages/instances/InstanceDisk"; const loadHidden = () => { const saved = localStorage.getItem("instanceListHiddenColumns"); - return saved ? (JSON.parse(saved) as string[]) : []; + return saved ? (JSON.parse(saved) as string[]) : [MEMORY, DISK]; }; const saveHidden = (columns: string[]) => { @@ -231,6 +235,14 @@ const InstanceList: FC = () => { }, ] : []), + { + content: MEMORY, + style: { width: `${COLUMN_WIDTHS[MEMORY]}px` }, + }, + { + content: DISK, + style: { width: `${COLUMN_WIDTHS[DISK]}px` }, + }, { content: DESCRIPTION, sortKey: "description", @@ -393,6 +405,22 @@ const InstanceList: FC = () => { }, ] : []), + { + content: , + role: "cell", + "aria-label": MEMORY, + onClick: openSummary, + className: "clickable-cell", + style: { width: `${COLUMN_WIDTHS[MEMORY]}px` }, + }, + { + content: , + role: "cell", + "aria-label": DISK, + onClick: openSummary, + className: "clickable-cell", + style: { width: `${COLUMN_WIDTHS[DISK]}px` }, + }, { content: (
@@ -616,6 +644,8 @@ const InstanceList: FC = () => { = ({ instance, onFailure }) => { Memory {instanceMetrics.memory ? ( -
- -
+ ) : ( "-" )} @@ -80,23 +63,7 @@ const InstanceOverviewMetrics: FC = ({ instance, onFailure }) => { Disk {instanceMetrics.disk ? ( -
- -
+ ) : ( "-" )} diff --git a/src/pages/instances/InstanceUsageMemory.tsx b/src/pages/instances/InstanceUsageMemory.tsx new file mode 100644 index 0000000000..a211faefcf --- /dev/null +++ b/src/pages/instances/InstanceUsageMemory.tsx @@ -0,0 +1,53 @@ +import { FC } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "util/queryKeys"; +import { fetchMetrics } from "api/metrics"; +import { humanFileSize } from "util/helpers"; +import { getInstanceMetrics } from "util/metricSelectors"; +import Meter from "components/Meter"; +import type { LxdInstance } from "types/instance"; +import { useAuth } from "context/auth"; + +interface Props { + instance: LxdInstance; +} + +const InstanceUsageMemory: FC = ({ instance }) => { + const { isRestricted } = useAuth(); + + const { data: metrics = [] } = useQuery({ + queryKey: [queryKeys.metrics], + queryFn: fetchMetrics, + refetchInterval: 15 * 1000, // 15 seconds + enabled: !isRestricted, + }); + + const instanceMetrics = getInstanceMetrics(metrics, instance); + + return instanceMetrics.memory ? ( +
+ +
+ ) : ( + "" + ); +}; + +export default InstanceUsageMemory; diff --git a/src/sass/_meter.scss b/src/sass/_meter.scss index af0c61a67f..19d045bb15 100644 --- a/src/sass/_meter.scss +++ b/src/sass/_meter.scss @@ -1,6 +1,7 @@ .p-meter { background-color: #d3e4ed; border-radius: 0.75rem; + display: flex; height: 0.75rem; margin-bottom: 0.375rem; width: 100%; @@ -11,4 +12,16 @@ border-right: 1px solid #f7f7f7; height: 100%; } + + .has-next-sibling { + border-bottom-right-radius: 0; + border-right: 0; + border-top-right-radius: 0; + } + + .has-previous-sibling { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + opacity: 0.3; + } } diff --git a/src/util/instanceTable.tsx b/src/util/instanceTable.tsx index 225ba24334..5cafaf58b2 100644 --- a/src/util/instanceTable.tsx +++ b/src/util/instanceTable.tsx @@ -3,6 +3,8 @@ export const NAME = "Name"; export const TYPE = "Type"; export const CLUSTER_MEMBER = "Cluster member"; export const DESCRIPTION = "Description"; +export const MEMORY = "Memory"; +export const DISK = "Disk"; export const IPV4 = "IPv4"; export const IPV6 = "IPv6"; export const SNAPSHOTS = "Snapshots"; @@ -12,6 +14,8 @@ export const COLUMN_WIDTHS: Record = { [NAME]: 170, [TYPE]: 130, [CLUSTER_MEMBER]: 150, + [MEMORY]: 150, + [DISK]: 150, [DESCRIPTION]: 150, [IPV4]: 150, [IPV6]: 330, @@ -25,8 +29,17 @@ export const SIZE_HIDEABLE_COLUMNS = [ IPV6, IPV4, DESCRIPTION, + MEMORY, + DISK, TYPE, STATUS, ]; -export const CREATION_SPAN_COLUMNS = [TYPE, DESCRIPTION, IPV4, IPV6, SNAPSHOTS]; +export const CREATION_SPAN_COLUMNS = [ + TYPE, + MEMORY, + DISK, + IPV4, + IPV6, + SNAPSHOTS, +]; diff --git a/src/util/metricSelectors.tsx b/src/util/metricSelectors.tsx index 252af6a6f4..82987e7f07 100644 --- a/src/util/metricSelectors.tsx +++ b/src/util/metricSelectors.tsx @@ -6,6 +6,7 @@ interface MemoryReport { | { free: number; total: number; + cached: number; } | undefined; disk: @@ -26,12 +27,14 @@ export const getInstanceMetrics = ( ?.metrics.find((item) => item.labels.name === instance.name)?.value; const memFree = memValue("lxd_memory_MemFree_bytes"); + const memCached = memValue("lxd_memory_Cached_bytes"); const memTotal = memValue("lxd_memory_MemTotal_bytes"); const memory = - memFree && memTotal + memFree && memTotal && memCached ? { free: memFree, total: memTotal, + cached: memCached, } : undefined;