PublishAppSidePanel() — supabase Function Reference
Architecture documentation for the PublishAppSidePanel() function in index.tsx from the supabase codebase.
Entity Profile
Relationship Graph
Source Code
apps/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx lines 41–429
export const PublishAppSidePanel = ({
visible,
selectedApp,
onClose,
onCreateSuccess,
}: PublishAppSidePanelProps) => {
const { slug } = useParams()
const uploadButtonRef = useRef<any>()
const { mutate: createOAuthApp } = useOAuthAppCreateMutation({
onSuccess: (res, variables) => {
toast.success(`Successfully created OAuth app "${variables.name}"!`)
onClose()
onCreateSuccess(res)
setIsSubmitting(false)
},
onError: (error) => {
toast.error(`Failed to create OAuth application: ${error.message}`)
setIsSubmitting(false)
},
})
const { mutate: updateOAuthApp } = useOAuthAppUpdateMutation({
onSuccess: (_, variables) => {
toast.success(`Successfully updated OAuth app "${variables.name}"!`)
onClose()
setIsSubmitting(false)
},
onError: (error) => {
toast.error(`Failed to update OAuth application: ${error.message}`)
setIsSubmitting(false)
},
})
// [Joshen] Separate submitting state as there are additional async logic involved in the creation
const [isSubmitting, setIsSubmitting] = useState(false)
const [showPreview, setShowPreview] = useState(false)
const [iconFile, setIconFile] = useState<File>()
const [iconUrl, setIconUrl] = useState<string>()
const [errors, setErrors] = useState<{ [key: string]: string }>({})
const [urls, setUrls] = useState<{ id: string; value: string }[]>([{ id: uuidv4(), value: '' }])
const [scopes, setScopes] = useState<OAuthScope[]>([])
useEffect(() => {
if (visible) {
setErrors({})
setIsSubmitting(false)
setIconFile(undefined)
if (selectedApp !== undefined) {
setUrls(
selectedApp.redirect_uris?.map((url) => {
return { id: uuidv4(), value: url }
}) ?? []
)
setScopes((selectedApp?.scopes ?? []) as OAuthScope[])
setIconUrl(selectedApp.icon === null ? undefined : selectedApp.icon)
} else {
setUrls([{ id: uuidv4(), value: '' }])
setScopes([])
setIconUrl(undefined)
}
}
}, [visible, selectedApp])
const onUpdateUrl = (id: string, value: string) => {
const updatedUrls = urls.map((url) => {
if (url.id === id) return { id, value }
else return url
})
setUrls(updatedUrls)
setErrors({})
}
const removeUrl = (id: string) => {
const updatedUrls = urls.filter((url) => url.id !== id)
setUrls(updatedUrls)
}
const onFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
event.persist()
const [file] = event.target.files || (event as any).dataTransfer.items
setIconFile(file)
setIconUrl(URL.createObjectURL(file))
event.target.value = ''
}
const validate = (values: any) => {
const errors: any = {}
if (!values.name) errors.name = 'Please provide a name for your application'
if (!values.website) errors.website = 'Please provide a URL for your site'
if (!isValidHttpUrl(values.website)) errors.website = 'Please provide a valid URL for your site'
return errors
}
const onSubmit = async (values: any) => {
if (!slug) return console.error('Slug is required')
const redirect_uris = urls.filter((url) => url.value.length > 0).map((url) => url.value)
if (redirect_uris.length === 0) {
setErrors({ urls: 'Please provide at least one URL' })
return
} else {
setErrors({})
}
setIsSubmitting(true)
const { name, website } = values
const uploadedIconUrl =
iconFile !== undefined
? await uploadAttachment('oauth-app-icons', `${slug}/${uuidv4()}.png`, iconFile)
: iconUrl
if (selectedApp === undefined) {
// Create application
createOAuthApp({
slug,
name,
website,
redirect_uris,
scopes,
icon: uploadedIconUrl,
})
} else {
// Update application
updateOAuthApp({
id: selectedApp.id,
slug,
name,
website,
redirect_uris,
scopes,
icon: uploadedIconUrl,
})
}
}
return (
<SidePanel
hideFooter
size="large"
visible={visible}
header={
selectedApp !== undefined ? 'Update OAuth application' : 'Publish a new OAuth application'
}
onCancel={() => onClose()}
>
<Form
validateOnBlur
className="h-full"
initialValues={{ name: '', website: '' }}
validate={validate}
onSubmit={onSubmit}
>
{({ resetForm, values }: { resetForm: any; values: any }) => {
// [Joshen] although this "technically" is breaking the rules of React hooks
// it won't error because the hooks are always rendered in the same order
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (visible && selectedApp !== undefined) {
const values = { name: selectedApp.name, website: selectedApp.website }
resetForm({ values, initialValues: values })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, selectedApp])
return (
<>
<div className="h-full flex flex-col">
<div className="flex-grow">
<SidePanel.Content>
<div className="py-4 flex items-start justify-between gap-10">
<div className="space-y-4 w-full">
<Input
id="name"
label="Application name"
descriptionText={selectedApp?.id && `ID: ${selectedApp.id}`}
/>
<Input
id="website"
label="Website URL"
placeholder="https://my-website.com"
/>
</div>
<div>
{iconUrl !== undefined ? (
<div
className={cn(
'shadow transition group relative',
'bg-center bg-cover bg-no-repeat',
'mt-4 mr-4 space-y-2 rounded-full h-[120px] w-[120px] flex flex-col items-center justify-center'
)}
style={{
backgroundImage: iconUrl ? `url("${iconUrl}")` : 'none',
}}
>
<div className="absolute bottom-1 right-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1">
<Edit />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
key="upload"
onClick={() => {
if (uploadButtonRef.current)
(uploadButtonRef.current as any).click()
}}
>
<p>Upload image</p>
</DropdownMenuItem>
<DropdownMenuItem
key="remove"
onClick={() => {
setIconFile(undefined)
setIconUrl(undefined)
}}
>
<p>Remove image</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
) : (
<div
className={cn(
'border border-strong transition opacity-75 hover:opacity-100',
'mt-4 mr-4 space-y-2 rounded-full h-[120px] w-[120px] flex flex-col items-center justify-center cursor-pointer'
)}
onClick={() => {
if (uploadButtonRef.current) (uploadButtonRef.current as any).click()
}}
>
<Upload size={18} strokeWidth={1.5} className="text-foreground" />
<p className="text-xs text-foreground-light">Upload logo</p>
</div>
)}
<input
multiple
type="file"
ref={uploadButtonRef}
className="hidden"
accept="image/png, image/jpeg"
onChange={onFileUpload}
/>
</div>
</div>
</SidePanel.Content>
<SidePanel.Separator />
<SidePanel.Content className="py-4">
<div className="mb-2 flex items-center justify-between">
<div>
<p className="text-foreground text-sm">Authorization callback URLs</p>
<p className="text-sm text-foreground-light">
All URLs must use HTTPS, except for localhost
</p>
</div>
<Button
type="default"
onClick={() => setUrls(urls.concat({ id: uuidv4(), value: '' }))}
>
Add URL
</Button>
</div>
<div className="space-y-2 pb-2">
{urls.map((url) => (
<Input
key={url.id}
value={url.value}
onChange={(event) => onUpdateUrl(url.id, event.target.value)}
placeholder="e.g https://my-website.com"
actions={[
urls.length > 1 ? (
<Button
key="remove-url"
type="default"
onClick={() => removeUrl(url.id)}
>
Remove
</Button>
) : null,
]}
/>
))}
{errors.urls && <p className="text-red-900 text-sm">{errors.urls}</p>}
</div>
</SidePanel.Content>
{selectedApp !== undefined && (
<>
<SidePanel.Separator />
<SidePanel.Content className="py-4">
<OAuthSecrets selectedApp={selectedApp} />
</SidePanel.Content>
</>
)}
<SidePanel.Separator />
<div className="p-6 ">
<div className="flex items-start justify-between space-x-4 pb-4">
<div className="flex flex-col">
<span className="text-sm text-foreground">Application permissions</span>
<span className="text-sm text-foreground-light">
The application permissions are organized in scopes and will be presented
to the user when adding an app to their organization and all of its
projects.
</span>
</div>
<DocsButton href={`${DOCS_URL}/guides/platform/oauth-apps/oauth-scopes`} />
</div>
<ScopesPanel scopes={scopes} setScopes={setScopes} />
</div>
</div>
<SidePanel.Separator />
<SidePanel.Content>
<div className="pt-2 pb-3 flex items-center justify-between">
<Button
type="default"
onClick={() => setShowPreview(true)}
disabled={values.name.length === 0 || values.website.length === 0}
>
Preview consent for users
</Button>
<div className="flex items-center space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
Confirm
</Button>
</div>
</div>
</SidePanel.Content>
</div>
<Modal
hideFooter
showCloseButton={false}
className="!max-w-[600px]"
visible={showPreview}
onCancel={() => setShowPreview(false)}
>
<Modal.Content>
<div className="flex items-center gap-x-2 justify-between">
<p className="truncate">Authorize API access for {values.name}</p>
<Badge variant="success">Preview</Badge>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<AuthorizeRequesterDetails
icon={iconUrl || null}
name={values.name}
domain={values.website}
scopes={scopes}
/>
<div className="pt-4 space-y-2">
<p className="prose text-sm">Select an organization to grant API access to</p>
<div className="border border-control text-foreground-light rounded px-4 py-2 text-sm bg-surface-200">
Organizations that you have access to will be listed here
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex items-center justify-between">
<p className="prose text-xs">
This is what your users will see when authorizing with your app
</p>
<Button type="default" onClick={() => setShowPreview(false)}>
Close
</Button>
</div>
</Modal.Content>
</Modal>
</>
)
}}
</Form>
</SidePanel>
)
}
Domain
Subdomains
Source
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free