diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
index ac705165304..68dca08580a 100644
--- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
+++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx
@@ -1,10 +1,3 @@
-import dayjs from 'dayjs'
-import { AlertCircle, ChevronRight, ExternalLink, Info } from 'lucide-react'
-import { useTheme } from 'next-themes'
-import Image from 'next/image'
-import Link from 'next/link'
-import { useMemo } from 'react'
-
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import {
@@ -14,7 +7,6 @@ import {
import { NoticeBar } from 'components/interfaces/DiskManagement/ui/NoticeBar'
import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import {
ScaffoldContainer,
ScaffoldDivider,
@@ -31,16 +23,27 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import type { ProjectAddonVariantMeta } from 'data/subscriptions/types'
+import dayjs from 'dayjs'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { useIsOrioleDbInAws, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import {
+ useIsOrioleDbInAws,
+ useIsProjectActive,
+ useSelectedProjectQuery,
+} from 'hooks/misc/useSelectedProject'
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
import { BASE_PATH, DOCS_URL, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants'
import { getDatabaseMajorVersion, getSemanticVersion } from 'lib/helpers'
+import { AlertCircle, ChevronRight, ExternalLink, Info } from 'lucide-react'
+import { useTheme } from 'next-themes'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useMemo } from 'react'
import { useAddonsPagePanel } from 'state/addons-page'
-import { Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
+import { Alert, Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { ComputeBadge } from 'ui-patterns/ComputeBadge'
import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
import CustomDomainSidePanel from './CustomDomainSidePanel'
import IPv4SidePanel from './IPv4SidePanel'
import PITRSidePanel from './PITRSidePanel'
diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
index 47cd9883c2b..0656bb21417 100644
--- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
@@ -1,17 +1,15 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useEffect, useState } from 'react'
-import { toast } from 'sonner'
-
import { useParams } from 'common'
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { PasswordStrengthBar } from 'components/ui/PasswordStrengthBar'
import { useDatabasePasswordResetMutation } from 'data/database/database-password-reset-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { useIsProjectActive, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
import { passwordStrength, PasswordStrengthScore } from 'lib/password-strength'
import { generateStrongPassword } from 'lib/project'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
import { Button, Card, CardContent, Input, Modal } from 'ui'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx
index 010cd814c07..b689f02bd1d 100644
--- a/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx
+++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx
@@ -1,17 +1,19 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Pause } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'sonner'
-
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useSetProjectStatus } from 'data/projects/project-detail-query'
import { useProjectPauseMutation } from 'data/projects/project-pause-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import {
+ useIsAwsK8sCloudProvider,
+ useIsProjectActive,
+ useSelectedProjectQuery,
+} from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
+import { Pause } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { toast } from 'sonner'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
const PauseProjectButton = () => {
diff --git a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx
index b2a9580b975..ccd34795724 100644
--- a/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx
+++ b/apps/studio/components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx
@@ -1,26 +1,28 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { ChevronDown } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'sonner'
-
import { useFlag } from 'common'
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useSetProjectStatus } from 'data/projects/project-detail-query'
import { useProjectRestartMutation } from 'data/projects/project-restart-mutation'
import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import {
+ useIsAwsK8sCloudProvider,
+ useIsProjectActive,
+ useSelectedProjectQuery,
+} from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
+import { ChevronDown } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { toast } from 'sonner'
import {
Button,
+ cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
- cn,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx
index fd2855a3997..848faca105c 100644
--- a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx
@@ -1,16 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Plus } from 'lucide-react'
-import { useState } from 'react'
-import { useForm } from 'react-hook-form'
-import { z } from 'zod'
-
import { FormField } from '@ui/components/shadcn/ui/form'
import { useParams } from 'common'
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { useIsProjectActive } from 'hooks/misc/useSelectedProject'
+import { Plus } from 'lucide-react'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
import {
Button,
Dialog,
@@ -29,6 +27,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { z } from 'zod'
interface CreateCredentialModalProps {
visible: boolean
diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
index 4878a5878bd..afe09252182 100644
--- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
@@ -1,14 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AlertTitle } from '@ui/components/shadcn/ui/alert'
-import Link from 'next/link'
-import { useEffect, useState } from 'react'
-import { SubmitHandler, useForm } from 'react-hook-form'
-import { toast } from 'sonner'
-import * as z from 'zod'
-
import { useParams } from 'common'
-import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
@@ -17,19 +10,22 @@ import { useProjectStorageConfigQuery } from 'data/config/project-storage-config
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { useIsProjectActive, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
import {
- AlertDescription_Shadcn_,
Alert_Shadcn_,
+ AlertDescription_Shadcn_,
Button,
Card,
CardContent,
CardFooter,
+ Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
- Form_Shadcn_,
- Separator,
Switch,
Table,
TableBody,
@@ -52,6 +48,8 @@ import {
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import * as z from 'zod'
+
import { CreateCredentialModal } from './CreateCredentialModal'
import { RevokeCredentialModal } from './RevokeCredentialModal'
import { StorageCredItem } from './StorageCredItem'
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx
index 6dcd99ce3e0..0b2878cee77 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/AddRowOperationItem.tsx
@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
+import { Plus, Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { Plus, X } from 'lucide-react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
-import { Button } from 'ui'
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
+import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { AddRowPayload } from '@/state/table-editor-operation-queue.types'
+import { Card, CardContent, CardHeader } from 'ui'
interface AddRowOperationItemProps {
operationId: string
@@ -47,40 +48,52 @@ export const AddRowOperationItem = ({
}
return (
-
-
+
+
-
-
-
{fullTableName}
-
-
New row
+
+
+ {fullTableName}
+
+
+ New row
-
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
onClick={handleDelete}
- className="shrink-0"
- aria-label="Remove operation"
+ tooltip={{
+ content: {
+ side: 'bottom',
+ align: 'end',
+ text: 'Revert change',
+ },
+ }}
/>
-
+
-
+
{previewColumns.map(([key, value]) => (
-
-
{key}:
-
+
+ +
+ {key}:
+
{formatOperationItemValue(value)}
))}
{remainingCount > 0 && (
- +{remainingCount} more column(s)
+
+ +
+ +{remainingCount} more column(s)
+
)}
-
-
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
index 8e641460788..56069c3b2c6 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx
@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
-import { Trash2, X } from 'lucide-react'
-import { Button } from 'ui'
-
+import { Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTableEditorStateSnapshot } from 'state/table-editor'
-import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
+
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
+import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
+import { DeleteRowPayload } from '@/state/table-editor-operation-queue.types'
+import { Card, CardContent, CardHeader } from 'ui'
interface DeleteRowOperationItemProps {
operationId: string
@@ -45,32 +46,40 @@ export const DeleteRowOperationItem = ({
}
return (
-
-
+
+
-
-
-
{fullTableName}
-
-
Delete row
-
·
-
where {whereClause}
+
+
+ {fullTableName}
+
+
+ Delete row
+ ·
+ where {whereClause}
-
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
onClick={handleDelete}
- className="shrink-0"
- aria-label="Remove operation"
+ tooltip={{
+ content: {
+ side: 'bottom',
+ align: 'end',
+ text: 'Revert change',
+ },
+ }}
/>
-
+
-
-
+
+ Row will be deleted
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
index c9476493de6..4773099a270 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationItem.tsx
@@ -1,12 +1,13 @@
import { useQueryClient } from '@tanstack/react-query'
+import { Undo2 } from 'lucide-react'
import { tableRowKeys } from 'data/table-rows/keys'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { X } from 'lucide-react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { formatOperationItemValue } from './OperationQueueSidePanel.utils'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { EditCellContentPayload } from '@/state/table-editor-operation-queue.types'
+import { Card, CardContent, CardHeader } from 'ui'
interface OperationItemProps {
operationId: string
@@ -44,42 +45,50 @@ export const OperationItem = ({ operationId, tableId, content }: OperationItemPr
}
return (
-
-
+
+
-
{fullTableName}
-
-
{columnName}
-
•
-
where {whereClause}
+
+ {fullTableName}
+
+
+ {columnName}
+ ·
+ where {whereClause}
}
+ aria-label="Revert change"
+ className="px-1.5"
+ icon={
}
onClick={handleDelete}
- className="shrink-0 w-7"
- aria-label="Remove operation"
- tooltip={{ content: { side: 'bottom', text: 'Remove operation' } }}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ align: 'end',
+ text: 'Revert change',
+ },
+ }}
/>
-
+
-
-
-
-
-
+
+
+ -
+
{formattedOldValue}
-
-
+
-
+
+ +
+
{formattedNewValue}
-
-
+
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
index 0468be02806..1c3dc3b7559 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/OperationList.tsx
@@ -15,7 +15,11 @@ interface OperationListProps {
export const OperationList = ({ operations }: OperationListProps) => {
if (operations.length === 0) {
- return
No pending changes
+ return (
+
+ )
}
const addOperations = operations.filter(isAddRowOperation)
@@ -23,12 +27,13 @@ export const OperationList = ({ operations }: OperationListProps) => {
const editOperations = operations.filter(isEditCellContentOperation)
return (
-
+
{deleteOperations.length > 0 && (
-
- Rows to Delete ({deleteOperations.length})
+
+ {deleteOperations.length} row deletion{deleteOperations.length !== 1 ? 's' : ''}
+
{deleteOperations.map((op) => (
{
{addOperations.length > 0 && (
-
- Rows to Add ({addOperations.length})
+
+ {addOperations.length} row addition{addOperations.length !== 1 ? 's' : ''}
{addOperations.map((op) => (
@@ -62,9 +67,10 @@ export const OperationList = ({ operations }: OperationListProps) => {
{editOperations.length > 0 && (
-
- Cell Edits ({editOperations.length})
+
+ {editOperations.length} cell edit{editOperations.length !== 1 ? 's' : ''}
+
{editOperations.map((op) => (
event.preventDefault()} // Prevent focus on first focussable element since it is the revert changes ButtonTooltip
header={
Pending changes
-
+
{operations.length} operation{operations.length !== 1 ? 's' : ''}
@@ -41,25 +42,29 @@ export const OperationQueueSidePanel = ({ visible, closePanel }: OperationQueueS
Close
- {modKey}.
+ {modKey}.
-
- Cancel All
+
+ Revert{operations.length > 1 && ' all'}
- Save All
- {modKey}S
+ Save{operations.length > 1 && ' all'}
+ {modKey}S
}
>
-
+
diff --git a/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx b/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx
new file mode 100644
index 00000000000..43840b62216
--- /dev/null
+++ b/apps/studio/components/layouts/AccessTokens/AccessTokensLayout.tsx
@@ -0,0 +1,72 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { PropsWithChildren } from 'react'
+
+import { useFlag } from 'common'
+import { NavMenu, NavMenuItem } from 'ui'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageHeader,
+ PageHeaderDescription,
+ PageHeaderMeta,
+ PageHeaderNavigationTabs,
+ PageHeaderSummary,
+ PageHeaderTitle,
+} from 'ui-patterns/PageHeader'
+
+const AccessTokensLayout = ({ children }: PropsWithChildren) => {
+ const router = useRouter()
+ const scopedTokensEnabled = useFlag('scopedPAT')
+
+ const navigationItems = [
+ {
+ label: 'Classic Tokens',
+ href: `/account/tokens`,
+ id: 'classic-tokens',
+ },
+ ...(scopedTokensEnabled
+ ? [
+ {
+ label: 'Scoped Tokens',
+ href: `/account/tokens/scoped`,
+ id: 'scoped-tokens',
+ },
+ ]
+ : []),
+ ]
+
+ const title = 'Access Tokens'
+ const description = 'Create and manage access tokens for API authentication.'
+
+ return (
+ <>
+
+
+
+ {title}
+ {description}
+
+
+ {navigationItems.length > 0 && (
+
+
+ {navigationItems.map((item) => {
+ const isActive = router.asPath.split('?')[0] === item.href
+ return (
+
+ {item.label}
+
+ )
+ })}
+
+
+ )}
+
+
+ {children}
+
+ >
+ )
+}
+
+export default AccessTokensLayout
diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx
index 33b83ecf4fa..84640554271 100644
--- a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx
+++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx
@@ -71,7 +71,8 @@ const AccountLayout = ({ children, title }: PropsWithChildren {
- const pathname = usePathname()
- const links = [
- {
- isActive: pathname === `/account/me`,
- label: 'Preferences',
- href: `/account/me`,
- key: `/account/me`,
- },
- {
- isActive: pathname === `/account/tokens`,
- label: 'Access Tokens',
- href: `/account/tokens`,
- key: `/account/tokens`,
- },
-
- {
- isActive: pathname === `/account/security`,
- label: 'Security',
- href: `/account/security`,
- key: `/account/security`,
- },
- ]
-
- return (
-
-
-
- {links.map((link, i) => (
-
-
-
- {link.label}
-
-
-
- ))}
-
-
- {children}
-
- )
-}
diff --git a/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx b/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx
deleted file mode 100644
index 5f5830ef2e1..00000000000
--- a/apps/studio/components/layouts/AppLayout/IncidentBanner.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
-import { InlineLink } from 'components/ui/InlineLink'
-
-/**
- * Used to display ongoing incidents
- */
-export const IncidentBanner = () => {
- return (
-
- Follow the status page for
- updates
- >
- }
- />
- )
-}
diff --git a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx b/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx
deleted file mode 100644
index 4c5610b36b6..00000000000
--- a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { LOCAL_STORAGE_KEYS } from 'common'
-import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
-import { InlineLink } from 'components/ui/InlineLink'
-
-import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
-
-/**
- * Used to display ongoing maintenance
- */
-export function MaintenanceBanner() {
- const { data: allStatusPageEvents } = useIncidentStatusQuery()
- const { maintenanceEvents = [] } = allStatusPageEvents ?? {}
- const currentEventId = maintenanceEvents[0]?.id ?? ''
-
- const [dismissed, setDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(currentEventId),
- false
- )
-
- if (dismissed) return null
-
- return (
-
- Follow the status page for
- updates
- >
- }
- onDismiss={() => setDismissed(true)}
- />
- )
-}
diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx
new file mode 100644
index 00000000000..b5ffe723f8c
--- /dev/null
+++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx
@@ -0,0 +1,68 @@
+import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
+import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
+import { InlineLink } from 'components/ui/InlineLink'
+
+import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+
+const BANNER_DESCRIPTION = (
+ <>
+ Follow the status page for updates
+ >
+)
+
+/**
+ * Used to display ongoing incidents or maintenances
+ */
+export const StatusPageBanner = () => {
+ const { data: allStatusPageEvents } = useIncidentStatusQuery()
+ const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}
+
+ // Only show incident banner for incidents with real impact (not "none")
+ const highImpactIncident = incidents.find((incident) => incident.impact !== 'none')
+ const incidentEventId = highImpactIncident?.id ?? ''
+
+ const showIncidentBannerOverride =
+ useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true'
+
+ const ongoingMaintenance = maintenanceEvents.length > 0
+ const maintenanceEventId = maintenanceEvents[0]?.id ?? ''
+
+ const [dismissedIncident, setDismissedIncident] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED(incidentEventId),
+ false
+ )
+
+ const [dismissedMaintenance, setDismissedMaintenance] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(maintenanceEventId),
+ false
+ )
+
+ if (showIncidentBannerOverride || (highImpactIncident && !dismissedIncident)) {
+ return (
+ setDismissedIncident(true)
+ }
+ />
+ )
+ }
+
+ if (ongoingMaintenance && !dismissedMaintenance) {
+ return (
+ setDismissedMaintenance(true)}
+ />
+ )
+ }
+
+ return null
+}
diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx
index e78f9cefaad..895917be4f9 100644
--- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx
+++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx
@@ -119,16 +119,16 @@ const EdgeFunctionDetailsLayout = ({
label: 'Logs',
href: `/project/${ref}/functions/${functionSlug}/logs`,
},
- {
- label: 'Code',
- href: `/project/${ref}/functions/${functionSlug}/code`,
- },
]
: []),
{
label: 'Details',
href: `/project/${ref}/functions/${functionSlug}/details`,
},
+ {
+ label: 'Code',
+ href: `/project/${ref}/functions/${functionSlug}/code`,
+ },
]
: []
@@ -264,15 +264,15 @@ const EdgeFunctionDetailsLayout = ({
/>
)}
- {IS_PLATFORM && (
- <>
-
-
- }>
- Download
-
-
-
+
+
+ }>
+ Download
+
+
+
+ {IS_PLATFORM && (
+ <>
-
- }
- onClick={downloadFunction}
- >
- Download as ZIP
-
-
-
-
+ >
+ )}
+
+ }
+ onClick={downloadFunction}
+ >
+ Download as ZIP
+
+
+
+
+ {IS_PLATFORM && (
+ <>
{!!functionSlug && (
+
diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
index 6ad7eb1e82a..5914bd62821 100644
--- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
@@ -1,5 +1,3 @@
-import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react'
-
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils'
import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils'
@@ -9,6 +7,7 @@ import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
import type { Project } from 'data/projects/project-detail-query'
import { Auth, Database, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
+import { Blocks, FileText, Lightbulb, List, Settings, Telescope } from 'lucide-react'
export const generateToolRoutes = (ref?: string, project?: Project, features?: {}): Route[] => {
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
@@ -102,7 +101,7 @@ export const generateProductRoutes = (
key: 'functions',
label: 'Edge Functions',
icon: ,
- link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/functions`),
+ link: ref && `/project/${ref}/functions`,
},
]
: []),
diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
index b17237bc37e..1ac054e7c8e 100644
--- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
@@ -1,7 +1,4 @@
import { PropsWithChildren } from 'react'
-
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { PROJECT_STATUS } from 'lib/constants'
import { DatabaseSelectorStateContextProvider } from 'state/database-selector'
import { RoleImpersonationStateContextProvider } from 'state/role-impersonation-state'
import { StorageExplorerStateContextProvider } from 'state/storage-explorer'
@@ -30,8 +27,3 @@ export const ProjectContextProvider = ({
)
}
-
-export const useIsProjectActive = () => {
- const { data: project } = useSelectedProjectQuery()
- return project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
-}
diff --git a/apps/studio/components/ui/FileExplorerAndEditor/index.tsx b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
index ac399cda6ce..0dd24dd779f 100644
--- a/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
+++ b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
@@ -27,6 +27,7 @@ import {
isBinaryFile,
isZipFile,
} from './FileExplorerAndEditor.utils'
+import { IS_PLATFORM } from 'common'
interface FileExplorerAndEditorProps {
files: FileData[]
@@ -373,9 +374,11 @@ export const FileExplorerAndEditor = ({
Files
- } onClick={addNewFile}>
- Add File
-
+ {IS_PLATFORM && (
+ } onClick={addNewFile}>
+ Add File
+
+ )}
}
isEditing={isEditing}
onEditSubmit={(value) => {
- if (originalId !== null) {
+ if (IS_PLATFORM && originalId !== null) {
handleFileNameChange(originalId, value)
}
}}
@@ -424,7 +427,7 @@ export const FileExplorerAndEditor = ({
}
}}
onDoubleClick={() => {
- if (originalId !== null) {
+ if (IS_PLATFORM && originalId !== null) {
handleStartRename(originalId)
}
}}
@@ -445,36 +448,38 @@ export const FileExplorerAndEditor = ({
/>
- e.stopPropagation()}>
- {
- if (originalId !== null) handleStartRename(originalId)
- }}
- onFocusCapture={(e) => e.stopPropagation()}
- >
-
- Rename file
-
+ {IS_PLATFORM && (
+ e.stopPropagation()}>
+ {
+ if (originalId !== null) handleStartRename(originalId)
+ }}
+ onFocusCapture={(e) => e.stopPropagation()}
+ >
+
+ Rename file
+
- {files.length > 1 && (
- <>
-
- {
- if (originalId !== null) {
- handleFileDelete(originalId)
- }
- }}
- onFocusCapture={(e) => e.stopPropagation()}
- >
-
- Delete file
-
- >
- )}
-
+ {files.length > 1 && (
+ <>
+
+ {
+ if (originalId !== null) {
+ handleFileDelete(originalId)
+ }
+ }}
+ onFocusCapture={(e) => e.stopPropagation()}
+ >
+
+ Delete file
+
+ >
+ )}
+
+ )}
)
}}
@@ -509,6 +514,7 @@ export const FileExplorerAndEditor = ({
padding: { top: 20, bottom: 20 },
lineNumbersMinChars: 3,
fixedOverflowWidgets: true,
+ readOnly: !IS_PLATFORM,
}}
/>
)}
diff --git a/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx
index 32e302cc630..5b4bf6735d5 100644
--- a/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx
+++ b/apps/studio/components/ui/RequestUpgradeToBillingOwners.tsx
@@ -1,9 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'
-import { PropsWithChildren, useState } from 'react'
-import { SubmitHandler, useForm } from 'react-hook-form'
-import { toast } from 'sonner'
-import z from 'zod'
-
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
import {
@@ -13,6 +8,9 @@ import {
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTrack } from 'lib/telemetry/track'
+import { PropsWithChildren, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
import {
Badge,
Button,
@@ -34,6 +32,7 @@ import {
TooltipTrigger,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import z from 'zod'
const FormSchema = z.object({
note: z.string().optional(),
diff --git a/apps/studio/components/ui/UpgradePlanButton.tsx b/apps/studio/components/ui/UpgradePlanButton.tsx
index 7c9851d2b5e..186feac161e 100644
--- a/apps/studio/components/ui/UpgradePlanButton.tsx
+++ b/apps/studio/components/ui/UpgradePlanButton.tsx
@@ -1,13 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import Link from 'next/link'
-import { PropsWithChildren } from 'react'
-
import { useFlag, useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+import Link from 'next/link'
+import { PropsWithChildren } from 'react'
import { Button } from 'ui'
+
import { ButtonTooltip } from './ButtonTooltip'
import { RequestUpgradeToBillingOwners } from './RequestUpgradeToBillingOwners'
@@ -24,6 +24,7 @@ interface UpgradePlanButtonProps {
featureProposition?: string
disabled?: boolean
className?: string
+ slug?: string
}
/**
@@ -40,18 +41,21 @@ export const UpgradePlanButton = ({
disabled,
children,
className,
+ slug: slugParam,
}: PropsWithChildren) => {
const { ref } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const isFreePlan = organization?.plan?.id === 'free'
- const slug = organization?.slug ?? '_'
+ const slug = slugParam ?? organization?.slug ?? '_'
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const { billingAll } = useIsFeatureEnabled(['billing:all'])
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
- 'stripe.subscriptions'
+ 'stripe.subscriptions',
+ undefined,
+ { organizationSlug: slug }
)
const subject = `Enquiry to upgrade ${!!plan ? `to ${plan} ` : ''}plan for organization`
diff --git a/apps/studio/data/access-tokens/keys.ts b/apps/studio/data/access-tokens/keys.ts
index 8d5e3a8f3da..e000d4a01bc 100644
--- a/apps/studio/data/access-tokens/keys.ts
+++ b/apps/studio/data/access-tokens/keys.ts
@@ -1,3 +1,4 @@
export const accessTokenKeys = {
list: () => ['access-tokens'] as const,
+ detail: (id: string) => ['access-tokens', id] as const,
}
diff --git a/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts b/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts
deleted file mode 100644
index 89b40815049..00000000000
--- a/apps/studio/data/database-cron-jobs/database-cron-job-run-details-estimate-query.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { QueryClient, useQuery } from '@tanstack/react-query'
-
-import type { ConnectionVars } from 'data/common.types'
-import { executeSql } from 'data/sql/execute-sql-query'
-import {
- getLiveTupleEstimate,
- getLiveTupleEstimateKey,
-} from 'data/sql/queries/get-live-tuple-stats'
-import type { UseCustomQueryOptions } from 'types'
-
-type DatabaseCronJobRunDetailsEstimateVariables = ConnectionVars
-
-const cronJobRunDetailsEstimateSql = getLiveTupleEstimate('job_run_details', 'cron')
-const cronJobRunDetailsEstimateKey = (projectRef: string | undefined) =>
- getLiveTupleEstimateKey(projectRef, 'job_run_details', 'cron')
-
-export async function getCronJobRunDetailsEstimate({
- projectRef,
- connectionString,
-}: DatabaseCronJobRunDetailsEstimateVariables) {
- if (!projectRef) throw new Error('Project ref is required')
-
- const { result } = await executeSql<{ live_tuple_estimate: number }[]>({
- projectRef,
- connectionString,
- sql: cronJobRunDetailsEstimateSql,
- queryKey: cronJobRunDetailsEstimateKey(projectRef),
- })
-
- return result?.[0]?.live_tuple_estimate
-}
-
-export type DatabaseCronJobRunDetailsEstimateData = Awaited<
- ReturnType
->
-export type DatabaseCronJobRunDetailsEstimateError = Error
-
-export const useCronJobRunDetailsEstimateQuery = (
- { projectRef, connectionString }: DatabaseCronJobRunDetailsEstimateVariables,
- {
- enabled = true,
- ...options
- }: UseCustomQueryOptions<
- DatabaseCronJobRunDetailsEstimateData,
- DatabaseCronJobRunDetailsEstimateError,
- TData
- > = {}
-) =>
- useQuery({
- queryKey: cronJobRunDetailsEstimateKey(projectRef),
- queryFn: () => getCronJobRunDetailsEstimate({ projectRef, connectionString }),
- enabled: enabled && projectRef !== undefined,
- staleTime: 5 * 60 * 1000,
- ...options,
- })
-
-export function prefetchCronJobRunDetailsEstimate(
- client: QueryClient,
- { projectRef, connectionString }: DatabaseCronJobRunDetailsEstimateVariables
-) {
- return client.fetchQuery({
- // Does not change if connection string changes
- // eslint-disable-next-line @tanstack/query/exhaustive-deps
- queryKey: cronJobRunDetailsEstimateKey(projectRef),
- queryFn: () => getCronJobRunDetailsEstimate({ projectRef, connectionString }),
- })
-}
diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts
index fb8d737f4bf..84cd0ee9c33 100644
--- a/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts
+++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-infinite-query.ts
@@ -1,12 +1,13 @@
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
-
-import { executeSql } from 'data/sql/execute-sql-query'
+import { COST_THRESHOLD_ERROR, executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError, UseCustomInfiniteQueryOptions } from 'types'
+
+import { getCronJobsSql } from '../sql/queries/get-cron-jobs'
import { databaseCronJobsKeys } from './keys'
-const CRON_JOBS_PAGE_LIMIT = 20
+export const CRON_JOBS_PAGE_LIMIT = 20
-type DatabaseCronJobRunsVariables = {
+export type DatabaseCronJobRunsVariables = {
projectRef?: string
connectionString?: string | null
searchTerm?: string
@@ -17,50 +18,11 @@ export type CronJob = {
jobname: string | null
active: boolean
command: string
- latest_run: string
schedule: string
- status: string
+ latest_run?: string
+ status?: string
}
-// [Joshen] Just to call out that I had AI help me with this, so please let me know if this can be optimized
-const getCronJobSql = ({ searchTerm, page }: { searchTerm?: string; page: number }) =>
- `
-WITH latest_runs AS (
- SELECT
- jobid,
- status,
- MAX(start_time) AS latest_run
- FROM cron.job_run_details
- GROUP BY jobid, status
-), most_recent_runs AS (
- SELECT
- jobid,
- status,
- latest_run
- FROM latest_runs lr1
- WHERE latest_run = (
- SELECT MAX(latest_run)
- FROM latest_runs lr2
- WHERE lr2.jobid = lr1.jobid
- )
-)
-SELECT
- job.jobid,
- job.jobname,
- job.schedule,
- job.command,
- job.active,
- mr.latest_run,
- mr.status
-FROM
- cron.job job
-LEFT JOIN most_recent_runs mr ON job.jobid = mr.jobid
-${!!searchTerm ? `WHERE job.jobname ILIKE '%${searchTerm}%'` : ''}
-ORDER BY job.jobid
-LIMIT ${CRON_JOBS_PAGE_LIMIT}
-OFFSET ${page * CRON_JOBS_PAGE_LIMIT};
-`.trim()
-
export async function getDatabaseCronJobs({
projectRef,
connectionString,
@@ -72,8 +34,9 @@ export async function getDatabaseCronJobs({
const { result } = await executeSql({
projectRef,
connectionString,
- sql: getCronJobSql({ searchTerm, page }),
+ sql: getCronJobsSql({ searchTerm, page, limit: CRON_JOBS_PAGE_LIMIT }),
queryKey: ['cron-jobs'],
+ preflightCheck: true,
})
return result
@@ -114,5 +77,9 @@ export const useCronJobsInfiniteQuery = (
if (!hasNextPage) return undefined
return page
},
+ retry: (failureCount, error) => {
+ if (error.message === COST_THRESHOLD_ERROR) return false
+ return failureCount < 3
+ },
...options,
})
diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts
new file mode 100644
index 00000000000..26106ae581a
--- /dev/null
+++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts
@@ -0,0 +1,67 @@
+import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
+import { executeSql } from 'data/sql/execute-sql-query'
+import type { ResponseError, UseCustomInfiniteQueryOptions } from 'types'
+
+import { getCronJobsMinimalSql } from '../sql/queries/get-cron-jobs'
+import {
+ CRON_JOBS_PAGE_LIMIT,
+ CronJob,
+ DatabaseCronJobRunsVariables,
+} from './database-cron-jobs-infinite-query'
+import { databaseCronJobsKeys } from './keys'
+
+export async function getDatabaseCronJobsMinimal({
+ projectRef,
+ connectionString,
+ searchTerm,
+ page = 0,
+}: DatabaseCronJobRunsVariables & { page: number }) {
+ if (!projectRef) throw new Error('Project ref is required')
+
+ const { result } = await executeSql({
+ projectRef,
+ connectionString,
+ sql: getCronJobsMinimalSql({ searchTerm, page, limit: CRON_JOBS_PAGE_LIMIT }),
+ queryKey: ['cron-jobs-minimal'],
+ })
+
+ return result
+}
+
+type DatabaseCronJobsMinimalInfiniteData = CronJob[]
+type DatabaseCronJobsMinimalInfiniteError = ResponseError
+
+export const useCronJobsMinimalInfiniteQuery = (
+ { projectRef, connectionString, searchTerm }: DatabaseCronJobRunsVariables,
+ {
+ enabled = true,
+ ...options
+ }: UseCustomInfiniteQueryOptions<
+ DatabaseCronJobsMinimalInfiniteData,
+ DatabaseCronJobsMinimalInfiniteError,
+ InfiniteData,
+ readonly unknown[],
+ number
+ > = {}
+) =>
+ useInfiniteQuery({
+ queryKey: databaseCronJobsKeys.listInfiniteMinimal(projectRef, searchTerm),
+ queryFn: ({ pageParam }) => {
+ return getDatabaseCronJobsMinimal({
+ projectRef,
+ connectionString,
+ searchTerm,
+ page: pageParam,
+ })
+ },
+ staleTime: 0,
+ enabled: enabled && typeof projectRef !== 'undefined',
+ initialPageParam: 0,
+ getNextPageParam(lastPage, pages) {
+ const page = pages.length
+ const hasNextPage = lastPage.length >= CRON_JOBS_PAGE_LIMIT
+ if (!hasNextPage) return undefined
+ return page
+ },
+ ...options,
+ })
diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts
index 6a6505fb085..964542f2c35 100644
--- a/apps/studio/data/database-cron-jobs/keys.ts
+++ b/apps/studio/data/database-cron-jobs/keys.ts
@@ -6,6 +6,8 @@ export const databaseCronJobsKeys = {
['projects', projectRef, 'cron-jobs', identifier] as const,
listInfinite: (projectRef: string | undefined, searchTerm: string | undefined) =>
['projects', projectRef, 'cron-jobs', { searchTerm }] as const,
+ listInfiniteMinimal: (projectRef: string | undefined, searchTerm: string | undefined) =>
+ ['projects', projectRef, 'cron-jobs-mininal', { searchTerm }] as const,
count: (projectRef: string | undefined) =>
['projects', projectRef, 'cron-jobs', 'count'] as const,
run: (projectRef: string | undefined, jobId: number) => [
diff --git a/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts b/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts
new file mode 100644
index 00000000000..7923c247524
--- /dev/null
+++ b/apps/studio/data/database-cron-jobs/schedule-clean-up-mutation.ts
@@ -0,0 +1,66 @@
+import { useMutation } from '@tanstack/react-query'
+import { executeSql } from 'data/sql/execute-sql-query'
+import { toast } from 'sonner'
+import type { ResponseError, UseCustomMutationOptions } from 'types'
+
+import {
+ getScheduleDeleteCronJobRunDetailsKey,
+ getScheduleDeleteCronJobRunDetailsSql,
+} from '../sql/queries/delete-cron-job-run-details'
+
+export type ScheduleCronJobRunDetailsCleanupVariables = {
+ projectRef: string
+ connectionString?: string | null
+ interval: string
+}
+
+export async function scheduleCronJobRunDetailsCleanup({
+ projectRef,
+ connectionString,
+ interval,
+}: ScheduleCronJobRunDetailsCleanupVariables) {
+ const { result } = await executeSql({
+ projectRef,
+ connectionString,
+ sql: getScheduleDeleteCronJobRunDetailsSql(interval),
+ queryKey: getScheduleDeleteCronJobRunDetailsKey(projectRef, interval),
+ })
+
+ return result
+}
+
+type ScheduleCronJobRunDetailsCleanupData = Awaited<
+ ReturnType
+>
+
+export const useScheduleCronJobRunDetailsCleanupMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions<
+ ScheduleCronJobRunDetailsCleanupData,
+ ResponseError,
+ ScheduleCronJobRunDetailsCleanupVariables
+ >,
+ 'mutationFn'
+> = {}) => {
+ return useMutation<
+ ScheduleCronJobRunDetailsCleanupData,
+ ResponseError,
+ ScheduleCronJobRunDetailsCleanupVariables
+ >({
+ mutationFn: (vars) => scheduleCronJobRunDetailsCleanup(vars),
+ async onSuccess(data, variables, context) {
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to schedule clean up job: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/edge-functions/edge-function-body-query.ts b/apps/studio/data/edge-functions/edge-function-body-query.ts
index 772a91ca6f2..8ee38e9a930 100644
--- a/apps/studio/data/edge-functions/edge-function-body-query.ts
+++ b/apps/studio/data/edge-functions/edge-function-body-query.ts
@@ -72,7 +72,6 @@ export const useEdgeFunctionBodyQuery = (
useQuery({
queryKey: edgeFunctionsKeys.body(projectRef, slug),
queryFn: ({ signal }) => getEdgeFunctionBody({ projectRef, slug }, signal),
- enabled:
- IS_PLATFORM && enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined',
+ enabled: enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined',
...options,
})
diff --git a/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts b/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts
index d8ece7ba77c..daf36dd9e8b 100644
--- a/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts
+++ b/apps/studio/data/organizations/organization-credit-code-redemption-mutation.ts
@@ -60,10 +60,13 @@ export const useOrganizationCreditCodeRedemptionMutation = ({
return useMutation({
mutationFn: (vars) => redeemCode(vars),
async onSuccess(data, variables, context) {
- const { slug, code } = variables
+ const { slug } = variables
+
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) }),
+ queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) }),
+ ])
- await queryClient.invalidateQueries({ queryKey: organizationKeys.customerProfile(slug) })
- await queryClient.invalidateQueries({ queryKey: subscriptionKeys.orgSubscription(slug) })
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/platform/incident-status-query.ts b/apps/studio/data/platform/incident-status-query.ts
index a6db924e096..7c767fc1cb5 100644
--- a/apps/studio/data/platform/incident-status-query.ts
+++ b/apps/studio/data/platform/incident-status-query.ts
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import type { IncidentInfo } from 'lib/api/incident-status'
-import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
+import { BASE_PATH, IS_PLATFORM, IS_TEST_ENV } from 'lib/constants'
import { partition } from 'lodash'
import { UseCustomQueryOptions } from 'types'
@@ -42,5 +42,6 @@ export const useIncidentStatusQuery = (
queryFn: ({ signal }) => getIncidentStatus(signal),
staleTime: 1000 * 60 * 5, // 5 minutes to match API cache
...options,
- enabled: IS_PLATFORM && (options.enabled ?? true),
+ // Enable in platform mode, or in test environment for E2E testing
+ enabled: (IS_PLATFORM || IS_TEST_ENV) && (options.enabled ?? true),
})
diff --git a/apps/studio/data/scoped-access-tokens/keys.ts b/apps/studio/data/scoped-access-tokens/keys.ts
new file mode 100644
index 00000000000..f3dc146c2bf
--- /dev/null
+++ b/apps/studio/data/scoped-access-tokens/keys.ts
@@ -0,0 +1,4 @@
+export const scopedAccessTokenKeys = {
+ list: () => ['scoped-access-tokens'] as const,
+ detail: (id: string) => ['scoped-access-tokens', id] as const,
+}
diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts b/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts
new file mode 100644
index 00000000000..3c02d2a794a
--- /dev/null
+++ b/apps/studio/data/scoped-access-tokens/scoped-access-token-create-mutation.ts
@@ -0,0 +1,55 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { components } from 'api-types'
+import { handleError, post } from 'data/fetchers'
+import type { ResponseError, UseCustomMutationOptions } from 'types'
+import { scopedAccessTokenKeys } from './keys'
+
+export type ScopedAccessTokenCreateVariables = components['schemas']['CreateScopedAccessTokenBody']
+
+export async function createScopedAccessToken({
+ name,
+ expires_at,
+ organization_slugs,
+ permissions,
+ project_refs,
+}: ScopedAccessTokenCreateVariables) {
+ const { data, error } = await post('/platform/profile/scoped-access-tokens', {
+ body: { name, organization_slugs, expires_at, permissions, project_refs },
+ })
+
+ if (error) handleError(error)
+
+ return data
+}
+
+export type NewScopedAccessToken = components['schemas']['CreateScopedAccessTokenResponse']
+
+export const useAccessTokenCreateMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (vars) => createScopedAccessToken(vars),
+ async onSuccess(data, variables, context) {
+ await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.list() })
+
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to create access token: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts b/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts
new file mode 100644
index 00000000000..849b3812e93
--- /dev/null
+++ b/apps/studio/data/scoped-access-tokens/scoped-access-token-query.ts
@@ -0,0 +1,60 @@
+import { useQuery } from '@tanstack/react-query'
+
+import { components } from 'api-types'
+import { get, handleError } from 'data/fetchers'
+import type { ResponseError, UseCustomQueryOptions } from 'types'
+import { scopedAccessTokenKeys } from './keys'
+
+export async function getScopedAccessTokens(signal?: AbortSignal) {
+ const { data, error } = await get('/platform/profile/scoped-access-tokens', { signal })
+
+ if (error) handleError(error)
+
+ return data
+}
+
+export async function getScopedAccessToken({ id }: { id: string }, signal?: AbortSignal) {
+ if (!id) throw new Error('Token ID is required')
+
+ const { data, error } = await get('/platform/profile/scoped-access-tokens/{id}', {
+ params: { path: { id } },
+ signal,
+ })
+
+ if (error) handleError(error)
+
+ return data
+}
+
+export type ScopedAccessTokensData = Awaited>
+export type ScopedAccessTokensError = ResponseError
+
+export type ScopedAccessToken =
+ components['schemas']['GetScopedAccessTokensResponse']['tokens'][number]
+
+export type ScopedAccessTokenData = Awaited>
+
+export const useScopedAccessTokensQuery = ({
+ enabled = true,
+ ...options
+}: UseCustomQueryOptions = {}) =>
+ useQuery({
+ queryKey: scopedAccessTokenKeys.list(),
+ queryFn: ({ signal }) => getScopedAccessTokens(signal),
+ enabled,
+ ...options,
+ })
+
+export const useScopedAccessTokenQuery = (
+ { id }: { id: string },
+ {
+ enabled = true,
+ ...options
+ }: UseCustomQueryOptions = {}
+) =>
+ useQuery({
+ queryKey: scopedAccessTokenKeys.detail(id),
+ queryFn: ({ signal }) => getScopedAccessToken({ id }, signal),
+ enabled: enabled && typeof id !== 'undefined',
+ ...options,
+ })
diff --git a/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts b/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts
new file mode 100644
index 00000000000..701991ad40c
--- /dev/null
+++ b/apps/studio/data/scoped-access-tokens/scoped-access-tokens-delete-mutation.ts
@@ -0,0 +1,50 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { del, handleError } from 'data/fetchers'
+import type { ResponseError } from 'types'
+import { scopedAccessTokenKeys } from './keys'
+
+export type ScopedAccessTokenDeleteVariables = {
+ id: string
+}
+
+export async function deleteScopedAccessToken({ id }: ScopedAccessTokenDeleteVariables) {
+ const { data, error } = await del('/platform/profile/scoped-access-tokens/{id}', {
+ params: { path: { id } },
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+type ScopedAccessTokenDeleteData = Awaited>
+
+export const useScopedAccessTokenDeleteMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (vars) => deleteScopedAccessToken(vars),
+ async onSuccess(data, variables, context) {
+ await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.list() })
+ await queryClient.invalidateQueries({ queryKey: scopedAccessTokenKeys.detail(variables.id) })
+
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to delete access token: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/sql/queries/get-cron-jobs.ts b/apps/studio/data/sql/queries/get-cron-jobs.ts
new file mode 100644
index 00000000000..aa930819ca7
--- /dev/null
+++ b/apps/studio/data/sql/queries/get-cron-jobs.ts
@@ -0,0 +1,72 @@
+import { literal } from '@supabase/pg-meta/src/pg-format'
+
+// [Joshen] Just omits the LEFT JOIN as that's the heavy part
+export const getCronJobsMinimalSql = ({
+ searchTerm,
+ page,
+ limit,
+}: {
+ searchTerm?: string
+ page: number
+ limit: number
+}) =>
+ `
+SELECT
+ job.jobid,
+ job.jobname,
+ job.schedule,
+ job.command,
+ job.active
+FROM
+ cron.job job
+${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''}
+ORDER BY job.jobid
+LIMIT ${limit}
+OFFSET ${page * limit};
+`.trim()
+
+export const getCronJobsSql = ({
+ searchTerm,
+ page,
+ limit,
+}: {
+ searchTerm?: string
+ page: number
+ limit: number
+}) =>
+ `
+WITH latest_runs AS (
+ SELECT
+ jobid,
+ status,
+ MAX(start_time) AS latest_run
+ FROM cron.job_run_details
+ GROUP BY jobid, status
+), most_recent_runs AS (
+ SELECT
+ jobid,
+ status,
+ latest_run
+ FROM latest_runs lr1
+ WHERE latest_run = (
+ SELECT MAX(latest_run)
+ FROM latest_runs lr2
+ WHERE lr2.jobid = lr1.jobid
+ )
+)
+SELECT
+ job.jobid,
+ job.jobname,
+ job.schedule,
+ job.command,
+ job.active,
+ mr.latest_run,
+ mr.status
+FROM
+ cron.job job
+LEFT JOIN most_recent_runs mr ON job.jobid = mr.jobid
+${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''}
+ORDER BY job.jobid
+LIMIT ${limit}
+OFFSET ${page * limit};
+`.trim()
diff --git a/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts b/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts
deleted file mode 100644
index dc894ff1f92..00000000000
--- a/apps/studio/hooks/misc/useCronJobsEstimatePrefetch.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useQueryClient } from '@tanstack/react-query'
-import { useEffect } from 'react'
-
-import { prefetchCronJobRunDetailsEstimate } from 'data/database-cron-jobs/database-cron-job-run-details-estimate-query'
-import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { useStaticEffectEvent } from 'hooks/useStaticEffectEvent'
-
-/**
- * Prefetches the cron job run details estimate when on a Cron integration page.
- * This avoids a query waterfall when navigating to the Jobs tab, since the
- * Jobs tab gates loading cron jobs on this estimate query completing first.
- */
-export function useCronJobsEstimatePrefetch(integrationId: string | undefined) {
- const queryClient = useQueryClient()
- const { data: project } = useSelectedProjectQuery()
-
- const prefetch = useStaticEffectEvent(() => {
- prefetchCronJobRunDetailsEstimate(queryClient, {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- })
-
- useEffect(() => {
- if (integrationId === 'cron' && project?.ref) {
- prefetch()
- }
- }, [integrationId, project?.ref, prefetch])
-}
diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts
index 9829532f968..48e208b939b 100644
--- a/apps/studio/hooks/misc/useSelectedProject.ts
+++ b/apps/studio/hooks/misc/useSelectedProject.ts
@@ -1,6 +1,6 @@
import { useParams } from 'common'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
-import { PROVIDERS } from 'lib/constants'
+import { PROJECT_STATUS, PROVIDERS } from 'lib/constants'
export function useSelectedProjectQuery({ enabled = true } = {}) {
const { ref } = useParams()
@@ -52,3 +52,8 @@ export const useIsOrioleDbInAws = () => {
project?.dbVersion?.endsWith('orioledb') && project?.cloud_provider === PROVIDERS.AWS.id
return isOrioleDbInAws
}
+
+export const useIsProjectActive = () => {
+ const { data: project } = useSelectedProjectQuery()
+ return project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
+}
diff --git a/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts b/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts
index 3cd98f49239..f5b783866a8 100644
--- a/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts
+++ b/apps/studio/lib/api/self-hosted/functions/fileSystemStore.ts
@@ -3,7 +3,7 @@ import { readdir, stat } from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
-import { FunctionArtifact } from './types'
+import { FunctionArtifact, FunctionFileEntry } from './types'
export class FileSystemFunctionsArtifactStore {
constructor(private folderPath: string) {}
@@ -29,6 +29,34 @@ export class FileSystemFunctionsArtifactStore {
return parseFolderToFunctionArtifact(functionFolder)
}
+
+ async getFileEntriesBySlug(slug: string): Promise> {
+ if (slug === 'main') return []
+
+ const functionFolderPath = path.resolve(this.folderPath, slug)
+ if (!functionFolderPath.startsWith(path.resolve(this.folderPath) + path.sep)) return []
+
+ const entries = await readdir(functionFolderPath, {
+ recursive: true,
+ withFileTypes: true,
+ })
+
+ const fileEntries = await Promise.all(
+ entries
+ .filter((entry) => entry.isFile())
+ .map(async (entry) => {
+ const absolutePath = path.join(entry.parentPath, entry.name)
+ const fileStat = await stat(absolutePath)
+ return {
+ absolutePath,
+ relativePath: path.relative(functionFolderPath, absolutePath),
+ size: fileStat.size,
+ }
+ })
+ )
+
+ return fileEntries
+ }
}
async function parseFolderToFunctionArtifact(
diff --git a/apps/studio/lib/api/self-hosted/functions/types.ts b/apps/studio/lib/api/self-hosted/functions/types.ts
index 0a124c69669..31f22cc79bc 100644
--- a/apps/studio/lib/api/self-hosted/functions/types.ts
+++ b/apps/studio/lib/api/self-hosted/functions/types.ts
@@ -4,3 +4,11 @@ export type FunctionArtifact = {
created_at: number
updated_at: number
}
+
+export type FunctionFileEntry = {
+ /** Absolute path on disk */
+ absolutePath: string
+ /** Path relative to the function folder, used as the multipart filename */
+ relativePath: string
+ size: number
+}
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 68d43465df2..beec72b34b0 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -63,7 +63,7 @@
"@supabase/mcp-utils": "^0.3.2",
"@supabase/pg-meta": "workspace:*",
"@supabase/realtime-js": "catalog:",
- "@supabase/shared-types": "0.1.83",
+ "@supabase/shared-types": "0.1.84",
"@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "catalog:",
"@tanstack/react-query": "^5.0.0",
@@ -84,6 +84,7 @@
"cron-parser": "^4.9.0",
"cronstrue": "^2.50.0",
"crypto-js": "^4.2.0",
+ "dev-tools": "workspace:*",
"d3-geo": "^3.1.1",
"dayjs": "^1.11.10",
"dnd-core": "^16.0.1",
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index 3156d1c20c0..5875b41e480 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -23,22 +23,11 @@ import { loader } from '@monaco-editor/react'
import * as Sentry from '@sentry/nextjs'
import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
-import dayjs from 'dayjs'
-import customParseFormat from 'dayjs/plugin/customParseFormat'
-import duration from 'dayjs/plugin/duration'
-import relativeTime from 'dayjs/plugin/relativeTime'
-import timezone from 'dayjs/plugin/timezone'
-import utc from 'dayjs/plugin/utc'
-import Head from 'next/head'
-import { NuqsAdapter } from 'nuqs/adapters/next/pages'
-import { type ComponentProps, ErrorInfo, useCallback } from 'react'
-import { ErrorBoundary } from 'react-error-boundary'
-
import {
FeatureFlagProvider,
- getFlags,
TelemetryTagManager,
ThemeProvider,
+ getFlags,
useThemeSandbox,
} from 'common'
import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router'
@@ -49,8 +38,15 @@ import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/Featur
import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider'
import { RouteValidationWrapper } from 'components/interfaces/App/RouteValidationWrapper'
import { MainScrollContainerProvider } from 'components/layouts/MainScrollContainerContext'
+import { DevToolbar, DevToolbarProvider } from 'dev-tools'
import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErrorBoundaryState'
import { useRootQueryClient } from 'data/query-client'
+import dayjs from 'dayjs'
+import customParseFormat from 'dayjs/plugin/customParseFormat'
+import duration from 'dayjs/plugin/duration'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import timezone from 'dayjs/plugin/timezone'
+import utc from 'dayjs/plugin/utc'
import { customFont, sourceCodePro } from 'fonts'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -58,6 +54,10 @@ import { AuthProvider } from 'lib/auth'
import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants'
import { ProfileProvider } from 'lib/profile'
import { Telemetry } from 'lib/telemetry'
+import Head from 'next/head'
+import { NuqsAdapter } from 'nuqs/adapters/next/pages'
+import { type ComponentProps, ErrorInfo, useCallback } from 'react'
+import { ErrorBoundary } from 'react-error-boundary'
import { AiAssistantStateContextProvider } from 'state/ai-assistant-state'
import type { AppPropsWithLayout } from 'types'
import { SonnerToaster, TooltipProvider } from 'ui'
@@ -173,19 +173,22 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
enableSystem
disableTransitionOnChange
>
-
-
-
-
- {getLayout( )}
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {getLayout( )}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/studio/pages/account/tokens.tsx b/apps/studio/pages/account/tokens.tsx
index 33ed3334fab..f875a7b55a4 100644
--- a/apps/studio/pages/account/tokens.tsx
+++ b/apps/studio/pages/account/tokens.tsx
@@ -1,9 +1,9 @@
import { ExternalLink, Search } from 'lucide-react'
import { useState } from 'react'
-
import { AccessTokenList } from 'components/interfaces/Account/AccessTokens/AccessTokenList'
-import { NewAccessTokenButton } from 'components/interfaces/Account/AccessTokens/NewAccessTokenButton'
-import { NewTokenBanner } from 'components/interfaces/Account/AccessTokens/NewTokenBanner'
+import { NewTokenButton } from 'components/interfaces/Account/AccessTokens/Classic/NewTokenButton'
+import { AccessTokenNewBanner } from '@/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner'
+import AccessTokensLayout from 'components/layouts/AccessTokens/AccessTokensLayout'
import AccountLayout from 'components/layouts/AccountLayout/AccountLayout'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
@@ -13,68 +13,54 @@ import { DOCS_URL } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Button } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
-import { PageContainer } from 'ui-patterns/PageContainer'
-import {
- PageHeader,
- PageHeaderDescription,
- PageHeaderMeta,
- PageHeaderSummary,
- PageHeaderTitle,
-} from 'ui-patterns/PageHeader'
const UserAccessTokens: NextPageWithLayout = () => {
const [newToken, setNewToken] = useState()
const [searchString, setSearchString] = useState('')
return (
- <>
-
-
-
- Access Tokens
-
- Create and manage personal access tokens for API authentication.
-
-
-
-
-
-
- {newToken &&
setNewToken(undefined)} />}
-
-
}
- value={searchString}
- onChange={(e: any) => setSearchString(e.target.value)}
- name="search"
- id="search"
- placeholder="Filter tokens"
- />
-
-
- {
- if (id === newToken?.id) setNewToken(undefined)
- }}
+
+
+ {newToken && (
+
setNewToken(undefined)}
+ getTokenValue={(token) => token.token}
/>
+ )}
+
-
- >
+ {
+ if (id === newToken?.id) setNewToken(undefined)
+ }}
+ />
+
+
)
}
diff --git a/apps/studio/pages/account/tokens/scoped.tsx b/apps/studio/pages/account/tokens/scoped.tsx
new file mode 100644
index 00000000000..a2a259fa9a1
--- /dev/null
+++ b/apps/studio/pages/account/tokens/scoped.tsx
@@ -0,0 +1,88 @@
+import { ExternalLink, Search } from 'lucide-react'
+import { useState } from 'react'
+
+import AccessTokensLayout from 'components/layouts/AccessTokens/AccessTokensLayout'
+import { NewScopedTokenButton } from 'components/interfaces/Account/AccessTokens/Scoped/NewScopedTokenButton'
+import { AccessTokenNewBanner } from '@/components/interfaces/Account/AccessTokens/AccessTokenNewBanner/AccessTokenNewBanner'
+
+import { ScopedTokenList } from 'components/interfaces/Account/AccessTokens/Scoped/ScopedTokenList'
+import AccountLayout from 'components/layouts/AccountLayout/AccountLayout'
+import AppLayout from 'components/layouts/AppLayout/AppLayout'
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import OrganizationLayout from 'components/layouts/OrganizationLayout'
+import { NewScopedAccessToken } from 'data/scoped-access-tokens/scoped-access-token-create-mutation'
+import type { NextPageWithLayout } from 'types'
+import { Button } from 'ui'
+import { Input } from 'ui-patterns/DataInputs/Input'
+
+const ScopedTokens: NextPageWithLayout = () => {
+ const [searchString, setSearchString] = useState('')
+ const [newToken, setNewToken] = useState()
+
+ return (
+
+
+ {newToken && (
+
setNewToken(undefined)}
+ getTokenValue={(token) => token.token}
+ getTokenPermissions={(token) => token.permissions}
+ />
+ )}
+
+
}
+ value={searchString}
+ onChange={(e: any) => setSearchString(e.target.value)}
+ name="search"
+ id="search"
+ placeholder="Filter by name"
+ />
+
+
+
+ {
+ if (id === newToken?.id) setNewToken(undefined)
+ }}
+ />
+
+
+ )
+}
+
+ScopedTokens.getLayout = (page) => (
+
+
+
+ {page}
+
+
+
+)
+
+export default ScopedTokens
diff --git a/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts b/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts
new file mode 100644
index 00000000000..0ffb91ed316
--- /dev/null
+++ b/apps/studio/pages/api/v1/projects/[ref]/functions/[slug]/body.ts
@@ -0,0 +1,81 @@
+import { createReadStream } from 'node:fs'
+import { pipeline } from 'node:stream/promises'
+import { type NextApiRequest, type NextApiResponse } from 'next'
+
+import apiWrapper from '@/lib/api/apiWrapper'
+import { getFunctionsArtifactStore } from '@/lib/api/self-hosted/functions'
+import { uuidv4 } from '@/lib/helpers'
+
+export default function handlerWithErrorCatching(req: NextApiRequest, res: NextApiResponse) {
+ return apiWrapper(req, res, handler, { withAuth: true })
+}
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'GET':
+ return handleGet(req, res)
+ default:
+ res.setHeader('Allow', ['GET'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+async function handleGet(req: NextApiRequest, res: NextApiResponse) {
+ const slugParam = req.query.slug
+ const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam
+ if (!slug) {
+ res.status(404).json({ error: { message: `Missing function 'slug' parameter` } })
+ return
+ }
+
+ const store = getFunctionsArtifactStore()
+ const fileEntries = await store.getFileEntriesBySlug(slug)
+
+ const boundary = `----FormBoundary${uuidv4().replace(/-/g, '')}`
+ const totalSize = fileEntries.reduce((sum, entry) => sum + entry.size, 0)
+
+ const metadata = {
+ // mock id, should be "__"
+ deployment_id: uuidv4(),
+ original_size: totalSize,
+ compressed_size: totalSize,
+ module_count: fileEntries.length,
+ }
+
+ res.setHeader('Content-Type', `multipart/form-data; boundary=${boundary}`)
+ res.status(200)
+
+ // Write metadata part
+ const metadataJson = JSON.stringify(metadata)
+ res.write(
+ `--${boundary}\r\n` +
+ `Content-Disposition: form-data; name="metadata"\r\n` +
+ `Content-Type: application/json\r\n` +
+ `\r\n` +
+ metadataJson +
+ `\r\n`
+ )
+
+ // Stream each file part
+ for (const entry of fileEntries) {
+ const safeName = entry.relativePath
+ .replace(/[\r\n]/g, '')
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ const encodedName = encodeURIComponent(entry.relativePath)
+ res.write(
+ `--${boundary}\r\n` +
+ `Content-Disposition: form-data; name="file"; filename="${safeName}"; filename*=UTF-8''${encodedName}\r\n` +
+ `Content-Type: text/plain\r\n` +
+ `\r\n`
+ )
+ await pipeline(createReadStream(entry.absolutePath), res, { end: false })
+ res.write(`\r\n`)
+ }
+
+ // Write closing boundary
+ res.write(`--${boundary}--\r\n`)
+ res.end()
+}
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx
index d0026fc5c0c..76cc03bb879 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/code.tsx
@@ -7,7 +7,7 @@ import { toast } from 'sonner'
import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils'
import { FileData } from '@/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.types'
import { useLatest } from '@/hooks/misc/useLatest'
-import { useParams } from 'common'
+import { IS_PLATFORM, useParams } from 'common'
import { DeployEdgeFunctionWarningModal } from 'components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal'
import { DefaultLayout } from 'components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
@@ -214,33 +214,35 @@ const CodePage = () => {
orgSlug: org?.slug,
}}
/>
-
-
- ) : (
-
-
-
- )
- }
- tooltip={{
- content: {
- side: 'top',
- text: !canDeployFunction
- ? 'You need additional permissions to update edge functions'
- : undefined,
- },
- }}
- >
- Deploy updates
-
-
+ {IS_PLATFORM && (
+
+
+ ) : (
+
+
+
+ )
+ }
+ tooltip={{
+ content: {
+ side: 'top',
+ text: !canDeployFunction
+ ? 'You need additional permissions to update edge functions'
+ : undefined,
+ },
+ }}
+ >
+ Deploy updates
+
+
+ )}
>
)}
diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx
index 0e83542b46e..4388c94e708 100644
--- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx
+++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx
@@ -1,12 +1,10 @@
-import { useRouter } from 'next/router'
-import { useEffect, useMemo } from 'react'
-
import { useParams } from 'common'
import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { DefaultLayout } from 'components/layouts/DefaultLayout'
import IntegrationsLayout from 'components/layouts/Integrations/layout'
-import { useCronJobsEstimatePrefetch } from 'hooks/misc/useCronJobsEstimatePrefetch'
+import { useRouter } from 'next/router'
+import { useEffect, useMemo } from 'react'
import type { NextPageWithLayout } from 'types'
import { Admonition } from 'ui-patterns'
import { PageContainer } from 'ui-patterns/PageContainer'
@@ -24,8 +22,6 @@ const IntegrationPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref, id, pageId, childId } = useParams()
- useCronJobsEstimatePrefetch(id)
-
const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } =
useInstalledIntegrations()
diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
index 421bb715e9b..ee4fdac3905 100644
--- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
+++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx
@@ -1,15 +1,13 @@
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import { useEffect, useMemo } from 'react'
-
import { useFlag, useParams } from 'common'
import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { DefaultLayout } from 'components/layouts/DefaultLayout'
import IntegrationsLayout from 'components/layouts/Integrations/layout'
import { UnknownInterface } from 'components/ui/UnknownInterface'
-import { useCronJobsEstimatePrefetch } from 'hooks/misc/useCronJobsEstimatePrefetch'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useMemo } from 'react'
import type { NextPageWithLayout } from 'types'
import {
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
@@ -44,8 +42,6 @@ const IntegrationPage: NextPageWithLayout = () => {
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration')
- useCronJobsEstimatePrefetch(id)
-
const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } =
useInstalledIntegrations()
diff --git a/apps/studio/pages/redeem.tsx b/apps/studio/pages/redeem.tsx
index 4518951f057..39156310bdf 100644
--- a/apps/studio/pages/redeem.tsx
+++ b/apps/studio/pages/redeem.tsx
@@ -1,7 +1,8 @@
+import { useFlag } from 'common'
import Link from 'next/link'
import { useState } from 'react'
-import { Button, Loading } from 'ui'
-import { ShimmeringLoader } from 'ui-patterns'
+import { Button } from 'ui'
+import { Admonition, ShimmeringLoader } from 'ui-patterns'
import { CreditCodeRedemption } from '@/components/interfaces/Organization/BillingSettings/CreditCodeRedemption'
import { OrganizationCard } from '@/components/interfaces/Organization/OrganizationCard'
@@ -12,67 +13,85 @@ import {
ScaffoldHeader,
ScaffoldTitle,
} from '@/components/layouts/Scaffold'
+import AlertError from '@/components/ui/AlertError'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import type { NextPageWithLayout } from '@/types'
const RedeemCreditsContent = () => {
- const {
- data: organizations,
- isLoading: areOrganizationsLoading,
- isFetched: isOrganizationsFetched,
- isSuccess: wasOrganizationsRequestSuccessful,
- } = useOrganizationsQuery()
-
+ const redeemCodeEnabled = useFlag('redeemCodeEnabled')
const [selectedOrg, setSelectedOrg] = useState(null)
- if (!isOrganizationsFetched) {
- return (
-
-
-
+ const {
+ data: organizations,
+ error: errorOrganizations,
+ isLoading: isLoadingOrganizations,
+ isError: isErrorOrganizations,
+ } = useOrganizationsQuery()
-
-
-
-
+ if (isLoadingOrganizations) {
+ return (
+
)
}
- if (!wasOrganizationsRequestSuccessful) {
- return
Error loading redeem credit page. Try again later.
+ if (isErrorOrganizations) {
+ return (
+
+ )
}
return (
-
-
- To redeem your credits, select one of your organizations. The credits will be applied to
- that organization only and cannot be transferred or shared between organizations.
-
-
- Want to start fresh? Create a new
- organization first. You will have to revisit this link after creating the organization to
- redeem the code.
-
-
- Create organization
-
+
+
+
To redeem your credits, select one of your organizations.
+
+ The credits will be applied to that organization only and cannot be transferred or
+ shared between organizations.
+
+
+
+
+
Want to start fresh?
+
+ Create a new organization first. You will have to revisit this link after creating the
+ organization to redeem the code.
+
+
+ Create organization
+
+
-
- {organizations?.map((org) => (
-
setSelectedOrg(org.slug)}>
-
-
- ))}
+
+ {redeemCodeEnabled ? (
+ organizations?.map((org) => (
+
setSelectedOrg(org.slug)}
+ />
+ ))
+ ) : (
+
+ )}
{selectedOrg && (
setSelectedOrg(null)}
/>
)}
diff --git a/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js b/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js
new file mode 100644
index 00000000000..28682989697
--- /dev/null
+++ b/apps/studio/public/monaco-editor/basic-languages/javascript/javascript.js
@@ -0,0 +1,10 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/javascript/javascript", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var x=Object.create;var a=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var b=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var y=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var w=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),h=(e,t)=>{for(var n in t)a(e,n,{get:t[n],enumerable:!0})},s=(e,t,n,c)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of f(t))!k.call(e,r)&&r!==n&&a(e,r,{get:()=>t[r],enumerable:!(c=u(t,r))||c.enumerable});return e},g=(e,t,n)=>(s(e,t,"default"),n&&s(n,t,"default")),p=(e,t,n)=>(n=e!=null?x(b(e)):{},s(t||!e||!e.__esModule?a(n,"default",{value:e,enumerable:!0}):n,e)),v=e=>s(a({},"__esModule",{value:!0}),e);var d=w((C,l)=>{var A=p(y("vs/editor/editor.api"));l.exports=A});var _={};h(_,{conf:()=>$,language:()=>T});var i={};g(i,p(d()));var m={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],onEnterRules:[{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,afterText:/^\s*\*\/$/,action:{indentAction:i.languages.IndentAction.IndentOutdent,appendText:" * "}},{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,action:{indentAction:i.languages.IndentAction.None,appendText:" * "}},{beforeText:/^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,action:{indentAction:i.languages.IndentAction.None,appendText:"* "}},{beforeText:/^(\t|(\ \ ))*\ \*\/\s*$/,action:{indentAction:i.languages.IndentAction.None,removeText:1}}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],folding:{markers:{start:new RegExp("^\\s*//\\s*#?region\\b"),end:new RegExp("^\\s*//\\s*#?endregion\\b")}}},o={defaultToken:"invalid",tokenPostfix:".ts",keywords:["abstract","any","as","asserts","bigint","boolean","break","case","catch","class","continue","const","constructor","debugger","declare","default","delete","do","else","enum","export","extends","false","finally","for","from","function","get","if","implements","import","in","infer","instanceof","interface","is","keyof","let","module","namespace","never","new","null","number","object","out","package","private","protected","public","override","readonly","require","global","return","satisfies","set","static","string","super","switch","symbol","this","throw","true","try","type","typeof","undefined","unique","unknown","var","void","while","with","yield","async","await","of"],operators:["<=",">=","==","!=","===","!==","=>","+","-","**","*","/","%","++","--","<<","",">>",">>>","&","|","^","!","~","&&","||","??","?",":","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=","@"],symbols:/[=>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"],[/`/,"string","@string_backtick"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([dgimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],string_single:[[/[^\\']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"]],string_backtick:[[/\$\{/,{token:"delimiter.bracket",next:"@bracketCounting"}],[/[^\\`$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/`/,"string","@pop"]],bracketCounting:[[/\{/,"delimiter.bracket","@bracketCounting"],[/\}/,"delimiter.bracket","@pop"],{include:"common"}]}};var $=m,T={defaultToken:"invalid",tokenPostfix:".js",keywords:["break","case","catch","class","continue","const","constructor","debugger","default","delete","do","else","export","extends","false","finally","for","from","function","get","if","import","in","instanceof","let","new","null","return","set","static","super","switch","symbol","this","throw","true","try","typeof","undefined","var","void","while","with","yield","async","await","of"],typeKeywords:[],operators:o.operators,symbols:o.symbols,escapes:o.escapes,digits:o.digits,octaldigits:o.octaldigits,binarydigits:o.binarydigits,hexdigits:o.hexdigits,regexpctl:o.regexpctl,regexpesc:o.regexpesc,tokenizer:o.tokenizer};return v(_);})();
+return moduleExports;
+});
diff --git a/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js b/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js
new file mode 100644
index 00000000000..b9edf510978
--- /dev/null
+++ b/apps/studio/public/monaco-editor/basic-languages/markdown/markdown.js
@@ -0,0 +1,10 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/markdown/markdown", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var s=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var i=Object.prototype.hasOwnProperty;var l=(t,e)=>{for(var o in e)s(t,o,{get:e[o],enumerable:!0})},m=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of c(e))!i.call(t,n)&&n!==o&&s(t,n,{get:()=>e[n],enumerable:!(a=r(e,n))||a.enumerable});return t};var d=t=>m(s({},"__esModule",{value:!0}),t);var b={};l(b,{conf:()=>p,language:()=>g});var p={comments:{blockComment:[""]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*"),end:new RegExp("^\\s*")}}},g={defaultToken:"",tokenPostfix:".md",control:/[\\`*_\[\]{}()#+\-\.!]/,noncontrol:/[^\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,jsescapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],tokenizer:{root:[[/^\s*\|/,"@rematch","@table_header"],[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/,["white","keyword","keyword","keyword"]],[/^\s*(=+|\-+)\s*$/,"keyword"],[/^\s*((\*[ ]?)+)\s*$/,"meta.separator"],[/^\s*>+/,"comment"],[/^\s*([\*\-+:]|\d+\.)\s/,"keyword"],[/^(\t|[ ]{4})[^ ].*$/,"string"],[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,{token:"string",next:"@codeblock"}],[/^\s*```\s*((?:\w|[\/\-#])+).*$/,{token:"string",next:"@codeblockgh",nextEmbedded:"$1"}],[/^\s*```\s*$/,{token:"string",next:"@codeblock"}],{include:"@linecontent"}],table_header:[{include:"@table_common"},[/[^\|]+/,"keyword.table.header"]],table_body:[{include:"@table_common"},{include:"@linecontent"}],table_common:[[/\s*[\-:]+\s*/,{token:"keyword",switchTo:"table_body"}],[/^\s*\|/,"keyword.table.left"],[/^\s*[^\|]/,"@rematch","@pop"],[/^\s*$/,"@rematch","@pop"],[/\|/,{cases:{"@eos":"keyword.table.right","@default":"keyword.table.middle"}}]],codeblock:[[/^\s*~~~\s*$/,{token:"string",next:"@pop"}],[/^\s*```\s*$/,{token:"string",next:"@pop"}],[/.*$/,"variable.source"]],codeblockgh:[[/```\s*$/,{token:"string",next:"@pop",nextEmbedded:"@pop"}],[/[^`]+/,"variable.source"]],linecontent:[[/&\w+;/,"string.escape"],[/@escapes/,"escape"],[/\b__([^\\_]|@escapes|_(?!_))+__\b/,"strong"],[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/,"strong"],[/\b_[^_]+_\b/,"emphasis"],[/\*([^\\*]|@escapes)+\*/,"emphasis"],[/`([^\\`]|@escapes)+`/,"variable"],[/\{+[^}]+\}+/,"string.target"],[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,["string.link","","string.link"]],[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/,"string.link"],{include:"html"}],html:[[/<(\w+)\/>/,"tag"],[/<(\w+)(\-|\w)*/,{cases:{"@empty":{token:"tag",next:"@tag.$1"},"@default":{token:"tag",next:"@tag.$1"}}}],[/<\/(\w+)(\-|\w)*\s*>/,{token:"tag"}],[//,"comment","@pop"],[/