diff --git a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx index 82de89e4f24..73db62030ff 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx @@ -163,7 +163,7 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo {dayjs(x.last_used_at).format('DD MMM YYYY')} - Last used on {dayjs(x.last_used_at).format('DD MMM, YYYY HH:mm')} + Last used on {dayjs(x.last_used_at).format('DD MMM, YYYY HH:mm:ss')} ) : ( @@ -179,7 +179,7 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo

Expired

- Expired on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm')} + Expired on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')} ) : ( @@ -190,7 +190,7 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo

- Expires on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm')} + Expires on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')} ) diff --git a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx b/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx index c6946b0c2d7..7d6a15f57d6 100644 --- a/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx +++ b/apps/studio/components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx @@ -6,6 +6,7 @@ import { type SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' +import { DatePicker } from 'components/ui/DatePicker' import { useAccessTokenCreateMutation } from 'data/access-tokens/access-tokens-create-mutation' import { Button, @@ -34,7 +35,6 @@ import { ExpiresAtOptions, NON_EXPIRING_TOKEN_VALUE, } from './AccessTokens.constants' -import { TokenDatePickerValue, TokensDatePicker } from './TokensDatePicker' const formId = 'new-access-token-form' @@ -59,9 +59,7 @@ export const NewAccessTokenDialog = ({ onOpenChange, onCreateToken, }: NewAccessTokenDialogProps) => { - const [customExpiryDate, setCustomExpiryDate] = useState( - undefined - ) + const [customExpiryDate, setCustomExpiryDate] = useState<{ date: string } | undefined>(undefined) const [isCustomExpiry, setIsCustomExpiry] = useState(false) const form = useForm>({ @@ -103,7 +101,7 @@ export const NewAccessTokenDialog = ({ if (value === CUSTOM_EXPIRY_VALUE) { setIsCustomExpiry(true) // Set a default custom date (today at 23:59:59) - const defaultCustomDate: TokenDatePickerValue = { + const defaultCustomDate = { date: dayjs().endOf('day').toISOString(), } setCustomExpiryDate(defaultCustomDate) @@ -115,7 +113,7 @@ export const NewAccessTokenDialog = ({ } } - const handleCustomDateChange = (value: TokenDatePickerValue) => { + const handleCustomDateChange = (value: { date: string }) => { setCustomExpiryDate(value) } @@ -212,37 +210,20 @@ export const NewAccessTokenDialog = ({ {isCustomExpiry && ( - { - const today = new Date() - const maxDate = new Date() - maxDate.setDate(today.getDate() + 364) // 364 days instead of 365 - - // Normalize dates to ignore time - const dateOnly = new Date( - date.getFullYear(), - date.getMonth(), - date.getDate() - ) - const todayOnly = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() - ) - const maxDateOnly = new Date( - maxDate.getFullYear(), - maxDate.getMonth(), - maxDate.getDate() - ) - - return dateOnly < todayOnly || dateOnly > maxDateOnly + { + if (date.to) handleCustomDateChange({ date: date.to }) }} - buttonTriggerProps={{ - size: 'small', - }} - /> + > + {customExpiryDate + ? `${dayjs(customExpiryDate.date).format('DD MMM, HH:mm')}` + : 'Select date'} + )} {field.value === NON_EXPIRING_TOKEN_VALUE && ( diff --git a/apps/studio/components/interfaces/Account/AccessTokens/TokensDatePicker.tsx b/apps/studio/components/interfaces/Account/AccessTokens/TokensDatePicker.tsx deleted file mode 100644 index 04ac87be830..00000000000 --- a/apps/studio/components/interfaces/Account/AccessTokens/TokensDatePicker.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import dayjs from 'dayjs' -import { ChevronLeft, ChevronRight, Clock } from 'lucide-react' -import { PropsWithChildren, useEffect, useRef, useState } from 'react' -import DatePicker from 'react-datepicker' - -import { TimeSplitInput } from 'components/ui/DatePicker/TimeSplitInput' -import { - Button, - ButtonProps, - PopoverContent_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, -} from 'ui' - -export type TokenDatePickerValue = { - date: string -} - -interface TokensDatePickerProps { - value: TokenDatePickerValue - buttonTriggerProps?: ButtonProps - popoverContentProps?: typeof PopoverContent_Shadcn_ - onSubmit: (value: TokenDatePickerValue) => void - disabled?: (date: Date) => boolean -} - -export const TokensDatePicker = ({ - value, - buttonTriggerProps, - popoverContentProps, - onSubmit, - disabled, -}: PropsWithChildren) => { - const [open, setOpen] = useState(false) - const todayButtonRef = useRef(null) - const timeInputRef = useRef(null) - - // Reset the state when the popover closes - useEffect(() => { - if (!open) { - const dateToSet = value.date ? new Date(value.date) : new Date() - setSelectedDate(dateToSet) - - // If there's an existing value, use its time, otherwise default to 23:59:59 - if (value.date) { - setSelectedTime({ - HH: dateToSet.getHours().toString().padStart(2, '0'), - mm: dateToSet.getMinutes().toString().padStart(2, '0'), - ss: dateToSet.getSeconds().toString().padStart(2, '0'), - }) - } else { - setSelectedTime({ - HH: '23', - mm: '59', - ss: '59', - }) - } - } - }, [open, value.date]) - - // Focus the time input when popover opens - useEffect(() => { - if (open && timeInputRef.current) { - // Small delay to ensure the popover is fully rendered - setTimeout(() => { - const firstInput = timeInputRef.current?.querySelector('input') - if (firstInput) { - firstInput.focus() - } - }, 100) - } - }, [open]) - - // Prevent parent dialog from stealing focus when popover is open - useEffect(() => { - if (open) { - const handleFocusIn = (event: FocusEvent) => { - const target = event.target as HTMLElement - const popover = timeInputRef.current?.closest('[data-radix-popper-content-wrapper]') - - // If focus is moving outside the popover, prevent it - if (popover && !popover.contains(target)) { - event.preventDefault() - event.stopPropagation() - - // Refocus the first time input - const firstInput = timeInputRef.current?.querySelector('input') - if (firstInput) { - firstInput.focus() - } - } - } - - document.addEventListener('focusin', handleFocusIn, true) - - return () => { - document.removeEventListener('focusin', handleFocusIn, true) - } - } - }, [open]) - - const [selectedDate, setSelectedDate] = useState( - value.date ? new Date(value.date) : new Date() - ) - const [selectedTime, setSelectedTime] = useState(() => { - if (value.date) { - const date = new Date(value.date) - return { - HH: date.getHours().toString().padStart(2, '0'), - mm: date.getMinutes().toString().padStart(2, '0'), - ss: date.getSeconds().toString().padStart(2, '0'), - } - } - return { - HH: '23', - mm: '59', - ss: '59', - } - }) - - function handleDatePickerChange(date: Date | null) { - setSelectedDate(date) - } - - function handleApply() { - if (!selectedDate) return - - // Add Time to the date - const finalDate = new Date( - selectedDate.setHours(+selectedTime.HH, +selectedTime.mm, +selectedTime.ss) - ) - - onSubmit({ - date: finalDate.toISOString(), - }) - - setOpen(false) - } - - return ( - - - - - -
-
-
- -
-
-
- 'cursor-pointer hover:bg-accent'} - filterDate={(date) => { - if (!disabled) return true - return !disabled(date) - }} - todayButton={ -
- -
- } - renderCustomHeader={({ - date, - decreaseMonth, - increaseMonth, - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => ( -
-
-
-
- )} - /> -
-
- - -
-
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx b/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx deleted file mode 100644 index 3ed160fdfa2..00000000000 --- a/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { ChevronDown, ExternalLink } from 'lucide-react' -import { useState } from 'react' -import { type SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import { z } from 'zod' - -import { useAccessTokenCreateMutation } from 'data/access-tokens/access-tokens-create-mutation' -import { - Button, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogSection, - DialogSectionSeparator, - DialogTitle, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - Form_Shadcn_, - FormControl_Shadcn_, - FormField_Shadcn_, - Input_Shadcn_, - Select_Shadcn_, - SelectContent_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, -} from 'ui' -import { Admonition } from 'ui-patterns' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { ExpiresAtOptions, NON_EXPIRING_TOKEN_VALUE } from './AccessTokens/AccessTokens.constants' - -export interface NewAccessTokenButtonProps { - onCreateToken: (token: any) => void -} - -const TokenSchema = z.object({ - tokenName: z.string().min(1, 'Please enter a name for the token'), - expiresAt: z.preprocess( - (val) => (val === NON_EXPIRING_TOKEN_VALUE ? undefined : val), - z.string().datetime().optional() - ), -}) - -const formId = 'new-access-token-form' - -export const NewAccessTokenButton = ({ onCreateToken }: NewAccessTokenButtonProps) => { - const [visible, setVisible] = useState(false) - const [tokenScope, setTokenScope] = useState<'V0' | undefined>(undefined) - - const form = useForm>({ - resolver: zodResolver(TokenSchema), - defaultValues: { tokenName: '', expiresAt: ExpiresAtOptions['month'].value }, - mode: 'onSubmit', - }) - const { mutate: createAccessToken, isLoading } = useAccessTokenCreateMutation() - - const onSubmit: SubmitHandler> = async (values) => { - createAccessToken( - { name: values.tokenName, scope: tokenScope, expires_at: values.expiresAt }, - { - onSuccess: (data) => { - toast.success(`Your access token "${data.name}" is ready.`) - form.reset() - onCreateToken(data) - setVisible(false) - }, - } - ) - } - - return ( - <> -
- - - -
- - { - if (!open) form.reset() - setVisible(open) - }} - > - - - - {tokenScope === 'V0' ? 'Generate token for experimental API' : 'Generate New Token'} - - - - - {tokenScope === 'V0' && ( - -

- These include deleting organizations and projects which cannot be undone. As - such, be very careful when using this API. -

- - - } - /> - )} - -
- ( - - - - - - )} - /> - ( - - - - - - - - {Object.values(ExpiresAtOptions).map((option) => ( - - {option.label} - - ))} - - - - {field.value === NON_EXPIRING_TOKEN_VALUE && ( - - )} - - )} - /> - -
-
- - - - -
-
- - ) -} diff --git a/apps/studio/components/ui/DatePicker/DatePicker.tsx b/apps/studio/components/ui/DatePicker/DatePicker.tsx index 64feb2cf4df..52a2825fbf8 100644 --- a/apps/studio/components/ui/DatePicker/DatePicker.tsx +++ b/apps/studio/components/ui/DatePicker/DatePicker.tsx @@ -1,7 +1,7 @@ import { format } from 'date-fns' import dayjs from 'dayjs' import { ArrowRight, Calendar, ChevronLeft, ChevronRight } from 'lucide-react' -import React, { useEffect, useState } from 'react' +import { ReactNode, useEffect, useState } from 'react' import ReactDatePicker from 'react-datepicker' import type { DatePickerToFrom } from 'components/interfaces/Settings/Logs/Logs.types' @@ -22,13 +22,15 @@ export interface DatePickerProps { triggerButtonType?: ButtonProps['type'] triggerButtonClassName?: string triggerButtonTitle?: string + triggerButtonSize?: 'tiny' | 'small' + contentSide?: 'bottom' | 'top' minDate?: Date maxDate?: Date hideTime?: boolean hideClear?: boolean selectsRange?: boolean - renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void - children?: React.ReactNode | React.ReactNode[] | null + renderFooter?: (args: DatePickerToFrom) => ReactNode | void + children?: ReactNode | ReactNode[] | null } const START_DATE_DEFAULT = new Date() @@ -37,13 +39,15 @@ const END_DATE_DEFAULT = new Date() const START_TIME_DEFAULT = { HH: '00', mm: '00', ss: '00' } const END_TIME_DEFAULT = { HH: '23', mm: '59', ss: '59' } -function DatePicker({ +export function DatePicker({ to, from, onChange, triggerButtonType = 'default', triggerButtonClassName = '', triggerButtonTitle, + triggerButtonSize, + contentSide = 'bottom', minDate, maxDate, hideTime = false, @@ -147,6 +151,7 @@ function DatePicker({ title={triggerButtonTitle} type={triggerButtonType} icon={} + size={triggerButtonSize} className={triggerButtonClassName} > {children !== undefined ? ( @@ -171,14 +176,14 @@ function DatePicker({ )} - + <> {hideTime ? null : ( <> -
+
{!selectsRange ? null : ( <> -
+
-
+
)} -
+