Skip to content

Commit b5d5517

Browse files
authored
feat(cluster-link) rich chip (canonical#2013)
## Done - Add clusterlink rich chip ## 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 @Kxiru or @edlerd for access. - With a local copy of this branch, [build and run as described in the docs](https://github.com/canonical/lxd-ui/blob/main/CONTRIBUTING.md#setting-up-for-development). 2. Perform the following QA steps: - The rich chip exists in the following places - Replicators detail and list pages. - Replicator create/delete modal. - Replicator create/edit toast notification. - Image registry detail page. ## Screenshots <img width="1139" height="687" alt="Screenshot from 2026-06-01 13-18-03" src="https://github.com/user-attachments/assets/55636895-6764-4feb-8b6a-bf6aada65fac" /> <img width="1139" height="687" alt="Screenshot from 2026-06-01 13-17-55" src="https://github.com/user-attachments/assets/9b9aecb6-ccd8-48bf-8668-dbe3e607d62e" />
2 parents 84a9df4 + 78af7eb commit b5d5517

15 files changed

Lines changed: 170 additions & 49 deletions

src/pages/cluster/ClusterLinkAddresses.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface Props {
1010
const ClusterLinkAddresses: FC<Props> = ({ clusterLink }) => {
1111
const addresses = clusterLink.config["volatile.addresses"];
1212
if (!addresses || addresses === "") {
13-
return null;
13+
return "-";
1414
}
1515

1616
return (

src/pages/cluster/ClusterLinkList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import EditClusterLinkPanel from "pages/cluster/panels/EditClusterLinkPanel";
2828
import { useClusterLinks } from "context/useClusterLinks";
2929
import ClusterLinkAddresses from "pages/cluster/ClusterLinkAddresses";
3030
import CreateClusterLink from "pages/cluster/CreateClusterLink";
31-
import { getLinkIdentity } from "util/clusterLinkStatus";
31+
import { getLinkIdentity } from "util/clusterLink";
3232

3333
interface Props {
3434
variant?: "main" | "panel";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { FC } from "react";
2+
import { Tooltip } from "@canonical/react-components";
3+
import { useIsScreenBelow } from "context/useIsScreenBelow";
4+
import ResourceLink from "components/ResourceLink";
5+
import { SMALL_TOOLTIP_BREAKPOINT } from "components/RichTooltipTable";
6+
import ClusterLinkRichTooltip from "./ClusterLinkRichTooltip";
7+
import { ROOT_PATH } from "util/rootPath";
8+
9+
interface Props {
10+
clusterLink: string;
11+
className?: string;
12+
disabled?: boolean;
13+
}
14+
15+
const ClusterLinkRichChip: FC<Props> = ({
16+
clusterLink,
17+
className,
18+
disabled,
19+
}) => {
20+
const showTooltip = !useIsScreenBelow(SMALL_TOOLTIP_BREAKPOINT, "height");
21+
22+
const url = `${ROOT_PATH}/ui/cluster/links`;
23+
const resourceLink = (
24+
<ResourceLink
25+
type="cluster-link"
26+
value={clusterLink}
27+
to={url}
28+
hasTitle={!showTooltip}
29+
className={className}
30+
disabled={disabled}
31+
/>
32+
);
33+
34+
if (!showTooltip) {
35+
return <>{resourceLink}</>;
36+
}
37+
38+
return (
39+
<Tooltip
40+
zIndex={1000}
41+
position="right"
42+
message={<ClusterLinkRichTooltip clusterLink={clusterLink} />}
43+
>
44+
{resourceLink}
45+
</Tooltip>
46+
);
47+
};
48+
49+
export default ClusterLinkRichChip;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type FC } from "react";
2+
import { Spinner } from "@canonical/react-components";
3+
import { type TooltipRow } from "components/RichTooltipRow";
4+
import { RichTooltipTable } from "components/RichTooltipTable";
5+
import { Link } from "react-router-dom";
6+
import ItemName from "components/ItemName";
7+
import { useClusterLink } from "context/useClusterLinks";
8+
import ClusterLinkStatus from "./ClusterLinkStatus";
9+
import ResourceLabel from "components/ResourceLabel";
10+
import { ROOT_PATH } from "util/rootPath";
11+
import ClusterLinkAddresses from "./ClusterLinkAddresses";
12+
import { getLinkIdentity } from "util/clusterLink";
13+
import { useIdentities } from "context/useIdentities";
14+
15+
interface Props {
16+
clusterLink: string;
17+
}
18+
19+
const ClusterLinkRichTooltip: FC<Props> = ({ clusterLink }) => {
20+
const { data: link, isLoading: isLinkLoading } = useClusterLink(clusterLink);
21+
const { data: identities = [] } = useIdentities();
22+
23+
if (!link && !isLinkLoading) {
24+
return (
25+
<>
26+
Cluster link <ResourceLabel type="cluster-link" value={clusterLink} />{" "}
27+
not found
28+
</>
29+
);
30+
}
31+
32+
const identity = getLinkIdentity(identities, clusterLink);
33+
const authGroupCount = identity?.groups?.length ?? 0;
34+
35+
const rows: TooltipRow[] = [
36+
{
37+
title: "Cluster link",
38+
value: link ? (
39+
<Link
40+
to={`${ROOT_PATH}/ui/cluster/links`}
41+
onClick={(e) => {
42+
e.stopPropagation();
43+
}}
44+
>
45+
<ItemName item={{ name: clusterLink }} />
46+
</Link>
47+
) : (
48+
<Spinner />
49+
),
50+
valueTitle: clusterLink,
51+
},
52+
{
53+
title: "Status",
54+
value: link ? <ClusterLinkStatus link={link} /> : "-",
55+
className: "status-row",
56+
},
57+
{
58+
title: "Description",
59+
value: link?.description || "-",
60+
valueTitle: link?.description || "",
61+
},
62+
{
63+
title: "Type",
64+
value: link?.type || "-",
65+
},
66+
{
67+
title: "Addresses",
68+
value: link ? <ClusterLinkAddresses clusterLink={link} /> : "-",
69+
},
70+
{
71+
title: "Auth groups",
72+
value: authGroupCount,
73+
},
74+
];
75+
76+
return (
77+
<RichTooltipTable rows={rows} className="cluster-link-rich-tooltip-table" />
78+
);
79+
};
80+
81+
export default ClusterLinkRichTooltip;

src/pages/cluster/ClusterLinkStatus.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Icon, Spinner } from "@canonical/react-components";
33
import type { LxdClusterLink, StatusCaption } from "types/cluster";
44
import { useClusterLinkState } from "context/useClusterLinks";
55
import { useIdentities } from "context/useIdentities";
6-
import { getClusterLinksStatus, getLinkIdentity } from "util/clusterLinkStatus";
6+
import { getClusterLinksStatus, getLinkIdentity } from "util/clusterLink";
77

88
interface Props {
99
link: LxdClusterLink;

src/pages/cluster/ReplicatorDetail.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ import { useParams } from "react-router-dom";
33
import { Col, CustomLayout, Row, Spinner } from "@canonical/react-components";
44
import NotFound from "components/NotFound";
55
import NotificationRow from "components/NotificationRow";
6-
import ResourceLink from "components/ResourceLink";
76
import { useClusterLink } from "context/useClusterLinks";
87
import { useReplicator } from "context/useReplicators";
98
import ClusterLinkStatus from "pages/cluster/ClusterLinkStatus";
109
import ReplicatorDetailHeader from "pages/cluster/ReplicatorDetailHeader";
1110
import ReplicatorRunTime from "pages/cluster/ReplicatorRunTime";
1211
import ReplicatorStatus from "pages/cluster/ReplicatorStatus";
1312
import ProjectRichChip from "pages/projects/ProjectRichChip";
14-
import { ROOT_PATH } from "util/rootPath";
13+
import ClusterLinkRichChip from "./ClusterLinkRichChip";
1514

1615
const ReplicatorDetail: FC = () => {
1716
const { project, name } = useParams<{
@@ -92,10 +91,8 @@ const ReplicatorDetail: FC = () => {
9291
<td>
9392
{replicator.config?.cluster ? (
9493
<div className="u-flex">
95-
<ResourceLink
96-
type="cluster-link"
97-
value={replicator.config?.cluster}
98-
to={`${ROOT_PATH}/ui/cluster/links`}
94+
<ClusterLinkRichChip
95+
clusterLink={replicator.config?.cluster}
9996
/>
10097
</div>
10198
) : (

src/pages/cluster/ReplicatorList.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ReplicatorStatus from "pages/cluster/ReplicatorStatus";
2222
import { useDocs } from "context/useDocs";
2323
import ProjectRichChip from "pages/projects/ProjectRichChip";
2424
import { ROOT_PATH } from "util/rootPath";
25-
import ResourceLink from "components/ResourceLink";
25+
import ClusterLinkRichChip from "./ClusterLinkRichChip";
2626

2727
interface Props {
2828
variant?: "main" | "panel";
@@ -93,10 +93,8 @@ const ReplicatorList: FC<Props> = ({ variant = "main" }) => {
9393
},
9494
{
9595
content: (
96-
<ResourceLink
97-
type="cluster-link"
98-
value={replicator.config?.cluster || "unknown"}
99-
to={`${ROOT_PATH}/ui/cluster/links`}
96+
<ClusterLinkRichChip
97+
clusterLink={replicator.config?.cluster || "unknown"}
10098
/>
10199
),
102100
role: "cell",

src/pages/cluster/actions/DeleteClusterLinkBtn.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ import {
88
useNotify,
99
useToastNotification,
1010
} from "@canonical/react-components";
11-
import ResourceLink from "components/ResourceLink";
1211
import type { LxdClusterLink } from "types/cluster";
1312
import ResourceLabel from "components/ResourceLabel";
14-
import { ROOT_PATH } from "util/rootPath";
1513
import { useClusterLinkEntitlements } from "util/entitlements/cluster-links";
14+
import ClusterLinkRichChip from "pages/cluster/ClusterLinkRichChip";
1615

1716
interface Props {
1817
clusterLink: LxdClusterLink;
@@ -34,11 +33,7 @@ const DeleteClusterLinkBtn: FC<Props> = ({ clusterLink }) => {
3433
toastNotify.success(
3534
<>
3635
Cluster link{" "}
37-
<ResourceLink
38-
type="cluster-link"
39-
value={clusterLink.name}
40-
to={`${ROOT_PATH}/ui/cluster/links`}
41-
/>{" "}
36+
<ResourceLabel type="cluster-link" value={clusterLink.name} />{" "}
4237
deleted.
4338
</>,
4439
);
@@ -66,7 +61,7 @@ const DeleteClusterLinkBtn: FC<Props> = ({ clusterLink }) => {
6661
children: (
6762
<p>
6863
This will permanently delete cluster link{" "}
69-
<ResourceLabel type="cluster-link" value={clusterLink.name} bold />.
64+
<ClusterLinkRichChip clusterLink={clusterLink.name} />.
7065
</p>
7166
),
7267
confirmButtonLabel: canDelete

src/pages/cluster/modals/CreateClusterLinkModal.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
Notification,
88
Tabs,
99
} from "@canonical/react-components";
10-
import ResourceLabel from "components/ResourceLabel";
1110
import CodeSnippetWithCopyButton from "components/CodeSnippetWithCopyButton";
1211
import ConfirmationCheckbox from "components/ConfirmationCheckbox";
12+
import ClusterLinkRichChip from "../ClusterLinkRichChip";
1313

1414
interface Props {
1515
onClose: () => void;
@@ -26,9 +26,7 @@ const CreateClusterLinkModal: FC<Props> = ({ onClose, token, linkName }) => {
2626
className="create-cluster-link"
2727
title={
2828
<>
29-
Cluster link{" "}
30-
<ResourceLabel type="cluster-link" value={linkName} bold truncate />{" "}
31-
created
29+
Cluster link <ClusterLinkRichChip clusterLink={linkName} /> created
3230
</>
3331
}
3432
buttonRow={[
@@ -62,8 +60,8 @@ const CreateClusterLinkModal: FC<Props> = ({ onClose, token, linkName }) => {
6260
/>
6361
<Notification severity="caution" title="Copy the trust token">
6462
The trust token can be used to establish the cluster link{" "}
65-
<ResourceLabel type="cluster-link" value={linkName} bold /> on the
66-
target cluster. <br />
63+
<ClusterLinkRichChip clusterLink={linkName} /> on the target
64+
cluster. <br />
6765
Make sure to copy it now as you will not be able to see this again.
6866
</Notification>
6967
<Accordion

src/pages/cluster/panels/CreateClusterLinkPanel.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ import { useFormik } from "formik";
1717
import { queryKeys } from "util/queryKeys";
1818
import { createClusterLink } from "api/cluster-links";
1919
import { base64EncodeObject, checkDuplicateName } from "util/helpers";
20-
import ResourceLink from "components/ResourceLink";
2120
import ClusterLinkForm, {
2221
type ClusterLinkFormValues,
2322
} from "pages/cluster/ClusterLinkForm";
24-
import { ROOT_PATH } from "util/rootPath";
23+
import ClusterLinkRichChip from "../ClusterLinkRichChip";
2524

2625
interface Props {
2726
onSuccess: (identityName: string, token: string) => void;
@@ -83,12 +82,7 @@ const CreateClusterLinkPanel: FC<Props> = ({ onSuccess }) => {
8382
} else {
8483
toastNotify.success(
8584
<>
86-
Cluster link{" "}
87-
<ResourceLink
88-
type="cluster-link"
89-
value={values.name}
90-
to={`${ROOT_PATH}/ui/cluster/links`}
91-
/>{" "}
85+
Cluster link <ClusterLinkRichChip clusterLink={values.name} />{" "}
9286
created.
9387
</>,
9488
);

0 commit comments

Comments
 (0)