diff --git a/src/pages/instances/InstanceOverviewDeviceDetail.tsx b/src/components/DeviceDetails.tsx similarity index 92% rename from src/pages/instances/InstanceOverviewDeviceDetail.tsx rename to src/components/DeviceDetails.tsx index 6f39ef06e0..226a2dde5c 100644 --- a/src/pages/instances/InstanceOverviewDeviceDetail.tsx +++ b/src/components/DeviceDetails.tsx @@ -9,7 +9,7 @@ interface Props { project: string; } -const InstanceOverviewDeviceDetail: FC = ({ device, project }) => { +const DeviceDetails: FC = ({ device, project }) => { if (device.type === "disk") { if (isRootDisk(device as FormDevice)) { return ( @@ -65,4 +65,4 @@ const InstanceOverviewDeviceDetail: FC = ({ device, project }) => { return "-"; }; -export default InstanceOverviewDeviceDetail; +export default DeviceDetails; diff --git a/src/components/DeviceListTable.tsx b/src/components/DeviceListTable.tsx new file mode 100644 index 0000000000..cb8d763adc --- /dev/null +++ b/src/components/DeviceListTable.tsx @@ -0,0 +1,99 @@ +import { FC } from "react"; +import { MainTable } from "@canonical/react-components"; +import { isRootDisk } from "util/instanceValidation"; +import { FormDevice } from "util/formDevices"; +import ResourceLink from "components/ResourceLink"; +import { isOtherDevice } from "util/devices"; +import DeviceDetails from "./DeviceDetails"; +import { useParams } from "react-router-dom"; +import { LxdDeviceValue, LxdDevices } from "types/device"; + +interface Props { + configBaseURL: string; + devices: LxdDevices; +} + +const DeviceListTable: FC = ({ configBaseURL, devices }) => { + const { project } = useParams<{ project: string }>(); + + const byTypeAndName = ( + a: [string, LxdDeviceValue], + b: [string, LxdDeviceValue], + ) => { + const nameA = a[0].toLowerCase(); + const nameB = b[0].toLowerCase(); + const typeA = a[1].type; + const typeB = b[1].type; + if (typeA === typeB) { + return nameA.localeCompare(nameB); + } + + return typeA.localeCompare(typeB); + }; + + // Identify non-NIC devices + const overviewDevices = Object.entries(devices ?? {}) + .filter(([_key, device]) => { + return device.type !== "nic" && device.type !== "none"; + }) + .sort(byTypeAndName); + + const hasDevices = overviewDevices.length > 0; + + const deviceHeaders = [ + { content: "Name", sortKey: "name", className: "u-text--muted" }, + { content: "Type", sortKey: "type", className: "u-text--muted" }, + { content: "Details", className: "u-text--muted" }, + ]; + + const deviceRows = overviewDevices.map(([devicename, device]) => { + return { + columns: [ + { + content: ( + + ), + role: "cell", + "aria-label": "Name", + }, + { + content: `${device.type} ${isRootDisk(device as FormDevice) ? "(root)" : ""}`, + + role: "cell", + "aria-label": "Type", + }, + { + content: ( + + ), + role: "cell", + "aria-label": "Details", + }, + ], + sortData: { + name: devicename.toLowerCase(), + type: device.type, + }, + }; + }); + + return ( + <> + {hasDevices && ( + + )} + {!hasDevices && <>-} + + ); +}; + +export default DeviceListTable; diff --git a/src/pages/instances/InstanceOverviewNetworks.tsx b/src/components/NetworkListTable.tsx similarity index 70% rename from src/pages/instances/InstanceOverviewNetworks.tsx rename to src/components/NetworkListTable.tsx index 7c7187460d..ea024d679a 100644 --- a/src/pages/instances/InstanceOverviewNetworks.tsx +++ b/src/components/NetworkListTable.tsx @@ -3,35 +3,38 @@ import { useQuery } from "@tanstack/react-query"; import { queryKeys } from "util/queryKeys"; import { MainTable } from "@canonical/react-components"; import Loader from "components/Loader"; -import { LxdInstance } from "types/instance"; import { fetchNetworks } from "api/networks"; import { isNicDevice } from "util/devices"; import ResourceLink from "components/ResourceLink"; +import { useParams } from "react-router-dom"; +import { LxdDevices } from "types/device"; interface Props { - instance: LxdInstance; onFailure: (title: string, e: unknown) => void; + devices: LxdDevices; } -const InstanceOverviewNetworks: FC = ({ instance, onFailure }) => { +const NetworkListTable: FC = ({ onFailure, devices }) => { + const { project } = useParams<{ project: string }>(); + const { data: networks = [], error, isLoading, } = useQuery({ - queryKey: [queryKeys.projects, instance.project, queryKeys.networks], - queryFn: () => fetchNetworks(instance.project), + queryKey: [queryKeys.projects, project, queryKeys.networks], + queryFn: () => fetchNetworks(project as string), }); if (error) { onFailure("Loading networks failed", error); } - const instanceNetworks = Object.values(instance.expanded_devices ?? {}) + const networkDevices = Object.values(devices ?? {}) .filter(isNicDevice) .map((network) => network.network); - const hasNetworks = instanceNetworks.length > 0; + const hasNetworks = networkDevices.length > 0; const networksHeaders = [ { content: "Name", sortKey: "name", className: "u-text--muted" }, @@ -49,9 +52,9 @@ const InstanceOverviewNetworks: FC = ({ instance, onFailure }) => { ]; const networksRows = networks - .filter((network) => instanceNetworks.includes(network.name)) + .filter((network) => networkDevices.includes(network.name)) .map((network) => { - const interfaceNames = Object.entries(instance.expanded_devices ?? {}) + const interfaceNames = Object.entries(devices ?? {}) .filter( ([_key, value]) => value.type === "nic" && value.network === network.name, @@ -65,25 +68,25 @@ const InstanceOverviewNetworks: FC = ({ instance, onFailure }) => { ), - role: "rowheader", + role: "cell", "aria-label": "Name", }, { content: interfaceNames.length > 0 ? interfaceNames.join(" ") : "-", - role: "rowheader", + role: "cell", "aria-label": "Interface", }, { content: network.type, - role: "rowheader", + role: "cell", "aria-label": "Type", }, { content: network.managed ? "Yes" : "No", - role: "rowheader", + role: "cell", "aria-label": "Managed", }, ], @@ -100,11 +103,16 @@ const InstanceOverviewNetworks: FC = ({ instance, onFailure }) => { <> {isLoading && } {!isLoading && hasNetworks && ( - + )} {!isLoading && !hasNetworks && <>-} ); }; -export default InstanceOverviewNetworks; +export default NetworkListTable; diff --git a/src/pages/instances/InstanceOverview.tsx b/src/pages/instances/InstanceOverview.tsx index d1bf79ce3d..385c986838 100644 --- a/src/pages/instances/InstanceOverview.tsx +++ b/src/pages/instances/InstanceOverview.tsx @@ -5,14 +5,15 @@ import { LxdInstance } from "types/instance"; import { instanceCreationTypes } from "util/instanceOptions"; import useEventListener from "@use-it/event-listener"; import { updateMaxHeight } from "util/updateMaxHeight"; -import InstanceOverviewNetworks from "./InstanceOverviewNetworks"; import InstanceOverviewProfiles from "./InstanceOverviewProfiles"; import InstanceOverviewMetrics from "./InstanceOverviewMetrics"; import InstanceIps from "pages/instances/InstanceIps"; import { useSettings } from "context/useSettings"; import NotificationRow from "components/NotificationRow"; -import InstanceOverviewDevices from "./InstanceOverviewDevices"; +import DeviceListTable from "components/DeviceListTable"; import ResourceLabel from "components/ResourceLabel"; +import NetworkListTable from "components/NetworkListTable"; +import { LxdDevices } from "types/device"; interface Props { instance: LxdInstance; @@ -131,7 +132,10 @@ const InstanceOverview: FC = ({ instance }) => {

Networks

- + @@ -139,7 +143,10 @@ const InstanceOverview: FC = ({ instance }) => {

Devices

- +
diff --git a/src/pages/instances/InstanceOverviewDevices.tsx b/src/pages/instances/InstanceOverviewDevices.tsx index ee0a3b7473..1ebda93780 100644 --- a/src/pages/instances/InstanceOverviewDevices.tsx +++ b/src/pages/instances/InstanceOverviewDevices.tsx @@ -6,7 +6,7 @@ import { isRootDisk } from "util/instanceValidation"; import { FormDevice } from "util/formDevices"; import ResourceLink from "components/ResourceLink"; import { isOtherDevice } from "util/devices"; -import InstanceOverviewDeviceDetail from "./InstanceOverviewDeviceDetail"; +import InstanceOverviewDeviceDetail from "components/DeviceDetails"; interface Props { instance: LxdInstance; diff --git a/src/pages/profiles/ProfileDetailOverview.tsx b/src/pages/profiles/ProfileDetailOverview.tsx index a4d9f2ddbd..88961b8656 100644 --- a/src/pages/profiles/ProfileDetailOverview.tsx +++ b/src/pages/profiles/ProfileDetailOverview.tsx @@ -1,6 +1,6 @@ import { FC, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; -import { Col, Notification, Row } from "@canonical/react-components"; +import { Col, Notification, Row, useNotify } from "@canonical/react-components"; import { LxdProfile } from "types/profile"; import useEventListener from "@use-it/event-listener"; import { updateMaxHeight } from "util/updateMaxHeight"; @@ -10,8 +10,8 @@ import classnames from "classnames"; import { CLOUD_INIT } from "./forms/ProfileFormMenu"; import { slugify } from "util/slugify"; import { getProfileInstances } from "util/usedBy"; -import ProfileNetworkList from "./ProfileNetworkList"; -import ProfileStorageList from "./ProfileStorageList"; +import NetworkListTable from "components/NetworkListTable"; +import DeviceListTable from "components/DeviceListTable"; interface Props { profile: LxdProfile; @@ -19,12 +19,17 @@ interface Props { } const ProfileDetailOverview: FC = ({ profile, featuresProfiles }) => { + const notify = useNotify(); const { project } = useParams<{ project: string }>(); if (!project) { return <>Missing project; } + const onFailure = (title: string, e: unknown) => { + notify.failure(title, e); + }; + const updateContentHeight = () => { updateMaxHeight("profile-overview-tab"); }; @@ -47,7 +52,7 @@ const ProfileDetailOverview: FC = ({ profile, featuresProfiles }) => {
{!featuresProfiles && ( - Modifications are only available in the{" "} + Modifications are only available in the default project @@ -75,27 +80,24 @@ const ProfileDetailOverview: FC = ({ profile, featuresProfiles }) => { - + + + +

Networks

+ + + + +
+

Devices

- - - - - - - - - - - -
Networks - -
Storage - -
+
diff --git a/src/sass/_device_detail_list.scss b/src/sass/_device_detail_list.scss new file mode 100644 index 0000000000..415c612891 --- /dev/null +++ b/src/sass/_device_detail_list.scss @@ -0,0 +1,15 @@ +.device-table { + th:nth-child(1) { + width: 30%; + } + + th:first-of-type, + td:first-of-type { + padding-left: 0; + } + + th:nth-child(2), + td:nth-child(2) { + width: 12rem; + } +} diff --git a/src/sass/_instance_detail_overview.scss b/src/sass/_instance_detail_overview.scss index 84b6a34a82..0186164923 100644 --- a/src/sass/_instance_detail_overview.scss +++ b/src/sass/_instance_detail_overview.scss @@ -54,13 +54,6 @@ } } - .devices { - th:nth-child(2), - td:nth-child(2) { - width: 12rem; - } - } - .profiles { th:nth-child(1) { width: 30%; diff --git a/src/sass/_network_detail_list.scss b/src/sass/_network_detail_list.scss new file mode 100644 index 0000000000..babeb7e59c --- /dev/null +++ b/src/sass/_network_detail_list.scss @@ -0,0 +1,10 @@ +.network-table { + th:nth-child(1) { + width: 30%; + } + + th:first-of-type, + td:first-of-type { + padding-left: 0; + } +} diff --git a/src/sass/_profile_detail_overview.scss b/src/sass/_profile_detail_overview.scss index ea71d08e82..9191766e61 100644 --- a/src/sass/_profile_detail_overview.scss +++ b/src/sass/_profile_detail_overview.scss @@ -51,6 +51,14 @@ } } + .devices { + border-bottom: 1px solid $color-mid-light; + } + + .networks { + border-bottom: 1px solid $color-mid-light; + } + .view-config { margin-top: $spv--x-small; } diff --git a/src/sass/styles.scss b/src/sass/styles.scss index f439dac63a..f658d51bef 100644 --- a/src/sass/styles.scss +++ b/src/sass/styles.scss @@ -77,6 +77,7 @@ $border-thin: 1px solid $color-mid-light !default; @import "detail_page"; @import "detail_panels"; @import "disk_device_form"; +@import "device_detail_list"; @import "empty_state"; @import "error_page"; @import "file_row"; @@ -93,6 +94,7 @@ $border-thin: 1px solid $color-mid-light !default; @import "meter"; @import "migrate_instance"; @import "modified_actions"; +@import "network_detail_list"; @import "network_detail_overview"; @import "network_form"; @import "network_forwards_form"; diff --git a/src/util/helpers.tsx b/src/util/helpers.tsx index c8e215e0fa..1b529df8e6 100644 --- a/src/util/helpers.tsx +++ b/src/util/helpers.tsx @@ -328,6 +328,12 @@ export const getInstanceDevices = ( return Object.entries(instance.expanded_devices ?? {}); }; +export const getProfileDevices = ( + profile: LxdProfile, +): [string, LxdDeviceValue][] => { + return Object.entries(profile.devices ?? {}); +}; + export const getRootPool = (instance: LxdInstance): string => { const rootStorage = Object.values(instance.expanded_devices ?? {}) .filter(isDiskDevice)