diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 6fa1482f7df..09291a4e6ea 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -652,7 +652,7 @@ Broadcast Changes allows you to trigger messages from your database. To achieve It uses partitioned tables per day which allows the deletion your previous images in a performant way by dropping the physical tables of this partitioned table. Tables older than 3 days old are deleted. -Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON packages. [Realtime Authorization]/docs/guides/realtime/authorization) is required and enabled by default to protect your data. +Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON packages. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data. The database broadcast feature provides two functions to help you send messages: diff --git a/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx b/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx index e82b691d082..a9e90325b41 100644 --- a/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx +++ b/apps/studio/components/interfaces/Account/NewAccessTokenButton.tsx @@ -4,7 +4,6 @@ import { useState } from 'react' import { useAccessTokenCreateMutation } from 'data/access-tokens/access-tokens-create-mutation' import { - Alert, Button, DialogFooter, DropdownMenu, @@ -15,6 +14,7 @@ import { Input, Modal, } from 'ui' +import { Admonition } from 'ui-patterns' export interface NewAccessTokenButtonProps { onCreateToken: (token: any) => void @@ -105,27 +105,30 @@ const NewAccessTokenButton = ({ onCreateToken }: NewAccessTokenButtonProps) => { <> {tokenScope === 'V0' && ( - -

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

-
- -
-
+ +

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

+
+ +
+ + } + />
)} diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index dd66b17e3be..0d35ca6f79a 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -15,7 +15,6 @@ import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { - Alert, Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -29,6 +28,7 @@ import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' import { Markdown } from 'components/interfaces/Markdown' +import { Admonition } from 'ui-patterns' export interface ProviderFormProps { config: components['schemas']['GoTrueConfigResponse'] @@ -264,9 +264,15 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => { ))} {provider?.misc?.alert && ( - - {provider.misc.alert.description} - + + {provider.misc.alert.description} + + } + /> )} {provider.misc.requiresRedirect && ( diff --git a/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx b/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx index 32b3beef3d4..59f7d064d3b 100644 --- a/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx +++ b/apps/studio/components/interfaces/Auth/Users/InviteUserModal.tsx @@ -44,52 +44,49 @@ const InviteUserModal = ({ visible, setVisible }: InviteUserModalProps) => { } return ( -
- +
- - {() => ( - <> - - } - type="email" - name="email" - placeholder="User email" - /> - + {() => ( + <> + + } + type="email" + name="email" + placeholder="User email" + /> + - - - - - )} -
-
-
+ + + + + )} + + ) } diff --git a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts index 97b20ae76c6..6d53666ea4d 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts +++ b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts @@ -1,6 +1,9 @@ import { BASE_PATH } from 'lib/constants' import { PROVIDER_PHONE, PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation' +// [Joshen] Temporary fix as bulk delete will fire n requests since Auth + API do not have a bulk delete endpoint yet +export const MAX_BULK_DELETE = 20 + export const PROVIDER_FILTER_OPTIONS = PROVIDERS_SCHEMAS.map((provider) => ({ name: provider.title, value: diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index d0d1633454d..0e101574dd7 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -1,12 +1,12 @@ import dayjs from 'dayjs' import { UserIcon } from 'lucide-react' import { UIEvent } from 'react' -import { Column } from 'react-data-grid' +import { Column, useRowSelection } from 'react-data-grid' import { User } from 'data/auth/users-infinite-query' import { BASE_PATH } from 'lib/constants' -import { cn } from 'ui' -import { HeaderCell } from './UsersGridComponents' +import { Checkbox_Shadcn_, cn } from 'ui' +import { HeaderCell, SelectHeaderCell } from './UsersGridComponents' import { ColumnConfiguration, USERS_TABLE_COLUMNS } from './UsersV2' const SUPPORTED_CSP_AVATAR_URLS = [ @@ -262,10 +262,18 @@ export const formatUserColumns = ({ minWidth: col.minWidth ?? 120, headerCellClass: 'z-50 outline-none !shadow-none', renderHeaderCell: () => { + // [Joshen] I'm on the fence to support "Select all" for users, as the results are infinitely paginated + // "Select all" wouldn't be an accurate representation if not all the pages have been fetched, but if decide + // to support - the component is ready as such: Just pass selectedUsers and allRowsSelected as props from parent + // if (col.id === 'img') return undefined return }, renderCell: ({ row }) => { + // This is actually a valid React component, so we can use hooks here + // eslint-disable-next-line react-hooks/rules-of-hooks + const [isRowSelected, onRowSelectionChange] = useRowSelection() + const value = row?.[col.id] const user = users?.find((u) => u.id === row.id) const formattedValue = @@ -278,7 +286,19 @@ export const formatUserColumns = ({ if (col.id === 'img') { return ( -
+
+ { + e.stopPropagation() + onRowSelectionChange({ + row, + type: 'ROW', + checked: !isRowSelected, + isShiftClick: e.shiftKey, + }) + }} + />
formatUserColumns for more info +export const SelectHeaderCell = ({ + selectedUsers, + allRowsSelected, +}: { + selectedUsers: Set + allRowsSelected: boolean +}) => { + const inputRef = useRef(null) + const [isRowSelected, onRowSelectionChange] = useRowSelection() + + const isIndeterminate = selectedUsers.size > 0 && !allRowsSelected + + useEffect(() => { + if (inputRef.current) inputRef.current.indeterminate = isIndeterminate + }, [isIndeterminate]) + + return ( +
+
+ onRowSelectionChange({ type: 'HEADER', checked: e.target.checked })} + /> +
+
+ ) +} + export const HeaderCell = ({ col, setSortByValue, diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index a2351632139..51dff52526f 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -1,7 +1,9 @@ +import { useQueryClient } from '@tanstack/react-query' import AwesomeDebouncePromise from 'awesome-debounce-promise' -import { ArrowDown, ArrowUp, Loader2, RefreshCw, Search, Users, X } from 'lucide-react' +import { ArrowDown, ArrowUp, Loader2, RefreshCw, Search, Trash, Users, X } from 'lucide-react' import { UIEvent, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' +import { toast } from 'sonner' import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' @@ -10,6 +12,8 @@ import AlertError from 'components/ui/AlertError' import APIDocsButton from 'components/ui/APIDocsButton' import { FilterPopover } from 'components/ui/FilterPopover' import { FormHeader } from 'components/ui/Forms/FormHeader' +import { authKeys } from 'data/auth/keys' +import { useUserDeleteMutation } from 'data/auth/user-delete-mutation' import { useUsersCountQuery } from 'data/auth/users-count-query' import { useUsersInfiniteQuery } from 'data/auth/users-infinite-query' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' @@ -36,13 +40,15 @@ import { SelectValue_Shadcn_, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import AddUserDropdown from './AddUserDropdown' import { UserPanel } from './UserPanel' -import { PROVIDER_FILTER_OPTIONS } from './Users.constants' +import { MAX_BULK_DELETE, PROVIDER_FILTER_OPTIONS } from './Users.constants' import { formatUserColumns, formatUsersData, isAtBottom } from './Users.utils' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' -type Filter = 'all' | 'verified' | 'unverified' | 'anonymous' +export type Filter = 'all' | 'verified' | 'unverified' | 'anonymous' export type UsersTableColumn = { id: string name: string @@ -52,7 +58,7 @@ export type UsersTableColumn = { } export type ColumnConfiguration = { id: string; width?: number } export const USERS_TABLE_COLUMNS: UsersTableColumn[] = [ - { id: 'img', name: '', minWidth: 65, width: 65, resizable: false }, + { id: 'img', name: '', minWidth: 95, width: 95, resizable: false }, { id: 'id', name: 'UID', width: 280 }, { id: 'name', name: 'Display name', minWidth: 0, width: 150 }, { id: 'email', name: 'Email', width: 300 }, @@ -66,6 +72,7 @@ export const USERS_TABLE_COLUMNS: UsersTableColumn[] = [ // [Joshen] Just naming it as V2 as its a rewrite of the old one, to make it easier for reviews // Can change it to remove V2 thereafter export const UsersV2 = () => { + const queryClient = useQueryClient() const { ref: projectRef } = useParams() const { project } = useProjectContext() const gridRef = useRef(null) @@ -75,10 +82,13 @@ export const UsersV2 = () => { const [search, setSearch] = useState('') const [filter, setFilter] = useState('all') const [filterKeywords, setFilterKeywords] = useState('') + const [selectedUsers, setSelectedUsers] = useState>(new Set([])) const [selectedColumns, setSelectedColumns] = useState([]) const [selectedProviders, setSelectedProviders] = useState([]) - const [selectedRow, setSelectedRow] = useState() + const [selectedUser, setSelectedUser] = useState() const [sortByValue, setSortByValue] = useState('created_at:desc') + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [isDeletingUsers, setIsDeletingUsers] = useState(false) const [ columnConfiguration, setColumnConfiguration, @@ -123,8 +133,12 @@ export const UsersV2 = () => { providers: selectedProviders, }) + const { mutateAsync: deleteUser } = useUserDeleteMutation() + const totalUsers = countData ?? 0 const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages]) + // [Joshen] Only relevant for when selecting one user only + const selectedUserToDelete = users.find((u) => u.id === [...selectedUsers][0]) const handleScroll = (event: UIEvent) => { if (isLoading || !isAtBottom(event)) return @@ -169,6 +183,33 @@ export const UsersV2 = () => { 500 ) + const handleDeleteUsers = async () => { + if (!projectRef) return console.error('Project ref is required') + const userIds = [...selectedUsers] + + setIsDeletingUsers(true) + try { + await Promise.all( + userIds.map((id) => deleteUser({ projectRef, userId: id, skipInvalidation: true })) + ) + // [Joshen] Skip invalidation within RQ to prevent multiple requests, then invalidate once at the end + await Promise.all([ + queryClient.invalidateQueries(authKeys.usersInfinite(projectRef)), + queryClient.invalidateQueries(authKeys.usersCount(projectRef)), + ]) + toast.success( + `Successfully deleted the selected ${selectedUsers.size} user${selectedUsers.size > 1 ? 's' : ''}` + ) + setShowDeleteModal(false) + setSelectedUsers(new Set([])) + + if (userIds.includes(selectedUser)) setSelectedUser(undefined) + } catch (error: any) { + toast.error(`Failed to delete selected users: ${error.message}`) + setIsDeletingUsers(false) + } + } + useEffect(() => { if ( !isRefetching && @@ -187,278 +228,346 @@ export const UsersV2 = () => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess, isRefetching, isSuccessStorage, isErrorStorage, errorStorage, users]) + }, [ + isSuccess, + isRefetching, + isSuccessStorage, + isErrorStorage, + errorStorage, + users, + selectedUsers, + ]) return ( -
- -
-
- } - placeholder="Search email, phone or UID" - value={search} - onChange={(e) => setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.code === 'Enter') { - setSearch(search.trim()) - setFilterKeywords(search.trim().toLocaleLowerCase()) - } - }} - actions={[ - search && ( + <> +
+ +
+ {selectedUsers.size > 0 ? ( +
+ + } + className="px-1.5" + onClick={() => setSelectedUsers(new Set([]))} + tooltip={{ content: { side: 'bottom', text: 'Cancel selection' } }} + /> +
+ ) : ( + <> +
+ } + placeholder="Search email, phone or UID" + value={search} + onChange={(e) => setSearch(e.target.value)} + onKeyDown={(e) => { + if (e.code === 'Enter') { + setSearch(search.trim()) + setFilterKeywords(search.trim().toLocaleLowerCase()) + } + }} + actions={[ + search && ( + + + + + + Sort by created at + + + Ascending + + + Descending + + + + + Sort by last sign in at + + + Ascending + + + Descending + + + + + Sort by email + + Ascending + + Descending + + + + + Sort by phone + + Ascending + + Descending + + + + + + +
+ +
+ {isNewAPIDocsEnabled && } - - - - - Sort by created at - - Ascending - - Descending - - - - - Sort by last sign in at - - - Ascending - - - Descending - - - - - Sort by email - - Ascending - Descending - - - - Sort by phone - - Ascending - Descending - - - - - + icon={} + type="default" + loading={isRefetching && !isFetchingNextPage} + onClick={() => refetch()} + > + Refresh + + +
+ + )}
+ + + +
+ { + const isSelected = row.id === selectedUser + return [ + `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, + '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', + '[&>.rdg-cell:first-child>div]:ml-4', + ].join(' ') + }} + rowKeyGetter={(row) => row.id} + selectedRows={selectedUsers} + onScroll={handleScroll} + onSelectedRowsChange={(rows) => { + if (rows.size > MAX_BULK_DELETE) { + toast(`Only up to ${MAX_BULK_DELETE} users can be selected at a time`) + } else setSelectedUsers(rows) + }} + onColumnResize={(idx, width) => saveColumnConfiguration('resize', { idx, width })} + onColumnsReorder={(source, target) => { + const sourceIdx = columns.findIndex((col) => col.key === source) + const targetIdx = columns.findIndex((col) => col.key === target) -
- {isNewAPIDocsEnabled && } - - -
-
- - - -
- { - const isSelected = idx === selectedRow - return [ - `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, - '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', - '[&>.rdg-cell:first-child>div]:ml-4', - ].join(' ') - }} - onScroll={handleScroll} - onColumnResize={(idx, width) => saveColumnConfiguration('resize', { idx, width })} - onColumnsReorder={(source, target) => { - const sourceIdx = columns.findIndex((col) => col.key === source) - const targetIdx = columns.findIndex((col) => col.key === target) + const updatedColumns = swapColumns(columns, sourceIdx, targetIdx) + setColumns(updatedColumns) - const updatedColumns = swapColumns(columns, sourceIdx, targetIdx) - setColumns(updatedColumns) - - saveColumnConfiguration('reorder', { columns: updatedColumns }) - }} - renderers={{ - renderRow(idx, props) { - return ( - { - if (typeof idx === 'number' && idx >= 0) { - setSelectedRow(idx) - gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) - } - }} - /> - ) - }, - noRowsFallback: isLoading ? ( -
- -
- ) : isError ? ( -
- -
- ) : ( -
- -
-

- {filter !== 'all' || filterKeywords.length > 0 - ? 'No users found' - : 'No users in your project'} -

-

- {filter !== 'all' || filterKeywords.length > 0 - ? 'There are currently no users based on the filters applied' - : 'There are currently no users who signed up to your project'} -

+ saveColumnConfiguration('reorder', { columns: updatedColumns }) + }} + renderers={{ + renderRow(id, props) { + return ( + { + const idx = users.indexOf(users.find((u) => u.id === id) ?? {}) + if (props.row.id) { + setSelectedUser(props.row.id) + gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) + } + }} + /> + ) + }, + noRowsFallback: isLoading ? ( +
+
-
- ), - }} + ) : isError ? ( +
+ +
+ ) : ( +
+ +
+

+ {filter !== 'all' || filterKeywords.length > 0 + ? 'No users found' + : 'No users in your project'} +

+

+ {filter !== 'all' || filterKeywords.length > 0 + ? 'There are currently no users based on the filters applied' + : 'There are currently no users who signed up to your project'} +

+
+
+ ), + }} + /> +
+ + {selectedUser !== undefined && ( + u.id === selectedUser)} + onClose={() => setSelectedUser(undefined)} /> -
-
- {selectedRow !== undefined && ( - setSelectedRow(undefined)} - /> - )} -
+ )} +
-
- {isLoading || isRefetching ? 'Loading users...' : `Total: ${totalUsers} users`} - {(isLoading || isRefetching || isFetchingNextPage) && ( - - Loading... - - )} +
+ {isLoading || isRefetching ? 'Loading users...' : `Total: ${totalUsers} users`} + {(isLoading || isRefetching || isFetchingNextPage) && ( + + Loading... + + )} +
-
+ + 1 ? 's' : ''}`} + loading={isDeletingUsers} + confirmLabel="Delete" + onCancel={() => setShowDeleteModal(false)} + onConfirm={() => handleDeleteUsers()} + alert={{ + title: `Deleting ${selectedUsers.size === 1 ? 'a user' : 'users'} is irreversible`, + description: `This will remove the selected ${selectedUsers.size === 1 ? '' : `${selectedUsers.size} `}user${selectedUsers.size > 1 ? 's' : ''} from the project and all associated data.`, + }} + > +

+ This is permanent! Are you sure you want to delete the{' '} + {selectedUsers.size === 1 ? '' : `selected ${selectedUsers.size} `}user + {selectedUsers.size > 1 ? 's' : ''} + {selectedUsers.size === 1 ? ( + + {' '} + {selectedUserToDelete?.email ?? selectedUserToDelete?.phone ?? 'this user'} + + ) : null} + ? +

+
+ ) } diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index 9a107bf7c3c..62951d6c126 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -268,7 +268,7 @@ const Connect = () => { /> Project region will stay the same: {project?.region || ''} -
  • - A project can only be restored to another project once.
    - - This is a temporary limitation. Please contact us if you need to restore a project - to multiple other projects. - -
    • What will be transferred?
    • diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx index 3119bada864..7a3285b3045 100644 --- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -7,9 +7,10 @@ import Table from 'components/to-be-cleaned/Table' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations-query' -import { Alert, Button, Input, SidePanel } from 'ui' +import { Button, Input, SidePanel } from 'ui' import MigrationsEmptyState from './MigrationsEmptyState' import { Search } from 'lucide-react' +import { Admonition } from 'ui-patterns' const Migrations = () => { const [search, setSearch] = useState('') @@ -39,26 +40,27 @@ const Migrations = () => {
      {isError && ( - - - Contact support - - , - ]} + description={ + <> +

      + Try refreshing your browser, but if the issue persists, please reach out to us via + support. +

      +

      Error: {(error as any)?.message ?? 'Unknown'}

      + + } > -

      - Try refreshing your browser, but if the issue persists, please reach out to us via - support. -

      -

      Error: {(error as any)?.message ?? 'Unknown'}

      -
      + + )} {isSuccess && (
      diff --git a/apps/studio/components/interfaces/Docs/Authentication.tsx b/apps/studio/components/interfaces/Docs/Authentication.tsx index 418e8f8025e..f6d24271122 100644 --- a/apps/studio/components/interfaces/Docs/Authentication.tsx +++ b/apps/studio/components/interfaces/Docs/Authentication.tsx @@ -48,7 +48,7 @@ const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => {

      Client API Keys

      -
      +

      Client keys allow "anonymous access" to your database, until the user has logged in. @@ -79,7 +79,7 @@ const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => {

      Service Keys

      -
      +

      Service keys have FULL access to your data, bypassing any security policies. Be VERY diff --git a/apps/studio/components/interfaces/Docs/Introduction.tsx b/apps/studio/components/interfaces/Docs/Introduction.tsx index e102f008de5..ae8dd1b4f9d 100644 --- a/apps/studio/components/interfaces/Docs/Introduction.tsx +++ b/apps/studio/components/interfaces/Docs/Introduction.tsx @@ -2,6 +2,7 @@ import { useParams } from 'common' import Snippets from 'components/interfaces/Docs/Snippets' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { InlineLink } from 'components/ui/InlineLink' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import CodeSnippet from './CodeSnippet' import PublicSchemaNotEnabledAlert from './PublicSchemaNotEnabledAlert' @@ -26,12 +27,26 @@ export default function Introduction({ selectedLang }: Props) { .includes('public') return ( -

      -
      - - - {!isPublicSchemaEnabled && } -
      -
      + <> +

      Connect to your project

      +
      +
      +

      + All projects have a RESTful endpoint that you can use with your project's API key to + query and manage your database. These can be obtained from the{' '} + API settings. +

      +

      + You can initialize a new Supabase client using the createClient() method. + The Supabase client is your entrypoint to the rest of the Supabase functionality and is + the easiest way to interact with everything we offer within the Supabase ecosystem. +

      +
      +
      + + {!isPublicSchemaEnabled && } +
      +
      + ) } diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 483a1fa9a84..aa42058a0a5 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -101,7 +101,7 @@ const EdgeFunctionDetails = () => { return ( <> -
      +
      {({ handleReset, values, initialValues, resetForm }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index 428d29b3372..269f0183ea0 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -36,7 +36,7 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) = { - router.push(`/project/${ref}/functions/${item.slug}/details`) + router.push(`/project/${ref}/functions/${item.slug}`) }} > diff --git a/apps/studio/components/interfaces/Functions/FunctionsNav.tsx b/apps/studio/components/interfaces/Functions/FunctionsNav.tsx index 47b6d4f734d..fe37ab1afca 100644 --- a/apps/studio/components/interfaces/Functions/FunctionsNav.tsx +++ b/apps/studio/components/interfaces/Functions/FunctionsNav.tsx @@ -11,17 +11,18 @@ const FunctionsNav = ({ item }: any) => { defaultActiveId="1" type="underlined" size="medium" - activeId={!activeRoute ? 'metrics' : activeRoute} + baseClassNames="!space-y-0" + activeId={!activeRoute ? 'overview' : activeRoute} onChange={(e: string) => { if (item?.slug) { - router.push(`/project/${ref}/functions/${item.slug}/${e === 'metrics' ? '' : e}`) + router.push(`/project/${ref}/functions/${item.slug}/${e === 'overview' ? '' : e}`) } }} > - - + + ) } diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx index 02539377fcd..a1c980eb923 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobCard.tsx @@ -1,4 +1,4 @@ -import { toString as CronToString } from 'cronstrue' +import dayjs from 'dayjs' import { Clock, History, Loader2, MoreVertical } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' @@ -7,21 +7,26 @@ import { useParams } from 'common' import { SQLCodeBlock } from 'components/interfaces/Auth/ThirdPartyAuthForm/SqlCodeBlock' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query' +import { useCronJobRunQuery } from 'data/database-cron-jobs/database-cron-jobs-run-query' import { useDatabaseCronJobToggleMutation } from 'data/database-cron-jobs/database-cron-jobs-toggle-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { TelemetryActions } from 'lib/constants/telemetry' import { + Badge, Button, + cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - Input, Label_Shadcn_, Switch, } from 'ui' +import { TimestampInfo } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { convertCronToString, getNextRun } from './CronJobs.utils' interface CronJobCardProps { job: CronJob @@ -35,17 +40,18 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard const [toggleConfirmationModalShown, showToggleConfirmationModal] = useState(false) + const { data } = useCronJobRunQuery({ + projectRef: ref, + connectionString: selectedProject?.connectionString, + jobId: job.jobid, + }) + const lastRun = data?.start_time ? dayjs(data.start_time).valueOf() : undefined + const nextRun = getNextRun(job.schedule, data?.start_time) + const schedule = convertCronToString(job.schedule) + const { mutate: sendEvent } = useSendEventMutation() const { mutate: toggleDatabaseCronJob, isLoading } = useDatabaseCronJobToggleMutation() - // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the - // original schedule when cronstrue throws - let schedule = job.schedule - try { - const scheduleString = CronToString(job.schedule) - schedule = scheduleString - } catch {} - return ( <>
      @@ -120,12 +126,56 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
      Schedule
      - + +
      +
      +
      + Last run +
      +
      + {lastRun ? ( + <> + + {data?.status && ( + + {data.status} + + )} + + ) : ( + 'Job has not been run yet' + )} +
      +
      +
      +
      + Next run +
      +
      + {nextRun ? ( + + ) : ( + 'Unable to parse next run for job' + )} +
      diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index 07210781d20..41fe71637d2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -1,4 +1,6 @@ +import parser from 'cron-parser' import { toString as CronToString } from 'cronstrue' +import dayjs from 'dayjs' import { CronJobType } from './CreateCronJobSheet' import { HTTPHeader } from './CronJobs.constants' @@ -190,3 +192,41 @@ export const formatScheduleString = (value: string) => { return '' } } + +export const convertCronToString = (schedule: string) => { + // pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the + // original schedule when cronstrue throws + try { + return CronToString(schedule) + } catch (error) { + return schedule + } +} + +export const getNextRun = (schedule: string, lastRun?: string) => { + // cron-parser can only deal with the traditional cron syntax but technically users can also + // use strings like "30 seconds" now, For the latter case, we try our best to parse the next run + // (can't guarantee as scope is quite big) + if (schedule.includes('*')) { + try { + const interval = parser.parseExpression(schedule) + return interval.next().getTime() + } catch (error) { + return undefined + } + } else { + // [Joshen] Only going to attempt to parse if the schedule is as simple as "n seconds", "n minutes", or "n days" + // Returned undefined otherwise - we can revisit this perhaps if we get feedback about this + const [value, unit] = schedule.split(' ') + if ( + ['seconds', 'minutes', 'days'].includes(unit) && + !Number.isNaN(Number(value)) && + lastRun !== undefined + ) { + const parsedLastRun = dayjs(lastRun).add(Number(value), unit as dayjs.ManipulateType) + return parsedLastRun.valueOf() + } else { + return undefined + } + } +} diff --git a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx index 5d427bcac86..4e65964934c 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Keys/EncryptionKeysManagement.tsx @@ -14,7 +14,8 @@ import { usePgSodiumKeyDeleteMutation } from 'data/pg-sodium-keys/pg-sodium-key- import { usePgSodiumKeysQuery } from 'data/pg-sodium-keys/pg-sodium-keys-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Key, Loader, Search, Trash, X } from 'lucide-react' -import { Alert, Button, Form, Input, Listbox, Modal, Separator } from 'ui' +import { Button, Form, Input, Listbox, Modal, Separator } from 'ui' +import { Admonition } from 'ui-patterns' const DEFAULT_KEY_NAME = 'No description provided' @@ -256,13 +257,12 @@ export const EncryptionKeysManagement = () => { header="Confirm to delete key" > - - Do ensure that the key is not currently in use to prevent any issues. - +

      The following key will be permanently removed and cannot be recovered.

      diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx index 62b27c9d2a2..e05ff07ce4b 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/DeletePaymentMethodModal.tsx @@ -1,9 +1,10 @@ import { useParams } from 'common' import { toast } from 'sonner' -import { Alert, Button, Modal } from 'ui' +import { Button, Modal } from 'ui' import { useOrganizationPaymentMethodDeleteMutation } from 'data/organizations/organization-payment-method-delete-mutation' import type { OrganizationPaymentMethod } from 'data/organizations/organization-payment-methods-query' +import { Admonition } from 'ui-patterns' export interface DeletePaymentMethodModalProps { selectedPaymentMethod?: OrganizationPaymentMethod @@ -55,9 +56,11 @@ const DeletePaymentMethodModal = ({ } > - -

      You can re-add the payment method any time.

      -
      +
      ) diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/DeleteAppModal.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/DeleteAppModal.tsx index 26c619e5633..e8472240b36 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/DeleteAppModal.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/DeleteAppModal.tsx @@ -4,7 +4,8 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useOAuthAppDeleteMutation } from 'data/oauth/oauth-app-delete-mutation' import type { OAuthApp } from 'data/oauth/oauth-apps-query' -import { Alert, Modal } from 'ui' +import { Modal } from 'ui' +import { Admonition } from 'ui-patterns' export interface DeleteAppModalProps { selectedApp?: OAuthApp @@ -37,10 +38,12 @@ const DeleteAppModal = ({ selectedApp, onClose }: DeleteAppModalProps) => { onConfirm={onConfirmDelete} > - - Deleting {selectedApp?.name} will invalidate any access tokens from this application that - were authorized by users. - +
        diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx index ab655d88edd..8913dabe0d7 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/RevokeAppModal.tsx @@ -1,10 +1,11 @@ import { useParams } from 'common' import { Lock } from 'lucide-react' import { toast } from 'sonner' -import { Alert, Modal } from 'ui' +import { Modal } from 'ui' import { useAuthorizedAppRevokeMutation } from 'data/oauth/authorized-app-revoke-mutation' import type { AuthorizedApp } from 'data/oauth/authorized-apps-query' +import { Admonition } from 'ui-patterns' export interface RevokeAppModalProps { selectedApp?: AuthorizedApp @@ -37,10 +38,12 @@ const RevokeAppModal = ({ selectedApp, onClose }: RevokeAppModalProps) => { onConfirm={onConfirmDelete} > - - {selectedApp?.name} application will no longer have access to your organization's settings - and projects. - +
          diff --git a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx index b270feccc1d..e73f464d5f7 100644 --- a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx +++ b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx @@ -26,7 +26,6 @@ import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-co import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { uuidv4 } from 'lib/helpers' import { - Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, @@ -45,6 +44,7 @@ import { JWT_SECRET_UPDATE_ERROR_MESSAGES, JWT_SECRET_UPDATE_PROGRESS_MESSAGES, } from './API.constants' +import { Admonition } from 'ui-patterns' const JWTSettings = () => { const { ref: projectRef } = useParams() @@ -192,12 +192,12 @@ const JWTSettings = () => { )}
      {isJwtSecretUpdateFailed ? ( - + Please try again. If the failures persist, please contact Supabase support with the following details:
      Change tracking ID: {data?.changeTrackingId}
      Error message: {jwtSecretUpdateErrorMessage} -
      + ) : null}
      diff --git a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx index 252cabdfd02..5958fdb3060 100644 --- a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx @@ -13,8 +13,9 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { formatCurrency } from 'lib/helpers' import { useAddonsPagePanel } from 'state/addons-page' -import { Alert, AlertDescription_Shadcn_, Alert_Shadcn_, Button, Radio, SidePanel, cn } from 'ui' -import { ExternalLink, AlertTriangle } from 'lucide-react' +import { Button, Radio, SidePanel, cn } from 'ui' +import { ExternalLink } from 'lucide-react' +import { Admonition } from 'ui-patterns' const IPv4SidePanel = () => { const { ref: projectRef } = useParams() @@ -187,10 +188,12 @@ const IPv4SidePanel = () => { {hasChanges && ( <> - - There might be some downtime when enabling the add-on since some DNS clients might - have cached the old DNS entry. Generally, this should be less than a minute. - + {selectedOption !== 'ipv4_none' && (

      By default, this is only applied to the Primary database for your project. If{' '} @@ -216,20 +219,14 @@ const IPv4SidePanel = () => { )} {isFreePlan && ( - - - View available plans - - - } - > - Upgrade your plan to enable a IPv4 address for your project - + +

      Upgrade your plan to enable a IPv4 address for your project

      + +
      )}
      diff --git a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/RemoveRestrictionModal.tsx b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/RemoveRestrictionModal.tsx index 46ad3225e8e..67bec6cbe47 100644 --- a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/RemoveRestrictionModal.tsx +++ b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/RemoveRestrictionModal.tsx @@ -1,9 +1,10 @@ import { useParams } from 'common' import { toast } from 'sonner' -import { Alert, Button, Modal } from 'ui' +import { Button, Modal } from 'ui' import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query' import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation' +import { Admonition } from 'ui-patterns' interface RemoveRestrictionModalProps { visible: boolean @@ -59,7 +60,6 @@ const RemoveRestrictionModal = ({ return ( {isRemovingOnlyRestriction && ( - - Removing all network restrictions will default to your database being accessible from - all IP addresses. - + )} diff --git a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx index f83f79c0f8d..91cc4ff5233 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogTable.tsx @@ -92,15 +92,10 @@ const LogTable = ({ const { profile } = useProfile() const { show: showContextMenu } = useContextMenu() + const downloadCsvRef = useRef(null) const [cellPosition, setCellPosition] = useState() - const [selectionOpen, setSelectionOpen] = useState(false) - - useEffect(() => { - if (selectedLog || isSelectedLogLoading) { - setSelectionOpen(true) - } - }, [selectedLog, isSelectedLogLoading]) + const [selectedRow, setSelectedRow] = useState(null) const canCreateLogQuery = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'log_sql', owner_id: profile?.id }, @@ -148,6 +143,7 @@ const LogTable = ({ }) let columns = DEFAULT_COLUMNS + if (!queryType) { columns } else { @@ -235,21 +231,10 @@ const LogTable = ({ } } - const copyResultsToClipboard = () => { - copyToClipboard(stringData, () => { - toast.success('Results copied to clipboard') - }) - } - - const downloadCsvRef = useRef(null) - function downloadCSV() { - downloadCsvRef.current?.click() - } - const LogsExplorerTableHeader = () => (
      - + { + downloadCsvRef.current?.click() + }} + className="space-x-2" + >
      Download CSV
      - + { + copyToClipboard(stringData, () => { + toast.success('Results copied to clipboard') + }) + }} + className="space-x-2" + >
      Copy to clipboard
      @@ -352,14 +349,22 @@ const LogTable = ({ else return } - const [selectedRow, setSelectedRow] = useState(null) - function onRowClick(row: LogData) { setSelectedRow(row) onSelectedLogChange?.(row) } + useEffect(() => { + if (selectedLog || isSelectedLogLoading) { + setSelectionOpen(true) + } + if (!isSelectedLogLoading && !selectedLog) { + setSelectedRow(null) + } + }, [selectedLog, isSelectedLogLoading]) + if (!data) return null + return (
      {!queryType && } @@ -369,7 +374,7 @@ const LogTable = ({ `m.project like '${value}%'`, }, + pg_cron_logs: { + ..._SQL_FILTER_COMMON, + }, } export enum LogsTableName { diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index ee03bbd0c93..8af8a9c74c3 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -39,6 +39,7 @@ interface LogsPreviewerProps { condensedLayout?: boolean tableName?: LogsTableName EmptyState?: React.ReactNode + filterPanelClassName?: string } export const LogsPreviewer = ({ projectRef, @@ -48,6 +49,7 @@ export const LogsPreviewer = ({ tableName, children, EmptyState, + filterPanelClassName, }: PropsWithChildren) => { const router = useRouter() const { s, ite, its, db } = useParams() @@ -150,6 +152,7 @@ export const LogsPreviewer = ({ } const handleSearch: LogSearchCallback = async (event, { query, to, from }) => { if (event === 'search-input-change') { + setSelectedLogId(null) setFilters((prev) => ({ ...prev, search_query: query })) router.push({ pathname: router.pathname, @@ -199,6 +202,7 @@ export const LogsPreviewer = ({ return (
      void filters: Filters onSelectedDatabaseChange: (id: string) => void + className?: string } /** @@ -61,6 +70,7 @@ const PreviewFilterPanel = ({ filters, table, onSelectedDatabaseChange, + className, }: PreviewFilterPanelProps) => { const router = useRouter() const { ref } = useParams() @@ -121,7 +131,13 @@ const PreviewFilterPanel = ({ const handleInputSearch = (query: string) => onSearch('search-input-change', { query }) return ( -
      +
      handleInputSearch(search)} - className="mx-2 text-foreground-light hover:text-foreground" - > - {'↲'} - - ) +
      + {hasEdits && ( + ↲} + type="text" + className="px-1 h-[20px]" + onClick={() => handleInputSearch(search)} + tooltip={{ content: { side: 'bottom', text: 'Search for events' } }} + /> + )} + + {search.length > 0 && ( + } + type="text" + className="p-[1px] h-[20px]" + onClick={() => handleInputSearch('')} + tooltip={{ content: { side: 'bottom', text: 'Clear search' } }} + /> + )} +
      } /> diff --git a/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx index 1b3a03d852d..bf5b9d1dc36 100644 --- a/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx +++ b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx @@ -89,12 +89,12 @@ const FunctionsLayout = ({ title, children }: PropsWithChildren {name && ( -
      +
      -
      {name}
      +

      {name}

      )}
      diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx index c45e703792a..9dfce64c2d1 100644 --- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx +++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -132,7 +132,7 @@ const DisplayApiSettings = ({ legacy }: { legacy?: boolean }) => { )} deleteUser(vars), { async onSuccess(data, variables, context) { - const { projectRef } = variables + const { projectRef, skipInvalidation = false } = variables - await Promise.all([ - queryClient.invalidateQueries(authKeys.usersInfinite(projectRef)), - queryClient.invalidateQueries(authKeys.usersCount(projectRef)), - ]) + if (!skipInvalidation) { + await Promise.all([ + queryClient.invalidateQueries(authKeys.usersInfinite(projectRef)), + queryClient.invalidateQueries(authKeys.usersCount(projectRef)), + ]) + } await onSuccess?.(data, variables, context) }, diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts new file mode 100644 index 00000000000..aaa92df92e2 --- /dev/null +++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-run-query.ts @@ -0,0 +1,53 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { executeSql } from 'data/sql/execute-sql-query' +import { ResponseError } from 'types' +import { CronJobRun } from './database-cron-jobs-runs-infinite-query' +import { databaseCronJobsKeys } from './keys' + +export type DatabaseCronJobRunVariables = { + projectRef?: string + connectionString?: string + jobId: number +} + +export async function getDatabaseCronJobRun({ + projectRef, + connectionString, + jobId, +}: DatabaseCronJobRunVariables) { + if (!projectRef) throw new Error('Project ref is required') + + const query = ` + SELECT * FROM cron.job_run_details + WHERE jobid = '${jobId}' + ORDER BY start_time DESC + LIMIT 1` + + const { result } = await executeSql({ + projectRef, + connectionString, + sql: query, + }) + + return result[0] +} + +export type DatabaseCronJobRunData = CronJobRun +export type DatabaseCronJobRunError = ResponseError + +export const useCronJobRunQuery = ( + { projectRef, connectionString, jobId }: DatabaseCronJobRunVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseCronJobsKeys.run(projectRef, jobId), + () => getDatabaseCronJobRun({ projectRef, connectionString, jobId }), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts index f132226a3d8..60f75b20a65 100644 --- a/apps/studio/data/database-cron-jobs/keys.ts +++ b/apps/studio/data/database-cron-jobs/keys.ts @@ -3,6 +3,13 @@ export const databaseCronJobsKeys = { delete: () => ['cron-jobs', 'delete'] as const, alter: () => ['cronjobs', 'alter'] as const, list: (projectRef: string | undefined) => ['projects', projectRef, 'cron-jobs'] as const, + run: (projectRef: string | undefined, jobId: number) => [ + 'projects', + projectRef, + 'cron-jobs', + jobId, + 'run', + ], runsInfinite: (projectRef: string | undefined, jobId: number, options?: object) => [ 'projects', projectRef, diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 1d9d736d17a..31e6a141987 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -461,13 +461,7 @@ const Wizard: NextPageWithLayout = () => { render={({ field }) => ( - { - field.onChange(event.target.value.replace(/\./g, '')) - }} - /> + )} diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index 053397213a9..7362374780a 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -151,7 +151,7 @@ const PageLayout: NextPageWithLayout = () => { return } return ( -
      +
      {CHART_INTERVALS.map((item, i) => { diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx index 29c2e48d83b..5ecb05564fd 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx @@ -19,9 +19,11 @@ export const LogPage: NextPageWithLayout = () => { return ( ) } diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx index c3d5282e6d9..e82144574d1 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx @@ -16,9 +16,11 @@ export const LogPage: NextPageWithLayout = () => { return ( ) } diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 9a819b83a6f..684fa07eda2 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -1,16 +1,5 @@ import { useParams } from 'common' -import { useState } from 'react' -import { - Button, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogSection, - DialogSectionSeparator, - DialogTrigger, - Modal, -} from 'ui' +import { Button, Dialog, DialogContent, DialogSection, DialogTrigger } from 'ui' import { EdgeFunctionsListItem, @@ -26,7 +15,6 @@ import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { ref } = useParams() - const [showTerminalInstructions, setShowTerminalInstructions] = useState(false) const { data: functions, @@ -55,9 +43,7 @@ const PageLayout: NextPageWithLayout = () => { } deployed`} - + diff --git a/apps/www/_blog/2024-12-20-how-to-hack-the-base.mdx b/apps/www/_blog/2024-12-19-how-to-hack-the-base.mdx similarity index 99% rename from apps/www/_blog/2024-12-20-how-to-hack-the-base.mdx rename to apps/www/_blog/2024-12-19-how-to-hack-the-base.mdx index 0a6e302d748..324cff5b9c5 100644 --- a/apps/www/_blog/2024-12-20-how-to-hack-the-base.mdx +++ b/apps/www/_blog/2024-12-19-how-to-hack-the-base.mdx @@ -10,7 +10,7 @@ categories: tags: - launch-week - security -date: '2024-12-20' +date: '2024-12-19' --- ## **How to hack-the-base?** diff --git a/apps/www/_blog/2024-12-20-ai-hackathon-at-y-combinator.mdx b/apps/www/_blog/2024-12-20-ai-hackathon-at-y-combinator.mdx new file mode 100644 index 00000000000..6f1f5721b5b --- /dev/null +++ b/apps/www/_blog/2024-12-20-ai-hackathon-at-y-combinator.mdx @@ -0,0 +1,224 @@ +--- +title: 'AI Hackathon at Y Combinator' +description: Announcing the winners of the Supabase AI Hackathon. +author: craig_cannon +image: 2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-og.png +thumb: 2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-og.png +categories: + - hackathon + - AI +date: '2024-12-20' +toc_depth: 2 +--- + +A few weeks ago we hosted an AI hackathon at [Y Combinator](https://www.ycombinator.com) in San Francisco. + +Over 150 people flew in from all around the world, including Greece, Australia, Turkey, New Zealand, and South Africa. Students from many of the top universities in the US traveled to attend as well. + +YC hackathon + +We kicked things off on Friday evening. Participants pitched their ideas, formed over 40 teams, and got to work. By the next afternoon there were 47 projects ready to demo. + +Because there were so many projects, we split the teams into three tracks and had a semi-final round of judging. Supabase team members (Ant Wilson, Greg Papas, Long Hoang, Paul Copplestone, and Wen Bo Xie) and YC founders (Amadeo Pellicce, Benjamin Swerdlow, Michael Rosenfield, Nikhara Nirghin, Rahul Asati, and Rohan Das) judged the semi-finals. + +YC hackathon + +YC hackathon + +YC hackathon + +Our semi-final judges selected 10 teams then we went next door to watch the presentations on stage. YC Partner [Jared Friedman](https://x.com/snowmaker), YC Visiting Partner [Eli Brown](https://x.com/iechoic), [Copple](https://x.com/kiwicopple), and [Ant](https://x.com/AntWilson) were there to judge the finals. + +YC hackathon + +After watching all the demos, the judges awarded the following teams: + +YC hackathon + +### **Overall Winner - Soshi** + +[A Twitter assistant](https://www.linkedin.com/posts/elijahmuraoka_everyones-been-asking-me-how-we-won-the-activity-7268717109218619392-Fegu/) that browses and generates replies based on your niche to help you grow your Twitter following. +_Elijah Muraoka, Michael Long, Soumil Rathi, Veer Doshi_ + +### **Most Entertaining - Dropout** + +Dropping out of college is a badge of honor for many founders. Use this app to find potential cofounders or hires that are still in college that you could convince to drop out. +_David Head, Kevin Fu, Jake Schwartz_ + +### **Most Technically Impressive - Explainstein** + +Upload your homework and have a video call with [your AI Tutor](https://www.linkedin.com/posts/sricharanguddanti_this-demo-won-us-most-technically-impressive-activity-7267335808121004032-xymA/). +_Finn Metz, Sricharan Guddanti, Sveinung Myhre_ + +### **Most Novel Use of Supabase - Suparova** + +[A Supabase-powered rover](https://www.linkedin.com/posts/kwkramer_team-suparova-at-the-supabase-ycombinator-activity-7266175490636296193-9ZzS/) that can speak, recognize faces, and, most importantly, dance! +_Kwindla Kramer, Simon Sturmer, Bassim Eledath_ + +--- + +We had a blast meeting everyone in the community and look forward to seeing you all at more events. + +And we'd like to thank everyone that attended, [Anthropic](https://www.anthropic.com) for providing Claude credits to all the teams, and Y Combinator for hosting us. See you next time! + +💚 + +--- + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon + +YC hackathon diff --git a/apps/www/_blog/2024-12-20-lw13-hackathon-winners.mdx b/apps/www/_blog/2024-12-20-lw13-hackathon-winners.mdx new file mode 100644 index 00000000000..a6376f25d29 --- /dev/null +++ b/apps/www/_blog/2024-12-20-lw13-hackathon-winners.mdx @@ -0,0 +1,123 @@ +--- +title: 'Supabase Launch Week 13 Hackathon Winners' +description: Announcing the winners of the Supabase Launch Week 13 Hackathon. +author: tyler_shukert +image: launch-week-13/hackathon-winners/lw13-hackathon.png +thumb: launch-week-13/hackathon-winners/lw13-hackathon.png +launchweek: '13' +categories: + - launch-week +tags: + - launch-week + - hackathon +date: '2024-12-20' +toc_depth: 2 +--- + +[Launch Week 13](https://supabase.com/launch-week) brought an incredible array of new Supabase features that got developers' creative engines revving. To harness this excitement, we challenged our amazing community with the Launch Week 13 Hackathon - and wow, did they deliver! + +The submissions were truly impressive, showcasing exceptional technical skill and creativity. Our team thoroughly enjoyed reviewing each project, making it challenging to select winners from such a strong pool of entries. You can browse through all the submissions [here](https://www.hackathon.dev/lw13). For this hackathon, we're excited to have collaborated with StackBlitz to offer special prizes for projects built with bolt.new! Now, let's meet the winners! + +## Best overall project + +### Winner + +[Brainrot GPT](https://github.com/tfrank11/brainrot-gpt-web) by [Timmy](https://twitter.com/timmyf111), and [Tiara](https://github.com/tjanuar) + +Brainrot GPT uses AI to turn PDFs into short form video summaries. Perfect if you have a sub-30 second attention span. + +![Brainrot GPT](/images/blog/launch-week-13/hackathon-winners/brainrot-gpt.png) + +### Runner Up + +[SupaSketch](https://github.com/swymbnsl/supasketch) by [Swayam bansal](https://twitter.com/swymbnsl), and [Anchal](https://twitter.com/AnchalGupta1117) + +SupaSketch: Sketch and compete with friends in an AI and Supabase powered multiplayer drawing challenge! + +![SupaSketch](/images/blog/launch-week-13/hackathon-winners/supasketch.png) + +## Best use of AI + +### Winner + +[Stravation](https://github.com/aaronByrne1/Stravation) by [Aaron Byrne](https://github.com/aaronbyrne1) + +AI-driven motivation, BLE heart rate tracking, & real-time connectivity to power your runs. + +![Stravation](/images/blog/launch-week-13/hackathon-winners/stravation.png) + +### Runner Up + +[Kontra](https://github.com/Tammilore/ai-contract-analyzer) by [Tami](https://twitter.com/blacqee_) + +AI-powered contract analysis tool that detects risky clauses and protects you from unfair legal agreements. + +![Kontra](/images/blog/launch-week-13/hackathon-winners/kontra.png) + +## Most fun / best easter egg + +### Winner + +[Gaze into the Abyss](https://github.com/laznic/gaze-into-the-abyss) by [Niklas Lepistö](https://twitter.com/laznic) + +An experimental project where participants join virtual rooms and track each other's eye movements and blinks using + +![Gaze into the Abyss](/images/blog/launch-week-13/hackathon-winners/gaze-into-the-abyss.png) + +### Runner Up + +[smart-style](https://github.com/JoseChipanaTica/smart-style) by [JoseChipana](https://twitter.com/josepaulct) + +This project enables users to generate outfits based on their own clothing + +![smart-style](/images/blog/launch-week-13/hackathon-winners/smart-style.png) + +## Most visually pleasing + +### Winner + +[Munchwise](https://github.com/asrvd/munchwise) by [Ashish](https://twitter.com/_asheeshh) + +Munchwise is an AI based calorie/meal tracking app, use natural language to track daily calorie & macro goals! + +![Munchwise](/images/blog/launch-week-13/hackathon-winners/munchwise.png) + +### Runner Up + +[Students.fyi - We help people make better decisions](https://github.com/imprakashraghu/students.fyi) by [Prakash](https://twitter.com/imprakashraghu) + +A consolidated view of an individual's career path where one could make decisions based on previous outcomes. + +![Students.fyi](/images/blog/launch-week-13/hackathon-winners/students-fyi.png) + +## Best project built with bolt.new + +### Winner + +[Screensaver](https://github.com/anishsrinivasan/screensaver) by [Anish Kumar Srinivasan](https://twitter.com/iamAnish), and [Zeeshan Rahman](https://github.com/zeeshan-ra) + +A beautiful ambient video screensaver app featuring high-quality visuals, built with bolt.new and powered by Supabase. + +![Screensaver](/images/blog/launch-week-13/hackathon-winners/screensaver.png) + +### Runner Up + +[Custom Sticker Designer](https://github.com/mmcspiritt/bolt-stickers) by [Matt McSpiritt](https://twitter.com/mmcspiritt) + +An easy-to-use sticker designer made with Bolt, Supabase, Stripe, and Twilio Segment. + +![Custom Sticker Designer](/images/blog/launch-week-13/hackathon-winners/custom-sticker-designer.png) + +## The Prizes + +![bolt.new hoodie](/images/blog/launch-week-13/hackathon/bolt-hoodie.jpeg) + +Each winner and runner-up will receive a Supabase swag kit, and the winner and the runner-up of the best bolt.new project will receive a bolt.new hoodie. + +## Getting Started Guides + +If you're inspired to build, check out some of the latest resources: + +- [Quick Start Guides](https://supabase.com/docs/guides/getting-started) +- [AI & Vectors Guide](https://supabase.com/docs/guides/ai) +- [Edge Functions Guide](https://supabase.com/docs/guides/functions) diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-1.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-1.jpg new file mode 100644 index 00000000000..4a80f2c9bbe Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-1.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-10.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-10.jpg new file mode 100644 index 00000000000..5a0de39bd36 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-10.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-11.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-11.jpg new file mode 100644 index 00000000000..ea487a2a5a5 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-11.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-12.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-12.jpg new file mode 100644 index 00000000000..811c66ec493 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-12.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-13.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-13.jpg new file mode 100644 index 00000000000..d4c17502279 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-13.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-14.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-14.jpg new file mode 100644 index 00000000000..0976da9b8f1 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-14.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-15.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-15.jpg new file mode 100644 index 00000000000..01bf5b96759 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-15.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-16.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-16.jpg new file mode 100644 index 00000000000..eb8607c6eb6 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-16.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-17.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-17.jpg new file mode 100644 index 00000000000..da39ded6df8 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-17.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-18.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-18.jpg new file mode 100644 index 00000000000..08b4fb58b26 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-18.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-19.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-19.jpg new file mode 100644 index 00000000000..80937a03a62 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-19.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-2.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-2.jpg new file mode 100644 index 00000000000..784e20666f5 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-2.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-20.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-20.jpg new file mode 100644 index 00000000000..3b3944edbdc Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-20.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-21.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-21.jpg new file mode 100644 index 00000000000..e25dac30709 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-21.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-22.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-22.jpg new file mode 100644 index 00000000000..a45478e850b Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-22.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-23.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-23.jpg new file mode 100644 index 00000000000..0c1dfb0b6da Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-23.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-24.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-24.jpg new file mode 100644 index 00000000000..56586b36246 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-24.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-25.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-25.jpg new file mode 100644 index 00000000000..83a93422fe4 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-25.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-26.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-26.jpg new file mode 100644 index 00000000000..cef2e17ec1c Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-26.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-27.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-27.jpg new file mode 100644 index 00000000000..05f428e72d5 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-27.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-28.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-28.jpg new file mode 100644 index 00000000000..f5a5252de93 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-28.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-29.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-29.jpg new file mode 100644 index 00000000000..0bb4e6c4f54 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-29.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-3.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-3.jpg new file mode 100644 index 00000000000..73dcd7242fa Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-3.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-30.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-30.jpg new file mode 100644 index 00000000000..2bff55eaea6 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-30.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-31.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-31.jpg new file mode 100644 index 00000000000..f5224ae2240 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-31.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-32.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-32.jpg new file mode 100644 index 00000000000..68d8e83ec12 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-32.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-33.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-33.jpg new file mode 100644 index 00000000000..57584297da2 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-33.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-34.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-34.jpg new file mode 100644 index 00000000000..2d89e066281 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-34.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-4.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-4.jpg new file mode 100644 index 00000000000..7ec3b76e75f Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-4.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-5.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-5.jpg new file mode 100644 index 00000000000..6ac7f258fc6 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-5.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-6.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-6.jpg new file mode 100644 index 00000000000..a1666822b1b Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-6.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-7.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-7.jpg new file mode 100644 index 00000000000..caab6f9d53d Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-7.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-8.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-8.jpg new file mode 100644 index 00000000000..d9411a9c57f Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-8.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-9.jpg b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-9.jpg new file mode 100644 index 00000000000..004099eed3e Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-9.jpg differ diff --git a/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-og.png b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-og.png new file mode 100644 index 00000000000..c614d92a0e3 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-20-ai-hackathon-at-y-combinator/yc-hackathon-og.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/brainrot-gpt.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/brainrot-gpt.png new file mode 100644 index 00000000000..084bd27e534 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/brainrot-gpt.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/custom-sticker-designer.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/custom-sticker-designer.png new file mode 100644 index 00000000000..f291e7797c9 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/custom-sticker-designer.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/gaze-into-the-abyss.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/gaze-into-the-abyss.png new file mode 100644 index 00000000000..9846ccaa506 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/gaze-into-the-abyss.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/kontra.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/kontra.png new file mode 100644 index 00000000000..01355d14735 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/kontra.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/lw13-hackathon.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/lw13-hackathon.png new file mode 100644 index 00000000000..5451aff966c Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/lw13-hackathon.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/munchwise.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/munchwise.png new file mode 100644 index 00000000000..4f49f24fca2 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/munchwise.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/screensaver.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/screensaver.png new file mode 100644 index 00000000000..79a249f3dca Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/screensaver.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/smart-style.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/smart-style.png new file mode 100644 index 00000000000..486054a26f5 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/smart-style.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/stravation.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/stravation.png new file mode 100644 index 00000000000..34812567491 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/stravation.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/students-fyi.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/students-fyi.png new file mode 100644 index 00000000000..fa9d1676662 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/students-fyi.png differ diff --git a/apps/www/public/images/blog/launch-week-13/hackathon-winners/supasketch.png b/apps/www/public/images/blog/launch-week-13/hackathon-winners/supasketch.png new file mode 100644 index 00000000000..0c6ea923ce5 Binary files /dev/null and b/apps/www/public/images/blog/launch-week-13/hackathon-winners/supasketch.png differ diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 9372a389ae1..0ad02776e2c 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -8,11 +8,32 @@ Fri, 20 Dec 2024 00:00:00 -0700 + https://supabase.com/blog/ai-hackathon-at-y-combinator + AI Hackathon at Y Combinator + https://supabase.com/blog/ai-hackathon-at-y-combinator + Announcing the winners of the Supabase AI Hackathon. + Fri, 20 Dec 2024 00:00:00 -0700 + + + https://supabase.com/blog/calendars-in-postgres-using-foreign-data-wrappers + Calendars in Postgres using Foreign Data Wrappers + https://supabase.com/blog/calendars-in-postgres-using-foreign-data-wrappers + Calendar data integration with Cal.com using Wasm foreign data wrapper on Supabase + Fri, 20 Dec 2024 00:00:00 -0700 + + + https://supabase.com/blog/lw13-hackathon-winners + Supabase Launch Week 13 Hackathon Winners + https://supabase.com/blog/lw13-hackathon-winners + Announcing the winners of the Supabase Launch Week 13 Hackathon. + Fri, 20 Dec 2024 00:00:00 -0700 + + https://supabase.com/blog/how-to-hack-the-base How to Hack the Base! https://supabase.com/blog/how-to-hack-the-base Played cool games, won cool prizes. - Fri, 20 Dec 2024 00:00:00 -0700 + Thu, 19 Dec 2024 00:00:00 -0700 https://supabase.com/blog/durable-workflows-in-postgres-dbos @@ -21,20 +42,6 @@ Technical deep dive into the new DBOS integration for Supabase Tue, 10 Dec 2024 00:00:00 -0700 - - https://supabase.com/blog/hack-the-base - Hack the Base! with Supabase - https://supabase.com/blog/hack-the-base - Play cool games, win cool prizes. - Fri, 06 Dec 2024 00:00:00 -0700 - - - https://supabase.com/blog/launch-week-13-top-10 - Top 10 Launches of Launch Week 13 - https://supabase.com/blog/launch-week-13-top-10 - Highlights from Launch Week 13 - Fri, 06 Dec 2024 00:00:00 -0700 - https://supabase.com/blog/database-build-v2 database.build v2: Bring-your-own-LLM @@ -49,6 +56,20 @@ Effortlessly Clone Data into a New Supabase Project Fri, 06 Dec 2024 00:00:00 -0700 + + https://supabase.com/blog/hack-the-base + Hack the Base! with Supabase + https://supabase.com/blog/hack-the-base + Play cool games, win cool prizes. + Fri, 06 Dec 2024 00:00:00 -0700 + + + https://supabase.com/blog/launch-week-13-top-10 + Top 10 Launches of Launch Week 13 + https://supabase.com/blog/launch-week-13-top-10 + Highlights from Launch Week 13 + Fri, 06 Dec 2024 00:00:00 -0700 + https://supabase.com/blog/supabase-queues Supabase Queues diff --git a/packages/ui/src/components/Alert/Alert.tsx b/packages/ui/src/components/Alert/Alert.tsx index c1ff4f2aea4..34f40e9d2f0 100644 --- a/packages/ui/src/components/Alert/Alert.tsx +++ b/packages/ui/src/components/Alert/Alert.tsx @@ -27,7 +27,7 @@ const icons: Record = { } /** - * @deprecated Use Alert_Shadcn_ + * @deprecated Use Alert_Shadcn_. For studio use Admonition */ export function Alert({ variant = 'neutral',