diff --git a/apps/docs/features/ui/CodeBlock/CodeBlock.tsx b/apps/docs/features/ui/CodeBlock/CodeBlock.tsx index c79c416b9a620..83c074ed5cff6 100644 --- a/apps/docs/features/ui/CodeBlock/CodeBlock.tsx +++ b/apps/docs/features/ui/CodeBlock/CodeBlock.tsx @@ -64,7 +64,7 @@ export async function CodeBlock({ 'group', 'relative', 'not-prose', - 'w-full overflow-hidden', + 'w-full overflow-x-auto', 'border border-default rounded-lg', 'bg-200', 'text-sm', diff --git a/apps/docs/styles/main.scss b/apps/docs/styles/main.scss index a581a22e6cd24..02fc3958d6478 100644 --- a/apps/docs/styles/main.scss +++ b/apps/docs/styles/main.scss @@ -359,8 +359,11 @@ th code { } /* Word wrap styles for code blocks */ -.shiki[data-wrapped='true'] .code-content { +.shiki[data-wrapped='true'] { overflow-x: hidden !important; +} + +.shiki[data-wrapped='true'] .code-content { white-space: pre-wrap !important; word-break: break-word !important; } diff --git a/apps/studio/components/grid/SupabaseGrid.utils.ts b/apps/studio/components/grid/SupabaseGrid.utils.ts index b6401bbbb8b33..289acb8791069 100644 --- a/apps/studio/components/grid/SupabaseGrid.utils.ts +++ b/apps/studio/components/grid/SupabaseGrid.utils.ts @@ -5,16 +5,15 @@ import { CalculatedColumn, CellKeyboardEvent } from 'react-data-grid' import type { Filter, SavedState } from 'components/grid/types' import { Entity, isTableLike } from 'data/table-editor/table-editor-types' +import { BASE_PATH } from 'lib/constants' import { useSearchParams } from 'next/navigation' -import { parseAsBoolean, parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs' +import { parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs' import { copyToClipboard } from 'ui' import { FilterOperatorOptions } from './components/header/filter/Filter.constants' import { STORAGE_KEY_PREFIX } from './constants' import type { Sort, SupaColumn, SupaTable } from './types' import { formatClipboardValue } from './utils/common' -export const LOAD_TAB_FROM_CACHE_PARAM = 'loadFromCache' - export function formatSortURLParams(tableName: string, sort?: string[]): Sort[] { if (Array.isArray(sort)) { return compact( @@ -29,7 +28,7 @@ export function formatSortURLParams(tableName: string, sort?: string[]): Sort[] return [] } -export function sortsToUrlParams(sorts: Sort[]) { +export function sortsToUrlParams(sorts: { column: string; ascending?: boolean }[]) { return sorts.map((sort) => `${sort.column}:${sort.ascending ? 'asc' : 'desc'}`) } @@ -54,7 +53,9 @@ export function formatFilterURLParams(filter?: string[]): Filter[] { ) as Filter[] } -export function filtersToUrlParams(filters: Filter[]) { +export function filtersToUrlParams( + filters: { column: string | Array; operator: string; value: string }[] +) { return filters.map((filter) => { const selectedOperator = FilterOperatorOptions.find( (option) => option.value === filter.operator @@ -134,50 +135,75 @@ export function getStorageKey(prefix: string, ref: string) { export function loadTableEditorStateFromLocalStorage( projectRef: string, - tableName: string, - schema?: string | null + tableId: number ): 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) if (!jsonStr) return const json = JSON.parse(jsonStr) - const tableKey = !schema || schema == 'public' ? tableName : `${schema}.${tableName}` - return json[tableKey] + return json[tableId] +} + +/** + * Builds a table editor URL with the given project reference, table ID. It will load the saved state from local storage + * and add the sort and filter parameters to the URL. + */ +export function buildTableEditorUrl({ + projectRef = 'default', + tableId, + schema, +}: { + projectRef?: string + tableId: number + schema?: string +}) { + const url = new URL(`${BASE_PATH}/project/${projectRef}/editor/${tableId}`, location.origin) + + // If the schema is provided, add it to the URL so that the left sidebar is opened to the correct schema + if (schema) { + url.searchParams.set('schema', schema) + } + + const savedState = loadTableEditorStateFromLocalStorage(projectRef, tableId) + if (savedState?.sorts && savedState.sorts.length > 0) { + savedState.sorts?.forEach((sort) => url.searchParams.append('sort', sort)) + } + if (savedState?.filters && savedState.filters.length > 0) { + savedState.filters?.forEach((filter) => url.searchParams.append('filter', filter)) + } + return url.toString() } export function saveTableEditorStateToLocalStorage({ projectRef, - tableName, - schema, + tableId, gridColumns, sorts, filters, }: { projectRef: string - tableName: string - schema?: string | null + tableId: number gridColumns?: CalculatedColumn[] sorts?: string[] filters?: string[] }) { const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef) const savedStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey) - const tableKey = !schema || schema == 'public' ? tableName : `${schema}.${tableName}` const config = { ...(gridColumns !== undefined && { gridColumns }), - ...(sorts !== undefined && { sorts }), - ...(filters !== undefined && { filters }), + ...(sorts !== undefined && { sorts: sorts.filter((sort) => sort !== '') }), + ...(filters !== undefined && { filters: filters.filter((filter) => filter !== '') }), } let savedJson if (savedStr) { savedJson = JSON.parse(savedStr) - const previousConfig = savedJson[tableKey] - savedJson = { ...savedJson, [tableKey]: { ...previousConfig, ...config } } + const previousConfig = savedJson[tableId] + savedJson = { ...savedJson, [tableId]: { ...previousConfig, ...config } } } else { - savedJson = { [tableKey]: config } + savedJson = { [tableId]: config } } // Save to both localStorage and sessionStorage so it's consistent to current tab localStorage.setItem(storageKey, JSON.stringify(savedJson)) @@ -193,8 +219,7 @@ function getLatestParams() { const queryParams = new URLSearchParams(window.location.search) const sort = queryParams.getAll('sort') const filter = queryParams.getAll('filter') - const loadFromCache = !!queryParams.get(LOAD_TAB_FROM_CACHE_PARAM) - return { sort, filter, loadFromCache } + return { sort, filter } } export function useSyncTableEditorStateFromLocalStorageWithUrl({ @@ -209,7 +234,6 @@ export function useSyncTableEditorStateFromLocalStorageWithUrl({ { sort: parseAsNativeArrayOf(parseAsString), filter: parseAsNativeArrayOf(parseAsString), - [LOAD_TAB_FROM_CACHE_PARAM]: parseAsBoolean.withDefault(false), }, { history: 'replace', @@ -220,8 +244,7 @@ export function useSyncTableEditorStateFromLocalStorageWithUrl({ const urlParams = useMemo(() => { const sort = searchParams.getAll('sort') const filter = searchParams.getAll('filter') - const loadFromCache = !!searchParams.get(LOAD_TAB_FROM_CACHE_PARAM) - return { sort, filter, loadFromCache } + return { sort, filter } }, [searchParams]) useEffect(() => { @@ -232,25 +255,12 @@ export function useSyncTableEditorStateFromLocalStorageWithUrl({ // `urlParams` from `useQueryStates` can be stale so always get the latest from the URL const latestUrlParams = getLatestParams() - if (latestUrlParams.loadFromCache) { - const savedState = loadTableEditorStateFromLocalStorage(projectRef, table.name, table.schema) - updateUrlParams( - { - sort: savedState?.sorts ?? [], - filter: savedState?.filters ?? [], - loadFromCache: false, - }, - { clearOnDefault: true } - ) - } else { - saveTableEditorStateToLocalStorage({ - projectRef, - tableName: table.name, - schema: table.schema, - sorts: latestUrlParams.sort, - filters: latestUrlParams.filter, - }) - } + saveTableEditorStateToLocalStorage({ + projectRef, + tableId: table.id, + sorts: latestUrlParams.sort, + filters: latestUrlParams.filter, + }) }, [urlParams, table, projectRef]) } diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx index f1a67a24999d1..c9f752df7aae7 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx @@ -81,6 +81,10 @@ export const BranchRow = ({ const page = router.pathname.split('/').pop() const daysFromNow = dayjs().diff(dayjs(branch.updated_at), 'day') + const willBeDeletedIn = branch.deletion_scheduled_at + ? dayjs(branch.deletion_scheduled_at).diff(dayjs(), 'minutes') + : null + const isDeletionPending = willBeDeletedIn !== null && willBeDeletedIn < 0 const formattedTimeFromNow = dayjs(branch.updated_at).fromNow() const formattedUpdatedAt = dayjs(branch.updated_at).format('DD MMM YYYY, HH:mm:ss (ZZ)') @@ -125,9 +129,19 @@ export const BranchRow = ({
-

- {daysFromNow > 1 ? `Updated on ${formattedUpdatedAt}` : `Updated ${formattedTimeFromNow}`} -

+ {branch.deletion_scheduled_at ? ( +

+ {isDeletionPending + ? 'Deletion pending...' + : `Will be deleted in ${willBeDeletedIn} minutes`} +

+ ) : ( +

+ {daysFromNow > 1 + ? `Updated on ${formattedUpdatedAt}` + : `Updated ${formattedTimeFromNow}`} +

+ )} {rowActions}
diff --git a/apps/studio/components/interfaces/BranchManagement/Overview.tsx b/apps/studio/components/interfaces/BranchManagement/Overview.tsx index aa498cb98aebf..20f9e947c25b3 100644 --- a/apps/studio/components/interfaces/BranchManagement/Overview.tsx +++ b/apps/studio/components/interfaces/BranchManagement/Overview.tsx @@ -39,6 +39,7 @@ import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels' import { EditBranchModal } from './EditBranchModal' import { PreviewBranchesEmptyState } from './EmptyStates' +import { useBranchRestoreMutation } from 'data/branches/branch-restore-mutation' interface OverviewProps { isGithubConnected: boolean @@ -63,8 +64,12 @@ export const Overview = ({ onSelectDeleteBranch, generateCreatePullRequestURL, }: OverviewProps) => { - const [persistentBranches, ephemeralBranches] = partition( + const [scheduledForDeletionBranches, aliveBranches] = partition( previewBranches, + (branch) => branch.deletion_scheduled_at !== undefined + ) + const [persistentBranches, ephemeralBranches] = partition( + aliveBranches, (branch) => branch.persistent ) const { ref: projectRef } = useParams() @@ -186,6 +191,35 @@ export const Overview = ({ ) })} + {/* Scheduled for deletion branches section */} + + {isLoading && } + {isSuccess && scheduledForDeletionBranches.length === 0 && ( +
+

No scheduled for deletion branches

+
+ )} + {isSuccess && + scheduledForDeletionBranches.map((branch) => { + return ( + onSelectDeleteBranch(branch)} + generateCreatePullRequestURL={generateCreatePullRequestURL} + /> + } + /> + ) + })} +
) } @@ -213,13 +247,14 @@ const PreviewBranchActions = ({ PermissionAction.UPDATE, 'preview_branches' ) + // If user can update branches, they can restore branches + const canRestoreBranches = canUpdateBranches const { data } = useBranchQuery({ projectRef, branchRef }) const isBranchActiveHealthy = data?.status === 'ACTIVE_HEALTHY' const isPersistentBranch = branch.persistent - const { hasAccess: hasAccessToPersistentBranching, isLoading: isLoadingEntitlement } = - useCheckEntitlements('branching_persistent') + const { hasAccess: hasAccessToPersistentBranching } = useCheckEntitlements('branching_persistent') const [showConfirmResetModal, setShowConfirmResetModal] = useState(false) const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false) @@ -245,6 +280,16 @@ const PreviewBranchActions = ({ } }, }) + const { mutate: restoreBranch } = useBranchRestoreMutation({ + onSuccess() { + toast.success('Success! Please allow a few minutes for the branch to restore.') + setShowBranchModeSwitch(false) + }, + }) + + const onRestoreBranch = () => { + restoreBranch({ branchRef, projectRef }) + } const onConfirmReset = () => { resetBranch({ branchRef, projectRef }) @@ -275,64 +320,66 @@ const PreviewBranchActions = ({ /> - { - 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, - }, - }} - > - Reset branch - - - { - e.stopPropagation() - setShowBranchModeSwitch(true) - }} - onClick={(e) => { - e.stopPropagation() - setShowBranchModeSwitch(true) - }} - tooltip={{ - content: { - side: 'left', - text: !isBranchActiveHealthy - ? 'Branch is still initializing. Please wait for it to become healthy before switching.' - : !branch.persistent && !hasAccessToPersistentBranching - ? 'Upgrade your plan to access persistent branches' + {!branch.deletion_scheduled_at && ( + { + 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, - }, - }} - > - {branch.persistent ? ( - <> - Switch to preview - - ) : ( - <> - Switch to persistent - - )} - - + }, + }} + > + Reset branch + + )} + {!branch.deletion_scheduled_at && ( + { + e.stopPropagation() + setShowBranchModeSwitch(true) + }} + onClick={(e) => { + e.stopPropagation() + setShowBranchModeSwitch(true) + }} + tooltip={{ + content: { + side: 'left', + text: !isBranchActiveHealthy + ? 'Branch is still initializing. Please wait for it to become healthy before switching.' + : !branch.persistent && !hasAccessToPersistentBranching + ? 'Upgrade your plan to access persistent branches' + : undefined, + }, + }} + > + {branch.persistent ? ( + <> + Switch to preview + + ) : ( + <> + Switch to persistent + + )} + + )} {/* Edit Branch (gitless) */} {gitlessBranching && ( )} + {branch.deletion_scheduled_at && ( + { + e.stopPropagation() + onRestoreBranch() + }} + onClick={(e) => { + e.stopPropagation() + onRestoreBranch() + }} + tooltip={{ + content: { + side: 'left', + text: !canRestoreBranches + ? 'You need additional permissions to restore branches' + : branch.preview_project_status !== 'INACTIVE' + ? 'Preview project is not fully paused or already coming up. Please wait for it to become fully paused before restoring.' + : undefined, + }, + }} + > + Restore branch + + )} diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx index cb80338d86edd..effd0546cdc23 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx @@ -1,4 +1,4 @@ -import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils' +import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.utils' import { DiamondIcon, ExternalLink, Fingerprint, Hash, Key, Table2 } from 'lucide-react' import Link from 'next/link' import { Handle, NodeProps } from 'reactflow' @@ -10,9 +10,10 @@ export const TABLE_NODE_WIDTH = 320 export const TABLE_NODE_ROW_HEIGHT = 40 export type TableNodeData = { - id?: number + id: number + schema: string name: string - ref: string + ref?: string isForeign: boolean columns: { id: string @@ -66,10 +67,14 @@ export const TableNode = ({ {data.name} - {data.id && !placeholder && ( + {!placeholder && (