Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions frontend/src/components/common/CopyLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<TestContext>
<Story />
</TestContext>
),
],
} as Meta;

const Template: StoryFn<CopyLabelProps> = args => <CopyLabel {...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)',
};
59 changes: 59 additions & 0 deletions frontend/src/components/common/CopyLabel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
'&:hover .copy-button': {
opacity: 1,
},
}}
>
<Box component="span">{children}</Box>
<Box
className="copy-button"
component="span"
sx={{
opacity: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this box is always invisible and why does it have className

transition: 'opacity 0.2s',
display: 'inline-flex',
alignItems: 'center',
}}
>
<CopyButton text={textToCopy} buttonStyle="icon" />
</Box>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
};
13 changes: 12 additions & 1 deletion frontend/src/components/common/NameValueTable/NameValueTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -99,6 +103,7 @@ export default function NameValueTable(props: NameValueTableProps) {
withHighlightStyle = false,
valueFullRow = false,
valueCellProps = {},
copyValue,
},
i
) => {
Expand Down Expand Up @@ -200,7 +205,13 @@ export default function NameValueTable(props: NameValueTableProps) {
{...otherValueCellProps}
{...valueCellProps}
>
<Value value={value} />
{copyValue ? (
<CopyLabel textToCopy={copyValue}>
<Value value={value} />
</CopyLabel>
) : (
<Value value={value} />
)}
</Grid>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<body>
<div>
<dl
class="MuiGrid-root MuiGrid-container css-kxuems-MuiGrid-root"
>
<dt
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-4 css-1iczkge-MuiGrid-root"
>
IP Address
</dt>
<dd
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-deb4a-MuiGrid-root"
>
<div
class="MuiBox-root css-10we5a7"
>
<span
class="MuiBox-root css-0"
>
<span
class="MuiTypography-root MuiTypography-body1 css-e06lsu-MuiTypography-root"
>
192.168.1.1
</span>
</span>
<span
class="copy-button MuiBox-root css-18ri021"
>
<button
aria-label="Copy to clipboard"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</span>
</div>
</dd>
<dt
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-4 css-1iczkge-MuiGrid-root"
>
Pod Name
</dt>
<dd
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-deb4a-MuiGrid-root"
>
<div
class="MuiBox-root css-10we5a7"
>
<span
class="MuiBox-root css-0"
>
<span
class="MuiTypography-root MuiTypography-body1 css-e06lsu-MuiTypography-root"
>
my-pod-12345
</span>
</span>
<span
class="copy-button MuiBox-root css-18ri021"
>
<button
aria-label="Copy to clipboard"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</span>
</div>
</dd>
<dt
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-4 css-iqixpy-MuiGrid-root"
>
Regular Value
</dt>
<dd
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-1xrovmc-MuiGrid-root"
>
<span
class="MuiTypography-root MuiTypography-body1 css-e06lsu-MuiTypography-root"
>
Not copyable
</span>
</dd>
</dl>
</div>
</body>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<body>
<div>
<div
class="MuiBox-root css-10we5a7"
>
<span
class="MuiBox-root css-0"
>
192.168.1.1
</span>
<span
class="copy-button MuiBox-root css-18ri021"
>
<button
aria-label="Copy to clipboard"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</body>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<body>
<div>
<div
class="MuiBox-root css-10we5a7"
>
<span
class="MuiBox-root css-0"
>
my-pod (copy name)
</span>
<span
class="copy-button MuiBox-root css-18ri021"
>
<button
aria-label="Copy to clipboard"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</body>
1 change: 1 addition & 0 deletions frontend/src/components/common/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const checkExports = [
'Chart',
'ConfirmDialog',
'ConfirmButton',
'CopyLabel',
'CreateResourceButton',
'Dialog',
'EmptyContent',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/endpointSlices/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 ? (
<CopyLabel textToCopy={addresses}>{addresses}</CopyLabel>
) : (
''
);
},
},
{
label: t('Conditions'),
Expand Down
Loading
Loading