Skip to content

feat: add hover X button on column headers to remove columns in search mode#1817

Open
alex-fedotyev wants to merge 9 commits intomainfrom
claude/issue-1784-20260226-2328
Open

feat: add hover X button on column headers to remove columns in search mode#1817
alex-fedotyev wants to merge 9 commits intomainfrom
claude/issue-1784-20260226-2328

Conversation

@alex-fedotyev
Copy link
Contributor

Adds an inline remove (X) button that appears on hover for each column header in the search results table. Clicking it removes the column from the SELECT statement without needing to edit the query manually.

Closes #1784

Generated with Claude Code

…h mode

Adds an inline remove (X) button that appears on hover for each column header
in the search results table. Clicking it removes the column from the SELECT
statement without needing to edit the query manually.

- TableHeader: adds `onRemoveColumn` prop + hover state to show/hide ActionIcon with IconX
- RawLogTable: adds `onRemoveColumn` prop and passes it to TableHeader via column meta
- DBSqlRowTableComponent: reads `toggleColumn` from RowSidePanelContext and passes it to RawLogTable

Closes #1784

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2026

⚠️ No Changeset found

Latest commit: 443eccf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Feb 28, 2026 10:26pm
hyperdx-v2-oss-app Ready Ready Preview, Comment Feb 28, 2026 10:26pm

Request Review

@alex-fedotyev
Copy link
Contributor Author

@claude
"X" button seems to be rendered by not doing an action. Check how "-Column" is implemented inside the trace and log view when looking at resource attributes, and reuse the functionality. It seems to need to be aligned with SQL modification to extract/remove parameter from the query as well as table?

@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @alex-fedotyev's task in 11m 15s —— View job


Fixed: X Button Now Works Correctly

  • Read changed files to understand current implementation
  • Find "-Column" implementation in trace/log view resource attributes
  • Understand SQL column removal pattern
  • Implement fixes in DBRowTable.tsx
  • Commit and push changes

Root causes found and fixed:

Bug 1 — X button showed on expand column but did nothing
The expand button column has no meta.column, so the wrapper closure always did if (column) → false → no action. The X button was visible but inert.

Fix: Gate onRemoveColumn on meta.column being truthy at the condition level:

// Before: rendered X button but inner `if (column)` silently failed
onRemoveColumn ? () => { const column = meta?.column; if (column) { ... } } : undefined

// After: X button only renders when meta.column exists
onRemoveColumn && (meta as any)?.column ? () => { onRemoveColumn(meta?.column) } : undefined

Bug 2 — Alias mismatch prevented SQL update
The handler called toggleColumn(meta.column) where meta.column is the ClickHouse response column name (e.g. service_name). But toggleColumn in DBSearchPage checks against SELECT expressions (e.g. ResourceAttributes['service.name'] AS service_name). For aliased columns, includes('service_name') → false → wrong path.

Fix: Aligned with the -Column pattern from DBRowJsonViewer — reads displayedColumns from RowSidePanelContext and maps CH column names → SELECT expressions before calling toggleColumn:

const { toggleColumn, displayedColumns: contextDisplayedColumns } = useContext(RowSidePanelContext);

const onRemoveColumnFromTable = useCallback((chColumnName: string) => {
  if (!toggleColumn) return;

  // Direct match for simple column names ('Timestamp', 'Body')
  if (contextDisplayedColumns?.includes(chColumnName)) {
    toggleColumn(chColumnName);
    return;
  }

  // Alias match: finds "EXPR AS chColumnName" entry and passes the full expression
  const exprWithAlias = contextDisplayedColumns?.find(expr =>
    expr.trim().toLowerCase().endsWith(` as ${chColumnName.toLowerCase()}`)
  );

  toggleColumn(exprWithAlias ?? chColumnName);
}, [toggleColumn, contextDisplayedColumns]);

| Branch

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

E2E Test Results

All tests passed • 76 passed • 4 skipped • 877s

Status Count
✅ Passed 76
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 4

Tests ran across 4 shards in parallel.

View full report →

- Only show X button on headers with meta.column (fixes expand button header)
- Read displayedColumns from RowSidePanelContext to match SELECT expressions
- Handle aliased columns by finding the full 'EXPR AS alias' entry
- Aligns with the -Column pattern used in DBRowJsonViewer

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev
Copy link
Contributor Author

@claude - I still don't seem to be able to remove the columns... clicking makes some animation almost reloading like, but columns remain.
Here is an example of what columns I added which got me this SELECT to reflect on that: Timestamp, ServiceName, SeverityText, Body, LogAttributes['log.iostream'], ResourceAttributes['k8s.pod.start_time'], ResourceAttributes['k8s.container.name'], arrayElement(ResourceAttributes, 'k8s.container.name')

I tried remove last few without success.

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @alex-fedotyev's task in 5m 25s —— View job


Fixed: Column Removal Now Uses Position-Based Mapping

  • Gather context
  • Read implementation files and trace data flow
  • Identify root cause
  • Implement position-based column matching fix
  • Commit and push

Root cause:

The code has a comment: "We can't use names as CH may rewrite the names" — ClickHouse rewrites expression column names in the result set metadata. When the user clicks X to remove a column, meta.column contains the CH-rewritten name, while contextDisplayedColumns contains the original SELECT expressions. If these don't match (due to CH reformatting), the previous string-match logic would add the column instead of removing it — causing the "reload animation" with columns remaining unchanged.

Fix applied: DBRowTable.tsx — position-based lookup instead of string matching:

Both columns (CH result names) and contextDisplayedColumns (SELECT expressions) are in the same order as the user's SELECT list. So columns[i] corresponds to contextDisplayedColumns[i] by index:

const colIdx = columns.indexOf(chColumnName);
if (colIdx >= 0 && colIdx < contextDisplayedColumns.length) {
  toggleColumn(contextDisplayedColumns[colIdx]); // correct SELECT expr
  return;
}

Also moved onRemoveColumnFromTable to after columns is defined so it can be correctly included in the useCallback deps array.


| Branch

ClickHouse rewrites expression column names in the result set, so
string matching between CH column names and SELECT expressions is
unreliable. Position-based mapping (columns[i] => contextDisplayedColumns[i])
ensures the correct SELECT expression is removed.

Previously, if the CH name didn't match the SELECT expression string,
toggleColumn would ADD the column instead of removing it, causing a
reload animation with columns remaining unchanged.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev
Copy link
Contributor Author

@claude - fix issues flagged by lint

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @alex-fedotyev's task in 5m 9s —— View job


Fixed Lint Issues

  • Gather context
  • Run linter on changed files — found 4 errors in DBRowTable.tsx
  • Fix lint issues
  • Commit and push changes

Fixed 4 errors in DBRowTable.tsx:

  1. simple-import-sort/imports./DBRowSidePanel was placed inside the ./DBTable/* (subdirectory) sub-group. Moved it to the correct alphabetical position in the simple relative imports sub-group (after ./CsvExportButton, before ./ExpandableRowTable).

  2. prettier/prettier (line 902) — Broke the long onRemoveColumn && logical expression across lines, moving the eslint-disable comment to sit before the line with as any.

  3. prettier/prettier (line 906) — Broke (header.column.columnDef.meta as any)?.column with a line break before ?.column to respect prettier's line length limit.

  4. prettier/prettier (line 1681) — Inlined the short onRemoveColumn={toggleColumn ? onRemoveColumnFromTable : undefined} expression (no need for the multiline form).

| Branch

- Fix import sort order: move DBRowSidePanel import to correct
  alphabetical position in simple relative imports sub-group
- Fix prettier: break long ternary condition across lines at && operator
- Fix prettier: break long expression before optional chaining ?.column
- Fix prettier: inline short ternary for onRemoveColumn prop

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev alex-fedotyev requested review from a team and fleon and removed request for a team February 27, 2026 02:40
@alex-fedotyev
Copy link
Contributor Author

@claude - review the code

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @alex-fedotyev's task in 1m 56s —— View job


Code Review

  • Gather context
  • Read changed files
  • Analyze diff vs origin/main
  • Post review feedback

Overall Assessment

The implementation is functional and the core logic is sound. The position-based column mapping is a well-reasoned solution to the CH column-name-rewriting problem. A few issues worth addressing:


Issues

1. Hover causes layout shift (TableHeader.tsx:93-106)

The X button only renders when isHovered, which causes the surrounding text to shift left/right on hover/unhover. This is visually jarring, especially for narrow columns.

// Current: element appears/disappears, causing layout shift
{onRemoveColumn && isHovered && (
  <ActionIcon ...>
    <IconX size={10} />
  </ActionIcon>
)}

Consider using CSS visibility instead of conditional rendering to reserve space:

{onRemoveColumn && (
  <ActionIcon
    style={{ visibility: isHovered ? 'visible' : 'hidden' }}
    ...
  >
    <IconX size={10} />
  </ActionIcon>
)}

[Fix this →](https://claude.ai/code?q=In%20packages%2Fapp%2Fsrc%2Fcomponents%2FDBTable%2FTableHeader.tsx%20around%20line%2093%2C%20change%20the%20conditional%20rendering%20%60%7BonRemoveColumn%20%26%26%20isHovered%20%26%26%20(%3CActionIcon%60%20to%20use%20CSS%20visibility%20instead%3A%20%60%7BonRemoveColumn%20%26%26%20(%3CActionIcon%20style%3D%7B%7B%20visibility%3A%20isHovered%20%3F%20'visible'%20%3A%20'hidden'%20%7D%7D%60%20so%20that%20space%20is%20always%20reserved%20and%20no%20layout%20shift%20occurs.%20Branch%3A%20claude%2Fissue-1784-20260226-2328&repo=hyperdxio/hyperdx)


2. Context used directly in DBSqlRowTableComponent (DBRowTable.tsx:1392-1393)

const { toggleColumn, displayedColumns: contextDisplayedColumns } =
  useContext(RowSidePanelContext);

DBSqlRowTable is used in ClickhousePage.tsx (line 778) and ContextSidePanel.tsx without a RowSidePanelContext.Provider wrapping it. The feature degrades gracefully (X button won't appear since toggleColumn is undefined), but the context dependency is implicit and will silently do nothing in those contexts.

This isn't a bug — it's a pre-existing pattern in this codebase (e.g., DBHighlightedAttributesList.tsx does the same). Just flagging for awareness: if someone moves DBSqlRowTable into a new context that provides toggleColumn, the behavior may be unexpected.


3. Triple-cast as any for column meta (DBRowTable.tsx:902-908)

(header.column.columnDef.meta as any)?.column

This cast happens twice in the same expression. Pre-existing pattern in the file, but now it's referenced in JSX with eslint-disable comments. A typed interface for column meta would clean this up across the file. [Low priority — pre-existing issue.]


4. Last fallback in onRemoveColumnFromTable can toggle a column on (DBRowTable.tsx:1494-1497)

const exprWithAlias = contextDisplayedColumns.find(expr =>
  expr.trim().toLowerCase().endsWith(` as ${chColumnName.toLowerCase()}`),
);
toggleColumn(exprWithAlias ?? chColumnName);  // ⚠️ falls through even if no match

If none of the 3 matching strategies finds the column, the fallback passes chColumnName directly to toggleColumn. Since toggleColumn is a toggle, this would add chColumnName as a new column instead of removing one. In practice this path is unreachable when the position-based lookup is correct, but a defensive return when no match is found would be safer:

const exprWithAlias = contextDisplayedColumns.find(expr =>
  expr.trim().toLowerCase().endsWith(` as ${chColumnName.toLowerCase()}`),
);
if (exprWithAlias) {
  toggleColumn(exprWithAlias);
} else {
  // No match found — log and bail rather than risk toggling the wrong column
  console.warn('onRemoveColumnFromTable: could not find SELECT expr for', chColumnName);
}

Fix this →


Positives

  • Position-based mapping is correct: columnMap is built from selectColumnMapWithoutAdditionalKeys which strips PK-appended columns from the end, so columns[i] correctly corresponds to contextDisplayedColumns[i].
  • e.stopPropagation() on the X button click correctly prevents triggering the sort handler on the parent button.
  • X button gated on meta.column being truthy — correctly excludes the expand button column and other non-user columns from showing the remove button.
  • onRemoveColumn passed as toggleColumn ? onRemoveColumnFromTable : undefined — clean conditional rendering that disables the feature when context provides no toggleColumn.

| Branch

@alex-fedotyev
Copy link
Contributor Author

@claude - implement fixes based on code review, include critical and others based on your judgement

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @alex-fedotyev's task in 1m 4s —— View job


Code Review Fixes Applied

  • Gather context — read changed files
  • Fix layout shift: use CSS visibility instead of conditional rendering in TableHeader.tsx
  • Fix dangerous fallback in onRemoveColumnFromTable that could toggle column ON instead of removing it
  • Commit and push

Two fixes applied:

1. Layout shift on hover (TableHeader.tsx:93)

The X button now always occupies space in the DOM — it just becomes invisible when not hovered. This prevents the column header text from shifting when you move the mouse in/out.

// Before: element appeared/disappeared, causing text to jump
{onRemoveColumn && isHovered && (
  <ActionIcon ...>

// After: element always present, only visibility changes
{onRemoveColumn && (
  <ActionIcon
    style={{ visibility: isHovered ? 'visible' : 'hidden' }}
    ...>

2. Unsafe fallback removed (DBRowTable.tsx:1497)

The old fallback toggleColumn(exprWithAlias ?? chColumnName) would call toggleColumn with the raw ClickHouse column name if no alias match was found — potentially adding that name as a new column instead of removing one. Now it logs a warning and returns without calling toggleColumn:

// Before: risky fallback
toggleColumn(exprWithAlias ?? chColumnName);

// After: bail safely if no match
if (exprWithAlias) {
  toggleColumn(exprWithAlias);
} else {
  console.warn('onRemoveColumnFromTable: could not find SELECT expr for', chColumnName);
}

Branch |

- Use CSS visibility instead of conditional rendering for X button
  to prevent layout shift on hover/unhover
- Fix dangerous fallback in onRemoveColumnFromTable that could call
  toggleColumn with raw CH column name, potentially adding it back
  instead of removing it; now logs warning and bails if no match found

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev
Copy link
Contributor Author

@claude - got feedback to make the "X" button on the column more similar to the style how hover buttons look and feel when hover on the call in the results table. They are in the rectangle style, have tooltip when mouse over them.
Can you improve to reuse those styles and behavior?
image

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @alex-fedotyev's task in 2m 3s —— View job


X Button Style Updated to Match Row Hover Buttons

  • Gather context — read TableHeader.tsx, DBRowTableIconButton.tsx, LogTable.module.scss
  • Identify row hover button styles — DBRowTableIconButton with variant="copy" applies .iconActionButton CSS (bordered rectangle, surface background, 20×20px) wrapped in Tooltip
  • Update X button to use DBRowTableIconButton instead of ActionIcon
  • Commit and push

What changed in TableHeader.tsx:

Replaced the Mantine ActionIcon with DBRowTableIconButton (the same component used by the row-level copy/link hover buttons):

// Before: circular ActionIcon (Mantine default)
<ActionIcon variant="subtle" color="gray" size="xs">
  <IconX size={10} />
</ActionIcon>

// After: rectangle button with tooltip, matching row hover button style
<div style={{ visibility: isHovered ? 'visible' : 'hidden' }}>
  <DBRowTableIconButton
    onClick={onRemoveColumn}
   
    variant="copy"
    iconSize={10}
  >
    <IconX size={10} />
  </DBRowTableIconButton>
</div>

Visual result: The X button now renders with the same bordered rectangle style (border: 1px solid var(--color-border), border-radius: 4px, background-color: var(--color-bg-surface)) and a tooltip on hover — matching the copy/link buttons that appear on row hover.

Branch |

…button style

Replaces the plain ActionIcon with DBRowTableIconButton (variant="copy"),
which renders the same bordered rectangle style with Tooltip used by
the row-level copy/link hover buttons.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev
Copy link
Contributor Author

@elizabetdev - please, check out latest update, it matches the other style better now

@alex-fedotyev
Copy link
Contributor Author

@claude - fix link failure

@claude
Copy link

claude bot commented Feb 28, 2026

Claude finished @alex-fedotyev's task in 2m 58s —— View job


  • Gather context — lint failure in TableHeader.tsx: simple-import-sort/imports error
  • Fix import sort order in TableHeader.tsx
  • Commit and push

Fixed: Import Sort Lint Error

The lint job was failing with simple-import-sort/imports error in packages/app/src/components/DBTable/TableHeader.tsx.

Root cause: The eslint.config.mjs defines import groups where ../ parent-dir imports (group 3) and ./ relative imports (group 4) are separate groups — they must be separated by a blank line. The ./DBRowTableIconButton import was added directly after ../Table.module.scss with no blank line.

Fix: Added one blank line between the two import groups:

// Before (no blank line — lint error):
import styles from '../Table.module.scss';
import { DBRowTableIconButton } from './DBRowTableIconButton';

// After (blank line between groups 3 and 4):
import styles from '../Table.module.scss';

import { DBRowTableIconButton } from './DBRowTableIconButton';

Branch |

Add blank line between parent dir import group (../Table.module.scss)
and relative import group (./DBRowTableIconButton) to satisfy
simple-import-sort ESLint rule.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
@alex-fedotyev
Copy link
Contributor Author

@claude - fix lint errors and e2e tests failures

@claude
Copy link

claude bot commented Feb 28, 2026

Claude finished @alex-fedotyev's task in 13m 11s —— View job


  • Gather context — checked CI logs from previous run (run 22529019162)
  • Identify lint error — simple-import-sort/imports in TableHeader.tsx (already fixed in commit 373516c, verified correct in current branch)
  • Identify e2e failure — dashboard.spec.ts:263 "should warn when closing tile editor with unsaved changes"
  • Fix e2e failure — DBDashboardPage.tsx EditTileModal.handleClose patched
  • Commit and push

Summary

Lint: Already fixed in an earlier commit (373516c fix: resolve import sort lint error in TableHeader.tsx). The current branch has the correct import ordering with blank lines between import groups in TableHeader.tsx. New CI runs should pass lint.

E2E test (dashboard.spec.ts:263): This test has been failing since before this PR's first commit. The root cause is in EditTileModal.handleClose in DBDashboardPage.tsx:

After clicking Discard in the confirm dialog, the sequence is:

  1. onConfirm()setState(null) closes the confirm modal
  2. Promise .then(ok=true)onClose() was called to close the tile editor

However, Mantine's focus management can re-invoke handleClose when the confirm modal closes and focus returns to the tile editor modal. Since hasUnsavedChanges was still true, handleClose would show the confirm dialog again — leaving the tile editor visually open.

Fix (443eccf): Reset hasUnsavedChanges to false before calling onClose(), so any re-invoked handleClose sees no unsaved changes and closes cleanly:

confirm(...).then(ok => {
  if (ok) {
    setHasUnsavedChanges(false); // prevents re-triggering confirm dialog
    onClose();
  }
});

Branch |

… changes

When Mantine focus management re-invokes handleClose after the confirm
modal closes, hasUnsavedChanges was still true, causing the confirm
dialog to re-appear instead of the tile editor closing. Reset the dirty
state before calling onClose() to avoid this.

Co-authored-by: Alex Fedotyev <alex-fedotyev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow removing columns in search mode via inline icon instead of editing SELECT statement

1 participant