From b67a46700734280141ff8ce04b6b9ccde4cc3634 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 1 Oct 2025 12:38:24 +0000
Subject: [PATCH 1/5] Initial plan
From 3f85770dcbb994c5fb0d8900778b81fd2a6a8367 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 1 Oct 2025 12:48:03 +0000
Subject: [PATCH 2/5] Add CopyLabel component and copyValue property to
NameValueTable
Co-authored-by: joaquimrocha <1029635+joaquimrocha@users.noreply.github.com>
---
.../components/common/CopyLabel.stories.tsx | 46 +++++++++++++++
frontend/src/components/common/CopyLabel.tsx | 59 +++++++++++++++++++
.../NameValueTable/NameValueTable.stories.tsx | 20 +++++++
.../common/NameValueTable/NameValueTable.tsx | 13 +++-
frontend/src/components/common/index.test.ts | 1 +
frontend/src/components/common/index.ts | 2 +
6 files changed, 140 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/components/common/CopyLabel.stories.tsx
create mode 100644 frontend/src/components/common/CopyLabel.tsx
diff --git a/frontend/src/components/common/CopyLabel.stories.tsx b/frontend/src/components/common/CopyLabel.stories.tsx
new file mode 100644
index 00000000000..311a10101fa
--- /dev/null
+++ b/frontend/src/components/common/CopyLabel.stories.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Meta, StoryFn } from '@storybook/react';
+import { TestContext } from '../../test';
+import CopyLabel, { CopyLabelProps } from './CopyLabel';
+
+export default {
+ title: 'CopyLabel',
+ component: CopyLabel,
+ argTypes: {},
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+} as Meta;
+
+const Template: StoryFn = args => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ textToCopy: '192.168.1.1',
+ children: '192.168.1.1',
+};
+
+export const WithDifferentContent = Template.bind({});
+WithDifferentContent.args = {
+ textToCopy: 'my-pod-12345',
+ children: 'my-pod (copy name)',
+};
diff --git a/frontend/src/components/common/CopyLabel.tsx b/frontend/src/components/common/CopyLabel.tsx
new file mode 100644
index 00000000000..74d2d455c4a
--- /dev/null
+++ b/frontend/src/components/common/CopyLabel.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Box from '@mui/material/Box';
+import React, { ReactNode } from 'react';
+import CopyButton from './Resource/CopyButton';
+
+export interface CopyLabelProps {
+ /** The text to copy when the copy button is clicked */
+ textToCopy: string;
+ /** The content to display (can be different from textToCopy) */
+ children: ReactNode;
+}
+
+/**
+ * A component that displays content with a copy button that appears on hover.
+ * This is useful for values that users may want to copy, like IPs, resource names, etc.
+ */
+export default function CopyLabel({ textToCopy, children }: CopyLabelProps) {
+ return (
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/NameValueTable/NameValueTable.stories.tsx b/frontend/src/components/common/NameValueTable/NameValueTable.stories.tsx
index 3e306475e51..0a515d579f3 100644
--- a/frontend/src/components/common/NameValueTable/NameValueTable.stories.tsx
+++ b/frontend/src/components/common/NameValueTable/NameValueTable.stories.tsx
@@ -72,3 +72,23 @@ WithHiddenLastChildren.args = {
},
],
};
+
+export const WithCopyableValue = Template.bind({});
+WithCopyableValue.args = {
+ rows: [
+ {
+ name: 'IP Address',
+ value: '192.168.1.1',
+ copyValue: '192.168.1.1',
+ },
+ {
+ name: 'Pod Name',
+ value: 'my-pod-12345',
+ copyValue: 'my-pod-12345',
+ },
+ {
+ name: 'Regular Value',
+ value: 'Not copyable',
+ },
+ ],
+};
diff --git a/frontend/src/components/common/NameValueTable/NameValueTable.tsx b/frontend/src/components/common/NameValueTable/NameValueTable.tsx
index bc942b86c75..017928eb9ef 100644
--- a/frontend/src/components/common/NameValueTable/NameValueTable.tsx
+++ b/frontend/src/components/common/NameValueTable/NameValueTable.tsx
@@ -17,6 +17,7 @@
import { GridProps } from '@mui/material/Grid';
import Grid from '@mui/material/Grid';
import React, { ReactNode } from 'react';
+import CopyLabel from '../CopyLabel';
import { ValueLabel } from '../Label';
// TODO: use ReactNode after migration to react 18
@@ -35,6 +36,9 @@ export interface NameValueTableRow {
withHighlightStyle?: boolean;
/** The ID to use for the name element, useful for accessibility */
nameID?: string;
+ /** If provided, shows a copy button on hover that copies this text.
+ * Useful for values like IPs, resource names, etc. that users may want to copy. */
+ copyValue?: string;
}
export interface NameValueTableProps {
@@ -99,6 +103,7 @@ export default function NameValueTable(props: NameValueTableProps) {
withHighlightStyle = false,
valueFullRow = false,
valueCellProps = {},
+ copyValue,
},
i
) => {
@@ -200,7 +205,13 @@ export default function NameValueTable(props: NameValueTableProps) {
{...otherValueCellProps}
{...valueCellProps}
>
-
+ {copyValue ? (
+
+
+
+ ) : (
+
+ )}
);
}
diff --git a/frontend/src/components/common/index.test.ts b/frontend/src/components/common/index.test.ts
index 1d8b6e2a18e..f9e90530703 100644
--- a/frontend/src/components/common/index.test.ts
+++ b/frontend/src/components/common/index.test.ts
@@ -36,6 +36,7 @@ const checkExports = [
'Chart',
'ConfirmDialog',
'ConfirmButton',
+ 'CopyLabel',
'CreateResourceButton',
'Dialog',
'EmptyContent',
diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts
index 4366e948074..b5d466f17a5 100644
--- a/frontend/src/components/common/index.ts
+++ b/frontend/src/components/common/index.ts
@@ -18,6 +18,8 @@ export * from './ActionButton';
export { default as ActionButton } from './ActionButton';
export * from './BackLink';
export * from './Chart';
+export * from './CopyLabel';
+export { default as CopyLabel } from './CopyLabel';
export * from './Dialog';
export * from './EmptyContent';
export { default as EmptyContent } from './EmptyContent';
From 90a203f680b24aa9ff47f3ace4e9c7f8525e1b77 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 1 Oct 2025 12:52:14 +0000
Subject: [PATCH 3/5] Add copy buttons to IPs in Service, Endpoint,
EndpointSlice, Node, and Pod details
Co-authored-by: joaquimrocha <1029635+joaquimrocha@users.noreply.github.com>
---
frontend/src/components/endpointSlices/Details.tsx | 11 +++++++++--
frontend/src/components/endpoints/Details.tsx | 8 +++++++-
frontend/src/components/node/Details.tsx | 1 +
frontend/src/components/pod/Details.tsx | 8 ++++++++
frontend/src/components/service/Details.tsx | 2 ++
5 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/endpointSlices/Details.tsx b/frontend/src/components/endpointSlices/Details.tsx
index 8d722ae8cbb..d99bab6ce93 100644
--- a/frontend/src/components/endpointSlices/Details.tsx
+++ b/frontend/src/components/endpointSlices/Details.tsx
@@ -18,7 +18,7 @@ import Box from '@mui/material/Box';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import EndpointSlice from '../../lib/k8s/endpointSlices';
-import { SectionBox, SimpleTable, StatusLabel } from '../common';
+import { CopyLabel, SectionBox, SimpleTable, StatusLabel } from '../common';
import { DetailsGrid } from '../common/Resource';
export default function EndpointSliceDetails(props: {
@@ -71,7 +71,14 @@ export default function EndpointSliceDetails(props: {
},
{
label: t('Addresses'),
- getter: endpoint => endpoint.addresses?.join(','),
+ getter: endpoint => {
+ const addresses = endpoint.addresses?.join(',');
+ return addresses ? (
+ {addresses}
+ ) : (
+ ''
+ );
+ },
},
{
label: t('Conditions'),
diff --git a/frontend/src/components/endpoints/Details.tsx b/frontend/src/components/endpoints/Details.tsx
index 9357f85527d..cd89f7dcf59 100644
--- a/frontend/src/components/endpoints/Details.tsx
+++ b/frontend/src/components/endpoints/Details.tsx
@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { ResourceClasses } from '../../lib/k8s';
import Endpoints, { KubeEndpoint } from '../../lib/k8s/endpoints';
+import CopyLabel from '../common/CopyLabel';
import Empty from '../common/EmptyContent';
import Link from '../common/Link';
import { DetailsGrid } from '../common/Resource';
@@ -68,7 +69,12 @@ export default function EndpointDetails(props: {
columns={[
{
label: t('IP'),
- getter: address => address.ip,
+ getter: address =>
+ address.ip ? (
+ {address.ip}
+ ) : (
+ ''
+ ),
},
{
label: t('Hostname'),
diff --git a/frontend/src/components/node/Details.tsx b/frontend/src/components/node/Details.tsx
index b88b92041bd..a120cb6e77a 100644
--- a/frontend/src/components/node/Details.tsx
+++ b/frontend/src/components/node/Details.tsx
@@ -80,6 +80,7 @@ export default function NodeDetails(props: { name?: string; cluster?: string })
return {
name: type,
value: address,
+ copyValue: address,
};
}) || []
);
diff --git a/frontend/src/components/pod/Details.tsx b/frontend/src/components/pod/Details.tsx
index aaebcac3325..583c2559f8c 100644
--- a/frontend/src/components/pod/Details.tsx
+++ b/frontend/src/components/pod/Details.tsx
@@ -542,6 +542,7 @@ export default function PodDetails(props: PodDetailsProps) {
{
name: t('Host IP'),
value: item.status.hostIP ?? '',
+ copyValue: item.status.hostIP ?? '',
},
]),
// Always include Host IPs, but hide if empty
@@ -550,6 +551,9 @@ export default function PodDetails(props: PodDetailsProps) {
value: item.status.hostIPs
? item.status.hostIPs.map((ipObj: { ip: string }) => ipObj.ip).join(', ')
: '',
+ copyValue: item.status.hostIPs
+ ? item.status.hostIPs.map((ipObj: { ip: string }) => ipObj.ip).join(', ')
+ : '',
hideLabel: !item.status.hostIPs || item.status.hostIPs.length === 0,
},
// Show Pod IP only if Pod IPs doesn't exist or is empty
@@ -559,6 +563,7 @@ export default function PodDetails(props: PodDetailsProps) {
{
name: t('Pod IP'),
value: item.status.podIP ?? '',
+ copyValue: item.status.podIP ?? '',
},
]),
// Always include Pod IPs, but hide if empty
@@ -567,6 +572,9 @@ export default function PodDetails(props: PodDetailsProps) {
value: item.status.podIPs
? item.status.podIPs.map((ipObj: { ip: string }) => ipObj.ip).join(', ')
: '',
+ copyValue: item.status.podIPs
+ ? item.status.podIPs.map((ipObj: { ip: string }) => ipObj.ip).join(', ')
+ : '',
hideLabel: !item.status.podIPs || item.status.podIPs.length === 0,
},
{
diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx
index c592851c8f9..de14d7529a6 100644
--- a/frontend/src/components/service/Details.tsx
+++ b/frontend/src/components/service/Details.tsx
@@ -70,10 +70,12 @@ export default function ServiceDetails(props: {
{
name: t('Cluster IP'),
value: item.spec.clusterIP,
+ copyValue: item.spec.clusterIP,
},
{
name: t('External IP'),
value: item.getExternalAddresses(),
+ copyValue: item.getExternalAddresses(),
hide: _.isEmpty,
},
{
From 4c760629405adbcbd78107004730a0f57c5cc397 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 1 Oct 2025 12:55:20 +0000
Subject: [PATCH 4/5] Update snapshots for copy button feature
Co-authored-by: joaquimrocha <1029635+joaquimrocha@users.noreply.github.com>
---
...eTable.WithCopyableValue.stories.storyshot | 96 +++++++++++++++++++
.../CopyLabel.Default.stories.storyshot | 28 ++++++
...bel.WithDifferentContent.stories.storyshot | 28 ++++++
.../EndpointDetails.Default.stories.storyshot | 50 +++++++++-
.../PodDetails.Error.stories.storyshot | 62 ++++++++++--
.../PodDetails.Initializing.stories.storyshot | 62 ++++++++++--
...odDetails.LivenessFailed.stories.storyshot | 62 ++++++++++--
.../PodDetails.PullBackOff.stories.storyshot | 62 ++++++++++--
.../PodDetails.Running.stories.storyshot | 62 ++++++++++--
.../ServiceDetails.Default.stories.storyshot | 62 ++++++++++--
...tails.ErrorWithEndpoints.stories.storyshot | 62 ++++++++++--
.../plugin/__snapshots__/pluginLib.snapshot | 1 +
12 files changed, 579 insertions(+), 58 deletions(-)
create mode 100644 frontend/src/components/common/NameValueTable/__snapshots__/NameValueTable.WithCopyableValue.stories.storyshot
create mode 100644 frontend/src/components/common/__snapshots__/CopyLabel.Default.stories.storyshot
create mode 100644 frontend/src/components/common/__snapshots__/CopyLabel.WithDifferentContent.stories.storyshot
diff --git a/frontend/src/components/common/NameValueTable/__snapshots__/NameValueTable.WithCopyableValue.stories.storyshot b/frontend/src/components/common/NameValueTable/__snapshots__/NameValueTable.WithCopyableValue.stories.storyshot
new file mode 100644
index 00000000000..ddc444e6cf7
--- /dev/null
+++ b/frontend/src/components/common/NameValueTable/__snapshots__/NameValueTable.WithCopyableValue.stories.storyshot
@@ -0,0 +1,96 @@
+
+
+
+
+ IP Address
+
+
+
+
+
+ 192.168.1.1
+
+
+
+
+
+
+
+
+ Pod Name
+
+
+
+
+
+ my-pod-12345
+
+
+
+
+
+
+
+
+ Regular Value
+
+
+
+ Not copyable
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/common/__snapshots__/CopyLabel.Default.stories.storyshot b/frontend/src/components/common/__snapshots__/CopyLabel.Default.stories.storyshot
new file mode 100644
index 00000000000..d9dd946a695
--- /dev/null
+++ b/frontend/src/components/common/__snapshots__/CopyLabel.Default.stories.storyshot
@@ -0,0 +1,28 @@
+
+
+
+
+ 192.168.1.1
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/common/__snapshots__/CopyLabel.WithDifferentContent.stories.storyshot b/frontend/src/components/common/__snapshots__/CopyLabel.WithDifferentContent.stories.storyshot
new file mode 100644
index 00000000000..d1628dd89ac
--- /dev/null
+++ b/frontend/src/components/common/__snapshots__/CopyLabel.WithDifferentContent.stories.storyshot
@@ -0,0 +1,28 @@
+
+
+
+
+ my-pod (copy name)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/endpoints/__snapshots__/EndpointDetails.Default.stories.storyshot b/frontend/src/components/endpoints/__snapshots__/EndpointDetails.Default.stories.storyshot
index f8101dae7e6..a7416a738ef 100644
--- a/frontend/src/components/endpoints/__snapshots__/EndpointDetails.Default.stories.storyshot
+++ b/frontend/src/components/endpoints/__snapshots__/EndpointDetails.Default.stories.storyshot
@@ -283,7 +283,30 @@