Files
Danny White 5f50338a2f fix(studio): keep region flags visible on light surfaces (#49517)
## What kind of change does this PR introduce?

Studio interface fix.

## What is the current behavior?

Region flags with light backgrounds, such as Japan, can disappear
against light Studio surfaces. Flag rendering is also repeated across
region selectors and infrastructure views.

## What is the new behavior?

Region flags use a shared `RegionFlag` component with the existing small
radius and a subtle 1px border. The treatment is applied consistently
across project creation, replication, read replicas, infrastructure, and
Edge Function observability.

| Before | After |
| --- | --- |
| <img width="746" height="246" alt="CleanShot 2026-08-25 at 14 01
07@2x"
src="https://github.com/user-attachments/assets/5ccffccd-f253-47ca-a587-e97d1e8306a8"
/> | <img width="746" height="234" alt="CleanShot 2026-08-25 at 14 09
20@2x"
src="https://github.com/user-attachments/assets/9d59f7f6-4364-4c2c-8b97-a9673616736a"
/> |

## To test

- Open the new project flow and inspect the selected region and region
menu. Confirm light flags remain visible without changing size or aspect
ratio.
- Open Database Replication and inspect region flags in the diagram and
destination form.
- Open `/project/<ref>/settings/infrastructure` and inspect flags in the
topology, map, and read replica details.
- Open Edge Function observability and inspect the region filter
options.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **UI Improvements**
* Standardized region flag displays across project creation,
replication, infrastructure, read replica, and observability views.
* Region flags now use consistent styling, rounded borders, and
rendering throughout the Studio interface.
* Improved visual consistency for selected regions, database nodes,
tooltips, and region options.
* Decorative flags in observability views are handled appropriately for
assistive technologies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-25 16:25:12 +10:00

108 lines
4.0 KiB
TypeScript

import { Handle, Position } from '@xyflow/react'
import { useParams } from 'common'
import { PropsWithChildren } from 'react'
import { AWS_REGIONS } from 'shared-data'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { DestinationIcon } from '../DestinationIcon'
import { getStatusName } from '../Pipeline.utils'
import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
import { getReplicationDestinationType } from './Nodes.utils'
import { RegionFlag } from '@/components/ui/RegionFlag'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query'
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const NODE_WIDTH = 480
const NodeContainer = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
return (
<div
style={{ width: NODE_WIDTH / 2 + 55 }}
className={cn(
'flex items-start justify-between p-3 rounded-sm bg-surface-100 border border-default',
className
)}
>
{children}
</div>
)
}
export const PrimaryDatabaseNode = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: destinationsData } = useReplicationDestinationsQuery({ projectRef })
const hasDestinations = (destinationsData?.destinations ?? []).length > 0
const region = Object.values(AWS_REGIONS).find((x) => x.code === project?.region)
return (
<NodeContainer>
<div className="text-sm flex flex-col gap-y-0.5">
<p>Primary Database</p>
<p className="text-foreground-light">{region?.displayName}</p>
<p className="text-foreground-light">{region?.code}</p>
</div>
{!!project && <RegionFlag className="mt-0.5 w-8" region={project.region} />}
<Handle
type="source"
position={Position.Right}
className={hasDestinations ? 'opacity-25' : 'opacity-0'}
/>
</NodeContainer>
)
}
export const ReplicationNode = ({ id }: { id: string }) => {
const { ref: projectRef } = useParams()
const { data: destinationsData } = useReplicationDestinationsQuery({ projectRef })
const destination = (destinationsData?.destinations ?? []).find((x) => x.id.toString() === id)
const { data: pipelinesData } = useReplicationPipelinesQuery({
projectRef,
})
const pipeline = (pipelinesData?.pipelines ?? []).find((x) => x.destination_id.toString() === id)
const { data: pipelineStatusData } = useReplicationPipelineStatusQuery(
{ projectRef, pipelineId: pipeline?.id },
{ refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
)
const statusName = getStatusName(pipelineStatusData?.status)
const type = getReplicationDestinationType(destination?.config)
return (
<NodeContainer className="justify-start gap-x-3">
{type ? <DestinationIcon type={type} size={20} className="text-foreground-light" /> : null}
<div className="text-sm flex flex-col gap-y-0.5">
<div className="flex items-center">
<p>{type}</p>
{(statusName === 'started' || statusName === 'failed') && (
<Tooltip>
<TooltipTrigger>
<div className="w-6 h-full flex items-center justify-center">
<div
className={cn(
'w-2 h-2 rounded-full',
statusName === 'started' ? 'bg-brand' : 'bg-destructive'
)}
/>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="capitalize">
{statusName}
</TooltipContent>
</Tooltip>
)}
</div>
<p className="text-foreground-light">{destination?.name}</p>
<p className="text-foreground-light">ID: {destination?.id}</p>
</div>
<Handle type="target" position={Position.Left} className="opacity-25" />
</NodeContainer>
)
}