mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore/rewrite roles form with shadcn (#19923)
* Fix new role form * Rewrite form * Commit a sin against ts * Make the afterLabel a ReactNode. * Fix minor type issues. * Fix type * Types * Use lodash sortBy instead of the mutating array.sort. * Fix the order of the roles with active connections. * Sort the roles by lowercased names. --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
1 parent
b6fa0f528c
commit
c37fa950ea
8 files changed
+220
-177
No files matched your search
@@ -5,11 +5,13 @@ import AlertError from 'components/ui/AlertError'
|
||||
import MultiSelect from 'components/ui/MultiSelect'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
import { sortBy } from 'lodash'
|
||||
|
||||
interface PolicyRolesProps {
|
||||
selectedRoles: string[]
|
||||
onUpdateSelectedRoles: (roles: string[]) => void
|
||||
}
|
||||
type SystemRole = (typeof SYSTEM_ROLES)[number]
|
||||
|
||||
const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps) => {
|
||||
const { project } = useProjectContext()
|
||||
@@ -17,9 +19,10 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const roles = (data ?? [])
|
||||
.filter((role) => !SYSTEM_ROLES.includes(role.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const roles = sortBy(
|
||||
(data ?? []).filter((role) => !SYSTEM_ROLES.includes(role.name as SystemRole)),
|
||||
(r) => r.name.toLocaleLowerCase()
|
||||
)
|
||||
|
||||
const formattedRoles = roles.map((role) => {
|
||||
return {
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { useRef } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button, Form, IconHelpCircle, Input, SidePanel, Toggle } from 'ui'
|
||||
import {
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormLabel_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
SidePanel,
|
||||
Switch,
|
||||
} from 'ui'
|
||||
import z from 'zod'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
|
||||
import { FormActions } from 'components/ui/Forms'
|
||||
import { useDatabaseRoleCreateMutation } from 'data/database-roles/database-role-create-mutation'
|
||||
import { ROLE_PERMISSIONS } from './Roles.constants'
|
||||
|
||||
@@ -13,26 +24,34 @@ interface CreateRolePanelProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().trim().min(1, 'You must provide a name').default(''),
|
||||
is_superuser: z.boolean().default(false),
|
||||
can_login: z.boolean().default(false),
|
||||
can_create_role: z.boolean().default(false),
|
||||
can_create_db: z.boolean().default(false),
|
||||
is_replication_role: z.boolean().default(false),
|
||||
can_bypass_rls: z.boolean().default(false),
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
name: '',
|
||||
is_superuser: false,
|
||||
can_login: false,
|
||||
can_create_role: false,
|
||||
can_create_db: false,
|
||||
is_replication_role: false,
|
||||
can_bypass_rls: false,
|
||||
}
|
||||
|
||||
const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
const formId = 'create-new-role'
|
||||
const submitRef: any = useRef()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
|
||||
const initialValues = {
|
||||
name: '',
|
||||
is_superuser: false,
|
||||
can_login: false,
|
||||
can_create_role: false,
|
||||
can_create_db: false,
|
||||
is_replication_role: false,
|
||||
can_bypass_rls: false,
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.name.length === 0) errors.name = 'Please provide a name for your role'
|
||||
return errors
|
||||
}
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
const { mutate: createDatabaseRole, isLoading: isCreating } = useDatabaseRoleCreateMutation({
|
||||
onSuccess: (res) => {
|
||||
@@ -41,7 +60,7 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (values: any) => {
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
createDatabaseRole({
|
||||
projectRef: project.ref,
|
||||
@@ -50,6 +69,11 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onClose()
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
size="large"
|
||||
@@ -57,106 +81,109 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
header="Create a new role"
|
||||
className="mr-0 transform transition-all duration-300 ease-in-out"
|
||||
loading={false}
|
||||
onCancel={onClose}
|
||||
onCancel={handleClose}
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-default px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClose}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
disabled={isCreating}
|
||||
loading={isCreating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
Create role
|
||||
</Button>
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isCreating}
|
||||
hasChanges={form.formState.isDirty}
|
||||
handleReset={() => form.reset()}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting, handleReset, values, initialValues }: any) => {
|
||||
return (
|
||||
<div>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel className="lg:!col-span-4">Role Configuration</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<Input id="name" label="Name" />
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<SidePanel.Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel className="lg:!col-span-4">Role Privileges</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={false} className="lg:!col-span-8">
|
||||
<div className="space-y-[9px]">
|
||||
{Object.keys(ROLE_PERMISSIONS).map((permission) => (
|
||||
<Toggle
|
||||
size="small"
|
||||
disabled={ROLE_PERMISSIONS[permission].disabled}
|
||||
className={[
|
||||
'roles-toggle',
|
||||
ROLE_PERMISSIONS[permission].disabled ? 'opacity-50' : '',
|
||||
].join(' ')}
|
||||
key={permission}
|
||||
id={permission}
|
||||
name={permission}
|
||||
label={ROLE_PERMISSIONS[permission].description}
|
||||
afterLabel={
|
||||
ROLE_PERMISSIONS[permission].disabled && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<IconHelpCircle
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
className="ml-2 relative top-[3px]"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs">
|
||||
This privilege cannot be granted via the dashboard
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
className="grid gap-6 w-full px-8 py-8"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="grid gap-2 md:grid md:grid-cols-12 space-y-0">
|
||||
<FormLabel_Shadcn_ className="flex flex-col space-y-2 col-span-4 text-sm justify-center text-foreground-light">
|
||||
Name
|
||||
</FormLabel_Shadcn_>
|
||||
<FormControl_Shadcn_ className="col-span-8">
|
||||
<Input_Shadcn_ {...field} className="w-full" />
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ className="col-start-5 col-span-8" />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<div className="grid gap-2 mt-4 md:grid md:grid-cols-12">
|
||||
<div className="col-span-4">
|
||||
<FormLabel_Shadcn_ className="flex flex-col space-y-2 col-span-4 text-sm justify-center text-foreground-light">
|
||||
Role privileges
|
||||
</FormLabel_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
<div className="col-span-8 grid gap-4">
|
||||
{(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[])
|
||||
.filter((permissionKey) => ROLE_PERMISSIONS[permissionKey].grant_by_dashboard)
|
||||
.map((permissionKey) => {
|
||||
const permission = ROLE_PERMISSIONS[permissionKey]
|
||||
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
key={permissionKey}
|
||||
control={form.control}
|
||||
name={permissionKey}
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="grid gap-2 md:grid md:grid-cols-12 space-y-0">
|
||||
<FormControl_Shadcn_ className="col-span-8 flex items-center gap-4">
|
||||
<div className="w-full text-sm">
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
<FormLabel_Shadcn_>{permission.description}</FormLabel_Shadcn_>
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ className="col-start-5 col-span-8" />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<SidePanel.Separator />
|
||||
|
||||
<div className="grid gap-4">
|
||||
<p className="text-sm">These privileges cannot be granted via the Dashboard:</p>
|
||||
{(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[])
|
||||
.filter((permissionKey) => !ROLE_PERMISSIONS[permissionKey].grant_by_dashboard)
|
||||
.map((permissionKey) => {
|
||||
const permission = ROLE_PERMISSIONS[permissionKey]
|
||||
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
key={permissionKey}
|
||||
control={form.control}
|
||||
name={permissionKey}
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="space-y-0 opacity-70">
|
||||
<FormControl_Shadcn_ className="flex items-center gap-4">
|
||||
<div className="w-full text-sm">
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled
|
||||
aria-readonly
|
||||
/>
|
||||
<FormLabel_Shadcn_>{permission.description}</FormLabel_Shadcn_>
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ className="col-start-5 col-span-8" />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,49 +161,52 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
<Collapsible.Content>
|
||||
<div className="group border-t border-default bg-surface-100 py-6 px-20 text-foreground">
|
||||
<div className="py-4 space-y-[9px]">
|
||||
{Object.keys(ROLE_PERMISSIONS).map((permission) => (
|
||||
<Toggle
|
||||
size="small"
|
||||
key={permission}
|
||||
id={permission}
|
||||
name={permission}
|
||||
label={ROLE_PERMISSIONS[permission].description}
|
||||
disabled={disabled || ROLE_PERMISSIONS[permission].disabled}
|
||||
className={[
|
||||
'roles-toggle',
|
||||
disabled || ROLE_PERMISSIONS[permission].disabled ? 'opacity-50' : '',
|
||||
].join(' ')}
|
||||
afterLabel={
|
||||
!disabled &&
|
||||
ROLE_PERMISSIONS[permission].disabled && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<IconHelpCircle
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
className="ml-2 relative top-[3px]"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs">
|
||||
This privilege cannot be updated via the dashboard
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[]).map(
|
||||
(permission) => (
|
||||
<Toggle
|
||||
size="small"
|
||||
key={permission}
|
||||
id={permission}
|
||||
name={permission}
|
||||
label={ROLE_PERMISSIONS[permission].description}
|
||||
disabled={disabled || ROLE_PERMISSIONS[permission].disabled}
|
||||
className={[
|
||||
'roles-toggle',
|
||||
disabled || ROLE_PERMISSIONS[permission].disabled ? 'opacity-50' : '',
|
||||
].join(' ')}
|
||||
afterLabel={
|
||||
!disabled && ROLE_PERMISSIONS[permission].disabled ? (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<IconHelpCircle
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
className="ml-2 relative top-[3px]"
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content align="center" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs">
|
||||
This privilege cannot be updated via the dashboard
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{!disabled && (
|
||||
<div className="py-4 flex items-center space-x-2 justify-end">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const SUPABASE_ROLES = [
|
||||
'pgsodium_keyiduser',
|
||||
'pgsodium_keymaker',
|
||||
'pgtle_admin',
|
||||
]
|
||||
] as const
|
||||
|
||||
// [Joshen] This was originally in the Roles mobx store
|
||||
// Just keeping it for now in case we need to differ it from ^ SUPABASE_ROLES
|
||||
@@ -30,32 +30,38 @@ export const SYSTEM_ROLES = [
|
||||
'pg_database_owner',
|
||||
'pg_read_all_data',
|
||||
'pg_write_all_data',
|
||||
]
|
||||
] as const
|
||||
|
||||
export const ROLE_PERMISSIONS: any = {
|
||||
export const ROLE_PERMISSIONS = {
|
||||
can_login: {
|
||||
disabled: false,
|
||||
description: 'User can login',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_create_role: {
|
||||
disabled: false,
|
||||
description: 'User can create roles',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_create_db: {
|
||||
disabled: false,
|
||||
description: 'User can create databases',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_bypass_rls: {
|
||||
disabled: true,
|
||||
description: 'User bypasses every row level security policy',
|
||||
grant_by_dashboard: false,
|
||||
},
|
||||
is_superuser: {
|
||||
disabled: true,
|
||||
description: 'User is a Superuser',
|
||||
grant_by_dashboard: false,
|
||||
},
|
||||
is_replication_role: {
|
||||
disabled: true,
|
||||
description:
|
||||
'User can initiate streaming replication and put the system in and out of backup mode',
|
||||
grant_by_dashboard: false,
|
||||
},
|
||||
}
|
||||
} as const
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition } from 'lodash'
|
||||
import { partition, sortBy } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import { Badge, Button, IconPlus, IconSearch, IconX, Input } from 'ui'
|
||||
|
||||
@@ -17,6 +17,8 @@ import RoleRow from './RoleRow'
|
||||
import RoleRowSkeleton from './RoleRowSkeleton'
|
||||
import { SUPABASE_ROLES } from './Roles.constants'
|
||||
|
||||
type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number]
|
||||
|
||||
const RolesList = () => {
|
||||
const { project } = useProjectContext()
|
||||
|
||||
@@ -37,21 +39,23 @@ const RolesList = () => {
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name))
|
||||
const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase())
|
||||
|
||||
const filteredRoles = (
|
||||
filterType === 'active' ? roles.filter((role) => role.active_connections > 0) : roles
|
||||
).filter((role) => role.name.includes(filterString))
|
||||
const [supabaseRoles, otherRoles] = partition(filteredRoles, (role) =>
|
||||
SUPABASE_ROLES.includes(role.name)
|
||||
SUPABASE_ROLES.includes(role.name as SUPABASE_ROLE)
|
||||
)
|
||||
|
||||
const totalActiveConnections = roles
|
||||
.map((role) => role.active_connections)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
const rolesWithActiveConnections = roles
|
||||
.filter((role) => role.active_connections > 0)
|
||||
.sort((a, b) => b.active_connections - a.active_connections)
|
||||
// order the roles with active connections by number of connections, most connections first
|
||||
const rolesWithActiveConnections = sortBy(
|
||||
roles.filter((role) => role.active_connections > 0),
|
||||
(r) => -r.active_connections
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { FormLayout } from '../../lib/Layout/FormLayout'
|
||||
import { useFormContext } from '../Form/FormContext'
|
||||
import styleHandler from '../../lib/theme/styleHandler'
|
||||
import { useFormContext } from '../Form/FormContext'
|
||||
|
||||
interface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'size'> {
|
||||
name?: string
|
||||
@@ -11,7 +11,7 @@ interface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'size'> {
|
||||
error?: string
|
||||
descriptionText?: string | React.ReactNode
|
||||
label?: string | React.ReactNode
|
||||
afterLabel?: string
|
||||
afterLabel?: string | React.ReactNode
|
||||
beforeLabel?: string
|
||||
labelOptional?: string
|
||||
className?: any
|
||||
|
||||
@@ -88,7 +88,7 @@ const FormLabel = React.forwardRef<
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
className={cn(error && 'text-destructive', className, 'leading-normal')}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
// @ts-ignore
|
||||
import defaultTheme from '../../theme/defaultTheme'
|
||||
// @ts-ignore
|
||||
@@ -19,7 +19,7 @@ type Props = {
|
||||
responsive?: boolean
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge'
|
||||
beforeLabel?: string
|
||||
afterLabel?: string
|
||||
afterLabel?: string | React.ReactNode
|
||||
nonBoxInput?: boolean
|
||||
labelLayout?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user