From b68b2ebc4fc135dc0d20b43a2b169bbe55c4cb76 Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Wed, 20 Aug 2025 15:37:55 +0800
Subject: [PATCH] Refactor access tokens to reuse existing DatePicker component
(#38054)
* Refactor access tokens to reuse existing DatePicker component
* Clean up
* Clean
* Fixy
---
.../Account/AccessTokens/AccessTokenList.tsx | 6 +-
.../AccessTokens/NewAccessTokenDialog.tsx | 53 ++--
.../Account/AccessTokens/TokensDatePicker.tsx | 239 ------------------
.../Account/NewAccessTokenButton.tsx | 220 ----------------
.../components/ui/DatePicker/DatePicker.tsx | 31 +--
apps/studio/pages/account/tokens.tsx | 1 +
6 files changed, 35 insertions(+), 515 deletions(-)
delete mode 100644 apps/studio/components/interfaces/Account/AccessTokens/TokensDatePicker.tsx
delete mode 100644 apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx
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 (
-
-
- } {...buttonTriggerProps}>
- {selectedDate ? `${dayjs(selectedDate).format('DD MMM, HH:mm')}` : 'Select date'}
-
-
-
-
-
-
-
'cursor-pointer hover:bg-accent'}
- filterDate={(date) => {
- if (!disabled) return true
- return !disabled(date)
- }}
- todayButton={
-
-
-
- }
- renderCustomHeader={({
- date,
- decreaseMonth,
- increaseMonth,
- prevMonthButtonDisabled,
- nextMonthButtonDisabled,
- }) => (
-
-
- }
- type="text"
- size="tiny"
- className="px-1.5"
- />
-
- {dayjs(date).format('MMMM YYYY')}
-
- }
- type="text"
- size="tiny"
- className="px-1.5"
- />
-
-
- )}
- />
-
-
-
-
-
-
-
-
- )
-}
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 (
- <>
-
-
-
-
- }
- />
-
-
- {
- setTokenScope('V0')
- setVisible(true)
- }}
- >
- Generate token for experimental API
-
-
-
-
-
-
- >
- )
-}
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 : (
<>
-