Skip to content

Commit c86ed55

Browse files
authored
feat(app): clarify selected source in source picker (#2651)
## Summary The source picker (`SourceSelect`) made it hard to tell which source was currently selected — the open dropdown gave no visual indication of the active source. This makes the current selection obvious and documents the component. - **Selected indicator**: the dropdown now marks the current source with a trailing check (mirroring the existing `SourceMultiSelect` pattern) plus a persistent highlight background. Root cause was the custom `renderOption` dropping Mantine's default selected treatment; the highlight targets `data-checked` (single `Select` never sets `data-combobox-selected`). - **Readability**: a small vertical gap between options so rows read as distinct pills. - **Theme token**: introduces a reusable `--color-bg-option-active` token (HyperDX + ClickStack, light + dark) for highlighting hovered/selected rows in floating surfaces (Select/Combobox options, Menu items, popover lists), instead of hardcoding a color. - **Storybook**: new `SourceSelect.stories.tsx` documenting the component (default/pre-selected, management menu, no selection, logs-only), with MSW-mocked sources. <img width="399" height="198" alt="Screenshot 2026-07-15 at 18 12 46" src="https://github.com/user-attachments/assets/c679b724-ce74-44f4-9536-e1b8856a9806" /> ## Test plan - [ ] Open the source picker on the search page — the selected source shows a check + highlight, and options have breathing room between them. - [ ] Verify in both light and dark mode, and in both HyperDX and ClickStack themes. - [ ] Run Storybook (`cd packages/app && yarn storybook`) → Components/SourceSelect renders all stories. Made with [Cursor](https://cursor.com)
1 parent ff05b3d commit c86ed55

6 files changed

Lines changed: 220 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat(app): make the selected source clearer in the source picker. The dropdown now marks the current source with a trailing check and a persistent highlight background, adds a small gap between options for readability, and documents the component in Storybook. Introduces a reusable `--color-bg-option-active` theme token (HyperDX + ClickStack, light + dark) for highlighting hovered/selected rows in floating surfaces.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { useForm } from 'react-hook-form';
3+
import { SourceKind } from '@hyperdx/common-utils/dist/types';
4+
import { Box } from '@mantine/core';
5+
import type { Meta, StoryObj } from '@storybook/nextjs';
6+
7+
import { SourceSelectControlled } from './SourceSelect';
8+
9+
const mockSources = [
10+
{
11+
id: 'source-logs',
12+
name: 'OpenAPI LB Logs New',
13+
kind: SourceKind.Log,
14+
connection: 'conn-1',
15+
from: { databaseName: 'default', tableName: 'otel_logs' },
16+
timestampValueExpression: 'Timestamp',
17+
},
18+
{
19+
id: 'source-console-logs',
20+
name: 'Console API LB Logs New',
21+
kind: SourceKind.Log,
22+
connection: 'conn-1',
23+
from: { databaseName: 'default', tableName: 'console_logs' },
24+
timestampValueExpression: 'Timestamp',
25+
},
26+
{
27+
id: 'source-traces',
28+
name: 'Data Plane Traces',
29+
kind: SourceKind.Trace,
30+
connection: 'conn-1',
31+
from: { databaseName: 'default', tableName: 'otel_traces' },
32+
timestampValueExpression: 'Timestamp',
33+
},
34+
{
35+
id: 'source-sessions',
36+
name: 'Sessions',
37+
kind: SourceKind.Session,
38+
connection: 'conn-1',
39+
from: { databaseName: 'default', tableName: 'hyperdx_sessions' },
40+
timestampValueExpression: 'Timestamp',
41+
},
42+
{
43+
id: 'source-metrics',
44+
name: 'System Metrics',
45+
kind: SourceKind.Metric,
46+
connection: 'conn-1',
47+
from: { databaseName: 'default', tableName: 'otel_metrics' },
48+
timestampValueExpression: 'TimeUnix',
49+
},
50+
];
51+
52+
const sourcesHandler = http.get('*/api/sources', () =>
53+
HttpResponse.json(mockSources),
54+
);
55+
56+
/**
57+
* `SourceSelectControlled` is the data-source picker used across the search,
58+
* chart, and dashboard surfaces. It is a `react-hook-form`-controlled Mantine
59+
* `Select` that lists the team's sources (optionally grouped by section), shows
60+
* the selected source's signal-kind icon on the left, and can render an
61+
* adjacent kebab menu for source-management actions.
62+
*
63+
* The dropdown marks the currently selected source with a trailing check so it
64+
* is easy to tell which source is active while scanning the list.
65+
*/
66+
const meta: Meta<typeof SourceSelectControlled> = {
67+
title: 'Components/SourceSelect',
68+
component: SourceSelectControlled,
69+
parameters: {
70+
layout: 'padded',
71+
msw: { handlers: { sources: sourcesHandler } },
72+
},
73+
argTypes: {
74+
size: {
75+
control: 'select',
76+
options: ['xs', 'sm', 'md', 'lg'],
77+
description: 'Mantine input size',
78+
},
79+
},
80+
};
81+
82+
export default meta;
83+
84+
type StoryArgs = {
85+
size?: string;
86+
defaultSourceId?: string;
87+
withMenu?: boolean;
88+
allowedSourceKinds?: SourceKind[];
89+
};
90+
91+
const SourceSelectWrapper = ({
92+
size,
93+
defaultSourceId = 'source-logs',
94+
withMenu,
95+
allowedSourceKinds,
96+
}: StoryArgs) => {
97+
const { control } = useForm<{ source: string }>({
98+
defaultValues: { source: defaultSourceId },
99+
});
100+
101+
return (
102+
<Box style={{ maxWidth: 420 }}>
103+
<SourceSelectControlled
104+
control={control}
105+
name="source"
106+
size={size}
107+
allowedSourceKinds={allowedSourceKinds}
108+
onSchemaPreview={withMenu ? () => {} : undefined}
109+
onEdit={withMenu ? () => {} : undefined}
110+
onManageSources={withMenu ? () => {} : undefined}
111+
onCreate={withMenu ? () => {} : undefined}
112+
/>
113+
</Box>
114+
);
115+
};
116+
117+
type Story = StoryObj<typeof SourceSelectWrapper>;
118+
119+
/**
120+
* A source is pre-selected. Open the dropdown to see the active source marked
121+
* with a check, alongside each source's signal-kind icon.
122+
*/
123+
export const Default: Story = {
124+
render: args => <SourceSelectWrapper {...args} />,
125+
args: {
126+
size: 'sm',
127+
defaultSourceId: 'source-logs',
128+
},
129+
};
130+
131+
/** With the adjacent source-management kebab menu (view schema, edit, etc.). */
132+
export const WithManagementMenu: Story = {
133+
name: 'With Management Menu',
134+
render: args => <SourceSelectWrapper {...args} />,
135+
args: {
136+
size: 'sm',
137+
defaultSourceId: 'source-traces',
138+
withMenu: true,
139+
},
140+
};
141+
142+
/** No source selected yet — shows the generic placeholder icon and text. */
143+
export const NoSelection: Story = {
144+
name: 'No Selection (Placeholder)',
145+
render: args => <SourceSelectWrapper {...args} />,
146+
args: {
147+
size: 'sm',
148+
defaultSourceId: '',
149+
},
150+
};
151+
152+
/** Restricted to a single signal kind via `allowedSourceKinds`. */
153+
export const LogsOnly: Story = {
154+
name: 'Logs Only',
155+
render: args => <SourceSelectWrapper {...args} />,
156+
args: {
157+
size: 'sm',
158+
defaultSourceId: 'source-logs',
159+
allowedSourceKinds: [SourceKind.Log],
160+
},
161+
};

‎packages/app/src/components/SourceSelect.tsx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Tooltip,
1212
} from '@mantine/core';
1313
import {
14+
IconCheck,
1415
IconCode,
1516
IconDotsVertical,
1617
IconPencil,
@@ -189,14 +190,19 @@ function SourceSelectControlledComponent({
189190

190191
const sourceKindMap = useSourceKindMap(data);
191192

193+
// Mantine passes `checked` to renderOption for the currently selected
194+
// option; render a trailing check so the active source is obvious in the
195+
// dropdown (the closed input only shows the kind icon + label).
192196
const renderOption = useCallback(
193-
({ option }: { option: ComboboxItem }) => {
197+
({ option, checked }: { option: ComboboxItem; checked?: boolean }) => {
194198
const icon = SOURCE_KIND_ICONS[sourceKindMap.get(option.value) ?? ''];
195-
if (!icon) return option.label;
196199
return (
197-
<Group gap="xs" wrap="nowrap">
200+
<Group gap="xs" wrap="nowrap" w="100%">
198201
{icon}
199-
{option.label}
202+
<span style={{ flex: 1 }}>{option.label}</span>
203+
{checked && (
204+
<IconCheck size={14} color="var(--color-text-brand)" stroke={2.5} />
205+
)}
200206
</Group>
201207
);
202208
},
@@ -234,6 +240,7 @@ function SourceSelectControlledComponent({
234240
input: styles.sourceSelectInput,
235241
groupLabel: styles.groupLabel,
236242
dropdown: styles.sourceSelectDropdown,
243+
option: styles.sourceSelectOption,
237244
}}
238245
renderOption={renderOption}
239246
filter={sourceSelectFilter}

‎packages/app/src/theme/themes/clickstack/_tokens.scss‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@
126126
--color-bg-header: var(--click-global-color-background-default);
127127
--color-bg-hover: var(--palette-neutral-725);
128128
--color-bg-active: var(--palette-neutral-800);
129+
130+
/* Highlight background for an interactive row inside a floating surface
131+
(Select/Combobox options, Menu items, popover/dropdown lists) when it is
132+
hovered or is the currently-selected entry. */
133+
--color-bg-option-active: var(--palette-neutral-712);
129134
--color-bg-field: var(--click-field-color-background-default);
130135
--color-bg-field-highlighted: var(--palette-neutral-650);
131136
--color-bg-field-addon: var(--palette-neutral-712);
@@ -328,6 +333,11 @@
328333
--color-bg-modal: var(--click-global-color-background-default);
329334
--color-bg-hover: #e9ecef;
330335
--color-bg-active: #dee2e6;
336+
337+
/* Highlight background for an interactive row inside a floating surface
338+
(Select/Combobox options, Menu items, popover/dropdown lists) when it is
339+
hovered or is the currently-selected entry. */
340+
--color-bg-option-active: var(--click-global-color-background-muted);
331341
--color-bg-field: var(--click-field-color-background-default);
332342
--color-bg-field-highlighted: var(--click-global-color-background-default);
333343
--color-bg-field-addon: var(--click-global-color-background-default);

‎packages/app/src/theme/themes/hyperdx/_tokens.scss‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
--color-bg-header: var(--mantine-color-dark-9);
4242
--color-bg-hover: var(--mantine-color-dark-6);
4343
--color-bg-active: var(--mantine-color-dark-5);
44+
45+
/* Highlight background for an interactive row inside a floating surface
46+
(Select/Combobox options, Menu items, popover/dropdown lists) when it is
47+
hovered or is the currently-selected entry. */
48+
--color-bg-option-active: var(--mantine-color-dark-5);
4449
--color-bg-field: var(--mantine-color-dark-7);
4550
--color-bg-field-highlighted: var(--mantine-color-dark-6);
4651
--color-bg-field-addon: var(--mantine-color-dark-6);
@@ -149,6 +154,11 @@
149154
--color-bg-modal: var(--mantine-color-white);
150155
--color-bg-hover: var(--mantine-color-gray-1);
151156
--color-bg-active: var(--mantine-color-gray-4);
157+
158+
/* Highlight background for an interactive row inside a floating surface
159+
(Select/Combobox options, Menu items, popover/dropdown lists) when it is
160+
hovered or is the currently-selected entry. */
161+
--color-bg-option-active: var(--color-bg-sidenav-link-active);
152162
--color-bg-field: #f9f9fc;
153163
--color-bg-field-highlighted: var(--mantine-color-white);
154164
--color-bg-field-addon: var(--mantine-color-white);

‎packages/app/styles/SourceSelectControlled.module.scss‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
.sourceSelectGroup {
3636
display: inline-flex;
3737
align-items: stretch;
38-
gap: 0;
3938

4039
&[data-with-menu] {
4140
/* Square only the right edge of the input. */
@@ -90,6 +89,29 @@
9089
flex-shrink: 0;
9190
}
9291

92+
/* Option hover + selection backgrounds, both driven by the shared
93+
* `--color-bg-option-active` floating-surface token. Mantine's Select marks the
94+
* chosen option with `data-checked` (the keyboard-highlighted row uses
95+
* `data-combobox-active` instead), so we target that for selection; hover reuses
96+
* the same token. The class + attribute selectors out-specify Mantine's own
97+
* zero-specificity `:where(...)` rules without `!important`. Selection also
98+
* carries a trailing check rendered in `renderOption`.
99+
*/
100+
.sourceSelectOption {
101+
/* Small breathing room between options so the rows (and their hover/selected
102+
backgrounds) read as distinct pills rather than one continuous list. */
103+
margin-block: 2px;
104+
105+
&:hover,
106+
&[data-checked] {
107+
background-color: var(--color-bg-option-active);
108+
}
109+
110+
&[data-checked] {
111+
color: inherit;
112+
}
113+
}
114+
93115
/* Grouped dropdown decoupled from the compact picker (comboboxProps
94116
* width="max-content", floored wider, capped to the viewport, pinned left
95117
* via position="bottom-start"). The input keeps its own width.

0 commit comments

Comments
 (0)