Skip to content

Commit

Permalink
feat: [WD-16007] Replace storage list with device list on profile det… (
Browse files Browse the repository at this point in the history
#968)

…ail page.

Work done (So far):
- Separate Devices section into Networks and Other devices (Currently
storage)
- Created a generic NetworkListTable file to generate a network list for
instance and profile overview pages.
- Note, the file ProfileNetworkList still exists for the
ProfileDetailPanelContent component.
- Create a generic DeviceListTable file to generate a device list for
instance and profile overview pages.

Fixes [list issues/bugs if needed]

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- [List the steps to QA the new feature(s) or prove that a bug has been
resolved]

## Screenshots


![image](https://github.com/user-attachments/assets/bf25d3a4-01bd-46d8-ad2c-533d3df7948a)

![image](https://github.com/user-attachments/assets/44f52f71-cff7-4531-843f-a0c4cfc80cf0)
  • Loading branch information
Kxiru authored Nov 11, 2024
2 parents 49e0323 + 0e20d58 commit 6171657
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Props {
project: string;
}

const InstanceOverviewDeviceDetail: FC<Props> = ({ device, project }) => {
const DeviceDetails: FC<Props> = ({ device, project }) => {
if (device.type === "disk") {
if (isRootDisk(device as FormDevice)) {
return (
Expand Down Expand Up @@ -65,4 +65,4 @@ const InstanceOverviewDeviceDetail: FC<Props> = ({ device, project }) => {
return "-";
};

export default InstanceOverviewDeviceDetail;
export default DeviceDetails;
99 changes: 99 additions & 0 deletions src/components/DeviceListTable.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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: (
<ResourceLink
type="device"
value={devicename}
to={`${configBaseURL}/${isOtherDevice(device) ? "other" : device.type}`}
/>
),
role: "cell",
"aria-label": "Name",
},
{
content: `${device.type} ${isRootDisk(device as FormDevice) ? "(root)" : ""}`,

role: "cell",
"aria-label": "Type",
},
{
content: (
<DeviceDetails device={device} project={project as string} />
),
role: "cell",
"aria-label": "Details",
},
],
sortData: {
name: devicename.toLowerCase(),
type: device.type,
},
};
});

return (
<>
{hasDevices && (
<MainTable
headers={deviceHeaders}
rows={deviceRows}
className={"device-table"}
sortable
/>
)}
{!hasDevices && <>-</>}
</>
);
};

export default DeviceListTable;
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ instance, onFailure }) => {
const NetworkListTable: FC<Props> = ({ 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" },
Expand All @@ -49,9 +52,9 @@ const InstanceOverviewNetworks: FC<Props> = ({ 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,
Expand All @@ -65,25 +68,25 @@ const InstanceOverviewNetworks: FC<Props> = ({ instance, onFailure }) => {
<ResourceLink
type="network"
value={network.name}
to={`/ui/project/${instance.project}/network/${network.name}`}
to={`/ui/project/${project}/network/${network.name}`}
/>
),
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",
},
],
Expand All @@ -100,11 +103,16 @@ const InstanceOverviewNetworks: FC<Props> = ({ instance, onFailure }) => {
<>
{isLoading && <Loader text="Loading networks..." />}
{!isLoading && hasNetworks && (
<MainTable headers={networksHeaders} rows={networksRows} sortable />
<MainTable
headers={networksHeaders}
rows={networksRows}
sortable
className={"network-table"}
/>
)}
{!isLoading && !hasNetworks && <>-</>}
</>
);
};

export default InstanceOverviewNetworks;
export default NetworkListTable;
15 changes: 11 additions & 4 deletions src/pages/instances/InstanceOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -131,15 +132,21 @@ const InstanceOverview: FC<Props> = ({ instance }) => {
<h2 className="p-heading--5">Networks</h2>
</Col>
<Col size={7}>
<InstanceOverviewNetworks instance={instance} onFailure={onFailure} />
<NetworkListTable
devices={instance.expanded_devices as LxdDevices}
onFailure={onFailure}
/>
</Col>
</Row>
<Row className="networks">
<Col size={3}>
<h2 className="p-heading--5">Devices</h2>
</Col>
<Col size={7}>
<InstanceOverviewDevices instance={instance} />
<DeviceListTable
configBaseURL={`/ui/project/${instance.project}/instance/${instance?.name}/configuration`}
devices={instance.expanded_devices as LxdDevices}
/>
</Col>
</Row>
<Row className="profiles">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/instances/InstanceOverviewDevices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
44 changes: 23 additions & 21 deletions src/pages/profiles/ProfileDetailOverview.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,21 +10,26 @@ 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;
featuresProfiles: boolean;
}

const ProfileDetailOverview: FC<Props> = ({ 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");
};
Expand All @@ -47,7 +52,7 @@ const ProfileDetailOverview: FC<Props> = ({ profile, featuresProfiles }) => {
<div className="profile-overview-tab">
{!featuresProfiles && (
<Notification severity="caution" title="Inherited profile">
Modifications are only available in the{" "}
Modifications are only available in the
<Link to={`/ui/project/default/profile/${profile.name}`}>
default project
</Link>
Expand Down Expand Up @@ -75,27 +80,24 @@ const ProfileDetailOverview: FC<Props> = ({ profile, featuresProfiles }) => {
</table>
</Col>
</Row>
<Row className="section">

<Row className="networks">
<Col size={3}>
<h2 className="p-heading--5">Networks</h2>
</Col>
<Col size={7}>
<NetworkListTable devices={profile.devices} onFailure={onFailure} />
</Col>
</Row>
<Row className="devices">
<Col size={3}>
<h2 className="p-heading--5">Devices</h2>
</Col>
<Col size={7}>
<table>
<tbody>
<tr className="list-wrapper">
<th className="u-text--muted">Networks</th>
<td>
<ProfileNetworkList profile={profile} project={project} />
</td>
</tr>
<tr className="list-wrapper">
<th className="u-text--muted">Storage</th>
<td>
<ProfileStorageList profile={profile} project={project} />
</td>
</tr>
</tbody>
</table>
<DeviceListTable
configBaseURL={`/ui/project/${project}/profile/${profile.name}/configuration`}
devices={profile.devices}
/>
</Col>
</Row>
<Row className="section">
Expand Down
15 changes: 15 additions & 0 deletions src/sass/_device_detail_list.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 0 additions & 7 deletions src/sass/_instance_detail_overview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@
}
}

.devices {
th:nth-child(2),
td:nth-child(2) {
width: 12rem;
}
}

.profiles {
th:nth-child(1) {
width: 30%;
Expand Down
Loading

0 comments on commit 6171657

Please sign in to comment.