Skip to content

Commit

Permalink
Add copy buttons to lookout UI (#4094)
Browse files Browse the repository at this point in the history
Add copy icon buttons, which appear on hover, next to appropriate values in the jobs table and in the key-value pair rows in the details sidebar. Also add a copy button for the job ID at the top of the sidebar, which always remains visible.

When the user clicks the button, a tooltip displays *Copied!* to confirm to the user that the value has been copied to their clipboard.

Co-authored-by: Maurice Yap <[email protected]>
  • Loading branch information
mauriceyap and Maurice Yap authored Dec 13, 2024
1 parent 3bddd53 commit 81c2942
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 20 deletions.
39 changes: 39 additions & 0 deletions internal/lookout/ui/src/components/CopyIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState } from "react"

import { ContentCopy } from "@mui/icons-material"
import { IconButton, IconButtonProps, styled, Tooltip } from "@mui/material"

const LEAVE_DELAY_MS = 1_000

const StyledIconButton = styled(IconButton)<IconButtonProps & { hidden: boolean }>(({ hidden }) => ({
padding: 0,
visibility: hidden ? "hidden" : "unset",
}))

export interface CopyIconButtonProps {
content: string
size?: IconButtonProps["size"]
onClick?: IconButtonProps["onClick"]
hidden?: boolean
}

export const CopyIconButton = ({ content, size, onClick, hidden = false }: CopyIconButtonProps) => {
const [tooltipOpen, setTooltipOpen] = useState(false)

return (
<Tooltip title="Copied!" onClose={() => setTooltipOpen(false)} open={tooltipOpen} leaveDelay={LEAVE_DELAY_MS}>
<StyledIconButton
size={size}
onClick={(e) => {
onClick?.(e)
navigator.clipboard.writeText(content)
setTooltipOpen(true)
}}
aria-label="copy"
hidden={hidden && !tooltipOpen}
>
<ContentCopy fontSize="inherit" />
</StyledIconButton>
</Tooltip>
)
}
32 changes: 32 additions & 0 deletions internal/lookout/ui/src/components/CopyableValueOnHover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactNode, useState } from "react"

import { styled } from "@mui/material"

import { CopyIconButton, CopyIconButtonProps } from "./CopyIconButton"

const OuterContainer = styled("div")({
display: "flex",
flexDirection: "row",
gap: "1ch",
})

export interface CopyableValueOnHoverProps {
children: ReactNode
copyContent: string
onCopyButtonClick?: CopyIconButtonProps["onClick"]
}

export const CopyableValueOnHover = ({ children, copyContent, onCopyButtonClick }: CopyableValueOnHoverProps) => {
const [copyIconButtonHidden, setCopyIconButtonHidden] = useState(true)
return (
<OuterContainer
onMouseEnter={() => setCopyIconButtonHidden(false)}
onMouseLeave={() => setCopyIconButtonHidden(true)}
>
<div>{children}</div>
<div>
<CopyIconButton content={copyContent} size="small" onClick={onCopyButtonClick} hidden={copyIconButtonHidden} />
</div>
</OuterContainer>
)
}
14 changes: 11 additions & 3 deletions internal/lookout/ui/src/components/lookoutV2/JobsTableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { RefObject } from "react"
import { KeyboardArrowRight, KeyboardArrowDown } from "@mui/icons-material"
import { TableCell, IconButton, TableSortLabel, Box } from "@mui/material"
import { Cell, ColumnResizeMode, flexRender, Header, Row } from "@tanstack/react-table"
import { JobRow, JobTableRow } from "models/jobsTableModels"
import { Match } from "models/lookoutV2Models"
import { getColumnMetadata, toColId } from "utils/jobsTableColumns"

import styles from "./JobsTableCell.module.css"
import { JobsTableFilter } from "./JobsTableFilter"
import { JobRow, JobTableRow } from "../../models/jobsTableModels"
import { Match } from "../../models/lookoutV2Models"
import { getColumnMetadata, toColId } from "../../utils/jobsTableColumns"
import { matchForColumn } from "../../utils/jobsTableUtils"
import { CopyableValueOnHover } from "../CopyableValueOnHover"

const sharedCellStyle = {
padding: 0,
Expand Down Expand Up @@ -254,6 +255,13 @@ export const BodyCell = ({ cell, rowIsGroup, rowIsExpanded, onExpandedChange, on
...cell.getContext(),
onClickRowCheckbox,
})
) : !rowIsGroup && Boolean(cell.getValue()) && columnMetadata.allowCopy ? (
<CopyableValueOnHover copyContent={String(cell.getValue())} onCopyButtonClick={(e) => e.stopPropagation()}>
{flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
onClickRowCheckbox,
})}
</CopyableValueOnHover>
) : (
flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const SingleContainerDetails = ({ container, openByDefault }: { container: Conta
{
key: "image",
value: container.image,
allowCopy: true,
},
]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { Table, TableBody, TableCell, TableRow, Link } from "@mui/material"
import validator from "validator"

import styles from "./KeyValuePairTable.module.css"
import { CopyableValueOnHover } from "../../CopyableValueOnHover"

export interface KeyValuePairTable {
data: {
key: string
value: string
isAnnotation?: boolean
allowCopy?: boolean
}[]
}

Expand All @@ -22,17 +24,23 @@ export const KeyValuePairTable = ({ data }: KeyValuePairTable) => {
return (
<Table size="small">
<TableBody>
{data.map(({ key, value, isAnnotation }) => {
{data.map(({ key, value, isAnnotation, allowCopy }) => {
const nodeToDisplay =
isAnnotation && validator.isURL(value) ? (
<Link href={ensureAbsoluteLink(value)} target="_blank">
{value}
</Link>
) : (
<span>{value}</span>
)
return (
<TableRow key={key}>
<TableCell className={styles.cell}>{key}</TableCell>
<TableCell className={styles.cell}>
{isAnnotation && validator.isURL(value) ? (
<Link href={ensureAbsoluteLink(value)} target="_blank">
{value}
</Link>
{allowCopy ? (
<CopyableValueOnHover copyContent={value}>{nodeToDisplay}</CopyableValueOnHover>
) : (
<span>{value}</span>
nodeToDisplay
)}
</TableCell>
</TableRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { memo, ReactNode } from "react"

import { Close } from "@mui/icons-material"
import { Box, IconButton, Typography } from "@mui/material"
import { Job } from "models/lookoutV2Models"
import { formatJobState, formatTimeSince } from "utils/jobsTableFormatters"

import { Job } from "../../../models/lookoutV2Models"
import { formatJobState, formatTimeSince } from "../../../utils/jobsTableFormatters"
import { CopyIconButton } from "../../CopyIconButton"
import { JobStateLabel } from "../JobStateLabel"

export interface SidebarHeaderProps {
Expand All @@ -16,7 +17,19 @@ export interface SidebarHeaderProps {
export const SidebarHeader = memo(({ job, onClose, className }: SidebarHeaderProps) => {
return (
<Box className={className}>
<HeaderSection title={"Job ID"} value={<Box sx={{ wordBreak: "break-all" }}>{job.jobId}</Box>} />
<HeaderSection
title="Job ID"
value={
<div style={{ display: "flex", flexDirection: "row", gap: "1ch" }}>
<div>
<Box sx={{ wordBreak: "break-all" }}>{job.jobId}</Box>
</div>
<div>
<CopyIconButton content={job.jobId} size="small" />
</div>
</div>
}
/>
<HeaderSection
title={"State"}
value={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export interface SidebarTabJobDetailsProps {

export const SidebarTabJobDetails = ({ job }: SidebarTabJobDetailsProps) => {
const details = [
{ key: "Queue", value: job.queue },
{ key: "Job Set", value: job.jobSet },
{ key: "Owner", value: job.owner },
...(job.namespace ? [{ key: "Namespace", value: job.namespace }] : []),
{ key: "Queue", value: job.queue, allowCopy: true },
{ key: "Job Set", value: job.jobSet, allowCopy: true },
{ key: "Owner", value: job.owner, allowCopy: true },
...(job.namespace ? [{ key: "Namespace", value: job.namespace, allowCopy: true }] : []),
{ key: "Priority", value: job.priority.toString() },
{ key: "Run Count", value: job.runs.length.toString() },
...(job.cancelReason ? [{ key: "Cancel Reason", value: job.cancelReason }] : []),
...(job.cancelReason ? [{ key: "Cancel Reason", value: job.cancelReason, allowCopy: true }] : []),
]
return (
<>
Expand All @@ -39,6 +39,7 @@ export const SidebarTabJobDetails = ({ job }: SidebarTabJobDetailsProps) => {
key: annotationKey,
value: job.annotations[annotationKey],
isAnnotation: true,
allowCopy: true,
}))}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export const SidebarTabJobResult = ({
<AccordionDetails sx={{ padding: 0 }}>
<KeyValuePairTable
data={[
{ key: "Run ID", value: run.runId },
{ key: "Run ID", value: run.runId, allowCopy: true },
{ key: "State", value: formatJobRunState(run.jobRunState) },
{ key: "Leased (UTC)", value: formatUtcDate(run.leased) },
{ key: "Pending (UTC)", value: formatUtcDate(run.pending) },
Expand All @@ -246,8 +246,8 @@ export const SidebarTabJobResult = ({
value:
run.started && run.finished ? formatTimeSince(run.started, new Date(run.finished).getTime()) : "",
},
{ key: "Cluster", value: run.cluster },
{ key: "Node", value: run.node ?? "" },
{ key: "Cluster", value: run.cluster, allowCopy: true },
{ key: "Node", value: run.node ?? "", allowCopy: true },
{ key: "Exit code", value: run.exitCode?.toString() ?? "" },
].filter((pair) => pair.value !== "")}
/>
Expand Down
14 changes: 14 additions & 0 deletions internal/lookout/ui/src/utils/jobsTableColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum FilterType {

export interface JobTableColumnMetadata {
displayName: string
allowCopy?: boolean
isRightAligned?: boolean

filterType?: FilterType
Expand Down Expand Up @@ -172,6 +173,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalMetadata: {
filterType: FilterType.Text,
defaultMatchType: Match.StartsWith,
allowCopy: true,
},
}),
accessorColumn({
Expand All @@ -186,6 +188,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalMetadata: {
filterType: FilterType.Text,
defaultMatchType: Match.StartsWith,
allowCopy: true,
},
}),
accessorColumn({
Expand All @@ -201,6 +204,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalMetadata: {
filterType: FilterType.Text,
defaultMatchType: Match.StartsWith,
allowCopy: true,
},
}),
accessorColumn({
Expand All @@ -215,6 +219,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalMetadata: {
filterType: FilterType.Text,
defaultMatchType: Match.Exact, // Job ID does not support startsWith
allowCopy: true,
},
}),
accessorColumn({
Expand Down Expand Up @@ -305,6 +310,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalMetadata: {
filterType: FilterType.Text,
defaultMatchType: Match.StartsWith,
allowCopy: true,
},
}),
accessorColumn({
Expand Down Expand Up @@ -363,6 +369,7 @@ export const JOB_COLUMNS: JobTableColumn[] = [
},
additionalMetadata: {
filterType: FilterType.Text,
allowCopy: true,
},
}),
accessorColumn({
Expand Down Expand Up @@ -405,6 +412,9 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalOptions: {
size: 200,
},
additionalMetadata: {
allowCopy: true,
},
}),
accessorColumn({
id: StandardColumnId.Cluster,
Expand All @@ -413,6 +423,9 @@ export const JOB_COLUMNS: JobTableColumn[] = [
additionalOptions: {
size: 200,
},
additionalMetadata: {
allowCopy: true,
},
}),
accessorColumn({
id: StandardColumnId.ExitCode,
Expand Down Expand Up @@ -569,6 +582,7 @@ export const createAnnotationColumn = (annotationKey: string): JobTableColumn =>
},
filterType: FilterType.Text,
defaultMatchType: Match.StartsWith,
allowCopy: true,
},
})
}
Expand Down

0 comments on commit 81c2942

Please sign in to comment.