Skip to content

Commit bc3aa6d

Browse files
committed
Large cleanup pass
1 parent 5695dea commit bc3aa6d

36 files changed

Lines changed: 2223 additions & 865 deletions

frontend/src/components/debug-helper/debug-dialog.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ function OverviewTab() {
718718
<KbdGroup>
719719
<Kbd size="xs">⌃/⌘</Kbd>
720720
<Kbd size="xs"></Kbd>
721-
<Kbd size="xs">U</Kbd>
721+
<Kbd size="xs">D</Kbd>
722722
</KbdGroup>
723723
</span>
724724
</DebugSection>
@@ -814,7 +814,7 @@ export function DebugDialog({ open, onOpenChange }: { open: boolean; onOpenChang
814814
<KbdGroup>
815815
<Kbd size="xs">⌃/⌘</Kbd>
816816
<Kbd size="xs"></Kbd>
817-
<Kbd size="xs">U</Kbd>
817+
<Kbd size="xs">D</Kbd>
818818
</KbdGroup>
819819
</span>
820820
<Button onClick={close} size="sm" variant="secondary-ghost">
@@ -836,9 +836,7 @@ export function DebugHelper() {
836836
onTrigger: () => setOpen((v) => !v),
837837
});
838838

839-
console.log('DebugHelper rendered; isDev=%s', IsDev);
840839
if (!IsDev) {
841-
console.log('RENDERBUT IS DEV FALSE');
842840
return null;
843841
}
844842

frontend/src/components/pages/rp-connect/onboarding/add-connector-dialog.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from 'components/redpanda-ui/components/dialog';
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogDescription,
5+
DialogHeader,
6+
DialogTitle,
7+
} from 'components/redpanda-ui/components/dialog';
28
import type { ComponentList } from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';
39

410
import { ConnectCommandPalette } from './connect-command-palette';
@@ -33,6 +39,9 @@ export const AddConnectorDialog = ({
3339
<DialogContent height="lg" size="xl">
3440
<DialogHeader>
3541
<DialogTitle>{title ?? 'Add a connector'}</DialogTitle>
42+
<DialogDescription>
43+
Search the component catalog, then select a component to add it to your pipeline.
44+
</DialogDescription>
3645
</DialogHeader>
3746
<ConnectCommandPalette
3847
allowedTypes={typeFilter}

frontend/src/components/pages/rp-connect/onboarding/connect-command-palette.test.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ComponentStatus } from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';
22
import { describe, expect, it } from 'vitest';
33

4-
import { asciidocToMarkdown, byProminence } from './connect-command-palette';
4+
import { asciidocToMarkdown, buildEmptyMessage, byProminence } from './connect-command-palette';
55
import type { ConnectComponentSpec } from '../types/schema';
66

77
const spec = (name: string, status: ComponentStatus = ComponentStatus.STABLE): ConnectComponentSpec =>
@@ -54,3 +54,38 @@ describe('asciidocToMarkdown', () => {
5454
expect(asciidocToMarkdown('* first\n* second')).toBe('- first\n- second');
5555
});
5656
});
57+
58+
describe('buildEmptyMessage', () => {
59+
it('reports an empty catalog when there is no query', () => {
60+
expect(buildEmptyMessage('')).toBe('No components available.');
61+
expect(buildEmptyMessage('', ['processor'])).toBe('No components available.');
62+
});
63+
64+
it('reports a plain miss without type locking', () => {
65+
expect(buildEmptyMessage('flurb')).toBe('No components match “flurb”.');
66+
});
67+
68+
it('names the locked type when the slot restricts the catalog', () => {
69+
expect(buildEmptyMessage('flurb', ['processor'])).toBe('No processors match “flurb”.');
70+
expect(buildEmptyMessage('flurb', ['rate_limit'])).toBe('No rate limits match “flurb”.');
71+
});
72+
73+
it('joins multiple locked types', () => {
74+
expect(buildEmptyMessage('flurb', ['cache', 'rate_limit'])).toBe('No caches or rate limits match “flurb”.');
75+
});
76+
77+
it('points at a single out-of-scope type the query exists as', () => {
78+
expect(buildEmptyMessage('generate', ['processor'], ['input'])).toBe(
79+
'No processors match “generate” — it exists as an input.'
80+
);
81+
});
82+
83+
it('lists multiple out-of-scope types with articles and "or"', () => {
84+
expect(buildEmptyMessage('kafka_franz', ['processor'], ['input', 'output'])).toBe(
85+
'No processors match “kafka_franz” — it exists as an input or an output.'
86+
);
87+
expect(buildEmptyMessage('redis', ['input'], ['cache', 'processor', 'rate_limit'])).toBe(
88+
'No inputs match “redis” — it exists as a cache, a processor or a rate limit.'
89+
);
90+
});
91+
});

frontend/src/components/pages/rp-connect/onboarding/connect-command-palette.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,54 @@ function HighlightedName({ name, query }: { name: string; query: string }) {
264264
);
265265
}
266266

267+
// "processors", "caches or rate limits" — plural label for the slot's allowed types.
268+
function pluralTypeLabel(types?: ConnectComponentType[]): string {
269+
if (!types || types.length === 0) {
270+
return 'components';
271+
}
272+
return types.map((type) => `${type.replace(/_/g, ' ')}s`).join(' or ');
273+
}
274+
275+
const STARTS_WITH_VOWEL = /^[aeiou]/;
276+
277+
// "an input", "an input or a processor" — out-of-scope types with indefinite articles.
278+
function withArticles(types: ConnectComponentType[]): string {
279+
const labeled = types.map((type) => {
280+
const label = type.replace(/_/g, ' ');
281+
return `${STARTS_WITH_VOWEL.test(label) ? 'an' : 'a'} ${label}`;
282+
});
283+
if (labeled.length <= 1) {
284+
return labeled[0] ?? '';
285+
}
286+
return `${labeled.slice(0, -1).join(', ')} or ${labeled.at(-1)}`;
287+
}
288+
289+
// Empty-state copy. A type-locked miss that matches other component types is explained
290+
// ("it exists as an input") so it doesn't read as a missing integration.
291+
export function buildEmptyMessage(
292+
query: string,
293+
allowedTypes?: ConnectComponentType[],
294+
outOfScopeTypes: ConnectComponentType[] = []
295+
): string {
296+
if (!query) {
297+
return 'No components available.';
298+
}
299+
if (outOfScopeTypes.length === 0) {
300+
return `No ${pluralTypeLabel(allowedTypes)} match “${query}”.`;
301+
}
302+
return `No ${pluralTypeLabel(allowedTypes)} match “${query}” — it exists as ${withArticles(outOfScopeTypes)}.`;
303+
}
304+
267305
function Row({
268306
component,
269307
query,
270308
onPreview,
309+
onCommit,
271310
}: {
272311
component: ConnectComponentSpec;
273312
query: string;
274313
onPreview: (component: ConnectComponentSpec) => void;
314+
onCommit: (component: ConnectComponentSpec) => void;
275315
}) {
276316
const category = component.categories?.[0];
277317
// Fall back to the humanized type (cache / rate_limit / …) so same-named components of different
@@ -281,6 +321,7 @@ function Row({
281321
<CommandItem
282322
className="gap-3 hover:bg-accent/50"
283323
key={`${component.type}-${component.name}`}
324+
onDoubleClick={() => onCommit(component)}
284325
onSelect={() => onPreview(component)}
285326
value={`${component.type}:${component.name}`}
286327
>
@@ -378,7 +419,7 @@ function DetailPane({ component }: { component?: ConnectComponentSpec }) {
378419
type ConnectCommandPaletteProps = {
379420
components?: ComponentList;
380421
additionalComponents?: ExtendedConnectComponentSpec[];
381-
/** Component types valid in the slot being filled — the palette locks to these unless "Show all" is toggled. */
422+
/** Component types valid in the slot being filled — the palette only lists components of these types. */
382423
allowedTypes?: ConnectComponentType[];
383424
onSelect: (connectionName: string, connectionType: ConnectComponentType) => void;
384425
/** Dismisses the picker without adding anything (wired to the footer Cancel button). */
@@ -447,6 +488,21 @@ export const ConnectCommandPalette = ({
447488
);
448489
}, [inScope, q]);
449490

491+
// When a type-locked search comes up empty, the component types (outside the slot's scope)
492+
// that DO match the query — surfaced in the empty state so the miss isn't read as a gap.
493+
const outOfScopeTypes = useMemo(() => {
494+
if (!q || results.length > 0 || !allowedTypes || allowedTypes.length === 0) {
495+
return [];
496+
}
497+
const types = new Set<ConnectComponentType>();
498+
for (const component of allComponents) {
499+
if (!typeAllowed(component.type) && matchRank(component, q, searchableText(component)) >= 0) {
500+
types.add(component.type);
501+
}
502+
}
503+
return [...types].sort();
504+
}, [q, results, allowedTypes, allComponents, typeAllowed]);
505+
450506
// Browse facets (no query): recents + suggested + everything grouped by category.
451507
const recentComponents = useMemo(
452508
() => recents.map((r) => byName.get(r.name)).filter((c): c is ConnectComponentSpec => Boolean(c)),
@@ -532,7 +588,8 @@ export const ConnectCommandPalette = ({
532588
return null; // "all" renders the grouped view, not a flat list.
533589
}, [currentTab, recentComponents, suggested, grouped]);
534590

535-
// Enter adds the highlighted component (fast keyboard path); clicking only previews, keeping insertion explicit.
591+
// Enter adds the highlighted component (fast keyboard path); a single click only previews,
592+
// keeping insertion explicit, while double-click commits directly.
536593
const handleKeyDown = (event: React.KeyboardEvent) => {
537594
if (event.key === 'Enter' && !event.nativeEvent.isComposing && activeComponent) {
538595
event.preventDefault();
@@ -574,14 +631,15 @@ export const ConnectCommandPalette = ({
574631

575632
<div className="flex min-h-0 flex-1">
576633
<CommandList className="h-full max-h-none min-w-0 flex-1 md:max-w-[28rem] md:border-r">
577-
<CommandEmpty>No components match “{query}”.</CommandEmpty>
634+
<CommandEmpty>{buildEmptyMessage(query.trim(), allowedTypes, outOfScopeTypes)}</CommandEmpty>
578635

579636
{q ? (
580637
<CommandGroup heading="Results">
581638
{results.map((component) => (
582639
<Row
583640
component={component}
584641
key={`${component.type}-${component.name}`}
642+
onCommit={handleCommit}
585643
onPreview={handlePreview}
586644
query={q}
587645
/>
@@ -593,6 +651,7 @@ export const ConnectCommandPalette = ({
593651
<Row
594652
component={component}
595653
key={`${component.type}-${component.name}`}
654+
onCommit={handleCommit}
596655
onPreview={handlePreview}
597656
query=""
598657
/>
@@ -605,6 +664,7 @@ export const ConnectCommandPalette = ({
605664
<Row
606665
component={component}
607666
key={`${component.type}-${component.name}`}
667+
onCommit={handleCommit}
608668
onPreview={handlePreview}
609669
query=""
610670
/>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright 2026 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { render, screen } from 'test-utils';
13+
import { beforeEach, describe, expect, it, vi } from 'vitest';
14+
15+
// The tips bar derives its key-chip text from the platform at module load, so each test picks the
16+
// platform first, then imports a fresh copy of the module.
17+
const platform = vi.hoisted(() => ({ mac: false }));
18+
vi.mock('utils/platform', () => ({ isMacOS: () => platform.mac }));
19+
20+
async function renderVisualTips(mac: boolean) {
21+
platform.mac = mac;
22+
vi.resetModules();
23+
const { EditorTipsBar } = await import('./editor-tips-bar');
24+
render(<EditorTipsBar context="visual" />);
25+
}
26+
27+
describe('EditorTipsBar — shortcut chip formatting', () => {
28+
beforeEach(() => {
29+
vi.resetModules();
30+
});
31+
32+
it('joins keys with "+" and spells out modifiers off macOS (Alt+Z, Ctrl+Z, Ctrl+Shift+Z)', async () => {
33+
await renderVisualTips(false);
34+
expect(screen.getByText('Alt+Z')).toBeInTheDocument();
35+
expect(screen.getByText('Ctrl+Z')).toBeInTheDocument();
36+
expect(screen.getByText('Ctrl+Shift+Z')).toBeInTheDocument();
37+
});
38+
39+
it('runs glyphs together on macOS (⌥Z, ⌘Z, ⌘⇧Z)', async () => {
40+
await renderVisualTips(true);
41+
expect(screen.getByText('⌥Z')).toBeInTheDocument();
42+
expect(screen.getByText('⌘Z')).toBeInTheDocument();
43+
expect(screen.getByText('⌘⇧Z')).toBeInTheDocument();
44+
});
45+
});

frontend/src/components/pages/rp-connect/pipeline/editor-tips-bar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const MAC = isMacOS();
1818
const MOD = MAC ? '⌘' : 'Ctrl';
1919
// The zoom-out modifier (Figma-style): Option on macOS, Alt elsewhere.
2020
const ALT = MAC ? '⌥' : 'Alt';
21+
const SHIFT = MAC ? '⇧' : 'Shift';
22+
// Chip text for a key combo: glyphs run together on macOS (⌘⇧Z); words join with "+" elsewhere
23+
// (Ctrl+Shift+Z), matching each platform's convention.
24+
const combo = (...keys: string[]): string => keys.join(MAC ? '' : '+');
2125

2226
/** Which editor surface the tips relate to — drives a different tip set. */
2327
export type TipContext = 'yaml' | 'visual';
@@ -42,7 +46,7 @@ function tipsFor(
4246
id: 'zoom',
4347
content: (
4448
<>
45-
Drag to pan, hold <Key>Z</Key> / <Key>{`${ALT}Z`}</Key> and click to zoom
49+
Drag to pan, hold <Key>Z</Key> / <Key>{combo(ALT, 'Z')}</Key> and click to zoom
4650
</>
4751
),
4852
},
@@ -60,7 +64,7 @@ function tipsFor(
6064
id: 'history',
6165
content: (
6266
<>
63-
<Key>{MOD}Z</Key> to undo, <Key>{MOD}⇧Z</Key> to redo
67+
<Key>{combo(MOD, 'Z')}</Key> to undo, <Key>{combo(MOD, SHIFT, 'Z')}</Key> to redo
6468
</>
6569
),
6670
});
@@ -84,11 +88,7 @@ function tipsFor(
8488
id: 'save',
8589
content: (
8690
<>
87-
Save with{' '}
88-
<Key>
89-
{MOD}
90-
{MAC ? '' : '+'}S
91-
</Key>
91+
Save with <Key>{combo(MOD, 'S')}</Key>
9292
</>
9393
),
9494
});

frontend/src/components/pages/rp-connect/pipeline/index.test.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,8 @@ describe('PipelinePage', () => {
614614
// from the config — its input/output components appear as tree rows once the pipeline loads.
615615
await waitFor(() => expect(screen.getByText('stdin')).toBeInTheDocument());
616616
expect(screen.getByText('stdout')).toBeInTheDocument();
617-
expect(screen.getByRole('tree')).toBeInTheDocument();
617+
// One labelled tree per non-empty section.
618+
expect(screen.getAllByRole('tree').length).toBeGreaterThan(0);
618619
});
619620

620621
it('shows the structure-tree side-lane even when the visual editor flag is off', async () => {
@@ -626,7 +627,7 @@ describe('PipelinePage', () => {
626627

627628
render(<PipelinePage />, { transport: createTransport() });
628629

629-
await waitFor(() => expect(screen.getByRole('tree')).toBeInTheDocument());
630+
await waitFor(() => expect(screen.getAllByRole('tree').length).toBeGreaterThan(0));
630631
expect(screen.queryByTestId('flow-canvas')).not.toBeInTheDocument();
631632
});
632633

@@ -683,7 +684,10 @@ describe('PipelinePage', () => {
683684
it('view page Visual lane renders the full pipeline diagram from the pipeline config', async () => {
684685
const user = userEvent.setup();
685686
mockUsePipelineMode.mockReturnValue({ mode: 'view', pipelineId: 'test-pipeline' });
686-
mockIsFeatureFlagEnabled.mockImplementation((flag: string) => flag === 'enableRpcnVisualEditor');
687+
// The visual editor builds on the diagrams flag, so both are required.
688+
mockIsFeatureFlagEnabled.mockImplementation(
689+
(flag: string) => flag === 'enableRpcnVisualEditor' || flag === 'enablePipelineDiagrams'
690+
);
687691
mockIsEmbedded.mockReturnValue(true);
688692

689693
render(<PipelinePage />, { transport: createTransport() });
@@ -697,7 +701,10 @@ describe('PipelinePage', () => {
697701
it('opens editing on the Visual lane when the visual editor is enabled, and YAML swaps in the editor', async () => {
698702
const user = userEvent.setup();
699703
mockUsePipelineMode.mockReturnValue({ mode: 'edit', pipelineId: 'test-pipeline' });
700-
mockIsFeatureFlagEnabled.mockImplementation((flag: string) => flag === 'enableRpcnVisualEditor');
704+
// The visual editor builds on the diagrams flag, so both are required.
705+
mockIsFeatureFlagEnabled.mockImplementation(
706+
(flag: string) => flag === 'enableRpcnVisualEditor' || flag === 'enablePipelineDiagrams'
707+
);
701708
mockIsEmbedded.mockReturnValue(true);
702709

703710
render(<PipelinePage />, { transport: createTransport() });

0 commit comments

Comments
 (0)