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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions apps/design-system/content/docs/ui-patterns/markdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,8 @@ import {
Code,
CodeBlockPre,
DefaultPre,
H1,
H2,
H3,
H4,
H5,
H6,
Hr,
Img,
InlineCode,
ListItem,
OrderedList,
Paragraph,
Quote,
SimplePre,
Expand Down
9 changes: 5 additions & 4 deletions apps/studio/components/grid/SupabaseGrid.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { safeLocalStorage, safeSessionStorage } from 'common'
import { compact } from 'lodash'
import { useSearchParams } from 'next/navigation'
import { parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs'
Expand Down Expand Up @@ -148,7 +149,7 @@ export function loadTableEditorStateFromLocalStorage(
): SavedState | undefined {
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
// Prefer sessionStorage (scoped to current tab) over localStorage
const jsonStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey)
const jsonStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey)
if (!jsonStr) return
const json = JSON.parse(jsonStr)
return json[tableId]
Expand Down Expand Up @@ -198,7 +199,7 @@ export function saveTableEditorStateToLocalStorage({
filters?: string[]
}) {
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
const savedStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey)
const savedStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey)

const config = {
...(gridColumns !== undefined && { gridColumns }),
Expand All @@ -215,8 +216,8 @@ export function saveTableEditorStateToLocalStorage({
savedJson = { [tableId]: config }
}
// Save to both localStorage and sessionStorage so it's consistent to current tab
localStorage.setItem(storageKey, JSON.stringify(savedJson))
sessionStorage.setItem(storageKey, JSON.stringify(savedJson))
safeLocalStorage.setItem(storageKey, JSON.stringify(savedJson))
safeSessionStorage.setItem(storageKey, JSON.stringify(savedJson))
}

export const saveTableEditorStateToLocalStorageDebounced = AwesomeDebouncePromise(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS, safeLocalStorage } from 'common'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
Expand Down Expand Up @@ -32,18 +32,18 @@ import { useProfile } from '@/lib/profile'
const setDeletionRequestFlag = () => {
const expiryDate = new Date()
expiryDate.setDate(expiryDate.getDate() + 30)
localStorage.setItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST, expiryDate.toString())
safeLocalStorage.setItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST, expiryDate.toString())
}

const hasActiveDeletionRequest = () => {
const expiryDateStr = localStorage.getItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
const expiryDateStr = safeLocalStorage.getItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
if (!expiryDateStr) return false

const expiryDate = new Date(expiryDateStr)
const now = new Date()

if (now > expiryDate) {
localStorage.removeItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
safeLocalStorage.removeItem(LOCAL_STORAGE_KEYS.ACCOUNT_DELETION_REQUEST)
return false
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FeatureFlagContext, LOCAL_STORAGE_KEYS, useFlag } from 'common'
import { FeatureFlagContext, LOCAL_STORAGE_KEYS, safeLocalStorage, useFlag } from 'common'
import { noop } from 'lodash'
import { useQueryState } from 'nuqs'
import {
Expand Down Expand Up @@ -39,14 +39,10 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) =
setFlags(
featurePreviews.reduce((a, b) => {
const defaultOptIn = b.isDefaultOptIn
try {
const localStorageValue = window.localStorage.getItem(b.key)
return {
...a,
[b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true',
}
} catch {
return { ...a, [b.key]: defaultOptIn }
const localStorageValue = safeLocalStorage.getItem(b.key)
return {
...a,
[b.key]: !localStorageValue ? defaultOptIn : localStorageValue === 'true',
}
}, {})
)
Expand All @@ -62,13 +58,7 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren) =
const value = {
flags,
onUpdateFlag: (key: string, value: boolean) => {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(key, value ? 'true' : 'false')
}
} catch {
// Silently fail in restricted storage modes (e.g. Safari private browsing)
}
safeLocalStorage.setItem(key, value ? 'true' : 'false')
const updatedFlags = { ...flags, [key]: value }
setFlags(updatedFlags)
},
Expand Down
105 changes: 82 additions & 23 deletions apps/studio/components/interfaces/BranchManagement/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Infinity,
MoreVertical,
Pencil,
Redo,
RefreshCw,
Shield,
Trash2,
Expand All @@ -30,6 +31,7 @@ import { EditBranchModal } from './EditBranchModal'
import { PreviewBranchesEmptyState } from './EmptyStates'
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useBranchPushMutation } from '@/data/branches/branch-push-mutation'
import { useBranchQuery } from '@/data/branches/branch-query'
import { useBranchResetMutation } from '@/data/branches/branch-reset-mutation'
import { useBranchRestoreMutation } from '@/data/branches/branch-restore-mutation'
Expand Down Expand Up @@ -262,6 +264,7 @@ const PreviewBranchActions = ({
setShowPersistentBranchDeleteConfirmationModal,
] = useState(false)
const [showEditBranchModal, setShowEditBranchModal] = useState(false)
const [showConfirmRetriggersModal, setShowConfirmRetriggersModal] = useState(false)

const { mutate: resetBranch, isPending: isResetting } = useBranchResetMutation({
onSuccess() {
Expand All @@ -286,6 +289,20 @@ const PreviewBranchActions = ({
},
})

const { mutate: branchPushMutate, isPending: isRetriggering } = useBranchPushMutation({
onSuccess() {
toast.success('Success! Please allow a few minutes for the branch to update.')
setShowConfirmRetriggersModal(false)
},
onError: (data) => {
toast.error(`Failed to trigger workflow: ${data.message}`)
},
})

const onRetriggerBranch = () => {
branchPushMutate({ branchRef, projectRef })
}

const onRestoreBranch = () => {
restoreBranch({ branchRef, projectRef })
}
Expand Down Expand Up @@ -345,28 +362,54 @@ const PreviewBranchActions = ({
</DropdownMenuItemTooltip>

{!branch.deletion_scheduled_at && (
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={isResetting || !isBranchActiveHealthy}
onSelect={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
tooltip={{
content: {
side: 'left',
text: !isBranchActiveHealthy
? 'Branch is still initializing. Please wait for it to become healthy before resetting.'
: undefined,
},
}}
>
<RefreshCw size={14} /> Reset branch
</DropdownMenuItemTooltip>
<>
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch}
onSelect={(e) => {
e.stopPropagation()
setShowConfirmRetriggersModal(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowConfirmRetriggersModal(true)
}}
tooltip={{
content: {
side: 'left',
text: !canUpdateBranches
? `You need additional permissions to ${branch.git_branch ? 'resync' : 'rebase'} branches`
: !isBranchActiveHealthy
? `Branch is still initializing. Please wait for it to become healthy before ${branch.git_branch ? 'resyncing' : 'rebasing'}.`
: undefined,
},
}}
>
<Redo size={14} /> {branch.git_branch ? 'Resync branch' : 'Rebase branch'}
</DropdownMenuItemTooltip>
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={isResetting || !isBranchActiveHealthy}
onSelect={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
tooltip={{
content: {
side: 'left',
text: !isBranchActiveHealthy
? 'Branch is still initializing. Please wait for it to become healthy before resetting.'
: undefined,
},
}}
>
<RefreshCw size={14} /> Reset branch
</DropdownMenuItemTooltip>
</>
)}

{!branch.deletion_scheduled_at && (
Expand Down Expand Up @@ -497,10 +540,26 @@ const PreviewBranchActions = ({
</p>
</ConfirmationModal>

<ConfirmationModal
variant="default"
visible={showConfirmRetriggersModal}
confirmLabel={branch.git_branch ? 'Resync' : 'Rebase'}
title={branch.git_branch ? 'Confirm branch resync' : 'Confirm branch rebase'}
loading={isRetriggering}
onCancel={() => setShowConfirmRetriggersModal(false)}
onConfirm={onRetriggerBranch}
>
<p className="text-sm text-foreground-light">
{branch.git_branch
? 'This will re-run all steps of the workflow based on the latest git branch state.'
: 'This will re-run all steps of the workflow based on the latest dashboard state.'}
</p>
</ConfirmationModal>

<ConfirmationModal
variant="warning"
visible={showPersistentBranchDeleteConfirmationModal}
confirmLabel={'Switch to preview'}
confirmLabel="Switch to preview"
title="Branch must be switched to preview before deletion"
loading={isUpdatingBranch}
onCancel={() => setShowPersistentBranchDeleteConfirmationModal(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const WorkflowLogs = ({ branch }: WorkflowLogsProps) => {
(workflowRuns.length > 0 ? (
<ul className="divide-y">
{workflowRuns.map((workflowRun) => (
<li key={workflowRun.id} className="px-4 py-3">
<li key={workflowRun.id} className="flex justify-between px-4 py-3 gap-2">
<button
type="button"
disabled={workflowRun.id === projectRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { uniqBy } from 'lodash'

import '@xyflow/react/dist/style.css'

import { LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS, safeLocalStorage } from 'common'

import { TableNodeData } from './Schemas.constants'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH } from './SchemaTableNode'
Expand Down Expand Up @@ -163,7 +163,7 @@ export async function getGraphDataFromTables(
}
}

const savedPositionsLocalStorage = localStorage.getItem(
const savedPositionsLocalStorage = safeLocalStorage.getItem(
LOCAL_STORAGE_KEYS.SCHEMA_VISUALIZER_POSITIONS(ref ?? 'project', schema?.id ?? 0)
)
const savedPositions = tryParseJson(savedPositionsLocalStorage)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
import { useState } from 'react'
import { cn, Dialog, DialogContent } from 'ui'
'use client'

export const FilesViewer = ({ files }: { files: string[] }) => {
const [selected, setSelected] = useState(files[0])
const [showDialog, setShowDialog] = useState(false)
import 'swiper/css'

import { Swiper, SwiperSlide } from 'swiper/react'
import { Image } from 'ui-patterns/Image'

export const FilesViewer = ({ files }: { files: { src: string; alt: string }[] }) => {
return (
<>
<div className="flex flex-col gap-y-4">
<button onClick={() => setShowDialog(true)}>
<img
alt={selected}
src={selected}
className="rounded-md border object-cover aspect-video"
<Swiper
className="w-full"
spaceBetween={12}
slidesPerView={1.4}
threshold={2}
watchOverflow
breakpoints={{
640: { slidesPerView: 1.4 },
1024: { slidesPerView: 3.2 },
}}
>
{files.map((file, i) => (
<SwiperSlide key={`${file.src}-${i}`}>
<Image
src={file.src}
alt={file.alt}
zoomable
width={400}
height={225}
className="rounded-md border object-cover w-full"
/>
</button>

{files.length > 1 && (
<div className="grid grid-cols-10 gap-x-2">
{files.map((x) => (
<button key={x} onClick={() => setSelected(x)}>
<img
alt={x}
src={x}
className={cn(
'col-span-1 bg-surface-100 rounded-md object-cover aspect-square border transition',
selected === x ? 'border-button-hover' : 'border-secondary'
)}
/>
</button>
))}
</div>
)}
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent size="xxlarge">
<img alt={selected} src={selected} className="rounded-md border" />
</DialogContent>
</Dialog>
</>
</SwiperSlide>
))}
</Swiper>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ export const MarkdownContent = ({

const content = remoteContent || localContent

return <Markdown>{content}</Markdown>
return <Markdown className="text-sm">{content}</Markdown>
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export type IntegrationDefinition = {
icon: (props?: { className?: string; style?: Record<string, string | number> }) => ReactNode
description: string | null
content?: string | null
files?: string[]
files?: { src: string; alt: string }[]
docsUrl: string | null
siteUrl?: string | null
author: {
Expand Down
Loading
Loading