chore(dashboard): remove v2 ui components from datatable (#3568)

This commit is contained in:
robertkasza
2025-11-12 11:26:32 +01:00
committed by GitHub
parent a6a378c5a6
commit 5066ef708a
72 changed files with 1369 additions and 1363 deletions

View File

@@ -102,7 +102,7 @@ test('should create a table with nullable columns', async ({
page.getByRole('link', { name: tableName, exact: true }),
).toBeVisible();
await page
.locator(`li:has-text("${tableName}") #table-management-menu button`)
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
.click();
await page.getByText('Edit Table').click();
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
@@ -143,7 +143,7 @@ test('should create a table with an identity column', async ({
page.getByRole('link', { name: tableName, exact: true }),
).toBeVisible();
await page
.locator(`li:has-text("${tableName}") #table-management-menu button`)
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
.click();
await page.getByText('Edit Table').click();
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
@@ -267,7 +267,7 @@ test('should be able to create a table with a composite key', async ({
).toBeVisible();
await page
.locator(`li:has-text("${tableName}") #table-management-menu button`)
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
.click();
await page.getByText('Edit Table').click();
await expect(page.locator('div[data-testid="id"]')).toBeVisible();

View File

@@ -41,7 +41,7 @@ test('should create a table with role permissions to select row', async ({
// Press three horizontal dots more options button next to the table name
await page
.locator(`li:has-text("${tableName}") #table-management-menu button`)
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
.click();
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
@@ -89,7 +89,7 @@ test('should create a table with role permissions and a custom check to select r
// Press three horizontal dots more options button next to the table name
await page
.locator(`li:has-text("${tableName}") #table-management-menu button`)
.locator(`li:has-text("${tableName}") #table-management-menu-${tableName}`)
.click();
await page.getByRole('menuitem', { name: /edit permissions/i }).click();
@@ -114,7 +114,7 @@ test('should create a table with role permissions and a custom check to select r
await page.getByText('Select variable...', { exact: true }).click();
const variableSelector = await page.locator('input[role="combobox"]');
const variableSelector = page.locator('input[role="combobox"]');
await variableSelector.fill('X-Hasura-User-Id');

View File

@@ -64,7 +64,6 @@ declare module 'react-table' {
export interface Cell<
D extends Record<string, unknown> = Record<string, unknown>,
V = any,
> extends UseGroupByCellProps<D>,
UseRowStateCellProps<D> {}

View File

@@ -123,7 +123,7 @@ const TimePickerInput = React.forwardRef<
id={id || picker}
name={name || picker}
className={cn(
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
'w-[48px] text-center font-mono text-base tabular-nums focus:bg-accent-background focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
className,
)}
value={value || calculatedValue}

View File

@@ -1,26 +1,25 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
import { Spinner } from '@/components/ui/v3/spinner';
import { cn } from '@/lib/utils';
export interface FormActivityIndicatorProps extends BoxProps {}
export interface FormActivityIndicatorProps {
className?: string;
}
export default function FormActivityIndicator({
className,
...props
}: FormActivityIndicatorProps) {
return (
<Box
<div
{...props}
className={twMerge(
'grid items-center justify-center px-6 py-4',
className={cn(
'box grid h-full items-center justify-center px-6 py-4',
className,
)}
>
<ActivityIndicator
circularProgressProps={{ className: 'w-5 h-5' }}
label="Loading form..."
/>
</Box>
<Spinner className="h-5 w-5" wrapperClassName="flex-row gap-1">
Loading form...
</Spinner>
</div>
);
}

View File

@@ -1,12 +1,16 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import { Input } from '@/components/ui/v3/input';
import { cn } from '@/lib/utils';
import { type ForwardedRef, forwardRef, type ReactNode } from 'react';
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
import { mergeRefs } from 'react-merge-refs';
const inputClasses =
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
@@ -17,43 +21,82 @@ interface FormInputProps<
> {
control: Control<TFieldValues>;
name: TName;
label: string;
label: ReactNode;
placeholder?: string;
className?: string;
type?: string;
inline?: boolean;
helperText?: string | null;
}
function FormInput<
function InnerFormInput<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
control,
name,
label,
placeholder,
className = '',
type = 'text',
}: FormInputProps<TFieldValues, TName>) {
>(
{
control,
name,
label,
placeholder,
className = '',
type = 'text',
inline,
helperText,
}: FormInputProps<TFieldValues, TName>,
ref: ForwardedRef<HTMLInputElement>,
) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input
type={type}
placeholder={placeholder || label}
{...field}
className={`${inputClasses} ${className}`}
/>
</FormControl>
<FormMessage />
<FormItem
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
>
<FormLabel
className={cn({
'w-52 max-w-52 flex-shrink-0': inline,
'mt-2 self-start': inline && !!helperText,
})}
>
{label}
</FormLabel>
<div
className={cn({
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
inline,
})}
>
<FormControl>
<Input
type={type}
placeholder={placeholder}
{...field}
ref={mergeRefs([field.ref, ref])}
className={cn(inputClasses, className)}
wrapperClassName={cn({ 'w-full': !inline })}
/>
</FormControl>
{!!helperText && (
<FormDescription className="break-all px-[1px]">
{helperText}
</FormDescription>
)}
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
const FormInput = forwardRef(InnerFormInput) as <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: FormInputProps<TFieldValues, TName> & {
ref?: ForwardedRef<HTMLInputElement>;
},
) => ReturnType<typeof InnerFormInput>;
export default FormInput;

View File

@@ -0,0 +1,91 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import {
Select,
SelectContent,
SelectTrigger,
SelectValue,
} from '@/components/ui/v3/select';
import { cn } from '@/lib/utils';
import type { PropsWithChildren, ReactNode } from 'react';
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
interface FormSelectProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
control: Control<TFieldValues>;
name: TName;
label: ReactNode;
placeholder?: string;
className?: string;
inline?: boolean;
helperText?: string | null;
}
function FormSelect<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
control,
name,
label,
placeholder,
className = '',
inline,
helperText,
children,
}: PropsWithChildren<FormSelectProps<TFieldValues, TName>>) {
return (
<FormField
control={control}
name={name}
render={({ field }) => {
const { onChange, ...selectProps } = field;
return (
<FormItem
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
>
<FormLabel
className={cn({
'w-52 max-w-52 flex-shrink-0': inline,
'mt-2 self-start': inline && !!helperText,
})}
>
{label}
</FormLabel>
<div
className={cn({
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
inline,
})}
>
<Select onValueChange={onChange} {...selectProps}>
<FormControl>
<SelectTrigger className={className}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
</FormControl>
<SelectContent>{children}</SelectContent>
</Select>
{!!helperText && (
<FormDescription className="break-all px-[1px]">
{helperText}
</FormDescription>
)}
<FormMessage />
</div>
</FormItem>
);
}}
/>
);
}
export default FormSelect;

View File

@@ -0,0 +1 @@
export { default as FormSelect } from './FormSelect';

View File

@@ -0,0 +1,98 @@
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import { Textarea } from '@/components/ui/v3/textarea';
import { cn } from '@/lib/utils';
import { forwardRef, type ForwardedRef, type ReactNode } from 'react';
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
import { mergeRefs } from 'react-merge-refs';
const inputClasses =
'!bg-transparent aria-[invalid=true]:border-red-500 aria-[invalid=true]:focus:border-red-500 aria-[invalid=true]:focus:ring-red-500';
interface FormTextareaProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
control: Control<TFieldValues>;
name: TName;
label: ReactNode;
placeholder?: string;
className?: string;
inline?: boolean;
helperText?: string | null;
}
function InnerFormTextarea<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
{
control,
name,
label,
placeholder,
className = '',
inline,
helperText,
}: FormTextareaProps<TFieldValues, TName>,
ref: ForwardedRef<HTMLTextAreaElement>,
) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem
className={cn({ 'flex w-full items-center gap-4 py-3': inline })}
>
<FormLabel
className={cn({
'w-52 max-w-52 flex-shrink-0': inline,
'mt-2 self-start': inline && !!helperText,
})}
>
{label}
</FormLabel>
<div
className={cn({
'flex w-[calc(100%-13.5rem)] max-w-[calc(100%-13.5rem)] flex-col gap-2':
inline,
})}
>
<FormControl>
<Textarea
placeholder={placeholder}
{...field}
ref={mergeRefs([field.ref, ref])}
className={cn(inputClasses, className)}
/>
</FormControl>
{!!helperText && (
<FormDescription className="break-all px-[1px]">
{helperText}
</FormDescription>
)}
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
const FormTextarea = forwardRef(InnerFormTextarea) as <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: FormTextareaProps<TFieldValues, TName> & {
ref: ForwardedRef<HTMLTextAreaElement>;
},
) => ReturnType<typeof InnerFormTextarea>;
export default FormTextarea;

View File

@@ -0,0 +1 @@
export { default as FormTextarea } from './FormTextarea';

View File

@@ -130,9 +130,12 @@ export default function AuthenticatedLayout({
{withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
<div
className={cn('relative flex h-full w-full flex-row bg-accent', {
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
})}
className={cn(
'bg-accent-background relative flex h-full w-full flex-row',
{
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
},
)}
>
{withMainNav && (!mainNavPinned || !isMdOrLarger) && (
<div className="flex h-full w-6 justify-center">

View File

@@ -482,9 +482,9 @@ export default function NavTree() {
}
}}
className={cn(
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent dark:hover:bg-muted',
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent',
{
'bg-[#ebf3ff] hover:bg-[#ebf3ff] dark:bg-muted':
'bg-[#ebf3ff] hover:bg-accent dark:bg-muted':
context.isFocused,
},
item.data.disabled && 'pointer-events-none opacity-50',

View File

@@ -1,9 +1,11 @@
import { Box } from '@/components/ui/v2/Box';
import type { TextProps } from '@/components/ui/v2/Text';
import { Text } from '@/components/ui/v2/Text';
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
import { cn } from '@/lib/utils';
import type {
DetailedHTMLProps,
ForwardedRef,
HTMLAttributes,
HTMLProps,
} from 'react';
import { forwardRef } from 'react';
import { twMerge } from 'tailwind-merge';
export type ReadOnlyToggleProps = Omit<
DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>,
@@ -24,7 +26,7 @@ export type ReadOnlyToggleProps = Omit<
/**
* Props passed to the label.
*/
label?: TextProps;
label?: HTMLAttributes<HTMLSpanElement>;
};
};
@@ -36,58 +38,44 @@ function ReadOnlyToggle(
<span
{...props}
{...(slotProps?.root || {})}
className={twMerge(
className={cn(
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
slotProps?.root?.className,
className,
)}
ref={ref}
>
<Box
component="span"
sx={{
backgroundColor: (theme) => {
if (checked) {
return theme.palette.mode === 'dark' ? 'grey.400' : 'grey.700';
}
return 'transparent';
<span
className={cn(
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 border-primary-text bg-transparent px-0.5',
checked && 'justify-end',
{
'border-transparent bg-primary-text px-0.5 dark:bg-[#363a43]':
checked,
},
borderColor: checked ? 'transparent' : 'grey.700',
}}
className={twMerge(
'box-border inline-grid h-3 w-5 items-center rounded-full border-1 px-0.5',
checked === true && 'justify-end',
)}
>
<Box
component="span"
sx={{
backgroundColor: (theme) => {
if (checked) {
return theme.palette.mode === 'dark' ? 'grey.700' : 'grey.200';
}
return 'grey.700';
<span
className={cn(
'inline-block h-2 w-2 rounded-full border-primary-text bg-primary-text',
{
'border-transparent bg-data-cell-bg px-0.5 dark:bg-[#f4f7f9]':
checked,
'my-px h-px justify-self-center': checked === null,
},
}}
className={twMerge(
'inline-block h-2 w-2 rounded-full',
checked === null && 'my-px h-px justify-self-center',
)}
/>
</Box>
</span>
<Text
<span
{...(slotProps?.label || {})}
component="span"
className={twMerge(
className={cn(
'truncate !text-xs font-normal',
slotProps?.label?.className,
)}
>
{String(checked)}
</Text>
</span>
</span>
);
}

View File

@@ -1,32 +0,0 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function KeyIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="Key"
{...props}
>
<path
d="M5.823 7.677a4.496 4.496 0 1 1 2.5 2.5L7.5 11H6v1.5H4.5V14H2v-2.5l3.823-3.823Z"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M10.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
fill="currentColor"
/>
</SvgIcon>
);
}
KeyIcon.displayName = 'NhostKeyIcon';
export default KeyIcon;

View File

@@ -1 +0,0 @@
export { default as KeyIcon } from './KeyIcon';

View File

@@ -16,8 +16,8 @@ const buttonVariants = cva(
outline:
'border bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
'bg-secondary text-secondary-foreground hover:bg-secondary-hover',
ghost: 'hover:bg-accent text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {

View File

@@ -11,15 +11,15 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:border-disabled disabled:bg-disabled disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
className={cn('flex items-center justify-center text-white')}
>
<Check className="h-4 w-4" />
<Check width={16} height={16} strokeWidth={3} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

View File

@@ -31,6 +31,7 @@ interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
disableOutsideClick?: boolean;
hideCloseButton?: boolean;
closeButtonClassName?: string;
}
const DialogContent = React.forwardRef<
@@ -38,7 +39,14 @@ const DialogContent = React.forwardRef<
DialogContentProps
>(
(
{ className, children, disableOutsideClick, hideCloseButton, ...props },
{
className,
children,
disableOutsideClick,
hideCloseButton,
closeButtonClassName,
...props
},
ref,
) => (
<DialogPortal>
@@ -58,7 +66,12 @@ const DialogContent = React.forwardRef<
>
{children}
{!hideCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent-background data-[state=open]:text-muted-foreground',
closeButtonClassName,
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>

View File

@@ -5,11 +5,11 @@ export function InlineCode({
children,
className,
...props
}: PropsWithChildren<{ className?: string }>) {
}: PropsWithChildren<React.HTMLAttributes<HTMLElement>>) {
return (
<code
className={cn(
'relative rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
'relative max-w-xs truncate rounded bg-[#eaedf0] px-1 font-mono text-[11px] dark:bg-[#2f363d]',
className,
)}
{...props}

View File

@@ -5,12 +5,13 @@ import { cn } from '@/lib/utils';
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
prefix?: React.ReactNode;
wrapperClassName?: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, prefix, ...props }, ref) => {
({ className, type, prefix, wrapperClassName, ...props }, ref) => {
return (
<div className="relative flex items-center">
<div className={cn('relative flex items-center', wrapperClassName)}>
{prefix && (
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
{prefix}
@@ -19,7 +20,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-background',
{ 'pl-6': prefix },
className,
)}

View File

@@ -33,6 +33,7 @@ interface SpinnerContentProps
VariantProps<typeof loaderVariants> {
className?: string;
children?: React.ReactNode;
wrapperClassName?: string;
}
export function Spinner({
@@ -40,10 +41,12 @@ export function Spinner({
show,
children,
className,
wrapperClassName,
}: SpinnerContentProps) {
return (
<span className={spinnerVariants({ show })}>
<span className={cn(spinnerVariants({ show }), wrapperClassName)}>
<Loader2
role="progressbar"
className={cn(
loaderVariants({ size }),
className,

View File

@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
ref={ref}

View File

@@ -24,12 +24,14 @@ function SignInWithEmailAndPassword({ onSubmit, isLoading }: Props) {
label="Email"
name="email"
type="email"
placeholder="Email"
/>
<FormInput
control={form.control}
label="Password"
name="password"
type="password"
placeholder="Password"
/>
<NextLink
href="/password/new"

View File

@@ -22,18 +22,25 @@ function SignUpWithEmailAndPasswordForm() {
onSubmit={form.handleSubmit(onSignUpWithPassword)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormInput control={form.control} label="Name" name="displayName" />
<FormInput
control={form.control}
label="Name"
name="displayName"
placeholder="Name"
/>
<FormInput
control={form.control}
label="Email"
name="email"
type="email"
placeholder="Email"
/>
<FormInput
control={form.control}
label="Password"
name="password"
type="password"
placeholder="Password"
/>
<FormField
control={form.control}

View File

@@ -22,12 +22,18 @@ function SignUpWithSecurityKeyForm() {
onSubmit={form.handleSubmit(onSignUpWithSecurityKey)}
className="grid grid-flow-row gap-4 bg-transparent"
>
<FormInput control={form.control} label="Name" name="displayName" />
<FormInput
control={form.control}
label="Name"
name="displayName"
placeholder="Name"
/>
<FormInput
control={form.control}
label="Email"
name="email"
type="email"
placeholder="Email"
/>
<FormField
control={form.control}

View File

@@ -52,7 +52,7 @@ export default function BillingDetails() {
<AccordionContent className="border-t-1 pb-0">
<div className="rounded-md">
<Table>
<TableHeader className="w-full bg-accent">
<TableHeader className="w-full bg-accent-background">
<TableRow>
<TableHead colSpan={3} className="w-full rounded-tl-md">
Item
@@ -72,7 +72,7 @@ export default function BillingDetails() {
</TableRow>
))}
</TableBody>
<TableFooter className="bg-accent">
<TableFooter className="bg-accent-background">
<TableRow>
<TableCell colSpan={3} className="rounded-bl-md">
Total

View File

@@ -62,7 +62,7 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
);
return (
<div className="mx-auto h-full overflow-auto bg-accent">
<div className="mx-auto h-full overflow-auto bg-accent-background">
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between gap-2 border-b bg-background p-2">
<Input
placeholder="Find Project"
@@ -85,7 +85,6 @@ export default function ProjectsGrid({ projects }: ProjectGridProps) {
</Link>
</Button>
</div>
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{filteredProjects.map((project) => (
<ProjectCard key={project.id} project={project} />

View File

@@ -1,12 +1,12 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { DatabaseRecordInputGroup } from '@/features/orgs/projects/database/dataGrid/components/DatabaseRecordInputGroup';
import type {
ColumnInsertOptions,
DataBrowserGridColumn,
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { cn } from '@/lib/utils';
import type { DialogFormProps } from '@/types/common';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
@@ -153,16 +153,19 @@ export default function BaseRecordForm({
description="These columns are nullable and don't require a value."
columns={optionalColumns}
autoFocusFirstInput={requiredColumns.length === 0}
sx={{ borderTopWidth: requiredColumns.length > 0 ? 1 : 0 }}
className="px-6 pt-3"
className={cn(
'px-6 pt-3',
requiredColumns.length > 0 ? 'border-t-1' : 'border-t-0',
)}
/>
)}
</div>
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
<div className="box grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
<Button
variant="borderless"
color="secondary"
variant="outline"
className="border-none"
size="sm"
onClick={onCancel}
tabIndex={isDirty ? -1 : 0}
>
@@ -172,12 +175,13 @@ export default function BaseRecordForm({
<Button
loading={isSubmitting}
disabled={isSubmitting}
size="sm"
type="submit"
className="justify-self-end"
>
{submitButtonText}
</Button>
</Box>
</div>
</Form>
);
}

View File

@@ -25,7 +25,10 @@ export default function ColumnEditorTable() {
return (
<>
<div role="table" className="col-span-8 overflow-x-auto">
<div
role="table"
className="col-span-8 overflow-x-auto min-[900px]:overflow-x-visible"
>
<div className="sticky top-0 z-10 flex w-full gap-2 pb-2 pt-1">
<div role="columnheader" className="w-52 flex-none">
<InputLabel as="span">

View File

@@ -1,5 +1,5 @@
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v2/Button';
import { Alert } from '@/components/ui/v3/alert';
import { Button } from '@/components/ui/v3/button';
import type { BaseRecordFormProps } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
import { BaseRecordForm } from '@/features/orgs/projects/database/dataGrid/components/BaseRecordForm';
import { useCreateRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useCreateRecordMutation';
@@ -30,7 +30,7 @@ export default function CreateRecordForm({
return { ...defaultValues, [column.id]: column.defaultValue };
}
return { ...defaultValues, [column.id]: null };
return { ...defaultValues, [column.id]: '' };
}, {}),
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
@@ -55,18 +55,17 @@ export default function CreateRecordForm({
{error && error instanceof Error ? (
<div className="-mt-3 mb-4 px-6">
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
variant="destructive"
className="grid grid-flow-col items-center justify-between border-none bg-[#f1315433] px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {error.message}
</span>
<Button
variant="borderless"
color="error"
size="small"
onClick={reset}
size="sm"
variant="destructive"
className="bg-transparent text-[#c91737] hover:bg-[#f131541a]"
>
Clear
</Button>

View File

@@ -1,7 +1,6 @@
import { Text } from '@/components/ui/v2/Text';
import { cn } from '@/lib/utils';
import Image from 'next/image';
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export interface DataBrowserEmptyStateProps
extends Omit<
@@ -26,7 +25,7 @@ export default function DataBrowserEmptyState({
}: DataBrowserEmptyStateProps) {
return (
<div
className={twMerge(
className={cn(
'grid w-full place-content-center gap-2 px-4 py-16 text-center',
className,
)}
@@ -41,12 +40,10 @@ export default function DataBrowserEmptyState({
priority
/>
</div>
<Text variant="h3" component="h1">
<h1 className="font-inter-var text-[1.125rem] font-medium !leading-6">
{title}
</Text>
<Text>{description}</Text>
</h1>
<p>{description}</p>
</div>
);
}

View File

@@ -15,16 +15,14 @@ import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGri
import {
POSTGRESQL_CHARACTER_TYPES,
POSTGRESQL_DATE_TIME_TYPES,
POSTGRESQL_DECIMAL_TYPES,
POSTGRESQL_INTEGER_TYPES,
POSTGRESQL_JSON_TYPES,
POSTGRESQL_NUMERIC_TYPES,
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
import { DataGrid } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
import { DataGridBooleanCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBooleanCell';
import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
import { DataGridDecimalCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDecimalCell';
import { DataGridIntegerCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridIntegerCell';
import { DataGridNumericCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridNumericCell';
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
import { useQueryClient } from '@tanstack/react-query';
import { KeyRound } from 'lucide-react';
@@ -68,6 +66,7 @@ export function createDataGridColumn(
isEditable,
type: 'text',
specificType: column.full_data_type,
dataType: column.data_type,
maxLength: column.character_maximum_length,
Cell: DataGridTextCell,
isPrimary: column.is_primary,
@@ -82,21 +81,12 @@ export function createDataGridColumn(
foreignKeyRelation: column.foreign_key_relation,
};
if (POSTGRESQL_INTEGER_TYPES.includes(column.data_type)) {
if (POSTGRESQL_NUMERIC_TYPES.includes(column.data_type)) {
return {
...defaultColumnConfiguration,
type: 'number',
width: 250,
Cell: DataGridIntegerCell,
};
}
if (POSTGRESQL_DECIMAL_TYPES.includes(column.data_type)) {
return {
...defaultColumnConfiguration,
type: 'text',
width: 250,
Cell: DataGridDecimalCell,
Cell: DataGridNumericCell,
};
}

View File

@@ -1,21 +1,20 @@
import { useDialog } from '@/components/common/DialogProvider';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Chip } from '@/components/ui/v2/Chip';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Badge } from '@/components/ui/v3/badge';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
import { cn } from '@/lib/utils';
import { triggerToast } from '@/utils/toast';
import { useQueryClient } from '@tanstack/react-query';
import { Plus } from 'lucide-react';
import { useState } from 'react';
import type { Row } from 'react-table';
import { twMerge } from 'tailwind-merge';
export interface DataBrowserGridControlsProps extends BoxProps {
export interface DataBrowserGridControlsProps {
/**
* Props passed to the pagination component.
*/
@@ -33,11 +32,9 @@ export interface DataBrowserGridControlsProps extends BoxProps {
// TODO: Get rid of Data Browser related code from here. This component should
// be generic and not depend on Data Browser related data types and logic.
export default function DataBrowserGridControls({
className,
paginationProps,
refetchData,
onInsertRowClick,
...props
}: DataBrowserGridControlsProps) {
const queryClient = useQueryClient();
const { openAlertDialog } = useDialog();
@@ -98,28 +95,26 @@ export default function DataBrowserGridControls({
}
return (
<Box
className={twMerge('sticky top-0 z-20 border-b-1 p-2', className)}
{...props}
>
<div className="box sticky top-0 z-20 border-b-1 p-2">
<div
className={twMerge(
className={cn(
'mx-auto grid min-h-[38px] grid-flow-col items-center gap-3',
numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end',
)}
>
{numberOfSelectedRows > 0 && (
<div className="grid grid-flow-col place-content-start items-center gap-2">
<Chip
size="small"
color="info"
label={`${numberOfSelectedRows} selected`}
/>
<Badge
variant="secondary"
className="!bg-[#ebf3ff] text-primary dark:!bg-[#1b2534]"
>
{`${numberOfSelectedRows} selected`}
</Badge>
<Button
variant="borderless"
color="error"
size="small"
variant="outline"
size="sm"
className="border-none text-destructive hover:bg-[#f131541a] hover:text-destructive"
loading={status === 'loading'}
onClick={() =>
openAlertDialog({
@@ -161,16 +156,12 @@ export default function DataBrowserGridControls({
/>
)}
<Button
startIcon={<PlusIcon className="h-4 w-4" />}
size="small"
onClick={onInsertRowClick}
>
Insert row
<Button onClick={onInsertRowClick} size="sm">
<Plus className="h-4 w-4" /> Insert row
</Button>
</div>
)}
</div>
</Box>
</div>
);
}

View File

@@ -1,42 +1,32 @@
import { useDialog } from '@/components/common/DialogProvider';
import { NavLink } from '@/components/common/NavLink';
import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
import { InlineCode } from '@/components/presentational/InlineCode';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Backdrop } from '@/components/ui/v2/Backdrop';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Chip } from '@/components/ui/v2/Chip';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { IconButton } from '@/components/ui/v2/IconButton';
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TerminalIcon } from '@/components/ui/v2/icons/TerminalIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Text } from '@/components/ui/v2/Text';
import { Badge } from '@/components/ui/v3/badge';
import { Button } from '@/components/ui/v3/button';
import { InlineCode } from '@/components/ui/v3/inline-code';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/v3/select';
import { Spinner } from '@/components/ui/v3/spinner';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
import { useDeleteTableWithToastMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteTableMutation';
import { isSchemaLocked } from '@/features/orgs/projects/database/dataGrid/utils/schemaHelpers';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
import { cn, isEmptyValue, isNotEmptyValue } from '@/lib/utils';
import { useQueryClient } from '@tanstack/react-query';
import { Info, Lock, Plus, Terminal } from 'lucide-react';
import dynamic from 'next/dynamic';
import Image from 'next/image';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import TableActions from './TableActions';
const CreateTableForm = dynamic(
() =>
@@ -71,16 +61,17 @@ const EditPermissionsForm = dynamic(
},
);
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
/**
* Function to be called when a sidebar item is clicked.
*/
export interface DataBrowserSidebarProps {
className?: string;
}
export interface DataBrowserSidebarContentProps {
onSidebarItemClick?: (tablePath?: string) => void;
}
function DataBrowserSidebarContent({
onSidebarItemClick,
}: Pick<DataBrowserSidebarProps, 'onSidebarItemClick'>) {
}: DataBrowserSidebarContentProps) {
const queryClient = useQueryClient();
const { openDrawer, openAlertDialog } = useDialog();
const { project } = useProject();
@@ -136,11 +127,12 @@ function DataBrowserSidebarContent({
if (status === 'loading') {
return (
<ActivityIndicator
delay={1000}
label="Loading schemas and tables..."
className="justify-center"
/>
<Spinner
wrapperClassName="flex-row text-[12px] leading-[1.66] font-normal gap-1"
className="h-4 w-4 justify-center"
>
Loading schemas and tables...
</Spinner>
);
}
@@ -252,7 +244,9 @@ function DataBrowserSidebarContent({
<span className="inline-grid grid-flow-col items-center gap-2">
Permissions
<InlineCode className="!text-sm+ font-normal">{table}</InlineCode>
<Chip label="Preview" size="small" color="info" component="span" />
<Badge variant="secondary" className="text-primary">
Preview
</Badge>
</span>
),
component: (
@@ -271,59 +265,46 @@ function DataBrowserSidebarContent({
}
return (
<Box className="flex h-full flex-col justify-between">
<Box className="flex flex-col px-2">
<div className="flex h-full flex-col justify-between">
<div className="box flex flex-col px-2">
{schemas && schemas.length > 0 && (
<Select
renderValue={(option) => (
<span className="grid grid-flow-col items-center gap-1">
{option?.label}
</span>
)}
slotProps={{
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
}}
value={selectedSchema}
onChange={(_event, value) => setSelectedSchema(value as string)}
>
{schemas.map((schema) => (
<Option
className="grid grid-flow-col items-center gap-1"
value={schema.schema_name}
key={schema.schema_name}
>
<Text className="text-sm">
<Text component="span" color="disabled">
schema.
</Text>
<Text component="span" className="font-medium">
{schema.schema_name}
</Text>
</Text>
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
<LockIcon
className="h-3 w-3"
sx={{ color: 'text.secondary' }}
/>
)}
</Option>
))}
<Select value={selectedSchema} onValueChange={setSelectedSchema}>
<SelectTrigger className="w-full min-w-[initial] max-w-[220px]">
<SelectValue placeholder="Is null?" />
</SelectTrigger>
<SelectContent>
{schemas.map((schema) => (
<SelectItem value={schema.schema_name} key={schema.schema_name}>
<div className="flex items-center gap-2">
<p className="text-sm">
<span className="text-disabled">schema.</span>
<span className="font-medium">{schema.schema_name}</span>
</p>
{(isSchemaLocked(schema.schema_name) ||
isGitHubConnected) && (
<Lock
className="text-[#556378] dark:text-[#a2b3be]"
size={12}
/>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
{isGitHubConnected && (
<Box className="mt-1.5 flex items-center gap-1 px-2">
<InfoIcon className="h-4 w-4" sx={{ color: 'text.secondary' }} />
<Text className="text-xs" color="secondary">
<div className="box mt-1.5 flex items-center gap-1 px-2">
<Info className="h-4 w-4 text-disabled" />
<p className="text-xs text-disabled">
GitHub connected - use the CLI for schema changes
</Text>
</Box>
</p>
</div>
)}
{!isSelectedSchemaLocked && (
<Button
variant="borderless"
endIcon={<PlusIcon />}
className="mt-1 w-full justify-between px-2"
variant="link"
className="mt-1 flex w-full justify-between px-[0.625rem] !text-sm+ text-primary hover:bg-accent hover:no-underline disabled:text-disabled"
onClick={() => {
openDrawer({
title: 'Create a New Table',
@@ -335,202 +316,136 @@ function DataBrowserSidebarContent({
}}
disabled={isGitHubConnected}
>
New Table
New Table <Plus className="h-4 w-4" />
</Button>
)}
{isNotEmptyValue(schemas) && isEmptyValue(tablesInSelectedSchema) && (
<Text className="px-2 py-1.5 text-xs" color="disabled">
No tables found.
</Text>
<p className="px-2 py-1.5 text-xs text-disabled">No tables found.</p>
)}
<nav aria-label="Database navigation">
{isNotEmptyValue(tablesInSelectedSchema) && (
<List className="grid gap-1 pb-6">
<ul className="w-full max-w-full pb-6">
{tablesInSelectedSchema.map((table) => {
const tablePath = `${table.table_schema}.${table.table_name}`;
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
return (
<ListItem.Root
className="group"
key={tablePath}
secondaryAction={
<Dropdown.Root
id="table-management-menu"
onOpen={() => setSidebarMenuTable(tablePath)}
onClose={() => setSidebarMenuTable(undefined)}
>
<Dropdown.Trigger
asChild
hideChevron
disabled={tablePath === removableTable}
>
<IconButton
variant="borderless"
color={isSelected ? 'primary' : 'secondary'}
className={twMerge(
!isSelected &&
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
)}
>
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-52' }}
>
{isGitHubConnected ? (
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
>
<UsersIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>View Permissions</span>
</Dropdown.Item>
) : (
[
!isSelectedSchemaLocked && (
<Dropdown.Item
key="edit-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
openDrawer({
title: 'Edit Table',
component: (
<EditTableForm
onSubmit={async (tableName) => {
await queryClient.refetchQueries([
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
]);
await refetch();
}}
schema={table.table_schema}
table={table}
/>
),
})
}
>
<PencilIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Table</span>
</Dropdown.Item>
),
!isSelectedSchemaLocked && (
<Divider
key="edit-table-separator"
component="li"
/>
),
<Dropdown.Item
key="edit-permissions"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
)
}
>
<UsersIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Permissions</span>
</Dropdown.Item>,
!isSelectedSchemaLocked && (
<Divider
key="edit-permissions-separator"
component="li"
/>
),
!isSelectedSchemaLocked && (
<Dropdown.Item
key="delete-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() =>
handleDeleteTableClick(
table.table_schema,
table.table_name,
)
}
>
<TrashIcon
className="h-4 w-4"
sx={{ color: 'error.main' }}
/>
<span>Delete Table</span>
</Dropdown.Item>
),
]
)}
</Dropdown.Content>
</Dropdown.Root>
}
>
<ListItem.Button
dense
selected={isSelected}
<li className="group pb-1" key={tablePath}>
<Button
asChild
variant="link"
size="sm"
disabled={tablePath === removableTable}
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
sx={{
paddingRight:
(isSelected || isSidebarMenuOpen) &&
'2.25rem !important',
}}
component={NavLink}
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
onClick={() => {
if (onSidebarItemClick) {
onSidebarItemClick(`default.${tablePath}`);
}
}}
className={cn(
'flex w-full max-w-full justify-between pl-0 text-sm+ hover:bg-accent hover:no-underline',
{
'bg-table-selected': isSelected,
},
)}
>
<ListItem.Text>{table.table_name}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
<div>
<NextLink
className={cn(
'flex h-full w-[calc(100%-1.6rem)] items-center p-[0.625rem] pr-0 text-left',
{
'text-primary-main': isSelected,
},
)}
onClick={() => {
if (onSidebarItemClick) {
onSidebarItemClick(`default.${tablePath}`);
}
}}
href={`/orgs/${orgSlug}/projects/${appSubdomain}/database/browser/default/${table.table_schema}/${table.table_name}`}
>
<span className="!truncate text-ellipsis">
{table.table_name}
</span>
</NextLink>
<TableActions
tableName={table.table_name}
disabled={tablePath === removableTable}
open={isSidebarMenuOpen}
onOpen={() => setSidebarMenuTable(tablePath)}
onClose={() => setSidebarMenuTable(undefined)}
className={cn(
'relative z-10 opacity-0 group-hover:opacity-100',
{
'opacity-100': isSelected || isSidebarMenuOpen,
},
)}
isSelectedNotSchemaLocked={!isSelectedSchemaLocked}
onViewPermissions={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
onEditTable={() =>
openDrawer({
title: 'Edit Table',
component: (
<EditTableForm
onSubmit={async (tableName) => {
await queryClient.refetchQueries([
`${dataSourceSlug}.${table.table_schema}.${tableName}`,
]);
await refetch();
}}
schema={table.table_schema}
table={table}
/>
),
})
}
onEditPermissions={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
)
}
onDelete={() =>
handleDeleteTableClick(
table.table_schema,
table.table_name,
)
}
/>
</div>
</Button>
</li>
);
})}
</List>
</ul>
)}
</nav>
</Box>
</div>
<Box className="border-t">
<ListItem.Button
dense
selected={asPath === sqlEditorHref}
className="flex border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
component={NavLink}
href={sqlEditorHref}
<div className="box border-t">
<Button
size="sm"
variant="link"
asChild
className={cn(
'flex rounded-none border text-sm+ hover:bg-accent hover:no-underline group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9',
{ 'bg-table-selected text-primary-main': asPath === sqlEditorHref },
)}
>
<div className="flex w-full flex-row items-center justify-center space-x-4">
<TerminalIcon />
<span className="flex">SQL Editor</span>
</div>
</ListItem.Button>
</Box>
</Box>
<NextLink href={sqlEditorHref}>
<div className="flex w-full flex-row items-center justify-center space-x-4">
<Terminal />
<span className="flex">SQL Editor</span>
</div>
</NextLink>
</Button>
</div>
</div>
);
}
export default function DataBrowserSidebar({
className,
onSidebarItemClick,
...props
}: DataBrowserSidebarProps) {
const isPlatform = useIsPlatform();
const { project } = useProject();
@@ -541,11 +456,7 @@ export default function DataBrowserSidebar({
setExpanded(!expanded);
}
function handleSidebarItemClick(tablePath?: string) {
if (onSidebarItemClick && tablePath) {
onSidebarItemClick(tablePath);
}
function handleSidebarItemClick() {
setExpanded(false);
}
@@ -586,24 +497,24 @@ export default function DataBrowserSidebar({
}}
/>
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
<aside
className={cn(
'box absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pb-0 sm:pt-2.5 sm:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
className,
)}
{...props}
>
<RetryableErrorBoundary>
<DataBrowserSidebarContent
onSidebarItemClick={handleSidebarItemClick}
/>
</RetryableErrorBoundary>
</Box>
</aside>
<IconButton
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full md:hidden"
<Button
variant="outline"
size="icon"
className="absolute bottom-4 left-8 z-[38] h-11 w-11 rounded-full bg-primary md:hidden"
onClick={toggleExpanded}
aria-label="Toggle sidebar"
>
@@ -613,7 +524,7 @@ export default function DataBrowserSidebar({
src="/assets/table.svg"
alt="A monochrome table"
/>
</IconButton>
</Button>
</>
);
}

View File

@@ -0,0 +1,111 @@
import { Button } from '@/components/ui/v3/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/v3/dropdown-menu';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { cn } from '@/lib/utils';
import { Ellipsis, SquarePen, Trash2, Users } from 'lucide-react';
const menuItemClassName =
'flex hover:cursor-pointer hover:bg-data-cell-bg h-9 font-medium items-center justify-start gap-2 rounded-none border border-b-1 text-sm+ leading-4';
type Props = {
tableName: string;
open: boolean;
className?: string;
onOpen: () => void;
onClose: () => void;
disabled: boolean;
isSelectedNotSchemaLocked: boolean;
onDelete: () => void;
onEditPermissions: () => void;
onViewPermissions: () => void;
onEditTable: () => void;
};
function TableActions({
tableName,
open,
className,
onClose,
onOpen,
disabled,
isSelectedNotSchemaLocked,
onDelete,
onEditPermissions,
onViewPermissions,
onEditTable,
}: Props) {
const { project } = useProject();
const isGitHubConnected = !!project?.githubRepository;
function handleOnOpenChange(newOpenState: boolean) {
if (newOpenState) {
onOpen();
} else {
onClose();
}
}
return (
<DropdownMenu open={open} onOpenChange={handleOnOpenChange}>
<DropdownMenuTrigger
className={cn(className)}
disabled={disabled}
asChild
>
<Button
id={`table-management-menu-${tableName}`}
variant="outline"
size="icon"
className="h-6 w-6 border-none bg-transparent px-0 hover:bg-transparent"
>
<Ellipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="w-52 p-0">
{isGitHubConnected ? (
<DropdownMenuItem
className={menuItemClassName}
onClick={onViewPermissions}
>
<Users className="h-4 w-4" /> <span>View Permissions</span>
</DropdownMenuItem>
) : (
<>
{isSelectedNotSchemaLocked && (
<DropdownMenuItem
className={menuItemClassName}
onClick={onEditTable}
>
<SquarePen className="h-4 w-4" /> <span>Edit Table</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
className={menuItemClassName}
onClick={onEditPermissions}
>
<Users className="h-4 w-4" /> <span>Edit Permissions</span>
</DropdownMenuItem>
{isSelectedNotSchemaLocked && (
<DropdownMenuItem
className={cn(
menuItemClassName,
'!text-sm+ font-medium !text-destructive',
)}
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
<span>Delete Table</span>
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
export default TableActions;

View File

@@ -1,19 +1,18 @@
import { InlineCode } from '@/components/presentational/InlineCode';
import { FormInput } from '@/components/form/FormInput';
import { FormSelect } from '@/components/form/FormSelect';
import { FormTextarea } from '@/components/form/FormTextarea';
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { KeyIcon } from '@/components/ui/v2/icons/KeyIcon';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Text } from '@/components/ui/v2/Text';
import { InlineCode } from '@/components/ui/v3/inline-code';
import { SelectItem } from '@/components/ui/v3/select';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { getInputType } from '@/features/orgs/projects/database/dataGrid/utils/inputHelpers';
import { normalizeDefaultValue } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDefaultValue';
import { Controller, useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import { cn } from '@/lib/utils';
import { KeyRound } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { useFormContext } from 'react-hook-form';
export interface DatabaseRecordInputGroupProps extends BoxProps {
export interface DatabaseRecordInputGroupProps {
/**
* List of columns for which input fields should be generated.
*/
@@ -30,6 +29,7 @@ export interface DatabaseRecordInputGroupProps extends BoxProps {
* Determines whether the first input field should be focused.
*/
autoFocusFirstInput?: boolean;
className?: string;
}
function getPlaceholder(
@@ -71,28 +71,30 @@ export default function DatabaseRecordInputGroup({
columns,
autoFocusFirstInput,
className,
...props
}: DatabaseRecordInputGroupProps) {
const {
control,
register,
formState: { errors },
} = useFormContext();
const { control } = useFormContext();
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
function getRef(index: number) {
return (element: HTMLTextAreaElement | HTMLInputElement | null) => {
if (element && index === 0 && autoFocusFirstInput) {
inputRef.current = element;
}
};
}
return (
<Box component="section" className={twMerge('py-3', className)} {...props}>
{title && (
<Text variant="h2" className="mb-1.5 mt-3 text-sm+ font-bold">
{title}
</Text>
)}
<section className={cn('box py-3 font-display', className)}>
{title && <h2 className="mb-1.5 mt-3 text-sm+ font-bold">{title}</h2>}
{description && (
<Text className="mb-3 text-xs" color="secondary">
{description}
</Text>
<p className="mb-3 text-xs text-secondary">{description}</p>
)}
<div>
{columns.map(
(
@@ -122,99 +124,75 @@ export default function DatabaseRecordInputGroup({
isNullable,
);
const InputLabel = (
const inputLabel = (
<span className="inline-grid grid-flow-col gap-1">
<span className="inline-grid grid-flow-col items-center gap-1">
{isPrimary && <KeyIcon className="text-base text-inherit" />}
<span className="inline-grid grid-flow-col items-center gap-1 break-all">
{isPrimary && (
<KeyRound className="text-base text-inherit" size={13} />
)}
<span>{columnId}</span>
</span>
<InlineCode className="h-[18px]">
<InlineCode
className="h-[1.125rem] overflow-hidden whitespace-nowrap leading-[1.125rem]"
style={{ textOverflow: 'clip' }}
title={specificType}
>
{specificType}
{maxLength ? `(${maxLength})` : null}
</InlineCode>
</span>
);
const commonFormControlProps = {
label: InputLabel,
error: Boolean(errors[columnId]),
helperText:
comment ||
(typeof errors[columnId]?.message === 'string'
? (errors[columnId]?.message as string)
: null),
hideEmptyHelperText: true,
fullWidth: true,
className: 'py-3',
};
const commonLabelProps = {
className: 'grid grid-flow-row justify-items-start gap-1',
};
if (type === 'boolean') {
return (
<Controller
<FormSelect
key={columnId}
inline
name={columnId}
control={control}
key={columnId}
render={({ field }) => (
<Select
{...commonFormControlProps}
{...field}
onChange={(_event, value) => field.onChange(value)}
variant="inline"
id={columnId}
value={field.value || 'null'}
placeholder="Select an option"
className={twMerge(
!field.value && 'text-sm font-normal',
'py-3',
)}
autoFocus={index === 0 && autoFocusFirstInput}
slotProps={{ label: commonLabelProps }}
>
<Option value="true">
<ReadOnlyToggle checked />
</Option>
label={inputLabel}
placeholder="Select an option"
helperText={comment}
>
<SelectItem value="true">
<ReadOnlyToggle checked />
</SelectItem>
<Option value="false">
<ReadOnlyToggle checked={false} />
</Option>
<SelectItem value="false">
<ReadOnlyToggle checked={false} />
</SelectItem>
{isNullable && (
<Option value="null">
<ReadOnlyToggle checked={null} />
</Option>
)}
</Select>
{isNullable && (
<SelectItem value="null">
<ReadOnlyToggle checked={null} />
</SelectItem>
)}
/>
</FormSelect>
);
}
const InputComponent = isMultiline ? FormTextarea : FormInput;
return (
<Input
{...commonFormControlProps}
{...register(columnId)}
variant="inline"
id={columnId}
<InputComponent
ref={getRef(index)}
key={columnId}
type={getInputType({ type, specificType })}
inline
name={columnId}
control={control}
label={inputLabel}
placeholder={placeholder}
multiline={isMultiline}
rows={5}
autoFocus={index === 0 && autoFocusFirstInput}
slotProps={{
label: commonLabelProps,
}}
helperText={comment}
className={cn(
{ 'resize-none': isMultiline },
'focus-visible:ring-0',
)}
type={getInputType({ type, specificType })}
/>
);
},
)}
</div>
</Box>
</section>
);
}

View File

@@ -1,4 +1,3 @@
import { Text } from '@/components/ui/v2/Text';
import {
Select,
SelectContent,
@@ -70,9 +69,9 @@ export default function RuleGroupControls({
</SelectContent>
</Select>
) : (
<Text className="p-2 !font-medium">
<p className="p-2 !font-medium">
{operatorDictionary[currentOperator]}
</Text>
</p>
)}
</div>
);

View File

@@ -147,7 +147,7 @@ function RuleValueInput({
if (operator === '_is_null') {
const defaultValue = comboboxValue ?? undefined;
const triggerClasses =
'border hover:bg-accent hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
'border hover:bg-accent-background hover:text-accent-foreground focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
return (
<Select
disabled={disabled}

View File

@@ -1,4 +1,5 @@
import { showLoadingToast, triggerToast } from '@/utils/toast';
import { getToastStyleProps } from '@/utils/constants/settings';
import { showLoadingToast } from '@/utils/toast';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import type { UseUpdateRecordMutationOptions } from './useUpdateRecordMutation';
@@ -24,6 +25,7 @@ export default function useUpdateRecordWithToastMutation(
if (status === 'loading') {
const loadingToastId = showLoadingToast('Saving data...', {
id: 'data-browser-data-save',
...getToastStyleProps(),
});
setToastId(loadingToastId);
@@ -35,9 +37,13 @@ export default function useUpdateRecordWithToastMutation(
}
if (status === 'success' && toastId) {
toast.remove(toastId);
triggerToast('Your changes were successfully saved.');
setTimeout(() => {
toast.remove(toastId);
toast.success(
'Your changes were successfully saved.',
getToastStyleProps(),
);
}, 300);
}
}, [status, toastId]);

View File

@@ -480,6 +480,7 @@ export interface DataBrowserGridColumn<TData extends object = {}>
* Determines whether or not the cell content is copiable.
*/
isCopiable?: boolean;
dataType?: string;
}
/**

View File

@@ -19,7 +19,7 @@ export const POSTGRESQL_ERROR_CODES = {
*
* @docs https://www.postgresql.org/docs/current/datatype-numeric.html
*/
export const POSTGRESQL_INTEGER_TYPES = [
export const POSTGRESQL_NUMERIC_TYPES = [
'smallint',
'integer',
'bigint',
@@ -27,10 +27,11 @@ export const POSTGRESQL_INTEGER_TYPES = [
'serial',
'bigserial',
'oid',
'numeric',
'real',
'double precision',
];
export const POSTGRESQL_DECIMAL_TYPES = ['numeric', 'real', 'double precision'];
/**
* Character data types in PostgreSQL.
*

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@/tests/testUtils';
import type { Column } from 'react-table';
import { expect, test } from 'vitest';
import { expect, it } from 'vitest';
import DataGrid from './DataGrid';
interface MockDataDetails {
@@ -18,50 +18,54 @@ const mockData: MockDataDetails[] = [
{ id: 2, name: 'bar' },
];
test('should render an empty state if columns are not available', () => {
render(<DataGrid columns={[]} data={[]} />);
describe('DataGrid', () => {
it('should render an empty state if columns are not available', () => {
render(<DataGrid columns={[]} data={[]} />);
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
});
test('should render columns and empty state message if data is unavailable', () => {
render(<DataGrid columns={mockColumns} data={[]} />);
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /id/i })).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: /name/i }),
).toBeInTheDocument();
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
});
test('should render custom empty state message if data is unavailable', () => {
const customEmptyStateMessage = 'custom empty state message';
render(
<DataGrid
columns={mockColumns}
data={[]}
emptyStateMessage={customEmptyStateMessage}
/>,
);
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
});
test('should display a loading indicator', async () => {
render(<DataGrid columns={mockColumns} data={[]} loading />);
// Activity indicator is not immediately displayed, so we need to wait
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
});
test('should render data if provided', () => {
render(<DataGrid columns={mockColumns} data={mockData} />);
expect(screen.getAllByRole('row')).toHaveLength(2);
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
});
it('should render columns and empty state message if data is unavailable', () => {
render(<DataGrid columns={mockColumns} data={[]} />);
expect(screen.getByRole('table')).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: /id/i }),
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: /name/i }),
).toBeInTheDocument();
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
});
it('should render custom empty state message if data is unavailable', () => {
const customEmptyStateMessage = 'custom empty state message';
render(
<DataGrid
columns={mockColumns}
data={[]}
emptyStateMessage={customEmptyStateMessage}
/>,
);
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
});
it('should display a loading indicator', async () => {
render(<DataGrid columns={mockColumns} data={[]} loading />);
// Activity indicator is not immediately displayed, so we need to wait
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
});
it('should render data if provided', () => {
render(<DataGrid columns={mockColumns} data={mockData} />);
expect(screen.getAllByRole('row')).toHaveLength(2);
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
});
});

View File

@@ -1,5 +1,4 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Spinner } from '@/components/ui/v3/spinner';
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
import type { UseDataGridOptions } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/useDataGrid';
import { DataGridBody } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBody';
@@ -7,11 +6,11 @@ import { DataGridConfigProvider } from '@/features/orgs/projects/storage/dataGri
import { DataGridFrame } from '@/features/orgs/projects/storage/dataGrid/components/DataGridFrame';
import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
import { cn } from '@/lib/utils';
import type { ForwardedRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import { mergeRefs } from 'react-merge-refs';
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
import { twMerge } from 'tailwind-merge';
import useDataGrid from './useDataGrid';
export interface DataGridProps<TColumnData extends object>
@@ -129,12 +128,11 @@ function DataGrid<TColumnData extends object>(
)}
{columns.length > 0 && (
<Box
<div
ref={mergeRefs([ref, tableRef])}
sx={{ backgroundColor: 'background.default' }}
className={twMerge(
'overflow-x-auto',
!loading && 'h-full',
className={cn(
'box overflow-x-auto bg-background',
{ 'h-full': !loading },
className,
)}
>
@@ -146,10 +144,10 @@ function DataGrid<TColumnData extends object>(
loading={loading}
/>
</DataGridFrame>
</Box>
</div>
)}
{loading && <ActivityIndicator delay={1000} className="my-4" />}
{loading && <Spinner className="my-4" />}
</>
</DataGridConfigProvider>
);

View File

@@ -1,4 +1,4 @@
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Checkbox } from '@/components/ui/v3/checkbox';
import type { MutableRefObject } from 'react';
import { useMemo } from 'react';
import type { PluginHook, TableInstance, TableOptions } from 'react-table';
@@ -76,25 +76,41 @@ export default function useDataGrid<T extends object>(
? hooks.visibleColumns.push((columns) => [
{
id: 'selection-column',
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => (
<Checkbox
disabled={rows.length === 0}
{...getToggleAllRowsSelectedProps({ style: null })}
style={{
...getToggleAllRowsSelectedProps().style,
cursor: rows.length === 0 ? 'default' : 'pointer',
}}
/>
),
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => {
const { indeterminate, style, onChange, ...props } =
getToggleAllRowsSelectedProps();
function handleCheckedChange(newCheckedState: boolean) {
onChange({ target: { checked: newCheckedState } });
}
return (
<Checkbox
disabled={rows.length === 0}
{...props}
style={{
...style,
cursor: rows.length === 0 ? 'default' : 'pointer',
}}
onCheckedChange={handleCheckedChange}
/>
);
},
Cell: ({ row }: any) => {
const originalValue = row.original as any;
const { indeterminate, onChange, ...props } =
row.getToggleRowSelectedProps();
function handleCheckedChange(newCheckedState: boolean) {
onChange({ target: { checked: newCheckedState } });
}
return (
<Checkbox
{...row.getToggleRowSelectedProps()}
{...props}
// disable selection if row is just a upload preview
checked={originalValue.uploading ? false : row.isSelected}
disabled={originalValue.uploading}
onCheckedChange={handleCheckedChange}
/>
);
},

View File

@@ -1,13 +1,11 @@
import { Box } from '@/components/ui/v2/Box';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid/DataGrid';
import { DataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import { isNotEmptyValue } from '@/lib/utils';
import { cn, isNotEmptyValue } from '@/lib/utils';
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
import { useRef } from 'react';
import type { Row } from 'react-table';
import { twMerge } from 'tailwind-merge';
export interface DataGridBodyProps<T extends object>
extends Omit<
@@ -143,29 +141,28 @@ export default function DataGridBody<T extends object>({
) => {
// Grey out files not uploaded
if (!row.values.isUploaded) {
return 'grey.200';
return '!bg-data-cell-bg';
}
if (column.isDisabled) {
return 'grey.100';
return '!bg-grey-100';
}
return 'background.paper';
return '!bg-paper';
};
return (
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
{rows.length === 0 && !loading && (
<div className="flex flex-nowrap">
<Box
className="inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs"
sx={{ color: 'text.secondary' }}
<div
className="box inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs !text-[#a2b3be]"
style={{
width: totalColumnsWidth,
}}
>
{emptyStateMessage}
</Box>
</div>
</div>
)}
@@ -182,7 +179,7 @@ export default function DataGridBody<T extends object>({
<div
{...rowProps}
id={row.id}
className="flex scroll-mt-10"
className="group flex scroll-mt-10"
role="row"
onKeyDown={(event) => handleKeyDown(event, row)}
tabIndex={-1}
@@ -205,16 +202,14 @@ export default function DataGridBody<T extends object>({
},
})}
cell={cell}
sx={{
backgroundColor: getBackgroundCellColor(row, column),
color: isCellDisabled ? 'text.secondary' : 'text.primary',
}}
className={twMerge(
className={cn(
'h-12 font-display text-xs motion-safe:transition-colors',
'border-b-1 border-r-1',
'scroll-ml-8 scroll-mt-[57px]',
column.id === 'selection-column' &&
'sticky left-0 z-20 justify-center px-0',
getBackgroundCellColor(row, column),
isCellDisabled ? 'text-secondary' : 'text-primary',
)}
isEditable={!column.isDisabled && column.isEditable}
id={cellIndex.toString()}

View File

@@ -1,33 +1,38 @@
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/v3/dropdown-menu';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import type { MouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
import { twMerge } from 'tailwind-merge';
import {
useState,
type MouseEvent,
type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
export type DataGridBooleanCellProps<TData extends object> =
CommonDataGridCellProps<TData, boolean | null | undefined>;
export interface DataGridBooleanCellProps<TData extends object>
extends CommonDataGridCellProps<TData, boolean | null | undefined> {
rowId?: string;
}
export default function DataGridBooleanCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
rowId,
cell: {
column: { isNullable },
column: { isNullable, getHeaderProps },
},
}: DataGridBooleanCellProps<TData>) {
const {
inputRef,
isEditing,
focusCell,
editCell,
cancelEditCell,
isSelected,
} = useDataGridCell<HTMLButtonElement>();
const { inputRef, focusCell, cancelEditCell, focusNextCell, editCell } =
useDataGridCell<HTMLButtonElement>();
const [open, setOpen] = useState(false);
async function handleMenuClick(
event: MouseEvent<HTMLLIElement> | ReactKeyboardEvent<HTMLLIElement>,
event: MouseEvent<HTMLDivElement> | ReactKeyboardEvent<HTMLDivElement>,
value: boolean | null,
) {
event.stopPropagation();
@@ -48,74 +53,80 @@ export default function DataGridBooleanCell<TData extends object>({
// We need to restore the temporary value, because editing was cancelled
if (event.key === 'Escape' && onTemporaryValueChange) {
event.stopPropagation();
onTemporaryValueChange(optimisticValue);
cancelEditCell();
focusCell();
}
if (event.key === 'Tab' && onSave) {
await onSave(temporaryValue);
cancelEditCell();
setOpen(false);
focusNextCell();
}
}
function handleTemporaryValueChange(value: boolean | null) {
if (onTemporaryValueChange) {
function handleTemporaryValueChange(
event: ReactKeyboardEvent<HTMLDivElement>,
value: boolean | null,
) {
if (onTemporaryValueChange && event.key === 'Enter') {
onTemporaryValueChange(value);
}
}
// needed to open with enter
function handleTriggerClick(event: MouseEvent) {
if (!open && !event.nativeEvent.isTrusted) {
setOpen(true);
}
}
return isSelected ? (
<Dropdown.Root id="boolean-data-editor" className="h-full w-full">
<Dropdown.Trigger
id="boolean-trigger"
className={twMerge(
'h-full w-full border-none p-0 outline-none',
isEditing && 'p-1.5',
)}
function handleOpenChange(newOpenState: boolean) {
if (newOpenState) {
editCell();
} else {
cancelEditCell();
}
setOpen(newOpenState);
}
return (
<DropdownMenu open={open} onOpenChange={handleOpenChange} modal={false}>
<DropdownMenuTrigger
ref={inputRef}
onClick={editCell}
autoFocus={false}
sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}
className="h-full w-full focus-visible:border-transparent focus-visible:outline-none"
onClick={handleTriggerClick}
>
<ReadOnlyToggle checked={optimisticValue} />
</Dropdown.Trigger>
<Dropdown.Content
menu
disablePortal
</DropdownMenuTrigger>
<DropdownMenuContent
id={rowId}
style={{ width: getHeaderProps().style?.width }}
onKeyDown={handleMenuKeyDown}
PaperProps={{ className: 'w-[200px]' }}
TransitionProps={{ onExited: focusCell }}
className="rounded-t-none"
>
<Dropdown.Item
selected={optimisticValue === true}
onKeyUp={() => handleTemporaryValueChange(true)}
<DropdownMenuCheckboxItem
checked={optimisticValue === true}
onKeyUp={(event) => handleTemporaryValueChange(event, true)}
onClick={(event) => handleMenuClick(event, true)}
>
<ReadOnlyToggle checked />
</Dropdown.Item>
<Dropdown.Item
selected={optimisticValue === false}
onKeyUp={() => handleTemporaryValueChange(false)}
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={optimisticValue === false}
onKeyUp={(event) => handleTemporaryValueChange(event, false)}
onClick={(event) => handleMenuClick(event, false)}
>
<ReadOnlyToggle checked={false} />
</Dropdown.Item>
</DropdownMenuCheckboxItem>
{isNullable && (
<Dropdown.Item
selected={optimisticValue === null}
onKeyUp={() => handleTemporaryValueChange(null)}
<DropdownMenuCheckboxItem
checked={optimisticValue === null}
onKeyUp={(event) => handleTemporaryValueChange(event, null)}
onClick={(event) => handleMenuClick(event, null)}
>
<ReadOnlyToggle checked={null} />
</Dropdown.Item>
</DropdownMenuCheckboxItem>
)}
</Dropdown.Content>
</Dropdown.Root>
) : (
<ReadOnlyToggle checked={optimisticValue} />
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,18 +1,18 @@
import { useDialog } from '@/components/common/DialogProvider';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Tooltip, useTooltip } from '@/components/ui/v2/Tooltip';
import { useTooltip } from '@/components/ui/v2/Tooltip';
import type {
ColumnType,
DataBrowserGridCell,
DataBrowserGridCellProps,
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { cn } from '@/lib/utils';
import { triggerToast } from '@/utils/toast';
import type {
FocusEvent,
JSXElementConstructor,
KeyboardEvent,
MouseEvent,
PropsWithChildren,
ReactElement,
ReactNode,
ReactPortal,
@@ -24,10 +24,15 @@ import {
useEffect,
useState,
} from 'react';
import { twMerge } from 'tailwind-merge';
import DataGridCellProvider from './DataGridCellProvider';
import useDataGridCell from './useDataGridCell';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/v3/tooltip';
export interface CommonDataGridCellProps<TData extends object, TValue = any>
extends DataBrowserGridCellProps<TData, TValue> {
/**
@@ -54,7 +59,7 @@ export interface CommonDataGridCellProps<TData extends object, TValue = any>
onTemporaryValueChange?: (value: TValue) => void;
}
export interface DataGridCellProps<TData extends object> extends BoxProps {
export interface DataGridCellProps<TData extends object> {
/**
* Current cell's props.
*/
@@ -67,6 +72,8 @@ export interface DataGridCellProps<TData extends object> extends BoxProps {
* Determines the column's type.
*/
columnType?: ColumnType;
className?: string;
id?: string;
}
function DataGridCellContent<TData extends object = {}>({
@@ -79,7 +86,7 @@ function DataGridCellContent<TData extends object = {}>({
row,
},
...props
}: DataGridCellProps<TData>) {
}: PropsWithChildren<DataGridCellProps<TData>>) {
const { openAlertDialog } = useDialog();
const {
@@ -194,6 +201,8 @@ function DataGridCellContent<TData extends object = {}>({
},
});
focusCell();
cancelEditCell();
// Syncing optimistic value with server-side value
setTemporaryValue(data.original[id.toString()]);
setOptimisticValue(data.original[id.toString()]);
@@ -203,17 +212,31 @@ function DataGridCellContent<TData extends object = {}>({
// Resetting values
setTemporaryValue(latestOptimisticValue);
setOptimisticValue(latestOptimisticValue);
activateInput();
}
}
async function handleBlur(event: FocusEvent<HTMLDivElement>) {
// We are deselecting cell only if focus target is not a descendant of it.
if (!isEditable || event.currentTarget.contains(event.relatedTarget)) {
const { id: currentRowId } = row.original as { id: string };
const isTargetDropdownMenu =
event.relatedTarget?.id === currentRowId ||
event.relatedTarget?.parentElement?.id === currentRowId;
if (
!isEditable ||
event.currentTarget.contains(event.relatedTarget) ||
(isEditing && type === 'boolean' && isTargetDropdownMenu)
) {
return;
}
await handleSave(temporaryValue);
closeTooltip();
if (type !== 'boolean') {
await handleSave(temporaryValue);
}
if (tooltipOpen) {
closeTooltip();
}
deselectCell();
}
@@ -227,8 +250,7 @@ function DataGridCellContent<TData extends object = {}>({
if (!isNullable) {
openTooltip(
<span>
<strong>{id}</strong>
is non-nullable.
<strong>{id}</strong> is non-nullable.
</span>,
);
@@ -308,14 +330,14 @@ function DataGridCellContent<TData extends object = {}>({
}
const content = (
<Box
<div
ref={cellRef}
className={twMerge(
'relative grid h-full w-full cursor-default grid-flow-col items-center gap-1',
className={cn(
'box relative grid h-full w-full cursor-default grid-flow-col items-center gap-1 bg-gray-200 px-2 py-1.5',
isEditable &&
'focus-within:outline-none focus-within:ring-0 focus:ring-0',
isSelected && 'shadow-outline',
isEditing ? 'p-0.5 shadow-outline-dark' : 'px-2 py-1.5',
isEditing && 'shadow-outline-dark',
className,
)}
onFocus={handleFocus}
@@ -324,7 +346,6 @@ function DataGridCellContent<TData extends object = {}>({
tabIndex={isEditable ? 0 : undefined}
onClick={handleClick}
role="textbox"
sx={{ backgroundColor: 'transparent' }}
{...props}
>
{Children.map(
@@ -338,7 +359,7 @@ function DataGridCellContent<TData extends object = {}>({
if (!isValidElement(child)) {
return null;
}
const { id: rowId } = row.original as { id: string };
return cloneElement(child, {
...child.props,
onSave: handleSave,
@@ -346,22 +367,26 @@ function DataGridCellContent<TData extends object = {}>({
onOptimisticValueChange: setOptimisticValue,
temporaryValue,
onTemporaryValueChange: setTemporaryValue,
rowId,
});
},
)}
</Box>
</div>
);
if (isEditable) {
return (
<Tooltip
disableHoverListener
disableFocusListener
delayDuration={100}
open={tooltipOpen}
title={tooltipTitle || ''}
TransitionProps={{ onExited: resetTooltipTitle }}
onOpenChange={(newState) => {
if (!newState) {
resetTooltipTitle();
}
}}
>
{content}
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>{tooltipTitle}</TooltipContent>
</Tooltip>
);
}
@@ -370,7 +395,7 @@ function DataGridCellContent<TData extends object = {}>({
}
export default function DataGridCell<TData extends object>(
props: DataGridCellProps<TData>,
props: PropsWithChildren<DataGridCellProps<TData>>,
) {
return (
<DataGridCellProvider>

View File

@@ -1,22 +1,20 @@
import { Input, inputClasses } from '@/components/ui/v2/Input';
import type { TextProps } from '@/components/ui/v2/Text';
import { Text } from '@/components/ui/v2/Text';
import { Input } from '@/components/ui/v3/input';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { cn } from '@/lib/utils';
import { getDateComponents } from '@/utils/getDateComponents';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { twMerge } from 'tailwind-merge';
import type { ChangeEvent, HTMLAttributes, KeyboardEvent, Ref } from 'react';
export interface DataGridDateCellProps<TData extends object>
extends CommonDataGridCellProps<TData, string> {
/**
* Props to be passed to date display.
*/
dateProps?: TextProps;
dateProps?: HTMLAttributes<HTMLParagraphElement>;
/**
* Props to be passed to time display.
*/
timeProps?: TextProps;
timeProps?: HTMLAttributes<HTMLParagraphElement>;
}
export default function DataGridDateCell<TData extends object>({
@@ -51,8 +49,7 @@ export default function DataGridDateCell<TData extends object>({
),
});
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
@@ -77,8 +74,6 @@ export default function DataGridDateCell<TData extends object>({
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
@@ -91,77 +86,49 @@ export default function DataGridDateCell<TData extends object>({
if (isEditing) {
return (
<Input
ref={inputRef}
ref={inputRef as Ref<HTMLInputElement>}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
onKeyDown={handleKeyDown}
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
/>
);
}
if (!optimisticValue) {
return (
<Text className="truncate text-xs" color="secondary">
null
</Text>
);
return <p className="truncate text-xs text-secondary">null</p>;
}
if (specificType === 'interval') {
return <Text className="truncate text-xs">{optimisticValue}</Text>;
return <p className="truncate text-xs">{optimisticValue}</p>;
}
return (
<div className={twMerge('grid grid-flow-row', className)}>
<div className={cn('grid grid-flow-row', className)}>
{specificType !== 'time' && specificType !== 'timetz' && (
<Text
className={twMerge('truncate text-xs', dateClassName)}
{...restDateProps}
>
<p className={cn('truncate text-xs', dateClassName)} {...restDateProps}>
{[year, month, day].filter(Boolean).join('-')}
</Text>
</p>
)}
{specificType !== 'date' && (
<Text
className={twMerge('truncate text-xs', timeClassName)}
color={
<p
className={cn(
'truncate text-xs',
timeClassName,
specificType === 'time' || specificType === 'timetz'
? 'primary'
: 'secondary'
}
: 'secondary',
)}
{...restTimeProps}
>
{[hour, minute, second].filter(Boolean).join(':')}
</Text>
</p>
)}
</div>
);

View File

@@ -1,108 +0,0 @@
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import type { ChangeEvent, KeyboardEvent } from 'react';
export type DataGridDecimalCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | string | null>;
export default function DataGridDecimalCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
}: DataGridDecimalCellProps<TData>) {
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
if (typeof temporaryValue === 'string') {
await onSave(parseFloat(temporaryValue));
} else if (typeof temporaryValue === 'number') {
await onSave(temporaryValue);
} else {
await onSave(null);
}
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
onTemporaryValueChange(event.target.value ?? null);
}
}
if (isEditing) {
return (
<Input
type="text"
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return (
<Text className="truncate !text-xs" color="disabled">
null
</Text>
);
}
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
}

View File

@@ -1,2 +0,0 @@
export * from './DataGridDecimalCell';
export { default as DataGridDecimalCell } from './DataGridDecimalCell';

View File

@@ -2,9 +2,7 @@ import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/da
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import { DataGridHeaderButton } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeaderButton';
import { cn } from '@/lib/utils';
import { useTheme } from '@mui/material';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { twMerge } from 'tailwind-merge';
export interface HeaderActionProps
extends DetailedHTMLProps<HTMLProps<HTMLElement>, HTMLElement> {}
@@ -22,10 +20,9 @@ export default function DataGridHeader({
...props
}: DataGridHeaderProps) {
const { flatHeaders } = useDataGridConfig();
const theme = useTheme();
return (
<div
className={twMerge(
className={cn(
'sticky top-0 z-30 inline-flex w-full items-center',
className,
)}
@@ -43,14 +40,11 @@ export default function DataGridHeader({
className={cn(
'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none',
'border-b-1 border-r-1',
'bg-paper',
{ 'sticky left-0 max-w-2': column.id === 'selection-column' },
'dark:text-[#dfecf5]',
)}
style={{
...headerProps.style,
backgroundColor: column.isDisabled
? theme.palette.background.default
: theme.palette.background.paper,
maxWidth:
column.id === 'selection-column'
? 32

View File

@@ -1,9 +1,9 @@
import { Button } from '@/components/ui/v3/button';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import { cn } from '@/lib/utils';
import { ArrowDown, ArrowUp } from 'lucide-react';
import type { TableHeaderProps } from 'react-table';
import { twMerge } from 'tailwind-merge';
interface DataGridHeaderButtonProps<T extends object> {
column: DataBrowserGridColumn<T>;
@@ -38,7 +38,7 @@ export default function DataGridHeaderButton<T extends object>({
return (
<Button
variant="ghost"
className={twMerge(
className={cn(
'h-fit p-0 text-xs focus:outline-none motion-safe:transition-colors dark:hover:bg-[#21262d]',
)}
disabled={column.isDisabled || column.disableSortBy}

View File

@@ -1,112 +0,0 @@
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import {
useDataGridCell,
type CommonDataGridCellProps,
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import type { ChangeEvent, KeyboardEvent } from 'react';
export type DataGridIntegerCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | null>;
export default function DataGridIntegerCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
}: DataGridIntegerCellProps<TData>) {
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
if (typeof temporaryValue === 'number') {
await onSave(temporaryValue);
} else {
await onSave(null);
}
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
if (event.target.value) {
onTemporaryValueChange(parseInt(event.target.value, 10));
} else {
onTemporaryValueChange(null);
}
}
}
if (isEditing) {
return (
<Input
type="number"
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return (
<Text className="truncate !text-xs" color="disabled">
null
</Text>
);
}
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
}

View File

@@ -1,2 +0,0 @@
export * from './DataGridIntegerCell';
export { default as DataGridIntegerCell } from './DataGridIntegerCell';

View File

@@ -0,0 +1,97 @@
import { Input } from '@/components/ui/v3/input';
import type { CommonDataGridCellProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { useDataGridCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { isNotEmptyValue } from '@/lib/utils';
import {
useEffect,
type ChangeEvent,
type KeyboardEvent,
type Ref,
} from 'react';
export type DataGridNumericCellProps<TData extends object> =
CommonDataGridCellProps<TData, number | null>;
export default function DataGridNumericCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
cell: {
column: { dataType },
},
}: DataGridNumericCellProps<TData>) {
const { inputRef, isEditing } = useDataGridCell<HTMLInputElement>();
useEffect(() => {
const controller = new AbortController();
function preventWheelInNumberInput(event: WheelEvent) {
event.preventDefault();
}
if (inputRef.current) {
inputRef.current.addEventListener('wheel', preventWheelInNumberInput, {
passive: false,
signal: controller.signal,
});
}
return () => controller.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputRef.current]);
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (isNotEmptyValue(onSave) && typeof temporaryValue !== 'undefined') {
if (event.key === 'Tab') {
await onSave?.(temporaryValue);
}
if (event.key === 'Enter') {
await onSave?.(temporaryValue);
}
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
const newValue = isNotEmptyValue(event.target.value)
? +event.target.value
: null;
onTemporaryValueChange(newValue);
}
}
if (isEditing) {
const step = dataType === 'integer' ? 1 : 0.1;
return (
<Input
type="number"
ref={inputRef as Ref<HTMLInputElement>}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onChange={handleChange}
onKeyDown={handleKeyDown}
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none [appearance:textfield] focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
step={step}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return <p className="truncate !text-xs text-disabled">null</p>;
}
return <p className="truncate !text-xs">{optimisticValue}</p>;
}

View File

@@ -0,0 +1,2 @@
export * from './DataGridNumericCell';
export { default as DataGridNumericCell } from './DataGridNumericCell';

View File

@@ -1,13 +1,8 @@
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import type { IconButtonProps } from '@/components/ui/v2/IconButton';
import { IconButton } from '@/components/ui/v2/IconButton';
import { ChevronLeftIcon } from '@/components/ui/v2/icons/ChevronLeftIcon';
import { ChevronRightIcon } from '@/components/ui/v2/icons/ChevronRightIcon';
import { Text } from '@/components/ui/v2/Text';
import clsx from 'clsx';
import { Button, type ButtonProps } from '@/components/ui/v3/button';
import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight } from 'lucide-react';
export interface DataGridPaginationProps extends BoxProps {
export interface DataGridPaginationProps {
/**
* Number of pages.
*/
@@ -27,11 +22,12 @@ export interface DataGridPaginationProps extends BoxProps {
/**
* Props to be passed to the next button component.
*/
nextButtonProps?: IconButtonProps;
nextButtonProps?: ButtonProps;
/**
* Props to be passed to the previous button component.
*/
prevButtonProps?: IconButtonProps;
prevButtonProps?: ButtonProps;
className?: string;
}
export default function DataGridPagination({
@@ -42,50 +38,48 @@ export default function DataGridPagination({
onOpenNextPage,
nextButtonProps,
prevButtonProps,
...props
}: DataGridPaginationProps) {
return (
<Box
className={clsx(
'grid grid-flow-col items-center justify-around rounded-md border-1',
<div
className={cn(
'grid grid-flow-col items-center justify-around rounded-md border-1 px-1',
className,
)}
{...props}
>
<IconButton
variant="borderless"
color="secondary"
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
onClick={onOpenPrevPage}
aria-label="Previous page"
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
{...prevButtonProps}
>
<ChevronLeftIcon className="h-4 w-4" />
</IconButton>
<ChevronLeft className="h-4 w-4" />
</Button>
<span
className={clsx(
className={cn(
'mx-1 inline-block font-display font-medium',
currentPage > 99 ? 'text-xs' : 'text-sm+',
)}
>
{currentPage}
<Text component="span" className="mx-1 inline-block" color="disabled">
/
</Text>
<span className="mx-1 inline-block text-disabled">/</span>
{totalPages}
</span>
<IconButton
variant="borderless"
color="secondary"
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={onOpenNextPage}
aria-label="Next page"
className="h-max w-max border-none bg-transparent dark:hover:bg-[#2f363d]"
{...nextButtonProps}
>
<ChevronRightIcon className="h-4 w-4" />
</IconButton>
</Box>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
}

View File

@@ -1,17 +1,20 @@
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { AudioPreviewIcon } from '@/components/ui/v2/icons/AudioPreviewIcon';
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
import { PDFPreviewIcon } from '@/components/ui/v2/icons/PDFPreviewIcon';
import { VideoPreviewIcon } from '@/components/ui/v2/icons/VideoPreviewIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from '@/components/ui/v3/dialog';
import { Spinner } from '@/components/ui/v3/spinner';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { usePreviewToggle } from '@/features/orgs/projects/storage/dataGrid/hooks/usePreviewToggle';
import { cn } from '@/lib/utils';
import { getHasuraAdminSecret } from '@/utils/env';
import clsx from 'clsx';
import { FileText } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useReducer, useState } from 'react';
import type { CellProps } from 'react-table';
@@ -246,124 +249,119 @@ export default function DataGridPreviewCell<TData extends object>({
}
}
function handleClose(openState: boolean) {
if (!openState) {
setShowModal(false);
dispatch({ type: 'CLEAR_PREVIEW' });
}
}
if (loading) {
return <ActivityIndicator delay={500} className="mx-auto" />;
return (
<div className="flex w-full justify-center">
<Spinner className="mx-auto h-4 w-4" />
</div>
);
}
if (error) {
return (
<Box
className="grid w-full grid-flow-col items-center justify-center gap-1 text-center"
sx={{ color: 'error.main' }}
>
<FilePreviewIcon error /> Error
</Box>
<div className="box grid w-full grid-flow-col items-center justify-center gap-1 text-center !text-error-main">
<FileText className="text-error-main" /> Error
</div>
);
}
return (
<>
<Modal
wrapperClassName="items-center"
showModal={showModal}
close={() => setShowModal(false)}
afterLeave={() => dispatch({ type: 'CLEAR_PREVIEW' })}
className={clsx(
previewableImages.includes(mimeType) || isVideo || isAudio
? 'mx-12 flex h-screen items-center justify-center'
: 'mt-4 inline-block h-near-screen w-full px-12',
)}
>
<Box
className={clsx(
!isJson && 'bg-checker-pattern',
'relative mx-auto flex overflow-hidden rounded-md',
)}
sx={{
backgroundColor: isJson ? 'background.default' : undefined,
color: 'text.primary',
}}
<Dialog open={showModal} onOpenChange={handleClose}>
<DialogTrigger asChild>
<button
type="button"
aria-label={alt}
onClick={handleOpenPreview}
className={cn('flex h-full w-full items-center justify-center')}
>
{!previewLoading && (
<IconButton
aria-label="Close"
variant="borderless"
color="secondary"
className="absolute right-2 top-2 z-50 p-2"
sx={{
[`&:hover, &:active, &:focus`]: {
backgroundColor: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.black';
}
return theme.palette.mode === 'dark'
? 'grey.800'
: 'grey.200';
},
},
}}
onClick={() => setShowModal(false)}
>
<XIcon
className="h-5 w-5"
sx={{
color: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.white';
}
return theme.palette.mode === 'dark'
? 'grey.100'
: 'grey.700';
},
}}
/>
</IconButton>
)}
{previewLoading && !previewUrl && (
<ActivityIndicator
delay={500}
className="mx-auto"
label="Loading preview..."
/>
)}
{previewError && (
<Box
className="px-6 py-3.5 pr-12 text-start font-medium"
sx={{ color: 'error.main' }}
>
<p>Error: Preview can&apos;t be loaded.</p>
<p>{previewError.message}</p>
</Box>
)}
{previewUrl && isImage && (
<picture className="h-auto max-h-near-screen min-h-38 min-w-38">
<source srcSet={previewUrl} type={mimeType} />
{previewEnabled &&
previewableImages.includes(mimeType) &&
objectUrl ? (
<picture className="h-full w-20">
<source srcSet={objectUrl} type={mimeType} />
<img
src={previewUrl}
src={objectUrl}
alt={alt}
className="h-full w-full object-scale-down"
/>
</picture>
)}
) : (
<>
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
{mimeType === 'application/pdf' && (
<PDFPreviewIcon className="h-5 w-5" />
)}
{!isVideo &&
!isAudio &&
mimeType !== 'application/pdf' &&
fallbackPreview}
</>
)}
</button>
</DialogTrigger>
<DialogContent
closeButtonClassName={cn({ 'text-white': isVideo || isAudio })}
className={cn(
{ 'bg-checker-pattern': !isJson && !previewError },
{ 'p-0': isVideo || isAudio },
isAudio ? '!w-auto' : 'h-[90vh] min-w-[96vw]',
'flex items-center justify-center overflow-hidden rounded-md',
)}
>
<>
<DialogTitle className="hidden">{alt}</DialogTitle>
<DialogDescription className="hidden">{alt}</DialogDescription>
{previewLoading && !previewUrl && (
<Spinner
className={cn('h-5 w-5', {
'!stroke-[#1e324b]': !isJson,
})}
wrapperClassName={cn('flex-row gap-1 text-xs', {
'text-disabled': isJson,
'text-gray-600': !isJson,
})}
>
Loading preview...
</Spinner>
)}
{previewError && (
<div className="px-6 py-3.5 pr-12 text-start font-medium !text-error-main">
<p>Error: Preview can&apos;t be loaded.</p>
<p>{previewError?.message}</p>
</div>
)}
{previewUrl && isImage && (
<picture className="flex h-full max-h-full items-center justify-center">
<source srcSet={previewUrl} type={mimeType} />
<img
src={previewUrl}
alt={alt}
className="h-full max-w-full object-contain"
/>
</picture>
)}
{previewUrl && isVideo && (
<video
autoPlay
controls
className="h-auto max-h-near-screen w-full bg-black"
className="h-full w-full rounded-sm bg-black"
>
<track kind="captions" />
<source src={previewUrl} type={mimeType} />
Your browser does not support the video tag.
</video>
)}
{previewUrl && isAudio && (
<audio autoPlay controls className="h-28 bg-black">
<track kind="captions" />
@@ -371,7 +369,6 @@ export default function DataGridPreviewCell<TData extends object>({
Your browser does not support the audio tag.
</audio>
)}
{!previewLoading &&
previewUrl &&
!previewableImages.includes(mimeType) &&
@@ -383,52 +380,8 @@ export default function DataGridPreviewCell<TData extends object>({
title="File preview"
/>
)}
</Box>
</Modal>
<div className="flex h-full w-full justify-center">
{previewEnabled && previewableImages.includes(mimeType) && objectUrl ? (
<button
type="button"
aria-label={alt}
onClick={handleOpenPreview}
className="mx-auto h-full"
>
<picture className="h-full w-20">
<source srcSet={objectUrl} type={mimeType} />
<img
src={objectUrl}
alt={alt}
className="h-full w-full object-scale-down"
/>
</picture>
</button>
) : null}
{(!previewableImages.includes(mimeType) ||
!objectUrl ||
!previewEnabled) && (
<button
type="button"
onClick={handleOpenPreview}
aria-label={alt}
className="grid h-full w-full items-center justify-center self-center"
>
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
{mimeType === 'application/pdf' && (
<PDFPreviewIcon className="h-5 w-5" />
)}
{!isVideo &&
!isAudio &&
mimeType !== 'application/pdf' &&
fallbackPreview}
</button>
)}
</div>
</>
</>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,12 +1,12 @@
import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Button } from '@/components/ui/v3/button';
import { Input } from '@/components/ui/v3/input';
import { Textarea } from '@/components/ui/v3/textarea';
import {
useDataGridCell,
type CommonDataGridCellProps,
} from '@/features/orgs/projects/storage/dataGrid/components/DataGridCell';
import { copy } from '@/utils/copy';
import { Copy } from 'lucide-react';
import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
import { useEffect } from 'react';
@@ -74,8 +74,6 @@ export default function DataGridTextCell<TData extends object>({
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
@@ -122,35 +120,12 @@ export default function DataGridTextCell<TData extends object>({
if (isEditing && isMultiline) {
return (
<Input
multiline
ref={inputRef as Ref<HTMLInputElement>}
<Textarea
ref={inputRef as Ref<HTMLTextAreaElement>}
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange}
onKeyDown={handleTextAreaKeyDown}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full min-h-38"
rows={5}
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
className="absolute left-0 top-0 z-10 h-25 min-h-25 w-full resize-none rounded-none !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
/>
);
}
@@ -162,39 +137,17 @@ export default function DataGridTextCell<TData extends object>({
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange}
onKeyDown={handleInputKeyDown}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
wrapperClassName="absolute top-0 z-10 w-full top-0 left-0 h-full"
className="h-full w-full resize-none rounded-none border-none px-2 py-1.5 !text-xs outline-none focus-within:rounded-none focus-within:border-transparent focus-within:bg-white focus-within:shadow-[inset_0_0_0_1.5px_rgba(0,82,205,1)] focus:outline-none focus:ring-0 dark:focus-within:bg-theme-grey-200"
/>
);
}
if (!optimisticValue) {
return (
<Text className="truncate !text-xs" color="secondary">
<p className="truncate !text-xs text-secondary">
{optimisticValue === '' ? 'empty' : 'null'}
</Text>
</p>
);
}
@@ -202,8 +155,8 @@ export default function DataGridTextCell<TData extends object>({
return (
<div className="grid grid-flow-col items-center justify-start gap-1">
<Button
variant="borderless"
color="secondary"
variant="outline"
size="icon"
onClick={(event) => {
event.stopPropagation();
@@ -214,32 +167,26 @@ export default function DataGridTextCell<TData extends object>({
copy(copiableValue, 'Value');
}}
className="-ml-px min-w-0 p-0"
className="-ml-px h-max w-max min-w-0 border-transparent bg-transparent p-[1px] text-disabled hover:bg-divider"
aria-label="Copy value"
sx={{
color: (theme) =>
theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.disabled',
}}
>
<CopyIcon className="h-4 w-4" />
<Copy className="h-4 w-4" />
</Button>
<Text className="truncate text-xs">
<p className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue}
</Text>
</p>
</div>
);
}
return (
<Text className="truncate text-xs">
<p className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue}
</Text>
</p>
);
}

View File

@@ -1,11 +1,11 @@
import type { DataGridProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
import { DataGrid } from '@/features/orgs/projects/storage/dataGrid/components/DataGrid';
import { DataGridBooleanCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridBooleanCell';
import { DataGridDateCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridDateCell';
import type { PreviewProps } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell';
import { DataGridPreviewCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPreviewCell';
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
@@ -93,7 +93,8 @@ const columns: (Column<StoredFile> & {
Header: 'isUploaded',
accessor: 'isUploaded',
width: 100,
Cell: DataGridBooleanCell,
// eslint-disable-next-line react/prop-types
Cell: ({ value }) => <ReadOnlyToggle checked={value} />,
sortType: 'basic',
},
{

View File

@@ -1,10 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Chip } from '@/components/ui/v2/Chip';
import type { InputProps } from '@/components/ui/v2/Input';
import { Input } from '@/components/ui/v2/Input';
import { Badge } from '@/components/ui/v3/badge';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { Input, type InputProps } from '@/components/ui/v3/input';
import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
@@ -12,6 +9,7 @@ import type { DataGridPaginationProps } from '@/features/orgs/projects/storage/d
import { DataGridPagination } from '@/features/orgs/projects/storage/dataGrid/components/DataGridPagination';
import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
import { FileUploadButton } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
import { cn } from '@/lib/utils';
import type { Files } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';
import { triggerToast } from '@/utils/toast';
@@ -22,11 +20,12 @@ import { twMerge } from 'tailwind-merge';
export type FilterProps = PropsWithoutRef<InputProps>;
export interface FilesDataGridControlsProps extends BoxProps {
export interface FilesDataGridControlsProps {
paginationProps?: DataGridPaginationProps;
fileUploadProps?: FileUploadButtonProps;
filterProps?: FilterProps;
refetchData?: () => Promise<any>;
className?: string;
}
export default function FilesDataGridControls({
@@ -101,22 +100,23 @@ export default function FilesDataGridControls({
}
return (
<Box
className={twMerge('sticky top-0 z-20 border-b-1 p-2', className)}
<div
className={cn('box sticky top-0 z-20 border-b-1 p-2', className)}
{...props}
>
{numberOfSelectedFiles > 0 ? (
<div className="mx-auto grid h-[40px] grid-flow-col items-center justify-start gap-2">
<Chip
color="info"
size="small"
label={`${numberOfSelectedFiles} selected`}
/>
<div className="flex h-[40px] items-center justify-start gap-2">
<Badge
variant="secondary"
className="!bg-[#ebf3ff] text-primary dark:!bg-[#1b2534]"
>
{`${numberOfSelectedFiles} selected`}
</Badge>
<Button
variant="borderless"
color="error"
size="small"
variant="outline"
size="sm"
className="border-none text-destructive hover:bg-[#f131541a] hover:text-destructive"
loading={deleteLoading}
onClick={() =>
openAlertDialog({
@@ -145,17 +145,13 @@ export default function FilesDataGridControls({
</Button>
</div>
) : (
<div className="mx-auto grid w-full grid-cols-12 gap-2">
<div className="flex w-full flex-grow gap-3">
<Input
className={twMerge(
'col-span-12 xs+:col-span-12 md:col-span-9 xl:col-span-10',
filterClassName,
)}
fullWidth
wrapperClassName={cn('w-full', filterClassName)}
{...restFilterProps}
/>
<div className="col-span-12 grid grid-flow-col gap-2 md:col-span-3 xl:col-span-2">
<div className="flex flex-shrink-0 gap-3">
<DataGridPagination
className={twMerge('col-span-6', paginationClassName)}
{...restPaginationProps}
@@ -173,6 +169,6 @@ export default function FilesDataGridControls({
</div>
</div>
)}
</Box>
</div>
);
}

View File

@@ -374,7 +374,7 @@ export default function OnboardingPage() {
>
{plansData?.plans?.map((plan) => (
<FormItem key={plan.id}>
<FormLabel className="flex w-full cursor-pointer items-center justify-between rounded-lg border p-4 hover:bg-accent">
<FormLabel className="flex w-full cursor-pointer items-center justify-between rounded-lg border p-4 hover:bg-accent-background">
<div className="flex items-center space-x-3">
<FormControl>
<RadioGroupItem value={plan.id} />

View File

@@ -14,7 +14,7 @@ export default function OrgBilling() {
}
return (
<div className="flex h-full flex-col gap-4 overflow-auto bg-accent p-4">
<div className="bg-accent-background flex h-full flex-col gap-4 overflow-auto p-4">
<SubscriptionPlan />
{showBillingEstimate && <BillingEstimate />}
</div>

View File

@@ -7,7 +7,7 @@ import type { ReactElement } from 'react';
export default function OrgMembers() {
const { org: { plan: { isFree } = {} } = {} } = useCurrentOrg();
return (
<div className="flex h-full flex-col gap-4 overflow-auto bg-accent p-4">
<div className="bg-accent-background flex h-full flex-col gap-4 overflow-auto p-4">
<MembersList />
{!isFree && <PendingInvites />}
</div>

View File

@@ -1,6 +1,5 @@
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Box } from '@/components/ui/v2/Box';
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
@@ -46,12 +45,9 @@ DataBrowserTableDetailsPage.getLayout = function getLayout(page: ReactElement) {
>
<DataBrowserSidebar className="w-full max-w-sidebar" />
<Box
className="flex w-full flex-auto flex-col overflow-x-hidden"
sx={{ backgroundColor: 'background.default' }}
>
<div className="box flex w-full flex-auto flex-col overflow-x-hidden">
{page}
</Box>
</div>
</OrgLayout>
);
};

View File

@@ -1,5 +1,4 @@
import { InlineCode } from '@/components/presentational/InlineCode';
import { Box } from '@/components/ui/v2/Box';
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
import { DataBrowserSidebar } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserSidebar';
@@ -45,12 +44,9 @@ DataBrowserDatabaseDetailsPage.getLayout = function getLayout(
>
<DataBrowserSidebar className="w-full max-w-sidebar" />
<Box
className="flex w-full flex-auto flex-col overflow-x-hidden"
sx={{ backgroundColor: 'background.default' }}
>
<div className="box flex w-full flex-auto flex-col overflow-x-hidden bg-default">
{page}
</Box>
</div>
</OrgLayout>
);
};

View File

@@ -35,7 +35,7 @@ export default function OrgProjects() {
if (apps?.length === 0) {
return (
<div className="flex h-full w-full items-start justify-center bg-accent p-4">
<div className="flex h-full w-full items-start justify-center bg-accent-background p-4">
<div className="flex w-full flex-col items-center justify-center space-y-8 rounded-md border bg-background p-12">
<div className="flex flex-col items-center justify-center">
<h2 className="text-xl font-medium">Welcome to Nhost!</h2>
@@ -58,7 +58,7 @@ export default function OrgProjects() {
}
return (
<div className="h-full bg-accent">
<div className="h-full bg-accent-background">
<ProjectsGrid projects={apps} />
</div>
);

View File

@@ -6,7 +6,7 @@ import type { ReactElement } from 'react';
export default function OrgSettings() {
return (
<div className="flex h-full flex-col gap-4 overflow-auto bg-accent p-4">
<div className="bg-accent-background flex h-full flex-col gap-4 overflow-auto p-4">
<GeneralSettings />
<Soc2Download />
<DeleteOrg />

View File

@@ -235,12 +235,14 @@ body {
--popover-foreground: 222.2 84% 4.9%;
--primary: 216 100% 40%;
--primary-foreground: 210 40% 98%;
--secondary: 204 29% 97%;
--secondary-foreground: 222.2 47.4% 11.2%;
--secondary: 215.4, 16.3%, 46.9%;
--secondary-foreground: 210 40% 98%;
--secondary-hover: 215.3, 19.3%, 34.5%;
--muted: 0 0% 98%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 204 29% 97%;
--accent: 216 100% 40.2% / 8%;
--accent-foreground: 222.2 47.4% 11.2%;
--accent-background: 204 29% 97%;
--destructive: 349 87% 57%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
@@ -252,10 +254,20 @@ body {
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--paper: 0, 0%, 100%;
--divider: 210 16.67% 92.94%;
--disabled: 215.56 15.79% 66.47%;
--background-default: 0, 0%, 98%;
--primary-text: 215.71 38.89% 21.18%;
--data-cell-bg: 204 29.41% 96.67%;
--primary-main: 216 100% 40.2%;
--table-selected: 216 100% 96.08%;
--default: 0 0% 98.04%;
--primary-highlight: 216 100% 40.2% / 8%;
}
.dark {
--background: 216 25% 12%;
--background: 216 24.59% 11.96%;
--foreground: 210 40% 98%;
--card: 216 25% 12%;
--card-foreground: 210 40% 98%;
@@ -263,12 +275,13 @@ body {
--popover-foreground: 210 40% 98%;
--primary: 216 100% 61%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--secondary: 216 17.07% 40.2%;
--secondary-foreground: 0, 0%, 85%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217 24% 11%;
--accent: 215.88 100% 60.98% / 16%;
--accent-foreground: 210 40% 98%;
--accent-background: 217 24% 11%;
--destructive: 349 87% 57%;
--destructive-foreground: 210 40% 98%;
--border: 210 13% 21%;
@@ -279,6 +292,16 @@ body {
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--paper: 216 24.58% 11.96%;
--divider: 210deg 12.96% 21.18%;
--disabled: 211.58 8.92% 58.24%;
--primary-text: 204.55 52.38% 91.76%;
--background-default: 210, 23%, 11%;
--data-cell-bg: 215 15.38% 15.29%;
--primary-main: 215.88 100% 60.98%;
--table-selected: 216 31.65% 15.49%;
--default: 216.92 23.64% 10.78%;
--primary-highlight: 215.88 100% 60.98% / 16%;
}
}
@@ -298,3 +321,7 @@ body {
::-webkit-scrollbar-thumb {
@apply rounded-full border-[1px] border-solid border-transparent bg-muted-foreground/50 bg-clip-padding;
}
.box {
@apply border-divider bg-paper text-primary-text;
}

View File

@@ -1,31 +1,18 @@
import { Box } from '@/components/ui/v2/Box';
import { createTheme } from '@/components/ui/v2/createTheme';
import { ThemeProvider } from '@mui/material';
import clsx from 'clsx';
import { cn } from '@/lib/utils';
import type { ReactNode } from 'react';
import { toast } from 'react-hot-toast';
import getColor from './getColor';
export default function triggerToast(text: ReactNode) {
const color = getColor();
toast.custom(
(t) => (
<ThemeProvider theme={createTheme(color)}>
<Box
className={clsx(
'rounded-sm+ px-2 py-1.5 text-center font-normal shadow-md',
t.visible ? 'animate-enter' : 'animate-leave',
)}
sx={{
color: 'common.white',
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.400' : 'grey.700',
}}
>
{text}
</Box>
</ThemeProvider>
<div
className={cn(
'rounded-sm+ bg-gray-700 px-2 py-1.5 text-center font-normal text-white shadow-md',
t.visible ? 'animate-enter' : 'animate-leave',
)}
>
{text}
</div>
),
{
id: typeof text === 'string' ? text : '',

View File

@@ -20,12 +20,22 @@ module.exports = {
github: '#24292E;',
brown: '#382D22',
copper: '#DD792D',
paper: '#171d26',
divider: '#2f363d',
'primary-main': '#0052cd',
paper: 'hsl(var(--paper))',
divider: 'hsl(var(--divider))',
disabled: 'hsl(var(--disabled))',
'data-cell-bg': 'hsl(var(--data-cell-bg))',
'primary-text': 'hsl(var(--primary-text))',
'primary-main': 'hsl(var(--primary-main))',
'primary-highlight': 'hsl(var(--primary-highlight))',
'primary-light': '#ebf3ff',
'primary-dark': '#063799',
'theme-grey-200': '#21262d',
'light-action-hover': '#f3f4f6',
'background-default': 'hsl(var(--background-default))',
'table-selected': 'hsl(var(--table-selected))',
'error-main': '#f13154',
'secondary-hover': '#475569',
default: 'hsl(var(--default))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
@@ -51,6 +61,7 @@ module.exports = {
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
background: 'hsl(var(--accent-background))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',

View File

@@ -21,6 +21,7 @@
"clean:all": "pnpm clean && rm -rf ./{{packages,examples/**,templates/**}/*,docs,dashboard}/{.nhost,node_modules} node_modules",
"clean": "rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
"clean:install": "pnpm clean:all && pnpm install",
"clean:build": "pnpm clean:install && pnpm build:nhost-js",
"dev:dashboard": "turbo run dev --filter=@nhost/dashboard"
},
"devDependencies": {