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
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { useQueryState } from 'nuqs'
import {
PropsWithChildren,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'

import { FeatureFlagContext, LOCAL_STORAGE_KEYS } from 'common'
import { useFlag, useIsRealtimeSettingsFFEnabled } from 'hooks/ui/useFlag'
import { EMPTY_OBJ } from 'lib/void'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'

Expand Down Expand Up @@ -89,3 +99,75 @@ export const useIsBranching2Enabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
}

export const useFeaturePreviewModal = () => {
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
const isRealtimeSettingsEnabled = useIsRealtimeSettingsFFEnabled()
const gitlessBranchingEnabled = useFlag('gitlessBranching')

const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null
const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null

// [Joshen] Use this if we want to feature flag previews
const isFeaturePreviewReleasedToPublic = useCallback(
(feature: (typeof FEATURE_PREVIEWS)[number]) => {
switch (feature.key) {
case 'supabase-ui-realtime-settings':
return isRealtimeSettingsEnabled
case 'supabase-ui-branching-2-0':
return gitlessBranchingEnabled
default:
return true
}
},
[isRealtimeSettingsEnabled, gitlessBranchingEnabled]
)

const selectedFeatureKey = !selectedFeatureKeyFromQuery
? FEATURE_PREVIEWS.filter((feature) => isFeaturePreviewReleasedToPublic(feature))[0].key
: selectedFeatureKeyFromQuery

const selectFeaturePreview = useCallback(
(featureKey: string) => {
setFeaturePreviewModal(featureKey)
},
[setFeaturePreviewModal]
)

const openFeaturePreviewModal = useCallback(() => {
selectFeaturePreview(selectedFeatureKey)
}, [selectFeaturePreview, selectedFeatureKey])

const closeFeaturePreviewModal = useCallback(() => {
setFeaturePreviewModal(null)
}, [setFeaturePreviewModal])

const toggleFeaturePreviewModal = useCallback(() => {
if (showFeaturePreviewModal) {
closeFeaturePreviewModal()
} else {
openFeaturePreviewModal()
}
}, [showFeaturePreviewModal, openFeaturePreviewModal, closeFeaturePreviewModal])

return useMemo(
() => ({
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
openFeaturePreviewModal,
closeFeaturePreviewModal,
toggleFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
}),
[
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
openFeaturePreviewModal,
closeFeaturePreviewModal,
toggleFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
]
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import { ReactNode } from 'react'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useIsRealtimeSettingsFFEnabled, useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { APISidePanelPreview } from './APISidePanelPreview'
import { Branching2Preview } from './Branching2Preview'
import { CLSPreview } from './CLSPreview'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'
import { useFeaturePreviewContext } from './FeaturePreviewContext'
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
import { InlineEditorPreview } from './InlineEditorPreview'
import { RealtimeSettingsPreview } from './RealtimeSettingsPreview'
import { Branching2Preview } from './Branching2Preview'

const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[key: string]: ReactNode
Expand All @@ -29,77 +27,61 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {

const FeaturePreviewModal = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const {
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
closeFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
} = useFeaturePreviewModal()
const org = useSelectedOrganization()
const featurePreviewContext = useFeaturePreviewContext()
const { mutate: sendEvent } = useSendEventMutation()
const isRealtimeSettingsEnabled = useIsRealtimeSettingsFFEnabled()
const gitlessBranchingEnabled = useFlag('gitlessBranching')

// [Joshen] Use this if we want to feature flag previews
function isReleasedToPublic(feature: (typeof FEATURE_PREVIEWS)[number]) {
switch (feature.key) {
case 'supabase-ui-realtime-settings':
return isRealtimeSettingsEnabled
case 'supabase-ui-branching-2-0':
return gitlessBranchingEnabled
default:
return true
}
}

const selectedFeatureKey =
snap.selectedFeaturePreview === ''
? FEATURE_PREVIEWS.filter((feature) => isReleasedToPublic(feature))[0].key
: snap.selectedFeaturePreview

const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature = FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey)
const selectedFeature =
FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey) ?? FEATURE_PREVIEWS[0]
const isSelectedFeatureEnabled = flags[selectedFeatureKey]

const allFeaturePreviews = IS_PLATFORM
? FEATURE_PREVIEWS
: FEATURE_PREVIEWS.filter((x) => !x.isPlatformOnly)

const toggleFeature = () => {
onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled)
onUpdateFlag(selectedFeature.key, !isSelectedFeatureEnabled)
sendEvent({
action: isSelectedFeatureEnabled ? 'feature_preview_disabled' : 'feature_preview_enabled',
properties: { feature: selectedFeatureKey },
properties: { feature: selectedFeature.key },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}

function handleCloseFeaturePreviewModal() {
snap.setShowFeaturePreviewModal(false)
}

return (
<Modal
hideFooter
showCloseButton
size="xlarge"
className="!max-w-4xl"
header="Dashboard feature previews"
visible={snap.showFeaturePreviewModal}
onCancel={handleCloseFeaturePreviewModal}
visible={showFeaturePreviewModal}
onCancel={closeFeaturePreviewModal}
>
{FEATURE_PREVIEWS.length > 0 ? (
<div className="flex">
<div>
<ScrollArea className="h-[550px] w-[280px] border-r">
{allFeaturePreviews
.filter((feature) => isReleasedToPublic(feature))
.filter((feature) => isFeaturePreviewReleasedToPublic(feature))
.map((feature) => {
const isEnabled = flags[feature.key] ?? false

return (
<div
key={feature.key}
onClick={() => snap.setSelectedFeaturePreview(feature.key)}
onClick={() => selectFeaturePreview(feature.key)}
className={cn(
'flex items-center space-x-3 p-4 border-b cursor-pointer bg transition',
selectedFeatureKey === feature.key ? 'bg-surface-300' : 'bg-surface-100'
selectedFeature.key === feature.key ? 'bg-surface-300' : 'bg-surface-100'
)}
>
{isEnabled ? (
Expand Down Expand Up @@ -134,7 +116,7 @@ const FeaturePreviewModal = () => {
</Button>
</div>
</div>
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeatureKey]}
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature.key]}
</div>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { isEmpty, noop } from 'lodash'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'

import { useAppStateSnapshot } from 'state/app-state'
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Modal } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
Expand Down Expand Up @@ -47,7 +47,7 @@ const PolicyEditorModal = ({
onUpdatePolicy,
onSaveSuccess = noop,
}: PolicyEditorModalProps) => {
const snap = useAppStateSnapshot()
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()

const newPolicyTemplate: PolicyFormField = {
schema,
Expand Down Expand Up @@ -95,7 +95,7 @@ const PolicyEditorModal = ({
const onSelectBackFromTemplates = () => setView(previousView)

const onToggleFeaturePreviewModal = () => {
snap.setShowFeaturePreviewModal(!snap.showFeaturePreviewModal)
toggleFeaturePreviewModal()
onSelectCancel()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import {
cn,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { PermissionAction } from '@supabase/shared-types/out/constants'

export const CreateBranchModal = () => {
const { ref } = useParams()
Expand Down Expand Up @@ -95,6 +97,8 @@ export const CreateBranchModal = () => {
},
})

const canCreateBranch = useCheckPermissions(PermissionAction.CREATE, 'preview_branches')

const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)

// Fetch production/default branch to inspect git_branch linkage
Expand Down Expand Up @@ -149,6 +153,14 @@ export const CreateBranchModal = () => {
})

const canSubmit = !isCreating && !isChecking
const isDisabled =
!isSuccessConnections ||
isCreating ||
!canSubmit ||
isChecking ||
(!gitlessBranching && !githubConnection) ||
promptProPlanUpgrade ||
!canCreateBranch

const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
Expand Down Expand Up @@ -198,6 +210,7 @@ export const CreateBranchModal = () => {
<DialogSectionSeparator />
</>
)}

<DialogSection
padding="medium"
className={cn('space-y-4', promptProPlanUpgrade && 'opacity-25 pointer-events-none')}
Expand Down Expand Up @@ -348,9 +361,7 @@ export const CreateBranchModal = () => {
</figure>
</div>
<div className="flex flex-col gap-y-1">
<p className="text-sm text-foreground">
Preview branches are billed $0.01344 per hour
</p>
<p className="text-sm text-foreground">Branches are billed $0.01344 per hour</p>
<p className="text-sm text-foreground-light">
This cost will continue for as long as the branch has not been removed.
</p>
Expand All @@ -370,14 +381,7 @@ export const CreateBranchModal = () => {
</Button>
<ButtonTooltip
form={formId}
disabled={
!isSuccessConnections ||
isCreating ||
!canSubmit ||
isChecking ||
(!gitlessBranching && !githubConnection) ||
promptProPlanUpgrade
}
disabled={isDisabled}
loading={isCreating}
type="primary"
htmlType="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const DatabaseDiffPanel = ({

return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 py-3">
<CardTitle>
<Link
href={`/project/${currentBranchRef}/database/schema`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export const PreviewBranchesEmptyState = ({
<div className="flex items-center flex-col justify-center w-full py-10">
<p>Create your first preview branch</p>
<p className="text-foreground-light mb-4">
Preview branches are used to experiment with changes to your database schema in a safe,
non-destructive environment.
Preview branches are short-lived environments that let you safely experiment with changes to
your database schema without affecting your main database.
</p>
<div className="flex items-center space-x-2">
<DocsButton href="https://supabase.com/docs/guides/platform/branching" />
Expand Down
25 changes: 14 additions & 11 deletions apps/studio/components/interfaces/BranchManagement/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
import { EditBranchModal } from './EditBranchModal'
import { PreviewBranchesEmptyState } from './EmptyStates'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'

interface OverviewProps {
isLoading: boolean
Expand Down Expand Up @@ -99,7 +100,8 @@ export const Overview = ({
<div className="flex items-center flex-col justify-center w-full py-10">
<p>No persistent branches</p>
<p className="text-foreground-light">
Persistent branches are long-lived and not automatically deleted.
Persistent branches are long-lived, cannot be reset, and are ideal for staging
environments.
</p>
</div>
)}
Expand Down Expand Up @@ -337,19 +339,20 @@ const PreviewBranchActions = ({
</DropdownMenuContent>
</DropdownMenu>

<ConfirmationModal
variant="destructive"
<TextConfirmModal
variant="warning"
visible={showConfirmResetModal}
confirmLabel="Reset branch"
title="Confirm branch reset"
loading={isResetting}
onCancel={() => setShowConfirmResetModal(false)}
onConfirm={onConfirmReset}
>
<p className="text-sm text-foreground-light">
Are you sure you want to reset the "{branch.name}" branch? All data will be deleted.
</p>
</ConfirmationModal>
loading={isResetting}
title="Reset branch"
confirmLabel="Reset branch"
confirmPlaceholder="Type in name of branch"
confirmString={branch?.name ?? ''}
alert={{
title: `Are you sure you want to reset the "${branch.name}" branch? All data will be deleted.`,
}}
/>

<ConfirmationModal
variant="default"
Expand Down
Loading
Loading