Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/docs/content/guides/realtime/broadcast.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,12 @@ This feature is in Public Beta. [Submit a support ticket](https://supabase.help)

</Admonition>

<Admonition type="note">

All the messages sent using Broadcast from the Database are stored in `realtime.messages` table and will be deleted after 3 days.

</Admonition>

You can send messages directly from your database using the `realtime.send()` function:

{/* prettier-ignore */}
Expand Down
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Peter Lyn
Qiao Han
Rafael Chacón
Raminder Singh
Raúl Barroso
Riccardo Busetti
Rodrigo Mansueli
Ronan Lehane
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import AlertError from 'components/ui/AlertError'
import { useAccessTokenDeleteMutation } from 'data/access-tokens/access-tokens-delete-mutation'
import { AccessToken, useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
import dayjs from 'dayjs'
import { MoreVertical, Trash } from 'lucide-react'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import AlertError from 'components/ui/AlertError'
import { useAccessTokenDeleteMutation } from 'data/access-tokens/access-tokens-delete-mutation'
import { AccessToken, useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
import { DATETIME_FORMAT } from 'lib/constants'
import {
Button,
Card,
CardContent,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
Expand All @@ -32,36 +32,35 @@ import {
const RowLoading = () => (
<TableRow>
<TableCell>
<Skeleton className="max-w-60 h-4 rounded-full" />
<Skeleton className="w-40 max-w-40 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="max-w-40 h-4 rounded-full" />
<Skeleton className="w-60 max-w-60 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="max-w-32 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="w-20 h-8 rounded-md" />
<Skeleton className="max-w-32 h-4 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="w-4 h-4 rounded-md" />
</TableCell>
</TableRow>
)

const tableHeaderClass = 'text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2'
const TableContainer = ({ children }: { children: React.ReactNode }) => (
<Card className="w-full overflow-hidden">
<CardContent className="p-0">
<Table className="p-5 table-auto">
<TableHeader>
<TableRow className="bg-200">
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Name
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Token
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Created
</TableHead>
<TableHead className="text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2" />
<TableHead className={tableHeaderClass}>Name</TableHead>
<TableHead className={tableHeaderClass}>Token</TableHead>
<TableHead className={tableHeaderClass}>Last used</TableHead>
<TableHead className={tableHeaderClass}>Expires</TableHead>
<TableHead className={cn(tableHeaderClass, '!text-right')} />
</TableRow>
</TableHeader>
<TableBody>{children}</TableBody>
Expand Down Expand Up @@ -159,16 +158,45 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo
<span className="font-mono text-foreground-light">{x.token_alias}</span>
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<p className="text-foreground-light">
{dayjs(x.created_at).format('D MMM YYYY')}
</p>
</TooltipTrigger>
<TooltipContent>
<p>Created on {dayjs(x.created_at).format(DATETIME_FORMAT)}</p>
</TooltipContent>
</Tooltip>
<p className="text-foreground-light">
{x.last_used_at ? (
<Tooltip>
<TooltipTrigger>{dayjs(x.last_used_at).format('DD MMM YYYY')}</TooltipTrigger>
<TooltipContent side="bottom">
Last used on {dayjs(x.last_used_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
) : (
'Never used'
)}
</p>
</TableCell>
<TableCell>
{x.expires_at ? (
dayjs(x.expires_at).isBefore(dayjs()) ? (
<Tooltip>
<TooltipTrigger>
<p className="text-foreground-light">Expired</p>
</TooltipTrigger>
<TooltipContent side="bottom">
Expired on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
) : (
<Tooltip>
<TooltipTrigger>
<p className="text-foreground-light">
{dayjs(x.expires_at).format('DD MMM YYYY')}
</p>
</TooltipTrigger>
<TooltipContent side="bottom">
Expires on {dayjs(x.expires_at).format('DD MMM, YYYY HH:mm:ss')}
</TooltipContent>
</Tooltip>
)
) : (
<p className="text-foreground-light">Never</p>
)}
</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-x-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import dayjs from 'dayjs'

export const NON_EXPIRING_TOKEN_VALUE = 'never'
export const CUSTOM_EXPIRY_VALUE = 'custom'

export const ExpiresAtOptions: Record<string, { value: string; label: string }> = {
hour: {
value: dayjs().add(1, 'hour').toISOString(),
label: '1 hour',
},
day: {
value: dayjs().add(1, 'days').toISOString(),
label: '1 day',
},
week: {
value: dayjs().add(7, 'days').toISOString(),
label: '7 days',
},
month: {
value: dayjs().add(30, 'days').toISOString(),
label: '30 days',
},
never: {
value: NON_EXPIRING_TOKEN_VALUE,
label: 'Never',
},
custom: {
value: CUSTOM_EXPIRY_VALUE,
label: 'Custom',
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ describe(`NewAccessTokenButton`, () => {
created_at: faker.date.past().toISOString(),
expires_at: null,
id: faker.number.int(),
last_used_at: null,
token_alias: faker.lorem.words(),
token: faker.lorem.words(),
last_used_at: faker.date.recent().toISOString(),
},
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { zodResolver } from '@hookform/resolvers/zod'
import dayjs from 'dayjs'
import { 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 { DatePicker } from 'components/ui/DatePicker'
import { useAccessTokenCreateMutation } from 'data/access-tokens/access-tokens-create-mutation'
import {
Button,
Expand All @@ -18,13 +21,29 @@ import {
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
WarningIcon,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
CUSTOM_EXPIRY_VALUE,
ExpiresAtOptions,
NON_EXPIRING_TOKEN_VALUE,
} from './AccessTokens.constants'

const formId = 'new-access-token-form'

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().optional()
),
})

export interface NewAccessTokenDialogProps {
Expand All @@ -40,16 +59,27 @@ export const NewAccessTokenDialog = ({
onOpenChange,
onCreateToken,
}: NewAccessTokenDialogProps) => {
const [customExpiryDate, setCustomExpiryDate] = useState<{ date: string } | undefined>(undefined)
const [isCustomExpiry, setIsCustomExpiry] = useState(false)

const form = useForm<z.infer<typeof TokenSchema>>({
resolver: zodResolver(TokenSchema),
defaultValues: { tokenName: '' },
defaultValues: { tokenName: '', expiresAt: ExpiresAtOptions['month'].value },
mode: 'onChange',
})
const { mutate: createAccessToken, isLoading } = useAccessTokenCreateMutation()

const onSubmit: SubmitHandler<z.infer<typeof TokenSchema>> = async (values) => {
// Use custom date if custom option is selected
let expiresAt = values.expiresAt

if (isCustomExpiry && customExpiryDate) {
// Use the date from the TokensDatePicker
expiresAt = customExpiryDate.date
}

createAccessToken(
{ name: values.tokenName, scope: tokenScope },
{ name: values.tokenName, scope: tokenScope, expires_at: expiresAt },
{
onSuccess: (data) => {
toast.success('Access token created successfully')
Expand All @@ -62,14 +92,40 @@ export const NewAccessTokenDialog = ({

const handleClose = () => {
form.reset({ tokenName: '' })
setCustomExpiryDate(undefined)
setIsCustomExpiry(false)
onOpenChange(false)
}

const handleExpiryChange = (value: string) => {
if (value === CUSTOM_EXPIRY_VALUE) {
setIsCustomExpiry(true)
// Set a default custom date (today at 23:59:59)
const defaultCustomDate = {
date: dayjs().endOf('day').toISOString(),
}
setCustomExpiryDate(defaultCustomDate)
form.setValue('expiresAt', value)
} else {
setIsCustomExpiry(false)
setCustomExpiryDate(undefined)
form.setValue('expiresAt', value)
}
}

const handleCustomDateChange = (value: { date: string }) => {
setCustomExpiryDate(value)
}

return (
<Dialog
open={open}
onOpenChange={(open) => {
if (!open) form.reset()
if (!open) {
form.reset()
setCustomExpiryDate(undefined)
setIsCustomExpiry(false)
}
onOpenChange(open)
}}
>
Expand Down Expand Up @@ -132,6 +188,55 @@ export const NewAccessTokenDialog = ({
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="expiresAt"
name="expiresAt"
control={form.control}
render={({ field }) => (
<FormItemLayout name="expiresAt" label="Expires in">
<div className="flex gap-2">
<FormControl_Shadcn_ className="flex-grow">
<Select_Shadcn_ value={field.value} onValueChange={handleExpiryChange}>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Expires at" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{Object.values(ExpiresAtOptions).map((option) => (
<SelectItem_Shadcn_ key={option.value} value={option.value}>
{option.label}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormControl_Shadcn_>
{isCustomExpiry && (
<DatePicker
selectsRange={false}
triggerButtonSize="small"
contentSide="top"
minDate={new Date()}
maxDate={dayjs().add(1, 'year').toDate()}
onChange={(date) => {
if (date.to) handleCustomDateChange({ date: date.to })
}}
>
{customExpiryDate
? `${dayjs(customExpiryDate.date).format('DD MMM, HH:mm')}`
: 'Select date'}
</DatePicker>
)}
</div>
{field.value === NON_EXPIRING_TOKEN_VALUE && (
<div className="w-full flex gap-x-2 items-center mt-3 mx-0.5">
<WarningIcon />
<span className="text-xs text-left text-foreground-lighter">
Make sure to keep your non-expiring token safe and secure.
</span>
</div>
)}
</FormItemLayout>
)}
/>
</form>
</Form_Shadcn_>
</DialogSection>
Expand All @@ -141,6 +246,8 @@ export const NewAccessTokenDialog = ({
disabled={isLoading}
onClick={() => {
form.reset()
setCustomExpiryDate(undefined)
setIsCustomExpiry(false)
onOpenChange(false)
}}
>
Expand Down
Loading
Loading