From ffc76c1e36a7277278ba1f054b8dd4e959bd60f6 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:31:27 +1000 Subject: [PATCH] feat(studio): give replication destinations a brand mark (#50251) ## What kind of change does this PR introduce? Studio UI polish. This is the first PR in the Pipelines review stack and targets `master`. Resolves DEPR-674. ## What is the current behaviour? Replication destinations use generic line icons across the destination picker, pipelines list, and replication diagram. The existing shared ClickHouse and Snowflake assets also use older artwork. ## What is the new behaviour? Adds a reusable `DestinationLogo` treatment and uses it consistently across Replication surfaces. BigQuery, ClickHouse, DuckLake, and Snowflake now use their colour brand marks, while destinations without a dedicated asset retain their existing line icon in the same frame. The refreshed ClickHouse and Snowflake artwork replaces the canonical shared assets, so existing consumers such as the Wrappers catalogue receive the updated marks too. | Light | Dark | | --- | --- | | CleanShot 2026-09-11 at 16 48
42@2x | CleanShot 2026-09-11 at 16 47
30@2x | | Add Pipeline form | | --- | | CleanShot 2026-09-11 at 16 48
57@2x | ## To test 1. Open `/project//database/replication` and start adding a pipeline. 2. Confirm the BigQuery, ClickHouse, DuckLake, and Snowflake marks appear consistently in the destination picker, pipelines list, and replication diagram. 3. Open the ClickHouse and Snowflake entries in the Wrappers catalogue and confirm they use the refreshed artwork. 4. Confirm Analytics Bucket retains its existing fallback icon within the same frame. ## Summary by CodeRabbit * **Visual Updates** * Destination logos now display dedicated brand marks for ClickHouse, DuckLake, Snowflake, and BigQuery. * Updated replication destination selectors, rows, and diagrams to use consistent destination logos. * Adjusted the destination type column width for improved layout. --- .../Database/Replication/DestinationLogo.tsx | 61 +++++++++++++++++++ .../DestinationTypeSelection.tsx | 14 +---- .../Database/Replication/DestinationRow.tsx | 8 +-- .../Database/Replication/Destinations.tsx | 2 +- .../Replication/ReplicationDiagram/Nodes.tsx | 6 +- .../studio/public/img/icons/bigquery-icon.svg | 8 ++- .../public/img/icons/clickhouse-icon.svg | 5 +- .../studio/public/img/icons/ducklake-icon.svg | 11 ++++ .../public/img/icons/snowflake-icon.svg | 11 +++- 9 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Replication/DestinationLogo.tsx create mode 100644 apps/studio/public/img/icons/ducklake-icon.svg diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationLogo.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationLogo.tsx new file mode 100644 index 00000000000..c58c9e2b42b --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/DestinationLogo.tsx @@ -0,0 +1,61 @@ +import { cn, StatusIcon } from 'ui' + +import { DestinationIcon } from './DestinationIcon' +import type { DestinationType } from './DestinationPanel/DestinationPanel.types' +import { BASE_PATH } from '@/lib/constants' + +// Destinations with a brand mark. Anything absent falls back to the line icon in the same frame, +// so a new destination type never renders an empty square. +const BRAND_MARK_BY_TYPE: Partial> = { + BigQuery: `${BASE_PATH}/img/icons/bigquery-icon.svg`, + ClickHouse: `${BASE_PATH}/img/icons/clickhouse-icon.svg`, + DuckLake: `${BASE_PATH}/img/icons/ducklake-icon.svg`, + Snowflake: `${BASE_PATH}/img/icons/snowflake-icon.svg`, +} + +const SIZE_CLASS_NAME = { + small: { frame: 'h-8 w-8 rounded-md', mark: 'h-4 w-4', icon: 16 }, + large: { frame: 'h-14 w-14 rounded-lg', mark: 'h-6 w-6', icon: 24 }, +} as const + +interface DestinationLogoProps { + type: DestinationType + size?: keyof typeof SIZE_CLASS_NAME + className?: string + /** Destructive badge on the bottom-right corner (e.g. table replication errors). */ + hasErrors?: boolean +} + +/** + * A destination's brand mark in a square app-icon frame, used wherever a destination is the + * subject: the list rows, the pipeline header, and the replication diagram. + */ +export const DestinationLogo = ({ + type, + size = 'small', + className, + hasErrors = false, +}: DestinationLogoProps) => { + const sizing = SIZE_CLASS_NAME[size] + const brandMark = BRAND_MARK_BY_TYPE[type] + + return ( + + + {brandMark === undefined ? ( + + ) : ( + + )} + + {hasErrors && ( + + + + )} + + ) +} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index 8ce8a4b5167..955e9616356 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -10,7 +10,7 @@ import { } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { DestinationIcon } from '../DestinationIcon' +import { DestinationLogo } from '../DestinationLogo' import { useDestinationInformation } from '../useDestinationInformation' import { useIsETLBigQueryPrivateAlpha, @@ -163,11 +163,7 @@ export const DestinationTypeSelection = () => { {selectedOption ? (
- + {selectedOption.label}
) : ( @@ -182,11 +178,7 @@ export const DestinationTypeSelection = () => { {group.options.map((option) => (
- +
{option.label} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx index a8498f19a8e..8b553a625c5 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx @@ -15,7 +15,7 @@ import { import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteDestination } from './DeleteDestination' -import { DestinationIcon } from './DestinationIcon' +import { DestinationLogo } from './DestinationLogo' import { PipelineStatus } from './PipelineStatus' import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants' import { getFormattedLagValue } from './ReplicationPipelineStatus/ReplicationPipelineStatus.utils' @@ -149,11 +149,7 @@ export const DestinationRow = ({ destinationId }: DestinationRowProps) => { )} {isPipelineSuccess && ( - - {type ? ( - - ) : null} - + {type ? : null} {isPipelineLoading ? ( diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index a37139e5f1e..c2cc0cc96f0 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -277,7 +277,7 @@ export const Destinations = () => { - + Name diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/Nodes.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/Nodes.tsx index 00a78324baa..df4a4716db0 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/Nodes.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/Nodes.tsx @@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react' import { AWS_REGIONS } from 'shared-data' import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import { DestinationIcon } from '../DestinationIcon' +import { DestinationLogo } from '../DestinationLogo' import { getStatusName } from '../Pipeline.utils' import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants' import { getReplicationDestinationType } from './Nodes.utils' @@ -75,8 +75,7 @@ export const ReplicationNode = ({ id }: { id: string }) => { const type = getReplicationDestinationType(destination?.config) return ( - - {type ? : null} +

{type}

@@ -101,6 +100,7 @@ export const ReplicationNode = ({ id }: { id: string }) => {

{destination?.name}

ID: {destination?.id}

+ {type ? : null} ) diff --git a/apps/studio/public/img/icons/bigquery-icon.svg b/apps/studio/public/img/icons/bigquery-icon.svg index 6aee1b1f440..16453f63229 100644 --- a/apps/studio/public/img/icons/bigquery-icon.svg +++ b/apps/studio/public/img/icons/bigquery-icon.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + + + diff --git a/apps/studio/public/img/icons/clickhouse-icon.svg b/apps/studio/public/img/icons/clickhouse-icon.svg index 5c61225e336..dd9e223dc5d 100644 --- a/apps/studio/public/img/icons/clickhouse-icon.svg +++ b/apps/studio/public/img/icons/clickhouse-icon.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/apps/studio/public/img/icons/ducklake-icon.svg b/apps/studio/public/img/icons/ducklake-icon.svg new file mode 100644 index 00000000000..c65b8238022 --- /dev/null +++ b/apps/studio/public/img/icons/ducklake-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/studio/public/img/icons/snowflake-icon.svg b/apps/studio/public/img/icons/snowflake-icon.svg index 1a70dd0d29f..86a17fd33d9 100644 --- a/apps/studio/public/img/icons/snowflake-icon.svg +++ b/apps/studio/public/img/icons/snowflake-icon.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + +