Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 4 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 @@ -118,6 +118,7 @@
"react-highlight-words": "^0.21.0",
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.4",
"react-simple-code-editor": "^0.14.1",
"react-syntax-highlighter": "^15.6.6",
"recharts": "2.15.4",
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';

import { isCloudManagedTagKey, isSystemTag } from './constants';

describe('isCloudManagedTagKey', () => {
it('returns true for cloud-managed tag keys', () => {
expect(isCloudManagedTagKey('rp_cloud_service_account_id')).toBe(true);
expect(isCloudManagedTagKey('rp_cloud_secret_id')).toBe(true);
});

it('returns false for user-defined tag keys', () => {
expect(isCloudManagedTagKey('env')).toBe(false);
expect(isCloudManagedTagKey('team')).toBe(false);
});
});

describe('isSystemTag', () => {
it('returns true for __-prefixed keys', () => {
expect(isSystemTag('__redpanda_cloud_pipeline_type')).toBe(true);
expect(isSystemTag('__internal')).toBe(true);
});

it('returns true for cloud-managed keys', () => {
expect(isSystemTag('rp_cloud_service_account_id')).toBe(true);
expect(isSystemTag('rp_cloud_secret_id')).toBe(true);
});

it('returns false for user-defined keys', () => {
expect(isSystemTag('env')).toBe(false);
expect(isSystemTag('team')).toBe(false);
expect(isSystemTag('_single_underscore')).toBe(false);
});
});
3 changes: 3 additions & 0 deletions frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ export const isCloudManagedTagKey = (key: string): boolean =>
Object.values(CLOUD_MANAGED_TAG_KEYS).includes(
key as (typeof CLOUD_MANAGED_TAG_KEYS)[keyof typeof CLOUD_MANAGED_TAG_KEYS]
);

/** Returns true if the tag key is a system tag that should be hidden from users. */
export const isSystemTag = (key: string): boolean => key.startsWith('__') || isCloudManagedTagKey(key);
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const createMockMessage = (): ChatMessage => ({

const makeArtifactEvent = (
text: string,
opts: { append?: boolean; lastChunk?: boolean } = {},
opts: { append?: boolean; lastChunk?: boolean } = {}
): TaskArtifactUpdateEvent => ({
kind: 'artifact-update',
contextId: 'test-context',
Expand Down Expand Up @@ -84,7 +84,7 @@ describe('handleStatusUpdateEvent', () => {
const statusBlocks = state.contentBlocks.filter((b) => b.type === 'task-status-update');
expect(statusBlocks).toHaveLength(1);
expect(statusBlocks[0].type === 'task-status-update' && statusBlocks[0].text).toContain(
'Artifact created successfully',
'Artifact created successfully'
);
});
});
Expand Down Expand Up @@ -127,11 +127,9 @@ describe('handleArtifactUpdateEvent', () => {

// Text must accumulate
expect(block1?.type === 'artifact' && block1.parts[0]?.kind === 'text' && block1.parts[0].text).toBe('Hello');
expect(block2?.type === 'artifact' && block2.parts[0]?.kind === 'text' && block2.parts[0].text).toBe(
'Hello world',
);
expect(block2?.type === 'artifact' && block2.parts[0]?.kind === 'text' && block2.parts[0].text).toBe('Hello world');
expect(block3?.type === 'artifact' && block3.parts[0]?.kind === 'text' && block3.parts[0].text).toBe(
'Hello world!',
'Hello world!'
);
});

Expand All @@ -147,7 +145,7 @@ describe('handleArtifactUpdateEvent', () => {
makeArtifactEvent('', { append: true, lastChunk: true }),
state,
assistantMessage,
onMessageUpdate,
onMessageUpdate
);

// activeTextBlock should be cleared
Expand All @@ -156,8 +154,8 @@ describe('handleArtifactUpdateEvent', () => {
// Artifact should be persisted in contentBlocks with accumulated text
const artifacts = state.contentBlocks.filter((b) => b.type === 'artifact');
expect(artifacts).toHaveLength(1);
expect(artifacts[0].type === 'artifact' && artifacts[0].parts[0]?.kind === 'text' && artifacts[0].parts[0].text).toBe(
'Hello world',
);
expect(
artifacts[0].type === 'artifact' && artifacts[0].parts[0]?.kind === 'text' && artifacts[0].parts[0].text
).toBe('Hello world');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,36 @@ export const AddConnectorDialog = ({
}: {
isOpen: boolean;
onCloseAddConnector: () => void;
connectorType?: ConnectComponentType;
connectorType?: ConnectComponentType | ConnectComponentType[];
onAddConnector: ((connectionName: string, connectionType: ConnectComponentType) => void) | undefined;
components: ComponentList;
}) => (
<Dialog onOpenChange={onCloseAddConnector} open={isOpen}>
<DialogContent size="xl">
<DialogHeader>
<DialogTitle>Add a connector</DialogTitle>
<DialogDescription>Add a connector to your pipeline.</DialogDescription>
</DialogHeader>
<DialogBody>
<ConnectTiles
className="px-0 pt-0"
components={components}
componentTypeFilter={connectorType ? [connectorType] : undefined}
gridCols={3}
hideHeader
onChange={onAddConnector}
variant="ghost"
/>
</DialogBody>
</DialogContent>
</Dialog>
);
}) => {
let typeFilter: ConnectComponentType[] | undefined;
if (Array.isArray(connectorType)) {
typeFilter = connectorType;
} else if (connectorType) {
typeFilter = [connectorType];
}

return (
<Dialog onOpenChange={onCloseAddConnector} open={isOpen}>
<DialogContent size="xl">
<DialogHeader>
<DialogTitle>Add a connector</DialogTitle>
<DialogDescription>Add a connector to your pipeline.</DialogDescription>
</DialogHeader>
<DialogBody>
<ConnectTiles
className="px-0 pt-0"
components={components}
componentTypeFilter={typeFilter}
gridCols={3}
hideHeader
onChange={onAddConnector}
variant="ghost"
/>
</DialogBody>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Badge } from 'components/redpanda-ui/components/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'components/redpanda-ui/components/card';
import { Button, type ButtonProps } from 'components/redpanda-ui/components/button';
import { Separator } from 'components/redpanda-ui/components/separator';
import { PlusIcon } from 'lucide-react';
import { memo } from 'react';

import { getConnectorTypeBadgeProps } from './connector-badges';
import { getConnectorTypeProps } from './connector-badges';
import type { ConnectComponentType } from '../types/schema';

const allowedConnectorTypes: ConnectComponentType[] = ['processor', 'cache', 'buffer'];
Expand All @@ -14,16 +13,18 @@ const SCANNER_SUPPORTED_INPUTS = ['aws_s3', 'gcp_cloud_storage', 'azure_blob_sto
const AddConnectorButton = ({
type,
onClick,
variant = 'secondary-outline',
}: {
type: ConnectComponentType;
onClick: (type: ConnectComponentType) => void;
variant?: ButtonProps['variant'];
}) => {
const { text, variant, icon } = getConnectorTypeBadgeProps(type);
const { text, icon } = getConnectorTypeProps(type);
return (
<Badge className="max-w-fit cursor-pointer" icon={icon} onClick={() => onClick(type)} variant={variant}>
<Button className="max-w-fit" icon={<PlusIcon />} onClick={() => onClick(type)} size="xs" variant={variant}>
{icon}
{text}
<PlusIcon className="mb-0.5 ml-3" size={12} />
</Badge>
</Button>
);
};

Expand All @@ -48,12 +49,8 @@ export const AddConnectorsCard = memo(
: false;

return (
<Card>
<CardHeader>
<CardTitle>Connectors</CardTitle>
<CardDescription>Add connectors to your pipeline.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4 space-y-0">
<div className="border-t p-4">
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2">
{allowedConnectorTypes.map((connectorType) => (
<AddConnectorButton key={connectorType} onClick={onAddConnector} type={connectorType} />
Expand All @@ -63,12 +60,12 @@ export const AddConnectorsCard = memo(
{!(hasInput && hasOutput) && (
<div className="flex flex-col gap-2">
<Separator className="mb-2" />
{!hasInput && <AddConnectorButton onClick={onAddConnector} type="input" />}
{!hasOutput && <AddConnectorButton onClick={onAddConnector} type="output" />}
{!hasInput && <AddConnectorButton onClick={onAddConnector} type="input" variant="outline" />}
{!hasOutput && <AddConnectorButton onClick={onAddConnector} type="output" variant="outline" />}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const AddSecretsDialog = ({
onClose: () => void;
missingSecrets: string[];
existingSecrets: string[];
onSecretsCreated: () => void;
onSecretsCreated: (secretNames?: string[]) => void;
onUpdateEditorContent?: (oldName: string, newName: string) => void;
}) => {
const [errorMessages, setErrorMessages] = useState<string[]>([]);
Expand All @@ -33,9 +33,9 @@ export const AddSecretsDialog = ({
setErrorMessages(errors);
};

const handleSecretsCreated = () => {
const handleSecretsCreated = (names?: string[]) => {
setErrorMessages([]);
onSecretsCreated();
onSecretsCreated(names);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ import { isUsingDefaultRetentionSettings, parseTopicConfigFromExisting, TOPIC_FO
type AddTopicStepProps = {
defaultTopicName?: string;
onValidityChange?: (isValid: boolean) => void;
selectionMode?: 'existing' | 'new' | 'both';
hideTitle?: boolean;
};

export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicStepProps & MotionProps>(
({ defaultTopicName, onValidityChange, ...motionProps }, ref) => {
({ defaultTopicName, onValidityChange, selectionMode = 'both', hideTitle, ...motionProps }, ref) => {
const queryClient = useQueryClient();

const { data: topicList } = useLegacyListTopicsQuery(create(ListTopicsRequestSchema, {}), {
Expand All @@ -72,9 +74,15 @@ export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicSt
[topicList]
);

const [topicSelectionType, setTopicSelectionType] = useState<CreatableSelectionType>(
topicList?.topics?.length === 0 ? CreatableSelectionOptions.CREATE : CreatableSelectionOptions.EXISTING
);
const [topicSelectionType, setTopicSelectionType] = useState<CreatableSelectionType>(() => {
if (selectionMode === 'new') {
return CreatableSelectionOptions.CREATE;
}
if (selectionMode === 'existing') {
return CreatableSelectionOptions.EXISTING;
}
return topicList?.topics?.length === 0 ? CreatableSelectionOptions.CREATE : CreatableSelectionOptions.EXISTING;
});

const createTopicMutation = useCreateTopicMutation();

Expand Down Expand Up @@ -228,16 +236,18 @@ export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicSt
}));

return (
<Card size="full" {...motionProps} animated>
<CardHeader className="max-w-2xl">
<CardTitle>
<Heading level={2}>Read or write data from a topic</Heading>
</CardTitle>
<CardDescription className="mt-4">
Select or create a topic to store data for this streaming pipeline. A topic can have multiple clients
writing data to it (producers) and reading data from it (consumers).
</CardDescription>
</CardHeader>
<Card size="full" {...motionProps} animated variant="ghost">
{!hideTitle && (
<CardHeader className="max-w-2xl">
<CardTitle>
<Heading level={2}>Read or write data from a topic</Heading>
</CardTitle>
<CardDescription className="mt-4">
Select or create a topic to store data for this streaming pipeline. A topic can have multiple clients
writing data to it (producers) and reading data from it (consumers).
</CardDescription>
</CardHeader>
)}
<CardContent className="min-h-[300px]">
<Form {...form}>
<div className="mt-4 max-w-2xl space-y-6">
Expand All @@ -247,26 +257,31 @@ export const AddTopicStep = forwardRef<BaseStepRef<AddTopicFormData>, AddTopicSt
Choose an existing topic to read or write data from, or create a new topic.
</FormDescription>
<div className="flex flex-col items-start gap-2">
<ToggleGroup
disabled={isPending}
onValueChange={(value) => {
// Prevent deselection - ToggleGroup emits empty string when trying to deselect
if (!value) {
return;
}
handleTopicSelectionTypeChange(value as CreatableSelectionType);
}}
type="single"
value={topicSelectionType}
variant="outline"
>
<ToggleGroupItem id={CreatableSelectionOptions.EXISTING} value={CreatableSelectionOptions.EXISTING}>
Existing
</ToggleGroupItem>
<ToggleGroupItem id={CreatableSelectionOptions.CREATE} value={CreatableSelectionOptions.CREATE}>
New
</ToggleGroupItem>
</ToggleGroup>
{selectionMode === 'both' && (
<ToggleGroup
disabled={isPending}
onValueChange={(value) => {
// Prevent deselection - ToggleGroup emits empty string when trying to deselect
if (!value) {
return;
}
handleTopicSelectionTypeChange(value as CreatableSelectionType);
}}
type="single"
value={topicSelectionType}
variant="outline"
>
<ToggleGroupItem
id={CreatableSelectionOptions.EXISTING}
value={CreatableSelectionOptions.EXISTING}
>
Existing
</ToggleGroupItem>
<ToggleGroupItem id={CreatableSelectionOptions.CREATE} value={CreatableSelectionOptions.CREATE}>
New
</ToggleGroupItem>
</ToggleGroup>
)}

<div className="flex gap-2">
<FormField
Expand Down
Loading
Loading