chore: migrate old <Select /> usage to the new Shadcn component (#45232)

## Problem

We want to reduce the code we ship and maintain.

## Solution

Migrate old `<Select />` usage to the new Shadcn component.

## Screenshots

### `www` Pricing 

Before:
<img width="637" height="697" alt="image"
src="https://github.com/user-attachments/assets/b6f261de-e587-411b-9408-faf94d709f1c"
/>

After:
<img width="644" height="756" alt="image"
src="https://github.com/user-attachments/assets/8cc4894c-64da-4e6a-960c-77cd162ac71d"
/>

### Observability

Before:
<img width="1015" height="452" alt="image"
src="https://github.com/user-attachments/assets/3d7e8613-e7a6-461d-a50d-e66c7c85fef1"
/>

After:
<img width="833" height="467" alt="image"
src="https://github.com/user-attachments/assets/98ace34f-25ec-48b5-aad3-fe812307b01d"
/>

### Docs Realtime

Used in pages:
- https://supabase.com/docs/guides/realtime/postgres-changes
- https://supabase.com/docs/guides/realtime/benchmarks

Before:
<img width="578" height="437" alt="image"
src="https://github.com/user-attachments/assets/22fa0048-be07-42e0-9153-65171fa3ccb9"
/>

After:
<img width="571" height="423" alt="image"
src="https://github.com/user-attachments/assets/e0adbde9-0c6f-48da-b377-516392185fb0"
/>

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

* **Refactor**
* Updated dropdown/select controls across the app to a consistent,
composable implementation
* Replaced advanced JWT generator in docs with a simplified JWT
generator component

* **Chores**
  * Removed legacy select component, associated styles and exports
  * Updated theme and tests to align with the new select implementation
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia
2026-04-27 15:35:50 +02:00
committed by GitHub
parent b4a6897b1b
commit bc3dc73240
17 changed files with 302 additions and 672 deletions

View File

@@ -1,142 +0,0 @@
import { KJUR } from 'jsrsasign'
import { ChangeEvent, useState } from 'react'
import { Button, Input, Select } from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
const JWT_HEADER = { alg: 'HS256', typ: 'JWT' }
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const fiveYears = new Date(now.getFullYear() + 5, now.getMonth(), now.getDate())
const anonToken = {
role: 'anon',
iss: 'supabase',
iat: Math.floor(today.valueOf() / 1000),
exp: Math.floor(fiveYears.valueOf() / 1000),
}
const serviceToken = {
role: 'service_role',
iss: 'supabase',
iat: Math.floor(today.valueOf() / 1000),
exp: Math.floor(fiveYears.valueOf() / 1000),
}
const generateRandomString = (length: number) => {
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
/**
* The number of possible Uint8 integers is 256. Since the length of `CHARS`
* doesn't fit exactly into 256, simply taking the modulus would create an
* uneven distribution that favors the earlier characters. To make a truly
* uniform distribution, we have to discard everything above the last full
* cycle, and pick again.
*
* The minus 1 is to account for 0-indexing.
*/
const MAX = Math.floor(256 / CHARS.length) * CHARS.length - 1
const randomUInt8Array = new Uint8Array(1)
for (let i = 0; i < length; i++) {
let randomNumber: number
do {
crypto.getRandomValues(randomUInt8Array)
randomNumber = randomUInt8Array[0]
/**
* Keep picking until we get a number in the valid range.
*/
} while (randomNumber > MAX)
result += CHARS[randomNumber % CHARS.length]
}
return result
}
export default function JwtGenerator() {
const secret = generateRandomString(40)
const [jwtSecret, setJwtSecret] = useState(secret)
const [token, setToken] = useState(anonToken)
const [signedToken, setSignedToken] = useState('')
const [err, setErr] = useState<string>('')
const handleKeySelection = (e: ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value
if (val == 'service') setToken(serviceToken)
else setToken(anonToken)
}
const handleClaimsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
try {
const newTok = JSON.parse(e.target.value)
setToken(newTok)
setErr('')
} catch (err) {
const errMessage =
!!err && typeof err === 'object' && 'message' in err && typeof err.message === 'string'
? err.message
: ''
setErr('Not a valid JSON body' + (errMessage ? `: ${errMessage}` : ''))
}
}
const generate = () => {
const signedJWT = KJUR.jws.JWS.sign(null, JWT_HEADER, token, jwtSecret)
setSignedToken(signedJWT)
}
return (
<div className="border rounded-lg p-4">
<div className="grid mb-8">
<label htmlFor="secret">JWT Secret:</label>
<Input
id="secret"
type="text"
placeholder="JWT Secret (at least 32 characters)"
value={jwtSecret}
style={{ fontFamily: 'monospace' }}
onChange={(e) => setJwtSecret(e.target.value)}
/>
</div>
<div className="grid mb-8">
<label htmlFor="service">Key:</label>
<Select id="service" style={{ fontFamily: 'monospace' }} onChange={handleKeySelection}>
<Select.Option value="anon">ANON_KEY</Select.Option>
<Select.Option value="service">SERVICE_KEY</Select.Option>
</Select>
</div>
<div className="grid mb-8">
<label htmlFor="token">The JWT will be generated from this info:</label>
<Input.TextArea
key={JSON.stringify(token)}
id="token"
type="text"
rows={6}
placeholder="A valid JWT Token"
defaultValue={JSON.stringify(token, null, 2)}
style={{ fontFamily: 'monospace' }}
onChange={handleClaimsChange}
/>
{err && (
<span className="text-sm text-destructive-600">Input must be valid JSON. {err}</span>
)}
</div>
<Button type="primary" onClick={generate}>
Generate JWT
</Button>
{signedToken && (
<div className="mt-8">
<h4>Generated Token:</h4>
<CodeBlock language="bash" className="relative font-mono">
{signedToken}
</CodeBlock>
</div>
)}
</div>
)
}

View File

@@ -3,17 +3,8 @@
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
const DynamicJwtGenerator = dynamic(() => import('./JwtGenerator'), { ssr: false })
const DynamicJwtGeneratorSimple = dynamic(() => import('./JwtGeneratorSimple'), { ssr: false })
const JwtGenerator = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<DynamicJwtGenerator />
</Suspense>
)
}
const JwtGeneratorSimple = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
@@ -22,4 +13,4 @@ const JwtGeneratorSimple = () => {
)
}
export { JwtGenerator, JwtGeneratorSimple }
export { JwtGeneratorSimple }

View File

@@ -1,7 +1,16 @@
import throughputTable from '~/data/realtime/throughput.json'
import { ChevronDown } from 'lucide-react'
import { useState } from 'react'
import { Button, Collapsible, Select } from 'ui'
import throughputTable from '~/data/realtime/throughput.json'
import {
Button,
Collapsible,
Label_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
export default function RealtimeLimitsEstimater({}) {
const findTableValue = ({ computeAddOn, filters, rls, concurrency }) => {
@@ -23,29 +32,28 @@ export default function RealtimeLimitsEstimater({}) {
const [expandPreview, setExpandPreview] = useState(false)
const handleComputeAddOnSelection = (e) => {
const val = e.target.value
const handleComputeAddOnSelection = (val) => {
setComputeAddOn(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn: val, filters, rls, concurrency: 500 }))
}
const handleFiltersSelection = (e) => {
const val = e.target.value.toLowerCase() === 'true'
const handleFiltersSelection = (value) => {
const val = value.toLowerCase() === 'true'
setFilters(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn, filters: val, rls, concurrency: 500 }))
}
const handleRLSSelection = (e) => {
const val = e.target.value.toLowerCase() === 'true'
const handleRLSSelection = (value) => {
const val = value.toLowerCase() === 'true'
setRLS(val)
setConcurrency(500)
setLimits(findTableValue({ computeAddOn, filters, rls: val, concurrency: 500 }))
}
const handleConcurrencySelection = (e) => {
const val = parseInt(e.target.value)
const handleConcurrencySelection = (value) => {
const val = parseInt(value)
setConcurrency(val)
setLimits(findTableValue({ computeAddOn, filters, rls, concurrency: val }))
}
@@ -55,51 +63,64 @@ export default function RealtimeLimitsEstimater({}) {
<h4>Set your expected parameters</h4>
<div className="grid mb-8 gap-y-8 gap-x-8 grid-cols-2 xl:grid-cols-4">
<div>
<label htmlFor="computeAddOn">Compute:</label>
<Select id="computeAddOn" className="font-mono" onChange={handleComputeAddOnSelection}>
<Select.Option value="micro">Micro</Select.Option>
<Select.Option value="small">Small to medium</Select.Option>
<Select.Option value="large">Large to 16XL</Select.Option>
</Select>
<Label_Shadcn_ htmlFor="computeAddOn">Compute:</Label_Shadcn_>
<Select_Shadcn_ onValueChange={handleComputeAddOnSelection} value={computeAddOn}>
<SelectTrigger_Shadcn_ id="computeAddOn">
<SelectValue_Shadcn_ className="font-mono" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="micro">Micro</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="small">Small to medium</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="large">Large to 16XL</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div>
<label htmlFor="filters">Filters:</label>
<Select
id="filters"
className="font-mono"
disabled={true}
onChange={handleFiltersSelection}
<Label_Shadcn_ htmlFor="filters">Filters:</Label_Shadcn_>
<Select_Shadcn_
onValueChange={handleFiltersSelection}
value={filters.toString()}
disabled
>
<Select.Option value="false">No</Select.Option>
<Select.Option value="true">Yes</Select.Option>
</Select>
<SelectTrigger_Shadcn_ id="filters">
<SelectValue_Shadcn_ className="font-mono" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="false">No</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="true">Yes</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div>
<label htmlFor="rls">RLS:</label>
<Select id="rls" className="font-mono" onChange={handleRLSSelection}>
<Select.Option value="false">No</Select.Option>
<Select.Option value="true">Yes</Select.Option>
</Select>
<Label_Shadcn_ htmlFor="rls">RLS:</Label_Shadcn_>
<Select_Shadcn_ onValueChange={handleRLSSelection} value={rls.toString()}>
<SelectTrigger_Shadcn_ id="rls">
<SelectValue_Shadcn_ className="font-mono" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="false">No</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="true">Yes</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div>
<label htmlFor="concurrency">Connected clients:</label>
<Select id="concurrency" className="font-mono" onChange={handleConcurrencySelection}>
{throughputTable
.filter(
(l) => l.computeAddOn === computeAddOn && l.filters === filters && l.rls === rls
)
.map((l) => (
<Select.Option
value={l.concurrency.toString()}
selected={l.concurrency === concurrency}
>
{Intl.NumberFormat().format(l.concurrency)}
</Select.Option>
))}
</Select>
<Label_Shadcn_ htmlFor="concurrency">Connected clients:</Label_Shadcn_>
<Select_Shadcn_ onValueChange={handleConcurrencySelection} value={concurrency.toString()}>
<SelectTrigger_Shadcn_ id="concurrency">
<SelectValue_Shadcn_ className="font-mono" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{throughputTable
.filter(
(l) => l.computeAddOn === computeAddOn && l.filters === filters && l.rls === rls
)
.map((l) => (
<SelectItem_Shadcn_ key={l.concurrency} value={l.concurrency.toString()}>
{Intl.NumberFormat().format(l.concurrency)}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
</div>

View File

@@ -1,11 +1,3 @@
import { ArrowDown, Check, X } from 'lucide-react'
import Link from 'next/link'
import { Badge, Button } from 'ui'
import { Admonition, type AdmonitionProps } from 'ui-patterns/admonition'
import { GlassPanel } from 'ui-patterns/GlassPanel'
import { IconPanel } from 'ui-patterns/IconPanel'
import SqlToRest from 'ui-patterns/SqlToRest'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex'
import { AiSkillsIndex } from '~/app/guides/getting-started/ai-skills/AiSkillsIndex'
import { AppleSecretGenerator } from '~/components/AppleSecretGenerator'
@@ -13,15 +5,15 @@ import AuthProviders from '~/components/AuthProviders'
import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig'
import { CostWarning } from '~/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings'
import ButtonCard from '~/components/ButtonCard'
import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable'
import { Extensions } from '~/components/Extensions'
import Image, { type ImageProps } from '~/components/Image'
import { JwtGenerator, JwtGeneratorSimple } from '~/components/JwtGenerator'
import { JwtGeneratorSimple } from '~/components/JwtGenerator'
import { MetricsStackCards } from '~/components/MetricsStackCards'
import { NavData } from '~/components/NavData'
import { Price } from '~/components/Price'
import { ProjectConfigVariables } from '~/components/ProjectConfigVariables'
import { RealtimeLimitsEstimator } from '~/components/RealtimeLimitsEstimator'
import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable'
import { RegionsList, SmartRegionsList } from '~/components/RegionsList'
import { SharedData } from '~/components/SharedData'
import StepHikeCompact from '~/components/StepHikeCompact'
@@ -32,6 +24,15 @@ import { CodeBlock } from '~/features/ui/CodeBlock/CodeBlock'
import InfoTooltip from '~/features/ui/InfoTooltip'
import { ShowUntil } from '~/features/ui/ShowUntil'
import { TabPanel, Tabs } from '~/features/ui/Tabs'
import { ArrowDown, Check, X } from 'lucide-react'
import Link from 'next/link'
import { Badge, Button } from 'ui'
import { Admonition, type AdmonitionProps } from 'ui-patterns/admonition'
import { GlassPanel } from 'ui-patterns/GlassPanel'
import { IconPanel } from 'ui-patterns/IconPanel'
import SqlToRest from 'ui-patterns/SqlToRest'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { ErrorCodes } from '../ui/ErrorCodes'
import { McpConfigPanel } from '../ui/McpConfigPanel'
@@ -64,7 +65,6 @@ const components = {
IconPanel,
IconX: X,
Image: (props: ImageProps) => <Image className="rounded-md w-full" {...props} />,
JwtGenerator,
JwtGeneratorSimple,
Link,
McpConfigPanel,

View File

@@ -1,5 +1,12 @@
import type { RenderEditCellProps } from 'react-data-grid'
import { Select } from 'ui'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
interface Props<TRow, TSummaryRow = unknown> extends RenderEditCellProps<TRow, TSummaryRow> {
isNullable?: boolean
@@ -15,8 +22,7 @@ export const BooleanEditor = <TRow, TSummaryRow = unknown>({
const value = row[column.key as keyof TRow] as unknown as string
const onBlur = () => onClose(false)
const onChange = (event: any) => {
const value = event.target.value
const onChange = (value: string) => {
if (value === 'null') {
onRowChange({ ...row, [column.key]: null }, true)
} else {
@@ -25,19 +31,21 @@ export const BooleanEditor = <TRow, TSummaryRow = unknown>({
}
return (
<Select
autoFocus
id="boolean-editor"
<Select_Shadcn_
name="boolean-editor"
size="small"
onBlur={onBlur}
onChange={onChange}
defaultValue={value === null || value === undefined ? 'null' : value.toString()}
style={{ width: `${column.width}px` }}
onValueChange={onChange}
>
<Select.Option value="true">TRUE</Select.Option>
<Select.Option value="false">FALSE</Select.Option>
{isNullable && <Select.Option value="null">NULL</Select.Option>}
</Select>
<SelectTrigger_Shadcn_ onBlur={onBlur} style={{ width: `${column.width}px` }}>
<SelectValue_Shadcn_ id="boolean-editor" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectItem_Shadcn_ value="true">TRUE</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="false">FALSE</SelectItem_Shadcn_>
{isNullable ? <SelectItem_Shadcn_ value="null">NULL</SelectItem_Shadcn_> : null}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)
}

View File

@@ -1,12 +1,19 @@
import type { RenderEditCellProps } from 'react-data-grid'
import { Select } from 'ui'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
interface SelectEditorProps<TRow, TSummaryRow = unknown> extends RenderEditCellProps<
TRow,
TSummaryRow
> {
isNullable?: boolean
options: { label: string; _value: string }[]
options: { label: string; value: string }[]
}
export function SelectEditor<TRow, TSummaryRow = unknown>({
@@ -19,11 +26,11 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
}: SelectEditorProps<TRow, TSummaryRow>) {
const value = row[column.key as keyof TRow] as unknown as string
function onChange(event: any) {
if (!event.target.value || event.target.value == '') {
function onChange(value: string) {
if (!value || value == '') {
onRowChange({ ...row, [column.key]: null }, true)
} else {
onRowChange({ ...row, [column.key]: event.target.value }, true)
onRowChange({ ...row, [column.key]: value }, true)
}
}
@@ -32,24 +39,20 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
}
return (
<Select
autoFocus
id="select-editor"
name="select-editor"
size="small"
defaultValue={value ?? ''}
className="sb-grid-select-editor !gap-2"
style={{ width: `${column.width}px` }}
// @ts-ignore
onChange={onChange}
onBlur={onBlur}
>
{isNullable && <Select.Option value="">NULL</Select.Option>}
{options.map(({ label, _value }) => (
<Select.Option key={_value} value={_value} selected={_value === value}>
{label}
</Select.Option>
))}
</Select>
<Select_Shadcn_ name="select-editor" defaultValue={value ?? ''} onValueChange={onChange}>
<SelectTrigger_Shadcn_ onBlur={onBlur} style={{ width: `${column.width}px` }}>
<SelectValue_Shadcn_ id="select-editor" placeholder="NULL" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{isNullable ? <SelectItem_Shadcn_ value={null as any}>NULL</SelectItem_Shadcn_> : null}
{options.map(({ label, value }) => (
<SelectItem_Shadcn_ key={value} value={value}>
{label}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)
}

View File

@@ -13,8 +13,15 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
Select,
Input_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DatePickerValue, LogsDatePicker } from '../Settings/Logs/Logs.DatePickers'
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
@@ -291,52 +298,82 @@ const ReportFilterBar = ({
</PopoverTrigger>
<PopoverContent align={filters.length > 0 ? 'end' : 'start'} className="p-0 w-60">
<div className="flex flex-col gap-3 p-3">
<Select
size="tiny"
value={addFilterValues.key}
onChange={(e) => {
setAddFilterValues((prev) => ({ ...prev, key: e.target.value }))
}}
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Attribute Filter"
className="gap-[2px]"
>
{filterKeys.map((key) => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
</Select>
<Select
size="tiny"
value={addFilterValues.compare}
onChange={(e) => {
setAddFilterValues((prev) => ({
...prev,
compare: e.target.value as ReportFilterItem['compare'],
}))
}}
>
<Select_Shadcn_
value={addFilterValues.key}
onValueChange={(value: string) =>
setAddFilterValues((prev) => ({ ...prev, key: value }))
}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="---" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{filterKeys.map((key) => (
<SelectItem_Shadcn_ key={key} value={key}>
{key}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Comparison"
className="gap-[2px]"
>
{['is', 'matches'].map((value) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
))}
</Select>
<Input
size="tiny"
>
<Select_Shadcn_
value={addFilterValues.compare}
onValueChange={(value: string) =>
setAddFilterValues((prev) => ({
...prev,
compare: value as ReportFilterItem['compare'],
}))
}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="---" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{['is', 'matches'].map((value) => (
<SelectItem_Shadcn_ key={value} value={value}>
{value}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Value"
className="gap-[2px]"
placeholder={
addFilterValues.compare === 'matches'
? 'Provide a regex expression'
: 'Provide a string'
}
onChange={(e) => {
setAddFilterValues((prev) => ({ ...prev, value: e.target.value }))
}}
/>
size="tiny"
>
<Input_Shadcn_
placeholder={
addFilterValues.compare === 'matches'
? 'Provide a regex expression'
: 'Provide a string'
}
value={addFilterValues.value}
onChange={(e) => {
setAddFilterValues((prev) => ({ ...prev, value: e.target.value }))
}}
/>
</FormItemLayout>
</div>
<div className="flex items-center justify-end gap-2 border-t border-default p-2">

View File

@@ -1,5 +1,12 @@
import { noop } from 'lodash'
import { Select } from 'ui'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import { POSTGRES_DATA_TYPES } from '../SidePanelEditor.constants'
import type { ColumnField } from '../SidePanelEditor.types'
@@ -36,21 +43,28 @@ const ColumnDefaultValue = ({
if (enumType !== undefined) {
return (
<Select
label="Default Value"
layout="vertical"
value={formattedValue}
onChange={(event: any) => onUpdateField({ defaultValue: event.target.value })}
>
<Select.Option key="empty-enum" value="">
NULL
</Select.Option>
{enumValues.map((value: string) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
))}
</Select>
<>
<label className="block text-foreground-light">Default Value</label>
<Select_Shadcn_
name="select-editor"
value={formattedValue}
onValueChange={(value) => onUpdateField({ defaultValue: value })}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ id="select-editor" placeholder="NULL" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectItem_Shadcn_ value={null as any}>NULL</SelectItem_Shadcn_>
{enumValues.map((value) => (
<SelectItem_Shadcn_ key={value} value={value}>
{value}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</>
)
}
}

View File

@@ -8,7 +8,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
Select,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
@@ -76,24 +75,34 @@ export const InputField = ({
)
} else {
return (
<Select
size="medium"
<FormItemLayout
isReactForm={false}
layout="horizontal"
value={field.value ?? ''}
label={field.name}
labelOptional={field.format}
descriptionText={field.comment}
disabled={!isEditable}
error={errors[field.name]}
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
description={field.comment}
className="[&>div:first-child>span]:text-foreground-lighter"
>
<Select.Option value="">---</Select.Option>
{field.enums.map((value: string) => (
<Select.Option key={value} value={value}>
{value}
</Select.Option>
))}
</Select>
<Select_Shadcn_
value={field.value ?? ''}
onValueChange={(value: string) => onUpdateField({ [field.name]: value })}
disabled={!isEditable}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="---" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectItem_Shadcn_ value={null as any}>---</SelectItem_Shadcn_>
{field.enums.map((value) => (
<SelectItem_Shadcn_ key={value} value={value}>
{value}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
)
}
}

View File

@@ -1,15 +1,24 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
import { plans } from 'shared-data/plans'
import { pricing } from 'shared-data/pricing'
import { Button, Select, cn } from 'ui'
import { PricingTableRowDesktop, PricingTableRowMobile } from '~/components/Pricing/PricingTableRow'
import Solutions from '~/data/MainProducts'
import { Organization } from '~/data/organizations'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import Link from 'next/link'
import { useState } from 'react'
import { plans } from 'shared-data/plans'
import { pricing } from 'shared-data/pricing'
import {
Button,
cn,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
} from 'ui'
import UpgradePlan from './UpgradePlan'
const MobileHeader = ({
@@ -117,20 +126,24 @@ const PricingComparisonTable = ({
{/* Free - Mobile */}
<div className="bg-background p-2 sticky top-14 z-10 pt-4">
<div className="bg-surface-100 rounded-lg border py-2 px-4 flex justify-between items-center">
<label className="text-foreground-lighter">Change plan</label>
<Select
id="change-plan"
<label className="text-foreground-lighter grow">Change plan</label>
<Select_Shadcn_
name="Change plan"
layout="vertical"
value={activeMobilePlan}
className="min-w-[120px]"
onChange={(e) => setActiveMobilePlan(e.target.value)}
onValueChange={(value) => setActiveMobilePlan(value)}
>
<Select.Option value="Free">Free</Select.Option>
<Select.Option value="Pro">Pro</Select.Option>
<Select.Option value="Team">Team</Select.Option>
<Select.Option value="Enterprise">Enterprise</Select.Option>
</Select>
<SelectTrigger_Shadcn_ id="change-plan" className="w-auto min-w-[120px]">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectItem_Shadcn_ value="Free">Free</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="Pro">Pro</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="Team">Team</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="Enterprise">Enterprise</SelectItem_Shadcn_>
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
</div>
{activeMobilePlan === 'Free' && (

View File

@@ -244,7 +244,8 @@ testRunner('table editor', () => {
// insert row with enum value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByText('Insert a new row into').click()
await page.getByRole('combobox').selectOption('value1')
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'value1' }).click()
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByTestId('side-panel-row-editor')).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value1' })).toBeVisible()
@@ -252,7 +253,8 @@ testRunner('table editor', () => {
// insert row with another enum value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByText('Insert a new row into').click()
await page.getByRole('combobox').selectOption('value2')
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'value2' }).click()
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible({ timeout: 10_000 })
@@ -903,7 +905,8 @@ testRunner('table editor', () => {
const updateTrueResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.selectOption('true')
await booleanEditor.click()
await page.getByRole('option', { name: 'true' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateTrueResponse
@@ -922,7 +925,8 @@ testRunner('table editor', () => {
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.selectOption('false')
await booleanEditor.click()
await page.getByRole('option', { name: 'false' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse
@@ -1019,7 +1023,8 @@ testRunner('table editor', () => {
const updateNullResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.selectOption('null')
await booleanEditor.click()
await page.getByRole('option', { name: 'null' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateNullResponse
@@ -1034,7 +1039,8 @@ testRunner('table editor', () => {
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.selectOption('false')
await booleanEditor.click()
await page.getByRole('option', { name: 'false' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse

View File

@@ -36,7 +36,6 @@ export * from './src/lib/utils'
// DATA ENTRY
export * from './src/components/Select'
export * from './src/components/Listbox'
export * from './src/components/Input'
export * from './src/components/Toggle'

View File

@@ -1,86 +0,0 @@
.sbui-select-container {
@apply relative;
}
.sbui-select {
@apply block w-full bg-white pl-3 pr-10 py-2 text-sm rounded-md shadow-sm transition-all;
@apply text-input-value-light border border-solid border-input-border-light;
@apply focus:ring-input-border-focus-light focus:border-input-border-focus-light focus:outline-none;
@apply dark:bg-transparent dark:text-input-value-dark dark:border-input-border-dark;
@apply dark:focus:border-input-border-focus-dark dark:focus:ring-input-border-focus-dark;
-webkit-appearance: none;
-moz-appearance: none;
text-overflow: '';
/* box-shadow: 0 0 0 2px rgba(255, 255, 255, 0); */
transition: box-shadow 0.3s ease-in-out;
/* // temporary fix
//
// temporary fix for supabase apps
// tailwind @base styles adds a dropdown chevron
// using background image as default
// */
background-image: none;
}
.sbui-select option {
@apply bg-white text-gray-800;
}
.sbui-select:focus {
box-shadow: 0 0 0 2px rgba(62, 207, 142, 0.1);
}
.sbui-select--error {
@apply border-red-500;
}
.sbui-select--borderless {
@apply border-transparent shadow-none;
}
/*
Select sizes
*/
.sbui-select--tiny {
@apply px-2.5 py-1.5 text-xs;
}
.sbui-select--small {
@apply px-3 py-2 text-sm leading-4;
}
.sbui-select--medium {
@apply px-4 py-2 text-sm;
}
.sbui-select--large {
@apply px-4 py-2 text-base;
}
.sbui-select--xlarge {
@apply px-6 py-3 text-base;
}
.sbui-select-actions-container {
@apply absolute inset-y-0 right-0 pl-3 pr-1 mr-5 flex items-center;
}
/*
Select icon
*/
.sbui-select--with-icon {
@apply pl-7;
}
/*
Select Chevron
*/
.sbui-select-chevron-container {
@apply absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none;
}
.sbui-select-chevron {
@apply h-5 w-5 text-gray-400;
}

View File

@@ -1,29 +0,0 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import { describe, expect, it } from 'vitest'
import Select from './Select'
describe('#Select', () => {
it('should render select correctly', async () => {
render(
<Select data-testid="form-select">
<option>1</option>
<option>2</option>
</Select>
)
expect(screen.queryByTestId('form-select')).toBeInTheDocument()
})
it('should have "form-select--error" class', () => {
render(
<Select error data-testid="form-select">
<option>1</option>
<option>2</option>
</Select>
)
expect(screen.queryByTestId('form-select')).toHaveClass(
'block box-border w-full rounded-md shadow-sm transition-all text-foreground border focus-visible:shadow-md outline-none focus:ring-current focus:ring-2 focus-visible:border-foreground-muted focus-visible:ring-background-control placeholder-foreground-muted appearance-none bg-none bg-destructive-200 border border-destructive-500 focus:ring-destructive-400 placeholder:text-destructive-400 text-base md:text-sm px-4 py-2'
)
})
})

View File

@@ -1,164 +0,0 @@
'use client'
import React from 'react'
import { FormLayout } from '../../lib/Layout/FormLayout/FormLayout'
import InputErrorIcon from '../../lib/Layout/InputErrorIcon'
import InputIconContainer from '../../lib/Layout/InputIconContainer'
import styleHandler from '../../lib/theme/styleHandler'
interface OptionProps {
value: string
children: React.ReactNode
selected?: boolean
}
interface OptGroupProps {
label: string
children: React.ReactNode
}
export interface Props extends Omit<React.InputHTMLAttributes<HTMLSelectElement>, 'size'> {
autofocus?: boolean
children: React.ReactNode
descriptionText?: string
error?: string
icon?: any
inputRef?: string
label?: string
afterLabel?: string
beforeLabel?: string
labelOptional?: string
layout?: 'horizontal' | 'vertical'
reveal?: boolean
actions?: React.ReactNode
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
borderless?: boolean
validation?: (x: any) => void
}
export const ColLayout = (props: any) => <div>{props.children}</div>
/**
* @deprecated Use `import { Select_shadcn_ } from "ui"` instead
*/
function Select({
autoComplete,
autofocus,
children,
className,
descriptionText,
disabled,
error,
icon,
id = '',
inputRef,
label,
afterLabel,
beforeLabel,
labelOptional,
layout,
name = '',
placeholder,
required,
value = undefined,
defaultValue = undefined,
style,
size = 'medium',
borderless = false,
validation,
...props
}: Props) {
const __styles = styleHandler('select')
let classesContainer = [__styles.container]
if (className) classesContainer.push(className)
let classes = [__styles.base]
if (error) classes.push(__styles.variants.error)
if (!error) classes.push(__styles.variants.standard)
if (icon) classes.push(__styles.with_icon[size])
if (size) classes.push(__styles.size[size])
if (disabled) classes.push(__styles.disabled)
return (
<FormLayout
label={label}
afterLabel={afterLabel}
beforeLabel={beforeLabel}
labelOptional={labelOptional}
layout={layout}
id={id}
error={error}
descriptionText={descriptionText}
className={className}
style={style}
size={size}
>
<div className={__styles.container}>
<select
id={id}
name={name}
data-size={size}
defaultValue={defaultValue}
autoComplete={autoComplete}
autoFocus={autofocus}
className={classes.join(' ')}
ref={inputRef}
value={value}
disabled={disabled}
required={required}
// @ts-ignore
placeholder={placeholder}
{...props}
>
{children}
</select>
{icon && <InputIconContainer size={size} icon={icon} />}
{error && (
<div className={__styles.actions_container}>
{error && <InputErrorIcon size={size} />}
</div>
)}
<span className={__styles.chevron_container}>
<svg
className={__styles.chevron}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</span>
</div>
</FormLayout>
)
}
/**
* @deprecated Use ./SelectItem_Shadcn_ instead
*/
export function Option({ value, children, selected }: OptionProps) {
return (
<option value={value} selected={selected}>
{children}
</option>
)
}
/**
* @deprecated Use ./SelectGroup_Shadcn_ instead
*/
export function OptGroup({ label, children }: OptGroupProps) {
return <optgroup label={label}>{children}</optgroup>
}
Select.Option = Option
Select.OptGroup = OptGroup
export default Select

View File

@@ -1,4 +0,0 @@
import Select from './Select'
export default Select
export { default as Select } from './Select'

View File

@@ -458,52 +458,6 @@ export default {
textarea_actions_container_items: 'flex items-center',
},
/*
* Select
*/
select: {
base: `
block
box-border
w-full
rounded-md
shadow-sm
transition-all
text-foreground
border
focus-visible:shadow-md
${defaults.focus}
focus-visible:border-foreground-muted
focus-visible:ring-background-control
${defaults.placeholder}
appearance-none
bg-none
`,
variants: {
standard: `
bg-background
border border-strong
`,
error: `
bg-destructive-200
border border-destructive-500
focus:ring-destructive-400
placeholder:text-destructive-400
`,
},
container: 'relative',
with_icon: with_icon_spacing_sizes,
size: {
...default__padding_and_text,
},
disabled: 'opacity-50',
actions_container: 'absolute inset-y-0 right-0 pl-3 pr-1 mr-5 flex items-center',
chevron_container: 'absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none',
chevron: 'h-5 w-5 text-foreground-lighter',
},
sidepanel: {
base: `
z-50