Add copy-to-clipboard buttons in RawLogTable for log line data and URLs (#1227)

Co-authored-by: Mike Shi <mike@hyperdx.io>
This commit is contained in:
Elizabet Oliveira 2025-10-10 15:50:34 +01:00 committed by GitHub
parent dbf16827a3
commit eaff49293c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 660 additions and 126 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": minor
---
Add toggle filters button, copy field, and per-row copy-to-clipboard for JSON data and modal URLs in RawLogTable

View file

@ -3,20 +3,12 @@
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
},
"rootDir": ".",
"outDir": "build",
"moduleResolution": "Node16"
},
"include": [
"src",
"migrations",
"scripts"
],
"exclude": [
"node_modules"
]
}
"include": ["src", "migrations", "scripts"],
"exclude": ["node_modules"]
}

View file

@ -1226,7 +1226,11 @@ function DBSearchPage() {
/>
)}
<OnboardingModal />
<form data-testid="search-form" onSubmit={onFormSubmit}>
<form
data-testid="search-form"
onSubmit={onFormSubmit}
className={searchPageStyles.searchForm}
>
{/* <DevTool control={control} /> */}
<Flex gap="sm" px="sm" pt="sm" wrap="nowrap">
<Group gap="4px" wrap="nowrap">
@ -1497,7 +1501,6 @@ function DBSearchPage() {
)}
<Flex
direction="column"
mt="sm"
style={{ overflow: 'hidden', height: '100%' }}
className="bg-hdx-dark"
>
@ -1535,12 +1538,8 @@ function DBSearchPage() {
{analysisMode === 'pattern' &&
histogramTimeChartConfig != null && (
<Flex direction="column" w="100%" gap="0px">
<Box style={{ height: 20, minHeight: 20 }} p="xs" pb="md">
<Group
justify="space-between"
mb={4}
style={{ width: '100%' }}
>
<Box className={searchPageStyles.searchStatsContainer}>
<Group justify="space-between" style={{ width: '100%' }}>
<SearchTotalCountChart
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
@ -1555,12 +1554,7 @@ function DBSearchPage() {
</Group>
</Box>
{!hasQueryError && (
<Box
style={{ height: 120, minHeight: 120 }}
p="xs"
pb="md"
mb="md"
>
<Box className={searchPageStyles.timeChartContainer}>
<DBTimeChart
sourceId={searchedConfig.source ?? undefined}
showLegend={false}
@ -1646,32 +1640,40 @@ function DBSearchPage() {
chartConfig &&
histogramTimeChartConfig && (
<>
<Box style={{ height: 20, minHeight: 20 }} p="xs" pb="md">
<Box className={searchPageStyles.searchStatsContainer}>
<Group
justify="space-between"
mb={4}
style={{ width: '100%' }}
>
<SearchTotalCountChart
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
/>
<SearchNumRows
config={{
...chartConfig,
dateRange: searchedTimeRange,
}}
enabled={isReady}
/>
<Group gap="sm" align="center">
{shouldShowLiveModeHint &&
analysisMode === 'results' &&
denoiseResults != true && (
<Button
size="compact-xs"
variant="outline"
onClick={handleResumeLiveTail}
>
<i className="bi text-success bi-lightning-charge-fill me-2" />
Resume Live Tail
</Button>
)}
<SearchNumRows
config={{
...chartConfig,
dateRange: searchedTimeRange,
}}
enabled={isReady}
/>
</Group>
</Group>
</Box>
{!hasQueryError && (
<Box
style={{ height: 120, minHeight: 120 }}
p="xs"
pb="md"
mb="md"
>
<Box className={searchPageStyles.timeChartContainer}>
<DBTimeChart
sourceId={searchedConfig.source ?? undefined}
showLegend={false}
@ -1803,31 +1805,6 @@ function DBSearchPage() {
</>
) : (
<>
{shouldShowLiveModeHint &&
analysisMode === 'results' &&
denoiseResults != true && (
<div
className="d-flex justify-content-center"
style={{ height: 0 }}
>
<div
style={{
position: 'relative',
top: -20,
zIndex: 2,
}}
>
<Button
size="compact-xs"
variant="outline"
onClick={handleResumeLiveTail}
>
<i className="bi text-success bi-lightning-charge-fill me-2" />
Resume Live Tail
</Button>
</div>
</div>
)}
{chartConfig &&
searchedConfig.source &&
dbSqlRowTableConfig &&

View file

@ -2,6 +2,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import cx from 'classnames';
import { Flex, Text, UnstyledButton } from '@mantine/core';
import { IconGripVertical } from '@tabler/icons-react';
import {
flexRender,
getCoreRowModel,

View file

@ -40,7 +40,11 @@ import 'react-modern-drawer/dist/index.css';
import styles from '@/../styles/LogSidePanel.module.scss';
export type RowSidePanelContextProps = {
onPropertyAddClick?: (keyPath: string, value: string) => void;
onPropertyAddClick?: (
keyPath: string,
value: string,
action?: 'only' | 'exclude' | 'include',
) => void;
generateSearchUrl?: ({
where,
whereLanguage,

View file

@ -47,9 +47,11 @@ import {
UnstyledButton,
} from '@mantine/core';
import {
IconChevronDown,
IconChevronUp,
IconDotsVertical,
IconCode,
IconDownload,
IconRotateClockwise,
IconSettings,
IconTextWrap,
} from '@tabler/icons-react';
import { FetchNextPageOptions, useQuery } from '@tanstack/react-query';
import {
@ -84,9 +86,10 @@ import {
logLevelColor,
useLocalStorage,
usePrevious,
useWindowSize,
} from '@/utils';
import DBRowTableFieldWithPopover from './DBTable/DBRowTableFieldWithPopover';
import DBRowTableRowButtons from './DBTable/DBRowTableRowButtons';
import TableHeader from './DBTable/TableHeader';
import { SQLPreview } from './ChartSQLPreview';
import { CsvExportButton } from './CsvExportButton';
@ -156,7 +159,7 @@ function inferLogLevelColumn(rows: Record<string, any>[]) {
return undefined;
}
const PatternTrendChartTooltip = (props: any) => {
const PatternTrendChartTooltip = () => {
return null;
};
@ -208,7 +211,7 @@ export const PatternTrendChart = ({
// tickFormatter={tick =>
// format(new Date(tick * 1000), 'MMM d HH:mm')
// }
tickFormatter={tick => ''}
tickFormatter={() => ''}
minTickGap={50}
tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }}
/>
@ -317,6 +320,7 @@ export const RawLogTable = memo(
onSortingChange,
sortOrder,
showExpandButton = true,
getRowWhere,
}: {
wrapLines?: boolean;
displayedColumns: string[];
@ -356,6 +360,7 @@ export const RawLogTable = memo(
enableSorting?: boolean;
sortOrder?: SortingState;
onSortingChange?: (v: SortingState | null) => void;
getRowWhere?: (row: Record<string, any>) => string;
}) => {
const generateRowMatcher = generateRowId;
@ -379,14 +384,12 @@ export const RawLogTable = memo(
}, [rows, dedupRows, generateRowMatcher]);
const _onRowExpandClick = useCallback(
({ __hyperdx_id, ...row }: Record<string, any>) => {
(row: Record<string, any>) => {
onRowDetailsClick?.(row);
},
[onRowDetailsClick],
);
const { width } = useWindowSize();
const isSmallScreen = (width ?? 1000) < 900;
const {
userPreferences: { isUTC },
} = useUserPreferences();
@ -749,32 +752,35 @@ export const RawLogTable = memo(
header={header}
isLast={isLast}
lastItemButtons={
<>
<Group gap={8} mr={8}>
{tableId &&
Object.keys(columnSizeStorage).length > 0 && (
<div
className="fs-8 text-muted-hover disabled"
role="button"
<UnstyledButton
onClick={() => setColumnSizeStorage({})}
title="Reset Column Widths"
>
<i className="bi bi-arrow-clockwise" />
</div>
<MantineTooltip label="Reset Column Widths">
<IconRotateClockwise size={16} />
</MantineTooltip>
</UnstyledButton>
)}
{config && (
<UnstyledButton
onClick={() => handleSqlModalOpen(true)}
title="Show generated SQL"
tabIndex={0}
>
<MantineTooltip label="Show generated SQL">
<i className="bi bi-code-square" />
<IconCode size={16} />
</MantineTooltip>
</UnstyledButton>
)}
<UnstyledButton
onClick={() => setWrapLinesEnabled(prev => !prev)}
title="Wrap lines"
>
<MantineTooltip label="Wrap lines">
<i className="bi bi-text-wrap" />
<IconTextWrap size={16} />
</MantineTooltip>
</UnstyledButton>
@ -786,19 +792,20 @@ export const RawLogTable = memo(
<MantineTooltip
label={`Download table as CSV (max ${maxRows.toLocaleString()} rows)${isLimited ? ' - data truncated' : ''}`}
>
<i className="bi bi-download" />
<IconDownload size={16} />
</MantineTooltip>
</CsvExportButton>
{onSettingsClick != null && (
<div
className="fs-8 text-muted-hover"
role="button"
<UnstyledButton
onClick={() => onSettingsClick()}
title="Settings"
>
<i className="bi bi-gear-fill" />
</div>
<MantineTooltip label="Settings">
<IconSettings size={16} />
</MantineTooltip>
</UnstyledButton>
)}
</>
</Group>
}
/>
);
@ -841,14 +848,17 @@ export const RawLogTable = memo(
</td>
)}
{/* Content columns grouped as one button */}
{/* Content columns grouped back to preserve row hover/click */}
<td
className="align-top overflow-hidden p-0"
colSpan={columns.length - (showExpandButton ? 1 : 0)}
>
<button
type="button"
className={styles.rowContentButton}
className={cx(styles.rowContentButton, {
[styles.isWrapped]: wrapLinesEnabled,
[styles.isTruncated]: !wrapLinesEnabled,
})}
onClick={e => {
e.stopPropagation();
_onRowExpandClick(row.original);
@ -857,25 +867,19 @@ export const RawLogTable = memo(
>
{row
.getVisibleCells()
.slice(showExpandButton ? 1 : 0) // Skip expand column
.map((cell, cellIndex) => {
.slice(showExpandButton ? 1 : 0) // Skip expand
.map(cell => {
const columnCustomClassName = (
cell.column.columnDef.meta as any
)?.className;
const columnSize = cell.column.getSize();
const totalContentCells =
row.getVisibleCells().length -
(showExpandButton ? 1 : 0);
const cellValue = cell.getValue<any>();
return (
<div
key={cell.id}
className={cx(
'flex-shrink-0 overflow-hidden',
{
'text-break': wrapLinesEnabled,
'text-truncate': !wrapLinesEnabled,
},
'flex-shrink-0 overflow-hidden position-relative',
columnCustomClassName,
)}
style={{
@ -889,13 +893,36 @@ export const RawLogTable = memo(
: 'none',
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
<div className={styles.fieldTextContainer}>
<DBRowTableFieldWithPopover
cellValue={cellValue}
wrapLinesEnabled={wrapLinesEnabled}
columnName={
(cell.column.columnDef.meta as any)
?.column
}
isChart={
(cell.column.columnDef.meta as any)
?.column === '__hdx_pattern_trend'
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</DBRowTableFieldWithPopover>
</div>
</div>
);
})}
{/* Row-level copy buttons */}
{getRowWhere && (
<DBRowTableRowButtons
row={row.original}
getRowWhere={getRowWhere}
sourceId={source?.id}
/>
)}
</button>
</td>
</tr>
@ -1402,6 +1429,7 @@ function DBSqlRowTableComponent({
enableSorting={true}
onSortingChange={_onSortingChange}
sortOrder={orderByArray}
getRowWhere={getRowWhere}
/>
</>
);

View file

@ -0,0 +1,214 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import cx from 'classnames';
import { Popover } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconCopy, IconFilter, IconFilterX } from '@tabler/icons-react';
import { RowSidePanelContext } from '../DBRowSidePanel';
import DBRowTableIconButton from './DBRowTableIconButton';
import styles from '../../../styles/LogTable.module.scss';
export interface DBRowTableFieldWithPopoverProps {
children: React.ReactNode;
cellValue: unknown;
wrapLinesEnabled: boolean;
columnName?: string;
isChart?: boolean;
}
export const DBRowTableFieldWithPopover = ({
children,
cellValue,
wrapLinesEnabled,
columnName,
isChart = false,
}: DBRowTableFieldWithPopoverProps) => {
const [opened, { close, open }] = useDisclosure(false);
const [isCopied, setIsCopied] = useState(false);
const [hoverDisabled, setHoverDisabled] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const hoverDisableTimeoutRef = useRef<NodeJS.Timeout>();
// Cleanup timeouts on unmount to prevent memory leaks
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (hoverDisableTimeoutRef.current) {
clearTimeout(hoverDisableTimeoutRef.current);
}
};
}, []);
// Get filter functionality from context
const { onPropertyAddClick } = useContext(RowSidePanelContext);
// Check if we have both the column name and filter function available
// Only show filter for ServiceName and SeverityText
const canFilter =
columnName &&
(columnName === 'ServiceName' || columnName === 'SeverityText') &&
onPropertyAddClick &&
cellValue != null;
const handleMouseEnter = () => {
if (hoverDisabled) return;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
open();
};
const handleMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
close();
}, 100); // Small delay to allow moving to popover
};
const handleClick = () => {
close();
setHoverDisabled(true);
if (hoverDisableTimeoutRef.current) {
clearTimeout(hoverDisableTimeoutRef.current);
}
// Prevent the popover from immediately reopening on hover for 1 second
// This gives users time to move their cursor or interact with modals
// without the popover interfering with their intended action
hoverDisableTimeoutRef.current = setTimeout(() => {
setHoverDisabled(false);
}, 1000);
};
const copyFieldValue = async () => {
try {
const value =
typeof cellValue === 'string' ? cellValue : String(cellValue ?? '');
await navigator.clipboard.writeText(value);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Optionally show an error toast notification to the user
}
};
const addFilter = () => {
if (canFilter) {
const value =
typeof cellValue === 'string' ? cellValue : String(cellValue ?? '');
onPropertyAddClick(columnName, value, 'include');
handleClick(); // Close the popover
}
};
const excludeFilter = () => {
if (canFilter) {
const value =
typeof cellValue === 'string' ? cellValue : String(cellValue ?? '');
onPropertyAddClick(columnName, value, 'exclude');
handleClick(); // Close the popover
}
};
const buttonSize = 20;
const gapSize = 4;
const numberOfButtons = canFilter ? 3 : 1; // Copy + Include Filter + Exclude Filter (if filtering available)
const numberOfGaps = numberOfButtons - 1;
// If it's a chart, just render the children without popover functionality
if (isChart) {
return (
<div
className={cx(styles.fieldText, {
[styles.chart]: isChart,
})}
style={{
display: 'flex',
alignItems: 'center',
}}
>
{children}
</div>
);
}
return (
<div
className={cx(styles.fieldText, {
[styles.truncated]: !wrapLinesEnabled && !isChart,
[styles.wrapped]: wrapLinesEnabled,
[styles.chart]: isChart,
})}
>
<Popover
width={buttonSize * numberOfButtons + gapSize * numberOfGaps}
position="top-start"
offset={5}
opened={opened}
zIndex={1}
>
<Popover.Target>
<span
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
tabIndex={-1}
aria-hidden="true"
style={{ cursor: 'pointer' }}
>
{children}
</span>
</Popover.Target>
<Popover.Dropdown
style={{ pointerEvents: 'auto' }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={styles.fieldTextPopover}
>
<div
style={{
display: 'flex',
gap: `${gapSize}px`,
alignItems: 'center',
}}
>
<DBRowTableIconButton
onClick={copyFieldValue}
variant="copy"
isActive={isCopied}
title={isCopied ? 'Copied!' : 'Copy field value'}
>
<IconCopy size={14} />
</DBRowTableIconButton>
{canFilter && (
<>
<DBRowTableIconButton
onClick={addFilter}
variant="copy"
title="Toggle filter for this value"
>
<IconFilter size={14} />
</DBRowTableIconButton>
<DBRowTableIconButton
onClick={excludeFilter}
variant="copy"
title="Exclude this value"
>
<IconFilterX size={14} />
</DBRowTableIconButton>
</>
)}
</div>
</Popover.Dropdown>
</Popover>
</div>
);
};
export default DBRowTableFieldWithPopover;

View file

@ -0,0 +1,63 @@
import React from 'react';
import cx from 'classnames';
import { Tooltip, UnstyledButton } from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
import styles from '../../../styles/LogTable.module.scss';
export interface DBRowTableIconButtonProps {
onClick: (e: React.MouseEvent) => void;
className?: string;
title?: string;
tabIndex?: number;
children: React.ReactNode;
variant?: 'copy' | 'default';
isActive?: boolean;
iconSize?: number;
}
export const DBRowTableIconButton: React.FC<DBRowTableIconButtonProps> = ({
onClick,
className,
title,
tabIndex = -1,
children,
variant = 'default',
isActive = false,
iconSize = 14,
}) => {
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
onClick(e);
};
const baseClasses =
variant === 'copy'
? cx('text-muted-hover', styles.iconActionButton, {
[styles.copied]: isActive,
})
: className;
return (
<Tooltip
label={title}
position="top"
withArrow
disabled={!title}
openDelay={300}
closeDelay={100}
fz="xs"
>
<UnstyledButton
onClick={handleClick}
className={baseClasses}
tabIndex={tabIndex}
>
{isActive ? <IconCheck size={iconSize} /> : children}
</UnstyledButton>
</Tooltip>
);
};
export default DBRowTableIconButton;

View file

@ -0,0 +1,105 @@
import React, { useState } from 'react';
import { IconCopy, IconLink } from '@tabler/icons-react';
import DBRowTableIconButton from './DBRowTableIconButton';
import styles from '../../../styles/LogTable.module.scss';
export interface DBRowTableRowButtonsProps {
row: Record<string, any>;
getRowWhere: (row: Record<string, any>) => string;
sourceId?: string;
}
export const DBRowTableRowButtons: React.FC<DBRowTableRowButtonsProps> = ({
row,
getRowWhere,
sourceId,
}) => {
const [isCopied, setIsCopied] = useState(false);
const [isUrlCopied, setIsUrlCopied] = useState(false);
const copyRowData = async () => {
try {
// Filter out internal metadata fields that start with __ or are generated IDs
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __hyperdx_id, ...cleanRow } = row;
// Parse JSON string fields to make them proper JSON objects
const parsedRow = Object.entries(cleanRow).reduce(
(acc, [key, value]) => {
if (
(typeof value === 'string' && value.startsWith('{')) ||
value.startsWith('[')
) {
try {
acc[key] = JSON.parse(value);
} catch {
// If parsing fails, keep the original string
acc[key] = value;
}
} else {
acc[key] = value;
}
return acc;
},
{} as Record<string, any>,
);
const rowData = JSON.stringify(parsedRow, null, 2);
await navigator.clipboard.writeText(rowData);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (error) {
console.error('Failed to copy row data to clipboard:', error);
// Optionally show an error toast notification to the user
}
};
const copyRowUrl = async () => {
try {
const rowWhere = getRowWhere(row);
const currentUrl = new URL(window.location.href);
// Add the row identifier as query parameters
currentUrl.searchParams.set('rowWhere', rowWhere);
if (sourceId) {
currentUrl.searchParams.set('rowSource', sourceId);
}
await navigator.clipboard.writeText(currentUrl.toString());
setIsUrlCopied(true);
setTimeout(() => setIsUrlCopied(false), 2000);
} catch (error) {
console.error('Failed to copy URL to clipboard:', error);
// Optionally show an error toast notification to the user
}
};
return (
<div className={styles.rowButtons}>
<DBRowTableIconButton
onClick={copyRowData}
variant="copy"
isActive={isCopied}
title={
isCopied ? 'Copied entire row as JSON!' : 'Copy entire row as JSON'
}
>
<IconCopy size={12} />
</DBRowTableIconButton>
<DBRowTableIconButton
onClick={copyRowUrl}
variant="copy"
isActive={isUrlCopied}
title={
isUrlCopied
? 'Copied shareable link!'
: 'Copy shareable link to this specific row'
}
>
<IconLink size={12} />
</DBRowTableIconButton>
</div>
);
};
export default DBRowTableRowButtons;

View file

@ -3,7 +3,7 @@ import { Button, Group, Text } from '@mantine/core';
import {
IconArrowDown,
IconArrowUp,
IconDotsVertical,
IconGripVertical,
} from '@tabler/icons-react';
import { flexRender, Header } from '@tanstack/react-table';
@ -87,7 +87,7 @@ export default function TableHeader({
header.column.getIsResizing() && 'isResizing',
)}
>
<IconDotsVertical size={12} />
<IconGripVertical size={12} />
</div>
)}
{isLast && (

View file

@ -10,6 +10,7 @@ export default function LogLevel({
return (
<Text
component="span"
size="xs"
c={
levelClass === 'error'

View file

@ -143,7 +143,12 @@ export default function useRowWhere({
);
return useCallback(
(row: Record<string, any>) => processRowToWhereClause(row, columnMap),
(row: Record<string, any>) => {
// Filter out synthetic columns that aren't in the database schema
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __hyperdx_id, ...dbRow } = row;
return processRowToWhereClause(dbRow, columnMap);
},
[columnMap],
);
}

View file

@ -1,22 +1,44 @@
@import './variables';
$button-height: 18px;
.table {
table-layout: fixed;
border-spacing: 0 2px;
border-spacing: 0;
border-collapse: separate;
}
.tableHead {
background: inherit;
background: var(--mantine-color-dark-8);
position: sticky;
top: 0;
z-index: 1;
height: 24px;
}
.tableRow {
position: relative;
&__selected {
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
font-weight: bold;
}
&:hover .rowButtons {
opacity: 1;
}
&:hover .rowContentButton {
background-color: var(--mantine-color-dark-5);
}
&:has(.expandButton:hover) .rowButtons {
opacity: 0 !important;
}
&:has(.expandButton:hover) .rowContentButton {
background-color: transparent !important;
}
}
.expandedRowContent {
@ -37,21 +59,22 @@
justify-content: center;
transition: background-color 0.15s ease-in-out;
border-radius: 4px;
min-height: $button-height;
&:hover {
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
}
&:focus,
&:focus-visible {
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
outline: none;
}
&:active {
outline: none;
box-shadow: none;
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
}
svg {
@ -66,8 +89,8 @@
.expandButtonSeparator {
width: 1px;
height: 12px;
border-right: 1px solid $slate-800;
height: calc($button-height - 8px);
border-right: 1px solid var(--mantine-color-dark-4);
margin-left: 2px;
margin-right: 2px;
display: inline-block;
@ -80,28 +103,123 @@
padding: 0;
margin: 0;
width: 100%;
height: 100%;
cursor: pointer;
display: flex;
align-items: stretch;
text-align: left;
color: inherit;
transition: background-color 0.15s ease-in-out;
border-radius: 4px;
min-height: $button-height;
display: flex;
align-items: center;
position: relative;
&:hover {
background-color: $slate-800;
&:hover .rowButtons {
opacity: 1;
}
&:focus,
&:focus-visible {
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
outline: none;
}
&:active {
outline: none;
box-shadow: none;
background-color: $slate-800;
background-color: var(--mantine-color-dark-5);
}
&.isWrapped {
align-items: flex-start;
}
&.isTruncated {
align-items: flex-start;
}
}
.rowButtons {
position: absolute;
top: 2px;
right: 0;
padding-left: 10px;
padding-right: 4px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
z-index: 0;
align-items: flex-start;
&:hover {
opacity: 1;
}
}
.iconActionButton {
padding: 4px;
border-radius: 4px;
background-color: var(--mantine-color-dark-4);
border: 1px solid var(--mantine-color-dark-3);
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease-in-out;
pointer-events: auto;
&:hover {
transform: scale(1.05);
}
&.copied {
color: var(--mantine-color-green-6);
}
}
.fieldTextContainer {
position: relative;
display: block;
width: 100%;
}
.fieldText {
overflow: hidden;
padding: 2px 4px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
> span:hover {
border-radius: 4px;
background-color: color-mix(
in srgb,
var(--mantine-color-dark-2) 30%,
transparent
);
}
&.chart {
overflow: visible;
min-height: 50px;
}
}
.fieldText.truncated {
text-overflow: ellipsis;
white-space: nowrap;
min-height: $button-height;
}
.fieldText.wrapped {
word-break: break-word;
white-space: normal;
display: flex;
}
.fieldTextPopover {
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
}

View file

@ -14,7 +14,7 @@
flex-grow: 0;
border-right: 1px solid var(--mantine-color-dark-6);
overflow: hidden;
z-index: 1;
z-index: 3; // higher z-index to be above other elements
:global {
.mantine-TextInput-wrapper {
@ -151,3 +151,24 @@
transform: scale(1);
}
}
.searchStatsContainer {
background-color: $bg-hdx-dark;
padding-inline: var(--mantine-spacing-xs);
padding-block: var(--mantine-spacing-xxs);
z-index: 3;
}
.timeChartContainer {
background-color: $bg-hdx-dark;
padding-inline: var(--mantine-spacing-xs);
padding-block: var(--mantine-spacing-xxs);
height: 120px;
z-index: 3;
}
.searchForm {
background-color: $body-bg;
z-index: 4;
padding-bottom: var(--mantine-spacing-xs);
}