Skip to content
Open
Show file tree
Hide file tree
Changes from all 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);
});
});
4 changes: 4 additions & 0 deletions frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const FEATURE_FLAGS = {
enableApiKeyConfigurationAgent: false,
enableDataplaneObservabilityServerless: false,
enableDataplaneObservability: false,
enablePipelineDiagrams: false,
};

// Cloud-managed tag keys for service account integration
Expand All @@ -27,3 +28,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);
5 changes: 5 additions & 0 deletions frontend/src/components/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export {
AlertCircle as AlertIcon, // MdOutlineError, MdOutlineErrorOutline, AiOutlineExclamationCircle, AlertIcon (Octicons)
AlertTriangle as WarningIcon, // MdOutlineWarning, MdOutlineWarningAmber, WarningIcon (Chakra)
Archive as ArchiveIcon, // ArchiveIcon (Heroicons)
ArrowBigUp as ArrowBigUpIcon,
ArrowLeft as ArrowLeftIcon,
ArrowRight as TabIcon, // MdKeyboardTab (no direct Tab icon in lucide)
Ban as BanIcon, // MdDoNotDisturb, CircleSlashIcon (Octicons)
Beaker as BeakerIcon, // BeakerIcon (Heroicons)
Expand All @@ -46,6 +48,7 @@ export {
ChevronRight as ChevronRightIcon, // ChevronRightIcon (Heroicons/Octicons)
ChevronUp as ChevronUpIcon, // ChevronUpIcon (Chakra)
Code as CodeIcon, // MdJavascript
Command as CommandIcon,
Copy as CopyIcon, // MdContentCopy
CopyPlus as CopyAllIcon, // MdOutlineCopyAll
Crown as CrownIcon, // FaCrown
Expand All @@ -72,10 +75,12 @@ export {
Pause as PauseIcon, // MdPause
PauseCircle as PauseCircleIcon, // MdOutlinePauseCircle
Pencil as EditIcon, // PencilIcon (Heroicons)
Play as PlayTriangleIcon,
PlayCircle as PlayIcon, // MdPlayCircleOutline
Plus as PlusIcon, // AiOutlinePlus, PlusIcon (Octicons)
RefreshCw as RefreshIcon, // MdOutlineCached, SyncIcon (Octicons)
Reply as ReplyIcon, // MdOutlineQuickreply
RotateCcw as RotateCcwIcon,
RotateCw as RotateCwIcon, // MdRefresh
Scale as ScaleIcon, // ScaleIcon (Heroicons)
Settings as SettingsIcon, // MdOutlineSettings, GearIcon (Octicons)
Expand Down
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