mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Enable horizontal scrolling on search results table (#1871)
## Summary
Closes HDX-2701
Enables horizontal scrolling on the search results table so that column content is no longer clipped when the viewport is narrower than the total column widths, and improves table header styling, resize interactions, and scrollbar aesthetics.
### Changes
- **Dynamic `minWidth` on the table element** — Computes the sum of all column widths from TanStack Table's sizing state (substituting a 200px minimum for the flexible last column) and sets it as an inline `min-width` on the `<table>`. When the container is narrower than this threshold, the table overflows and the wrapper scrolls horizontally. When wider, `width: 100%` ensures the table fills the container normally.
- **Dynamic last column width** — Uses a `ResizeObserver` to track container width and computes the last column size as the remaining space after all other columns, instead of using the `UNDEFINED_WIDTH` sentinel. This ensures the last column fills remaining space responsively while respecting a 200px minimum.
- **Minimum column width** — Added `MIN_COLUMN_WIDTH` (50px) via TanStack Table's `defaultColumn.minSize` to prevent columns from being resized below a usable size. Explicitly set `minSize: 32` on the expand button column to prevent it from inheriting the 50px default.
- **Last column resizing** — Removed the `!isLast` guard so the last column now has a resize handle, making all columns consistently resizable.
- **Resize handle redesign** — Replaced the `IconGripVertical` drag icon with a minimal 1px vertical line using a CSS `::after` pseudo-element, styled with the new `--color-border-emphasis` token.
- **`min-width: 0` on flex containers** — Added `miw={0}` to the `DBSearchPage` results and pattern containers so flex children can shrink below their content size, allowing overflow to trigger scrolling.
- **Consolidate utility classes into SCSS module** — Moved `overflow: auto`, `height: 100%`, and `font-size` from inline Bootstrap utility classes into `.tableWrapper`, and moved `width: 100%` into `.table` in `LogTable.module.scss`.
- **Replace `Button` with `UnstyledButton` for sort headers** — Replaced Mantine `Button` with `UnstyledButton` for column sort headers, with custom SCSS that only darkens text on hover (no background).
- **Consolidate header styles into `TableHeader.module.scss`** — Moved `.cursorColResize` from `Table.module.scss` and `.headerCellWithAction`/`.headerRemoveButton` from `LogTable.module.scss` into a new `TableHeader.module.scss`, co-located with `TableHeader.tsx`.
- **Refactor `CsvExportButton`** — Removed the `UnstyledButton` wrapper around `CSVDownloader` to eliminate invalid `<a>` inside `<button>` markup. The `CSVDownloader` is now the root element with inline flex styling.
- **New `--color-border-emphasis` design token** — Added a slightly more prominent border color token for UI elements like resize handles, defined across HyperDX and ClickStack themes in both light and dark modes.
- **Global thin scrollbar styling** — Added app-wide custom scrollbar styles in `globals.css` for both WebKit and Firefox, providing thin (6px), rounded, semi-transparent scrollbars using theme color tokens.
### Files changed
- `packages/app/src/components/DBRowTable.tsx` — Added `containerWidth` tracking via `ResizeObserver`; added `tableMinWidth` and `lastColumnWidth` computations; set inline `minWidth` on table; added `defaultColumn.minSize`; moved utility classes to SCSS
- `packages/app/src/DBSearchPage.tsx` — Added `miw={0}` to results and pattern container Flex wrappers
- `packages/app/src/components/DBTable/TableHeader.tsx` — Replaced `Button` with `UnstyledButton`; removed `IconGripVertical`; enabled last column resizing; consolidated all style imports to `TableHeader.module.scss`
- `packages/app/src/components/DBTable/TableHeader.module.scss` *(new)* — Styles for `.sortButton`, `.resizer` (with `::after` pseudo-element line), `.headerCellWithAction`, and `.headerRemoveButton`
- `packages/app/src/components/CsvExportButton.tsx` — Removed `UnstyledButton` wrapper; `CSVDownloader` is now the root element with flex layout
- `packages/app/src/components/ExpandableRowTable.tsx` — Added `minSize: 32` to expand button column to prevent inheriting 50px default
- `packages/app/src/components/Table.module.scss` — Removed `.cursorColResize` (moved to `TableHeader.module.scss`)
- `packages/app/src/tableUtils.tsx` — Added `MIN_LAST_COLUMN_WIDTH` and `MIN_COLUMN_WIDTH` constants
- `packages/app/src/theme/themes/hyperdx/_tokens.scss` — Added `--color-border-emphasis` token
- `packages/app/src/theme/themes/clickstack/_tokens.scss` — Added `--color-border-emphasis` token
- `packages/app/src/theme/semanticColorsGrouped.ts` — Registered `color-border-emphasis` in borders group
- `packages/app/styles/LogTable.module.scss` — Added `overflow`, `height`, `font-size` to `.tableWrapper`; added `width: 100%` to `.table`; removed header styles (moved to `TableHeader.module.scss`)
- `packages/app/styles/globals.css` — Added global thin scrollbar styles for WebKit and Firefox
## Test plan
- [x] Open the Search page with multiple columns selected (e.g. Timestamp, ServiceName, SeverityText, Body, ScopeName)
- [x] Narrow the browser window — verify a horizontal scrollbar appears and columns are not cut off
- [x] Scroll horizontally — verify all column content is accessible
- [x] Widen the browser window — verify the table fills the container and no unnecessary scrollbar appears
- [x] Verify the last column still expands to fill remaining space on wide viewports
- [x] Resize columns via drag handles — verify horizontal scroll adjusts dynamically and columns cannot be resized below ~50px
- [x] Resize the **last** column — verify it now has a resize handle and works correctly
- [x] Verify the resize handle appears as a thin vertical line (not the old grip icon)
- [x] Hover over column sort headers — verify text darkens with no background change
- [x] Hover over column headers with remove button — verify remove button appears on hover
- [x] Click "Download table as CSV" — verify it works without layout issues
- [x] Verify scrollbars across the app are thin, rounded, and semi-transparent
- [x] Verify wrap lines toggle still works correctly
- [x] Switch to Event Patterns analysis mode — verify no layout regressions
- [x] Check the table in other contexts (Dashboard tiles, Pattern side panel) to confirm no layout regressions
This commit is contained in:
parent
2227f747d5
commit
1e0f8ec79b
14 changed files with 270 additions and 108 deletions
5
.changeset/search-table-horizontal-scroll.md
Normal file
5
.changeset/search-table-horizontal-scroll.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: enable horizontal scrolling on search results table for small screens
|
||||
|
|
@ -1812,7 +1812,7 @@ function DBSearchPage() {
|
|||
</ErrorBoundary>
|
||||
{analysisMode === 'pattern' &&
|
||||
histogramTimeChartConfig != null && (
|
||||
<Flex direction="column" w="100%" gap="0px" mih="0">
|
||||
<Flex direction="column" w="100%" gap="0px" mih="0" miw={0}>
|
||||
<Box className={searchPageStyles.searchStatsContainer}>
|
||||
<Group justify="space-between" style={{ width: '100%' }}>
|
||||
<SearchTotalCountChart
|
||||
|
|
@ -1877,7 +1877,7 @@ function DBSearchPage() {
|
|||
/>
|
||||
)}
|
||||
{analysisMode === 'results' && (
|
||||
<Flex direction="column" mih="0">
|
||||
<Flex direction="column" mih="0" miw={0}>
|
||||
{chartConfig && histogramTimeChartConfig && (
|
||||
<>
|
||||
<Box className={searchPageStyles.searchStatsContainer}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useCSVDownloader } from 'react-papaparse';
|
||||
import { UnstyledButton } from '@mantine/core';
|
||||
import React, { useCallback } from 'react';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
interface CsvExportButtonProps {
|
||||
data: Record<string, any>[];
|
||||
|
|
@ -24,11 +23,8 @@ export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
|
|||
onExportStart,
|
||||
onExportComplete,
|
||||
onExportError,
|
||||
...props
|
||||
}) => {
|
||||
const { CSVDownloader } = useCSVDownloader();
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = useCallback(() => {
|
||||
try {
|
||||
if (data.length === 0) {
|
||||
onExportError?.(new Error('No data to export'));
|
||||
|
|
@ -36,21 +32,40 @@ export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
|
|||
}
|
||||
|
||||
onExportStart?.();
|
||||
|
||||
const csv = Papa.unparse(data, {
|
||||
quotes: true,
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
delimiter: ',',
|
||||
header: true,
|
||||
});
|
||||
const blob = new Blob([`\ufeff${csv}`], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
onExportComplete?.();
|
||||
} catch (error) {
|
||||
onExportError?.(
|
||||
error instanceof Error ? error : new Error('Export failed'),
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [data, filename, onExportStart, onExportComplete, onExportError]);
|
||||
|
||||
if (disabled || data.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
title={disabled ? 'Export disabled' : 'No data to export'}
|
||||
style={{ opacity: 0.5, cursor: 'not-allowed' }}
|
||||
{...props}
|
||||
style={{ opacity: 0.5, cursor: 'not-allowed', display: 'flex' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -58,36 +73,23 @@ export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<UnstyledButton
|
||||
className={className}
|
||||
title={title}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
title={title}
|
||||
className={className}
|
||||
style={{
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CSVDownloader
|
||||
data={data}
|
||||
filename={filename}
|
||||
config={{
|
||||
quotes: true,
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
delimiter: ',',
|
||||
header: true,
|
||||
}}
|
||||
style={{
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CSVDownloader>
|
||||
</UnstyledButton>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,7 +85,11 @@ import useRowWhere, {
|
|||
} from '@/hooks/useRowWhere';
|
||||
import { useTableSearch } from '@/hooks/useTableSearch';
|
||||
import { useSource } from '@/source';
|
||||
import { UNDEFINED_WIDTH } from '@/tableUtils';
|
||||
import {
|
||||
MIN_COLUMN_WIDTH,
|
||||
MIN_LAST_COLUMN_WIDTH,
|
||||
UNDEFINED_WIDTH,
|
||||
} from '@/tableUtils';
|
||||
import { FormatTime } from '@/useFormatTime';
|
||||
import { useUserPreferences } from '@/useUserPreferences';
|
||||
import {
|
||||
|
|
@ -140,6 +144,24 @@ function retrieveColumnValue(column: string, row: Row): any {
|
|||
return accessor(row, column);
|
||||
}
|
||||
|
||||
function getResolvedColumnSize(
|
||||
column: string,
|
||||
opts: {
|
||||
aliasMap?: Record<string, string>;
|
||||
columnTypeMap: Map<string, { _type: JSDataType | null }>;
|
||||
logLevelColumn?: string;
|
||||
columnSizeStorage: Record<string, number>;
|
||||
},
|
||||
): number {
|
||||
const columnId = opts.aliasMap?.[column] ? `"${column}"` : column;
|
||||
const stored = opts.columnSizeStorage[columnId];
|
||||
if (stored != null) return stored;
|
||||
const jsType = opts.columnTypeMap.get(column)?._type;
|
||||
if (jsType === JSDataType.Date) return 170;
|
||||
if (column === opts.logLevelColumn) return 115;
|
||||
return 160;
|
||||
}
|
||||
|
||||
function inferLogLevelColumn(rows: Record<string, any>[]) {
|
||||
const MAX_ROWS_TO_INSPECT = 100;
|
||||
const levelCounts: Record<string, number> = {};
|
||||
|
|
@ -442,6 +464,19 @@ export const RawLogTable = memo(
|
|||
[],
|
||||
);
|
||||
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!tableContainerRef) return;
|
||||
setContainerWidth(tableContainerRef.clientWidth);
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
setContainerWidth(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
observer.observe(tableContainerRef);
|
||||
return () => observer.disconnect();
|
||||
}, [tableContainerRef]);
|
||||
|
||||
// Get the alias map from the config so we resolve correct column ids
|
||||
const { data: aliasMap } = useAliasMapFromChartConfig(config);
|
||||
|
||||
|
|
@ -486,6 +521,38 @@ export const RawLogTable = memo(
|
|||
debounceMs: 300,
|
||||
});
|
||||
|
||||
const columnSizeOpts = useMemo(
|
||||
() => ({ aliasMap, columnTypeMap, logLevelColumn, columnSizeStorage }),
|
||||
[aliasMap, columnTypeMap, logLevelColumn, columnSizeStorage],
|
||||
);
|
||||
|
||||
const lastColumnWidth = useMemo(() => {
|
||||
if (displayedColumns.length === 0) return MIN_LAST_COLUMN_WIDTH;
|
||||
|
||||
const lastCol = displayedColumns[displayedColumns.length - 1];
|
||||
const lastColId = columnSizeOpts.aliasMap?.[lastCol]
|
||||
? `"${lastCol}"`
|
||||
: lastCol;
|
||||
const storedLast = columnSizeOpts.columnSizeStorage[lastColId];
|
||||
|
||||
if (storedLast != null) {
|
||||
return Math.max(MIN_LAST_COLUMN_WIDTH, storedLast);
|
||||
}
|
||||
|
||||
const expandWidth = showExpandButton ? 32 : 0;
|
||||
const nonLastSum = displayedColumns
|
||||
.slice(0, -1)
|
||||
.reduce(
|
||||
(total, column) =>
|
||||
total + getResolvedColumnSize(column, columnSizeOpts),
|
||||
0,
|
||||
);
|
||||
return Math.max(
|
||||
MIN_LAST_COLUMN_WIDTH,
|
||||
containerWidth - nonLastSum - expandWidth,
|
||||
);
|
||||
}, [displayedColumns, columnSizeOpts, showExpandButton, containerWidth]);
|
||||
|
||||
const columns = useMemo<ColumnDef<any>[]>(
|
||||
() => [
|
||||
...(showExpandButton
|
||||
|
|
@ -500,7 +567,6 @@ export const RawLogTable = memo(
|
|||
...(displayedColumns.map((column, i) => {
|
||||
const jsColumnType = columnTypeMap.get(column)?._type;
|
||||
const isDate = jsColumnType === JSDataType.Date;
|
||||
const isMaybeSeverityText = column === logLevelColumn;
|
||||
return {
|
||||
meta: {
|
||||
column,
|
||||
|
|
@ -576,9 +642,8 @@ export const RawLogTable = memo(
|
|||
},
|
||||
size:
|
||||
i === displayedColumns.length - 1
|
||||
? UNDEFINED_WIDTH // last column is always whatever is left
|
||||
: (columnSizeStorage[column] ??
|
||||
(isDate ? 170 : isMaybeSeverityText ? 115 : 160)),
|
||||
? lastColumnWidth
|
||||
: getResolvedColumnSize(column, columnSizeOpts),
|
||||
};
|
||||
}) as ColumnDef<any>[]),
|
||||
],
|
||||
|
|
@ -586,7 +651,7 @@ export const RawLogTable = memo(
|
|||
isUTC,
|
||||
highlightedLineId,
|
||||
displayedColumns,
|
||||
columnSizeStorage,
|
||||
columnSizeOpts,
|
||||
columnNameMap,
|
||||
columnTypeMap,
|
||||
logLevelColumn,
|
||||
|
|
@ -594,6 +659,7 @@ export const RawLogTable = memo(
|
|||
toggleRowExpansion,
|
||||
showExpandButton,
|
||||
aliasMap,
|
||||
lastColumnWidth,
|
||||
tableSearch.searchQuery,
|
||||
tableSearch.matchIndices,
|
||||
tableSearch.currentMatchIndex,
|
||||
|
|
@ -651,6 +717,9 @@ export const RawLogTable = memo(
|
|||
state: {
|
||||
sorting: sortOrder ?? [],
|
||||
},
|
||||
defaultColumn: {
|
||||
minSize: MIN_COLUMN_WIDTH,
|
||||
},
|
||||
enableColumnResizing: true,
|
||||
columnResizeMode: 'onChange' as ColumnResizeMode,
|
||||
} satisfies TableOptions<any>;
|
||||
|
|
@ -678,6 +747,24 @@ export const RawLogTable = memo(
|
|||
|
||||
const table = useReactTable(reactTableProps);
|
||||
|
||||
// Sum actual column widths to derive a pixel min-width for the table.
|
||||
// This enables horizontal scrolling when the viewport is narrower than
|
||||
// the total column widths.
|
||||
const tableMinWidth = useMemo(() => {
|
||||
const EXPAND_COLUMN_SIZE = 32;
|
||||
const expandWidth = showExpandButton ? EXPAND_COLUMN_SIZE : 0;
|
||||
return (
|
||||
expandWidth +
|
||||
displayedColumns.reduce((total, column, i) => {
|
||||
const size = getResolvedColumnSize(column, columnSizeOpts);
|
||||
if (i === displayedColumns.length - 1) {
|
||||
return total + Math.max(MIN_LAST_COLUMN_WIDTH, size);
|
||||
}
|
||||
return total + size;
|
||||
}, 0)
|
||||
);
|
||||
}, [displayedColumns, columnSizeOpts, showExpandButton]);
|
||||
|
||||
const { rows: _rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
|
|
@ -862,7 +949,7 @@ export const RawLogTable = memo(
|
|||
/>
|
||||
<div
|
||||
data-testid="search-results-table"
|
||||
className={cx('overflow-auto h-100 fs-8', styles.tableWrapper, {
|
||||
className={cx(styles.tableWrapper, {
|
||||
[styles.muted]: variant === 'muted',
|
||||
})}
|
||||
onScroll={e => {
|
||||
|
|
@ -884,7 +971,11 @@ export const RawLogTable = memo(
|
|||
config={config}
|
||||
/>
|
||||
)}
|
||||
<table className={cx('w-100', styles.table)} id={tableId}>
|
||||
<table
|
||||
className={styles.table}
|
||||
style={{ minWidth: tableMinWidth }}
|
||||
id={tableId}
|
||||
>
|
||||
<thead className={styles.tableHead}>
|
||||
{displayedColumns.length > 0 &&
|
||||
table.getHeaderGroups().map(headerGroup => (
|
||||
|
|
@ -909,12 +1000,18 @@ export const RawLogTable = memo(
|
|||
: undefined
|
||||
}
|
||||
lastItemButtons={
|
||||
<Group gap={8} mr={8}>
|
||||
<Group
|
||||
gap={8}
|
||||
mr={8}
|
||||
wrap="nowrap"
|
||||
align="center"
|
||||
>
|
||||
{tableId &&
|
||||
Object.keys(columnSizeStorage).length > 0 && (
|
||||
<UnstyledButton
|
||||
onClick={() => setColumnSizeStorage({})}
|
||||
title="Reset Column Widths"
|
||||
display="flex"
|
||||
>
|
||||
<MantineTooltip label="Reset Column Widths">
|
||||
<IconRotateClockwise size={16} />
|
||||
|
|
@ -926,6 +1023,7 @@ export const RawLogTable = memo(
|
|||
onClick={() => handleSqlModalOpen(true)}
|
||||
title="Show Generated SQL"
|
||||
tabIndex={0}
|
||||
display="flex"
|
||||
>
|
||||
<MantineTooltip label="Show Generated SQL">
|
||||
<IconCode size={16} />
|
||||
|
|
@ -937,6 +1035,7 @@ export const RawLogTable = memo(
|
|||
setWrapLinesEnabled(prev => !prev)
|
||||
}
|
||||
title={`${wrapLinesEnabled ? 'Disable' : 'Enable'} Wrap Lines`}
|
||||
display="flex"
|
||||
>
|
||||
<MantineTooltip
|
||||
label={`${wrapLinesEnabled ? 'Disable' : 'Enable'} Wrap Lines`}
|
||||
|
|
@ -964,6 +1063,7 @@ export const RawLogTable = memo(
|
|||
<UnstyledButton
|
||||
onClick={() => onSettingsClick()}
|
||||
title="Settings"
|
||||
display="flex"
|
||||
>
|
||||
<MantineTooltip label="Settings">
|
||||
<IconSettings size={16} />
|
||||
|
|
@ -1049,12 +1149,16 @@ export const RawLogTable = memo(
|
|||
style={{
|
||||
width:
|
||||
columnSize === UNDEFINED_WIDTH
|
||||
? 'auto'
|
||||
? 0
|
||||
: `${columnSize}px`,
|
||||
flex:
|
||||
columnSize === UNDEFINED_WIDTH
|
||||
? '1'
|
||||
? '1 1 0'
|
||||
: 'none',
|
||||
minWidth:
|
||||
columnSize === UNDEFINED_WIDTH
|
||||
? MIN_LAST_COLUMN_WIDTH
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<div className={styles.fieldTextContainer}>
|
||||
|
|
|
|||
51
packages/app/src/components/DBTable/TableHeader.module.scss
Normal file
51
packages/app/src/components/DBTable/TableHeader.module.scss
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
.sortButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
align-self: stretch;
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
background-color: var(--color-border-emphasis);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerCellWithAction {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--log-table-muted-bg);
|
||||
}
|
||||
|
||||
&:hover .headerRemoveButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.headerRemoveButton {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
import cx from 'classnames';
|
||||
import { Button, Group, Text } from '@mantine/core';
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconGripVertical,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { Group, Text, UnstyledButton } from '@mantine/core';
|
||||
import { IconArrowDown, IconArrowUp, IconX } from '@tabler/icons-react';
|
||||
import { flexRender, Header } from '@tanstack/react-table';
|
||||
|
||||
import { UNDEFINED_WIDTH } from '@/tableUtils';
|
||||
|
||||
import { DBRowTableIconButton } from './DBRowTableIconButton';
|
||||
|
||||
import logTableStyles from '../../../styles/LogTable.module.scss';
|
||||
import styles from '../Table.module.scss';
|
||||
import headerStyles from './TableHeader.module.scss';
|
||||
|
||||
export default function TableHeader({
|
||||
isLast,
|
||||
|
|
@ -30,13 +24,12 @@ export default function TableHeader({
|
|||
return (
|
||||
<th
|
||||
className={cx('overflow-hidden', {
|
||||
[logTableStyles.headerCellWithAction]: !!onRemoveColumn,
|
||||
[headerStyles.headerCellWithAction]: !!onRemoveColumn,
|
||||
})}
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
style={{
|
||||
width: header.getSize() === UNDEFINED_WIDTH ? '100%' : header.getSize(),
|
||||
// Allow unknown width columns to shrink to 0
|
||||
minWidth: header.getSize() === UNDEFINED_WIDTH ? 0 : header.getSize(),
|
||||
textAlign: 'left',
|
||||
}}
|
||||
|
|
@ -47,14 +40,10 @@ export default function TableHeader({
|
|||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Text>
|
||||
) : (
|
||||
<Button
|
||||
size="xxs"
|
||||
p={1}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
<UnstyledButton
|
||||
className={headerStyles.sortButton}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
flex="1"
|
||||
justify="space-between"
|
||||
data-testid="raw-log-table-sort-button"
|
||||
>
|
||||
<>
|
||||
|
|
@ -86,12 +75,12 @@ export default function TableHeader({
|
|||
</div>
|
||||
)}
|
||||
</>
|
||||
</Button>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
|
||||
<Group gap={0} wrap="nowrap" align="center">
|
||||
{onRemoveColumn && (
|
||||
<div className={logTableStyles.headerRemoveButton}>
|
||||
<div className={headerStyles.headerRemoveButton}>
|
||||
<DBRowTableIconButton
|
||||
onClick={onRemoveColumn}
|
||||
title="Remove column"
|
||||
|
|
@ -102,22 +91,17 @@ export default function TableHeader({
|
|||
</DBRowTableIconButton>
|
||||
</div>
|
||||
)}
|
||||
{header.column.getCanResize() && !isLast && (
|
||||
{isLast && (
|
||||
<Group gap={2} wrap="nowrap" align="center">
|
||||
{lastItemButtons}
|
||||
</Group>
|
||||
)}
|
||||
{header.column.getCanResize() && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={cx(
|
||||
`resizer ${styles.cursorColResize}`,
|
||||
header.column.getIsResizing() && 'isResizing',
|
||||
)}
|
||||
>
|
||||
<IconGripVertical size={12} />
|
||||
</div>
|
||||
)}
|
||||
{isLast && (
|
||||
<Group gap={2} wrap="nowrap">
|
||||
{lastItemButtons}
|
||||
</Group>
|
||||
className={headerStyles.resizer}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ export const createExpandButtonColumn = (
|
|||
);
|
||||
},
|
||||
size: 32,
|
||||
minSize: 32,
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
|
|
|
|||
|
|
@ -127,9 +127,3 @@ $horizontalPadding: 12px;
|
|||
border-color: var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.cursorColResize {
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
|
||||
export const UNDEFINED_WIDTH = 99999;
|
||||
|
||||
export const MIN_LAST_COLUMN_WIDTH = 200;
|
||||
|
||||
export const MIN_COLUMN_WIDTH = 50;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,12 @@ export const semanticColorsGrouped = {
|
|||
'color-bg-code',
|
||||
'color-bg-kbd',
|
||||
],
|
||||
borders: ['color-border', 'color-border-code', 'color-border-muted'],
|
||||
borders: [
|
||||
'color-border',
|
||||
'color-border-emphasis',
|
||||
'color-border-code',
|
||||
'color-border-muted',
|
||||
],
|
||||
text: [
|
||||
'color-text',
|
||||
'color-text-inverted',
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
|
||||
/* Borders & Dividers */
|
||||
--color-border: var(--click-global-color-stroke-default);
|
||||
--color-border-emphasis: var(--mantine-color-dark-4);
|
||||
--color-border-muted: var(--click-global-color-stroke-default);
|
||||
|
||||
/* Text - Using Click UI tokens */
|
||||
|
|
@ -313,6 +314,7 @@
|
|||
|
||||
/* Borders & Dividers */
|
||||
--color-border: var(--click-global-color-stroke-default);
|
||||
--color-border-emphasis: var(--mantine-color-dark-1);
|
||||
--color-border-muted: rgb(0 0 0 / 8%);
|
||||
|
||||
/* Text - Using Click UI tokens */
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
/* Borders & Dividers */
|
||||
--color-border: var(--mantine-color-dark-5);
|
||||
--color-border-emphasis: var(--mantine-color-dark-4);
|
||||
--color-border-muted: rgb(255 255 255 / 8%);
|
||||
|
||||
/* Text */
|
||||
|
|
@ -146,6 +147,7 @@
|
|||
|
||||
/* Borders & Dividers - inverted */
|
||||
--color-border: var(--mantine-color-gray-1);
|
||||
--color-border-emphasis: var(--mantine-color-gray-2);
|
||||
--color-border-muted: rgb(0 0 0 / 5%);
|
||||
|
||||
/* Text - inverted */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
$button-height: 18px;
|
||||
|
||||
.tableWrapper {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
|
||||
/* CSS custom properties for variant support */
|
||||
--log-table-bg: var(--color-bg-body);
|
||||
--log-table-muted-bg: var(--color-bg-muted);
|
||||
|
|
@ -12,6 +16,7 @@ $button-height: 18px;
|
|||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
|
|
@ -229,20 +234,3 @@ $button-height: 18px;
|
|||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.headerCellWithAction {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--log-table-muted-bg);
|
||||
}
|
||||
|
||||
&:hover .headerRemoveButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.headerRemoveButton {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,26 @@ a {
|
|||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
|
|
|
|||
Loading…
Reference in a new issue