mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): group pipeline destinations by release stage (#49669)
## What kind of change does this PR introduce? UI clarity improvement for the current pipeline creation sheet. ## What is the current behavior? All destination types appear under a generic Pipelines heading, with release-stage badges repeated beside each option. ## What is the new behavior? Destination types are grouped under Public Alpha, Early Access, and Deprecated headings. The selected value stays compact, while its release-stage guidance remains below the field. | Before | After | | --- | --- | | <img width="1280" height="750" alt="CleanShot 2026-08-28 at 15 45 29@2x" src="https://github.com/user-attachments/assets/5be32928-21fa-4911-bc68-3376c068703f" /> | <img width="1280" height="888" alt="CleanShot 2026-08-28 at 15 44 49@2x" src="https://github.com/user-attachments/assets/2046d174-dd4f-41f1-98da-3d8d48af8a1c" /> | ## To test 1. Open a project, then go to Database → Replication and select Add destination. 2. Open the Type selector. 3. Confirm available destinations are grouped by Public Alpha, Early Access, and Deprecated. 4. Select BigQuery and confirm the field still explains that it is in public alpha. 5. Edit an existing destination and confirm the Type selector remains disabled. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI Improvements** * Destination options are now organized into clear release-stage groups: Public Alpha, Early Access, and Deprecated. * Added group headings and separators to make destination selection easier to scan. * Removed individual stage badges from destination labels for a cleaner, more consistent layout. * **Tests** * Updated coverage to verify grouping and visibility across supported destination types. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
19e3844a0e
commit
27dba8d02d
2 files changed
+79
-73
No files matched your search
+9
-4
@@ -76,10 +76,10 @@ describe('DestinationTypeSelection', () => {
|
||||
expect(await screen.findByText('Select a destination type')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('renders the Pipelines group with BigQuery when the flag is enabled', async () => {
|
||||
test('groups destinations by release stage', async () => {
|
||||
mockBigQueryEnabled.mockReturnValue(true)
|
||||
mockIcebergEnabled.mockReturnValue(false)
|
||||
mockDucklakeEnabled.mockReturnValue(false)
|
||||
mockIcebergEnabled.mockReturnValue(true)
|
||||
mockDucklakeEnabled.mockReturnValue(true)
|
||||
mockSnowflakeEnabled.mockReturnValue(false)
|
||||
mockClickHouseEnabled.mockReturnValue(false)
|
||||
addBackgroundMocks()
|
||||
@@ -88,8 +88,13 @@ describe('DestinationTypeSelection', () => {
|
||||
|
||||
fireEvent.click(await screen.findByRole('combobox'))
|
||||
|
||||
expect(await screen.findByText('Pipelines')).toBeInTheDocument()
|
||||
expect(await screen.findByText('Public Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Early Access')).toBeInTheDocument()
|
||||
expect(screen.getByText('Deprecated')).toBeInTheDocument()
|
||||
expect(screen.getByText('BigQuery')).toBeInTheDocument()
|
||||
expect(screen.getByText('DuckLake')).toBeInTheDocument()
|
||||
expect(screen.getByText('Analytics Bucket')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Pipelines')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('hides destinations behind disabled feature flags', async () => {
|
||||
|
||||
+70
-69
@@ -1,11 +1,11 @@
|
||||
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
|
||||
import {
|
||||
Badge,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
@@ -30,13 +30,9 @@ interface DestinationTypeOption {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const STAGE_BADGE_VARIANT: Record<
|
||||
NonNullable<DestinationTypeOption['stage']>,
|
||||
'warning' | 'destructive' | 'default'
|
||||
> = {
|
||||
'Early Access': 'warning',
|
||||
Deprecated: 'destructive',
|
||||
'Public Alpha': 'default',
|
||||
interface DestinationTypeGroup {
|
||||
label: NonNullable<DestinationTypeOption['stage']>
|
||||
options: DestinationTypeOption[]
|
||||
}
|
||||
|
||||
export const DestinationTypeSelection = () => {
|
||||
@@ -72,46 +68,64 @@ export const DestinationTypeSelection = () => {
|
||||
const isOptionVisible = (value: DestinationType, hasAccess: boolean) =>
|
||||
editMode ? destinationType === value : hasAccess
|
||||
|
||||
const options: DestinationTypeOption[] = (
|
||||
[
|
||||
{
|
||||
value: 'Analytics Bucket',
|
||||
label: 'Analytics Bucket',
|
||||
description: 'Write Apache Iceberg tables to Supabase Storage for analytics workflows',
|
||||
stage: 'Deprecated',
|
||||
enabled: isOptionVisible('Analytics Bucket', etlEnableIceberg),
|
||||
},
|
||||
{
|
||||
value: 'BigQuery',
|
||||
label: 'BigQuery',
|
||||
description: "Replicate changes to Google Cloud's data warehouse for analytics and BI",
|
||||
stage: 'Public Alpha',
|
||||
enabled: isOptionVisible('BigQuery', etlEnableBigQuery),
|
||||
},
|
||||
{
|
||||
value: 'DuckLake',
|
||||
label: 'DuckLake',
|
||||
description: 'Replicate changes to a DuckLake catalog backed by S3-compatible storage',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('DuckLake', etlEnableDucklake),
|
||||
},
|
||||
{
|
||||
value: 'Snowflake',
|
||||
label: 'Snowflake',
|
||||
description:
|
||||
'Replicate changes to Snowflake for warehouse analytics and downstream data workflows',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('Snowflake', etlEnableSnowflake),
|
||||
},
|
||||
{
|
||||
value: 'ClickHouse',
|
||||
label: 'ClickHouse',
|
||||
description: 'Stream changes to a ClickHouse cluster for fast columnar analytics',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('ClickHouse', etlEnableClickHouse),
|
||||
},
|
||||
] satisfies DestinationTypeOption[]
|
||||
).filter((option) => option.enabled)
|
||||
const groups: DestinationTypeGroup[] = [
|
||||
{
|
||||
label: 'Public Alpha',
|
||||
options: [
|
||||
{
|
||||
value: 'BigQuery',
|
||||
label: 'BigQuery',
|
||||
description: "Replicate changes to Google Cloud's data warehouse for analytics and BI",
|
||||
stage: 'Public Alpha',
|
||||
enabled: isOptionVisible('BigQuery', etlEnableBigQuery),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Early Access',
|
||||
options: [
|
||||
{
|
||||
value: 'DuckLake',
|
||||
label: 'DuckLake',
|
||||
description: 'Replicate changes to a DuckLake catalog backed by S3-compatible storage',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('DuckLake', etlEnableDucklake),
|
||||
},
|
||||
{
|
||||
value: 'Snowflake',
|
||||
label: 'Snowflake',
|
||||
description:
|
||||
'Replicate changes to Snowflake for warehouse analytics and downstream data workflows',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('Snowflake', etlEnableSnowflake),
|
||||
},
|
||||
{
|
||||
value: 'ClickHouse',
|
||||
label: 'ClickHouse',
|
||||
description: 'Stream changes to a ClickHouse cluster for fast columnar analytics',
|
||||
stage: 'Early Access',
|
||||
enabled: isOptionVisible('ClickHouse', etlEnableClickHouse),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Deprecated',
|
||||
options: [
|
||||
{
|
||||
value: 'Analytics Bucket',
|
||||
label: 'Analytics Bucket',
|
||||
description: 'Write Apache Iceberg tables to Supabase Storage for analytics workflows',
|
||||
stage: 'Deprecated',
|
||||
enabled: isOptionVisible('Analytics Bucket', etlEnableIceberg),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const visibleGroups = groups
|
||||
.map((group) => ({ ...group, options: group.options.filter((option) => option.enabled) }))
|
||||
.filter((group) => group.options.length > 0)
|
||||
const options = visibleGroups.flatMap((group) => group.options)
|
||||
|
||||
const selectedOption = options.find((option) => option.value === destinationType)
|
||||
|
||||
@@ -154,24 +168,18 @@ export const DestinationTypeSelection = () => {
|
||||
size={20}
|
||||
className="shrink-0 text-foreground-light"
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="text-sm text-foreground">{selectedOption.label}</span>
|
||||
{selectedOption.stage && (
|
||||
<Badge variant={STAGE_BADGE_VARIANT[selectedOption.stage]}>
|
||||
{selectedOption.stage}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-foreground">{selectedOption.label}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-foreground-lighter">Select a destination type</span>
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{options.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Pipelines</SelectLabel>
|
||||
{options.map((option) => (
|
||||
{visibleGroups.map((group, index) => (
|
||||
<SelectGroup key={group.label}>
|
||||
{index > 0 && <SelectSeparator />}
|
||||
<SelectLabel>{group.label}</SelectLabel>
|
||||
{group.options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value} className="py-2">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<DestinationIcon
|
||||
@@ -180,14 +188,7 @@ export const DestinationTypeSelection = () => {
|
||||
className="shrink-0 text-foreground-light"
|
||||
/>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="text-foreground">{option.label}</span>
|
||||
{option.stage && (
|
||||
<Badge variant={STAGE_BADGE_VARIANT[option.stage]}>
|
||||
{option.stage}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-foreground">{option.label}</span>
|
||||
<span className="text-xs text-foreground-lighter">
|
||||
{option.description}
|
||||
</span>
|
||||
@@ -196,7 +197,7 @@ export const DestinationTypeSelection = () => {
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItemLayout>
|
||||
|
||||
Reference in new issue
Block a user