Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
539024b
UX-1338 - RPCN visual editor
SpicyPete Jul 7, 2026
a0dc079
Remove custom colours
SpicyPete Jul 7, 2026
394dc31
comment redution
SpicyPete Jul 7, 2026
3d5107a
lookout drift comment
SpicyPete Jul 7, 2026
005d9a1
PR comment follow up
SpicyPete Jul 7, 2026
1da4e67
Better yaml parsing
SpicyPete Jul 7, 2026
e068852
Some re-organize
SpicyPete Jul 8, 2026
edbbcd6
improve area shapes
SpicyPete Jul 8, 2026
c7e08b3
some cleanup
SpicyPete Jul 8, 2026
26421a7
Merge branch 'master' into UX-1338/rpcn-visual
SpicyPete Jul 8, 2026
1ed1407
some code quality improvements
SpicyPete Jul 10, 2026
735c36a
Dead code and cleanup pass
SpicyPete Jul 10, 2026
0529f78
lint fixes
SpicyPete Jul 10, 2026
5703a88
merge main
SpicyPete Jul 10, 2026
dc01afb
PR comments phase 1
SpicyPete Jul 10, 2026
17e9da9
PR Review P2
SpicyPete Jul 10, 2026
3ec0fed
PR comments 3
SpicyPete Jul 10, 2026
1d89c23
yaml and schema pass
SpicyPete Jul 10, 2026
b356299
PR comment improvements P4
SpicyPete Jul 10, 2026
7d7f1a1
More PR comments improvements
SpicyPete Jul 10, 2026
4d8e9b0
increase 1 size
SpicyPete Jul 10, 2026
df8acb9
more cleanup pass
SpicyPete Jul 10, 2026
fff71d2
One more set of PR comment addressing
SpicyPete Jul 13, 2026
bed96e8
More cleanups addressed
SpicyPete Jul 13, 2026
a83057b
Some moer cleanup
SpicyPete Jul 13, 2026
8517ba6
some more cleanup
SpicyPete Jul 13, 2026
548e971
More cleanup
SpicyPete Jul 13, 2026
c9db5e8
One more cleanup pass
SpicyPete Jul 13, 2026
baba5da
Fixing predicate warning focus
SpicyPete Jul 13, 2026
00d6525
Some more correctness findings from reviews
SpicyPete Jul 14, 2026
b7c23ef
small cleanup
SpicyPete Jul 14, 2026
ee6bb22
Some PR fixes
SpicyPete Jul 14, 2026
2e371d2
Fixing bug when deleting the last node
SpicyPete Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-query": "^2.2.0",
"@connectrpc/connect-web": "^2.1.0",
"@dagrejs/dagre": "^3.0.0",
"@emotion/css": "^11.13.5",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^5.2.2",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const FEATURE_FLAGS = {
enableRemoteMcpInConsole: false,
enableRpcnTiles: false,
enableRpcnTemplateGallery: false,
enableRpcnVisualEditor: false,
enableServerlessOnboardingWizard: false,
enableApiKeyConfigurationAgent: false,
enableDataplaneObservabilityServerless: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from 'components/redpanda-ui/components/dialog';
import { Link } from 'components/redpanda-ui/components/typography';
import type { ComponentList } from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';

import { ConnectTiles } from './connect-tiles';
import { ConnectCommandPalette } from './connect-command-palette';
import type { ConnectComponentType } from '../types/schema';

function getDocsUrl(connectorType?: ConnectComponentType | ConnectComponentType[]): string | null {
const type = Array.isArray(connectorType) ? connectorType[0] : connectorType;
if (!type) {
return null;
}
return `https://docs.redpanda.com/redpanda-cloud/develop/connect/components/${type}s/about/`;
}

export const AddConnectorDialog = ({
isOpen,
onCloseAddConnector,
Expand All @@ -44,34 +34,22 @@ export const AddConnectorDialog = ({
typeFilter = [connectorType];
}

const docsUrl = getDocsUrl(connectorType);

return (
<Dialog onOpenChange={onCloseAddConnector} open={isOpen}>
<DialogContent size="xl">
<DialogContent height="lg" size="xl">
<DialogHeader>
<DialogTitle>{title ?? 'Add a connector'}</DialogTitle>
<DialogDescription>
Configure your pipeline.{' '}
{docsUrl ? (
<Link href={docsUrl} rel="noopener noreferrer" target="_blank">
Learn more
</Link>
) : null}
Search the component catalog, then select a component to add it to your pipeline.
</DialogDescription>
</DialogHeader>
<DialogBody>
<ConnectTiles
className="px-0 py-0"
components={components}
componentTypeFilter={typeFilter}
gridCols={3}
hideHeader
onChange={onAddConnector}
searchPlaceholder={searchPlaceholder}
variant="ghost"
/>
Comment on lines -64 to -73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make an action item that once this is GA and well tested we remove the old RPCN UXs. potentially Q3 or Q4

</DialogBody>
<ConnectCommandPalette
allowedTypes={typeFilter}
components={components}
onCancel={onCloseAddConnector}
onSelect={(name, type) => onAddConnector?.(name, type)}
searchPlaceholder={searchPlaceholder}
/>
</DialogContent>
</Dialog>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ComponentStatus } from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';
import { describe, expect, it } from 'vitest';

import {
aliasTermsForName,
asciidocToMarkdown,
buildEmptyMessage,
byProminence,
COMPONENT_ALIASES,
computeSuggested,
matchRank,
searchableText,
} from './connect-command-palette-utils';
import type { ConnectComponentSpec, ConnectComponentType } from '../types/schema';

const spec = (
name: string,
status: ComponentStatus = ComponentStatus.STABLE,
type: ConnectComponentType = 'processor'
): ConnectComponentSpec => ({ name, type, status }) as ConnectComponentSpec;

const sorted = (specs: ConnectComponentSpec[]) => [...specs].sort(byProminence).map((s) => s.name);

describe('byProminence', () => {
it('sorts common components ahead of the long tail', () => {
// `mapping` is a common processor; `avro` is not.
expect(sorted([spec('avro'), spec('mapping')])).toEqual(['mapping', 'avro']);
});

it('demotes deprecated and experimental below stable, even when common', () => {
const result = sorted([
spec('mapping', ComponentStatus.DEPRECATED),
spec('avro', ComponentStatus.STABLE),
spec('parquet', ComponentStatus.EXPERIMENTAL),
]);
// Stable (even uncommon) sorts above demoted ones; deprecated/experimental sink.
expect(result[0]).toBe('avro');
expect(result.slice(1).sort()).toEqual(['mapping', 'parquet']);
});

it('falls back to alphabetical within the same prominence bucket', () => {
expect(sorted([spec('zzz'), spec('aaa')])).toEqual(['aaa', 'zzz']);
});
});

describe('matchRank', () => {
const gen = spec('generate');
const text = searchableText(gen);

it('ranks exact-name matches highest', () => {
expect(matchRank(gen, 'generate', text)).toBe(0);
});

it('ranks a name prefix above a mid-name substring', () => {
expect(matchRank(gen, 'gen', text)).toBe(1);
expect(matchRank(gen, 'erat', text)).toBe(2);
});

it('ranks a body-text match below any name match', () => {
const withSummary = { ...spec('kafka'), summary: 'streams events' } as ConnectComponentSpec;
expect(matchRank(withSummary, 'events', searchableText(withSummary))).toBe(3);
});

it('returns -1 when nothing matches', () => {
expect(matchRank(gen, 'flurb', text)).toBe(-1);
});
});

describe('component aliases', () => {
it('maps intent terms to a component name via its fragments', () => {
// "queue" lists kafka, so kafka_franz surfaces under that intent.
expect(aliasTermsForName('kafka_franz')).toContain('queue');
expect(aliasTermsForName('kafka_franz')).toContain('stream');
});

it('maps transform/map intents to bloblang/mapping', () => {
expect(aliasTermsForName('mapping')).toEqual(expect.arrayContaining(['transform', 'map']));
expect(aliasTermsForName('bloblang')).toContain('transform');
});

it('returns no alias terms for a name no fragment matches', () => {
expect(aliasTermsForName('totally_unrelated_xyz')).toEqual([]);
});

it('matches fragments on `_`/word boundaries, not mid-token substrings', () => {
// Whole multi-token fragments and `_`-delimited tokens match...
expect(aliasTermsForName('rate_limit')).toEqual(expect.arrayContaining(['throttle', 'ratelimit']));
expect(aliasTermsForName('json_schema')).toContain('json');
// ...but a fragment buried mid-token doesn't (`log` inside `catalog`).
expect(aliasTermsForName('catalog')).not.toContain('log');
});

it('keeps alias keys and fragments lowercase', () => {
for (const [key, fragments] of Object.entries(COMPONENT_ALIASES)) {
expect(key).toBe(key.toLowerCase());
for (const fragment of fragments) {
expect(fragment).toBe(fragment.toLowerCase());
}
}
});
});

describe('searchableText', () => {
it('folds name, summary, description and categories into one lowercased haystack', () => {
const component = {
...spec('http_client'),
summary: 'Sends HTTP requests',
description: 'Talks to REST endpoints',
categories: ['Network'],
} as ConnectComponentSpec;
const text = searchableText(component);
expect(text).toContain('http_client');
expect(text).toContain('sends http requests');
expect(text).toContain('rest endpoints');
expect(text).toContain('network');
expect(text).toBe(text.toLowerCase());
});
});

describe('asciidocToMarkdown', () => {
it('turns AsciiDoc section titles into Markdown headings instead of leaking "=="', () => {
const out = asciidocToMarkdown('== Performance\nThis output benefits from batching.');
expect(out).toBe('#### Performance\nThis output benefits from batching.');
expect(out).not.toContain('== ');
});

it('handles multiple heading levels and keeps paragraphs separated', () => {
const out = asciidocToMarkdown('Intro paragraph.\n\n=== Delivery Guarantees\nAt least once.');
expect(out).toBe('Intro paragraph.\n\n#### Delivery Guarantees\nAt least once.');
});

it('converts link/xref macros to label text and bare URL macros to Markdown links', () => {
expect(asciidocToMarkdown('See xref:guides:about.adoc[the guide] for details.')).toBe('See the guide for details.');
expect(asciidocToMarkdown('Uses https://github.com/twmb/franz-go[franz-go] under the hood.')).toBe(
'Uses [franz-go](https://github.com/twmb/franz-go) under the hood.'
);
});

it('converts AsciiDoc bullets to Markdown list items', () => {
expect(asciidocToMarkdown('* first\n* second')).toBe('- first\n- second');
});
});

describe('buildEmptyMessage', () => {
it('reports an empty catalog when there is no query', () => {
expect(buildEmptyMessage('')).toBe('No components available.');
expect(buildEmptyMessage('', ['processor'])).toBe('No components available.');
});

it('reports a plain miss without type locking', () => {
expect(buildEmptyMessage('flurb')).toBe('No components match “flurb”.');
});

it('names the locked type when the slot restricts the catalog', () => {
expect(buildEmptyMessage('flurb', ['processor'])).toBe('No processors match “flurb”.');
expect(buildEmptyMessage('flurb', ['rate_limit'])).toBe('No rate limits match “flurb”.');
});

it('joins multiple locked types', () => {
expect(buildEmptyMessage('flurb', ['cache', 'rate_limit'])).toBe('No caches or rate limits match “flurb”.');
});

it('points at a single out-of-scope type the query exists as', () => {
expect(buildEmptyMessage('generate', ['processor'], ['input'])).toBe(
'No processors match “generate” — it exists as an input.'
);
});

it('lists multiple out-of-scope types with articles and "or"', () => {
expect(buildEmptyMessage('kafka_franz', ['processor'], ['input', 'output'])).toBe(
'No processors match “kafka_franz” — it exists as an input or an output.'
);
expect(buildEmptyMessage('redis', ['input'], ['cache', 'processor', 'rate_limit'])).toBe(
'No inputs match “redis” — it exists as a cache, a processor or a rate limit.'
);
});
});

describe('computeSuggested', () => {
const byName = new Map<string, ConnectComponentSpec>([
['generate', spec('generate', ComponentStatus.STABLE, 'input')],
['kafka_franz', spec('kafka_franz', ComponentStatus.STABLE, 'input')],
['file', spec('file', ComponentStatus.STABLE, 'input')],
]);

it('resolves the curated names for the allowed types against the catalog', () => {
const result = computeSuggested(['input'], byName, []).map((c) => c.name);
// Only the curated input names present in `byName` survive (redpanda/http_client are absent).
expect(result).toEqual(['kafka_franz', 'generate', 'file']);
});

it('drops anything already shown under Recent', () => {
const recent = [spec('generate', ComponentStatus.STABLE, 'input')];
const result = computeSuggested(['input'], byName, recent).map((c) => c.name);
expect(result).not.toContain('generate');
expect(result).toEqual(['kafka_franz', 'file']);
});

it('returns nothing when there are no allowed types', () => {
expect(computeSuggested(undefined, byName, [])).toEqual([]);
});
});
Loading
Loading