Edge functions secrets updates (#31329)
* Rename secrets to env vars, add inline form * Add multiple * Fix adding multiple * Change name back * Sm update * Add docs for secrets management * Fix file paths * Prettier * Fix image paths * Images * clean up * Fix form submit * Minor fixes. * Remove console logs. --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
---
|
||||
id: 'functions-secrets'
|
||||
title: 'Managing Environment Variables'
|
||||
title: 'Managing Secrets (Environment Variables)'
|
||||
description: 'Managing secrets and environment variables.'
|
||||
subtitle: 'Managing secrets and environment variables.'
|
||||
---
|
||||
|
||||
It's common that you will need to use sensitive information or environment-specific variables inside your Edge Functions. You can access these using Deno's built-in handler
|
||||
It's common that you will need to use environment variables or other sensitive information Edge Functions. You can manage secrets using the CLI or the Dashboard.
|
||||
|
||||
You can access these using Deno's built-in handler
|
||||
|
||||
```js
|
||||
Deno.env.get('MY_SECRET_NAME')
|
||||
@@ -57,7 +59,25 @@ When the function starts you should see the name “Yoda” output to the termin
|
||||
|
||||
## Production secrets
|
||||
|
||||
Let's create a `.env` for production. In this case we'll just use the same as our local secrets:
|
||||
You will also need to set secrets for your production Edge Functions. You can do this via the Dashboard or using the CLI.
|
||||
|
||||
### Using the Dashboard
|
||||
|
||||
1. Visit [Edge Function Secrets Management](https://supabase.com/dashboard/project/_/settings/functions) page in your Dashboard.
|
||||
2. Add the Key and Value for your secret and press Save.
|
||||
3. Note that you can paste multiple secrets at a time.
|
||||
|
||||
<Image
|
||||
alt="Edge Functions Secrets Management"
|
||||
src={{
|
||||
light: '/docs/img/edge-functions-secrets--light.jpg',
|
||||
dark: '/docs/img/edge-functions-secrets.jpg',
|
||||
}}
|
||||
/>
|
||||
|
||||
### Using the CLI
|
||||
|
||||
Let's create a `.env` to help us deploy our secrets to production. In this case we'll just use the same as our local secrets:
|
||||
|
||||
```bash
|
||||
cp ./supabase/.env.local ./supabase/.env
|
||||
@@ -67,7 +87,7 @@ This creates a new file `./supabase/.env` for storing your production secrets.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Never check your `.env` files into Git!
|
||||
Never check your `.env` files into Git! You only use the `.env` file to help deploy your secrets to production. Don't commit it to your repository.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
BIN
apps/docs/public/img/edge-functions-secrets--light.jpg
Normal file
BIN
apps/docs/public/img/edge-functions-secrets--light.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
BIN
apps/docs/public/img/edge-functions-secrets.jpg
Normal file
BIN
apps/docs/public/img/edge-functions-secrets.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
@@ -37,7 +37,7 @@ const EdgeFunctionDetails = () => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, functionSlug } = useParams()
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [showInstructions, setShowInstructions] = useState(false)
|
||||
const [showInstructions, setShowInstructions] = useState(true)
|
||||
|
||||
const { data: settings } = useProjectSettingsV2Query({ projectRef })
|
||||
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
|
||||
@@ -220,7 +220,7 @@ const EdgeFunctionDetails = () => {
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded border bg-foreground p-2 text-background">
|
||||
<Terminal size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<h4>Command line access</h4>
|
||||
<h4>Deploy, invoke and manage secrets</h4>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setShowInstructions(!showInstructions)}>
|
||||
{showInstructions ? (
|
||||
@@ -231,11 +231,11 @@ const EdgeFunctionDetails = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 className="text-base">Deployment management</h5>
|
||||
<h5 className="text-base">Deploy your function</h5>
|
||||
<CommandRender commands={managementCommands} />
|
||||
<h5 className="text-base">Invoke </h5>
|
||||
<h5 className="text-base">Invoke your function</h5>
|
||||
<CommandRender commands={invokeCommands} />
|
||||
<h5 className="text-base">Secrets management</h5>
|
||||
<h5 className="text-base">Manage secrets</h5>
|
||||
<CommandRender commands={secretCommands} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useState } from 'react'
|
||||
import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation'
|
||||
import { Eye, EyeOff, MinusCircle } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormLabel_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
|
||||
type SecretPair = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const FormSchema = z.object({
|
||||
secrets: z.array(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Please provide a name for your secret')
|
||||
.refine((value) => !value.match(/^(SUPABASE_).*/), {
|
||||
message: 'Name must not start with the SUPABASE_ prefix',
|
||||
}),
|
||||
value: z.string().min(1, 'Please provide a value for your secret'),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const defaultValues = {
|
||||
secrets: [{ name: '', value: '' }],
|
||||
}
|
||||
const AddNewSecretForm = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [showSecretValue, setShowSecretValue] = useState(false)
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'secrets',
|
||||
})
|
||||
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
e.preventDefault()
|
||||
const text = e.clipboardData?.getData('text')
|
||||
if (!text) return
|
||||
|
||||
const pairs: Array<SecretPair> = []
|
||||
|
||||
try {
|
||||
const jsonData = JSON.parse(text)
|
||||
Object.entries(jsonData).forEach(([key, value]) => {
|
||||
pairs.push({ name: key, value: String(value) })
|
||||
})
|
||||
} catch {
|
||||
// Try KEY=VALUE format (multiple lines)
|
||||
const lines = text.split(/\n/)
|
||||
lines.forEach((line) => {
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
if (key && valueParts.length) {
|
||||
pairs.push({
|
||||
name: key.trim(),
|
||||
value: valueParts.join('=').trim(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (pairs.length) {
|
||||
// Replace all fields with new pairs
|
||||
form.reset({ secrets: pairs })
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: createSecret, isLoading: isCreating } = useSecretsCreateMutation({
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(`Successfully created new secret "${variables.secrets[0].name}"`)
|
||||
// RHF recommends using setTimeout/useEffect to reset the form
|
||||
setTimeout(() => form.reset(), 0)
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
|
||||
createSecret({ projectRef, secrets: data.secrets })
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
<Panel.Content className="grid gap-4">
|
||||
<h2 className="text-sm">Add new secrets</h2>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form className="w-full" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{fields.map((fieldItem, index) => (
|
||||
<div key={fieldItem.id} className="grid grid-cols-[1fr_1fr_auto] gap-4 mb-4">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name={`secrets.${index}.name`}
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="w-full">
|
||||
<FormLabel_Shadcn_>Key</FormLabel_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="e.g. CLIENT_KEY"
|
||||
onPaste={(e) => handlePaste(e.nativeEvent)}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name={`secrets.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="w-full relative">
|
||||
<FormLabel_Shadcn_>Value</FormLabel_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
type={showSecretValue ? 'text' : 'password'}
|
||||
actions={
|
||||
<div className="mr-1">
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={showSecretValue ? <EyeOff /> : <Eye />}
|
||||
onClick={() => setShowSecretValue(!showSecretValue)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<FormMessage_Shadcn_ />
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
className="self-end h-9 flex"
|
||||
icon={<MinusCircle />}
|
||||
onClick={() => (fields.length > 1 ? remove(index) : form.reset(defaultValues))}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
const formValues = form.getValues('secrets')
|
||||
const isEmptyForm = formValues.every((field) => !field.name && !field.value)
|
||||
if (isEmptyForm) {
|
||||
fields.forEach((_, index) => remove(index))
|
||||
append({ name: '', value: '' })
|
||||
} else {
|
||||
append({ name: '', value: '' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add another
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2 col-span-2 -mx-6 px-6 border-t pt-4 mt-4">
|
||||
<Button type="primary" htmlType="submit" disabled={isCreating} loading={isCreating}>
|
||||
{isCreating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddNewSecretForm
|
||||
@@ -1,132 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Modal } from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
interface AddNewSecretModalProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const submitRef = useRef<HTMLButtonElement>(null)
|
||||
const [showSecretValue, setShowSecretValue] = useState(false)
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Please provide a name for your secret')
|
||||
.refine((value) => !value.match(/^(SUPABASE_).*/), {
|
||||
message: 'Name must not start with the SUPABASE_ prefix',
|
||||
}),
|
||||
value: z.string().min(1, 'Please provider a value for your secret'),
|
||||
})
|
||||
const defaultValues = { name: '', value: '' }
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
|
||||
createSecret({ projectRef, secrets: [data] })
|
||||
}
|
||||
|
||||
const { mutate: createSecret, isLoading: isCreating } = useSecretsCreateMutation({
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(`Successfully created new secret "${variables.secrets[0].name}"`)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.reset(defaultValues)
|
||||
setShowSecretValue(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="small"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
header="Create a new secret"
|
||||
alignFooter="right"
|
||||
customFooter={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" onClick={onClose} disabled={isCreating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isCreating}
|
||||
loading={isCreating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
{isCreating ? 'Creating secret' : 'Create secret'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id="create-secret-form"
|
||||
className="w-full flex flex-col gap-y-2"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Secret name">
|
||||
<FormControl_Shadcn_>
|
||||
<Input {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="Secret value">
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
type={showSecretValue ? 'text' : 'password'}
|
||||
actions={
|
||||
<div className="mr-1">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={showSecretValue ? <EyeOff /> : <Eye />}
|
||||
onClick={() => setShowSecretValue(!showSecretValue)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
<button className="hidden" type="submit" ref={submitRef} />
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddNewSecretModal
|
||||
@@ -6,23 +6,20 @@ import { toast } from 'sonner'
|
||||
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useSecretsDeleteMutation } from 'data/secrets/secrets-delete-mutation'
|
||||
import { ProjectSecret, useSecretsQuery } from 'data/secrets/secrets-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { Badge } from 'ui'
|
||||
import { Badge, Separator } from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import AddNewSecretModal from './AddNewSecretModal'
|
||||
import AddNewSecretForm from './AddNewSecretForm'
|
||||
import EdgeFunctionSecret from './EdgeFunctionSecret'
|
||||
|
||||
const EdgeFunctionSecrets = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const [showCreateSecret, setShowCreateSecret] = useState(false)
|
||||
const [selectedSecret, setSelectedSecret] = useState<ProjectSecret>()
|
||||
|
||||
const canReadSecrets = useCheckPermissions(PermissionAction.SECRETS_READ, '*')
|
||||
@@ -48,92 +45,78 @@ const EdgeFunctionSecrets = () => {
|
||||
return (
|
||||
<>
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve project secrets" />}
|
||||
|
||||
{isSuccess && (
|
||||
<div className="space-y-4">
|
||||
{!canReadSecrets ? (
|
||||
<NoPermission resourceText="view this project's edge function secrets" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2">
|
||||
<Input
|
||||
size="small"
|
||||
className="w-full md:w-80"
|
||||
placeholder="Search for a secret"
|
||||
value={searchString}
|
||||
onChange={(e: any) => setSearchString(e.target.value)}
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<DocsButton href="https://supabase.com/docs/guides/functions/secrets" />
|
||||
<ButtonTooltip
|
||||
disabled={!canUpdateSecrets}
|
||||
onClick={() => setShowCreateSecret(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canUpdateSecrets
|
||||
? 'You need additional permissions to update edge function secrets'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Add new secret
|
||||
</ButtonTooltip>
|
||||
{isSuccess && canUpdateSecrets && (
|
||||
<>
|
||||
<div className="grid gap-5">
|
||||
<AddNewSecretForm />
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="space-y-4 mt-4">
|
||||
{!canReadSecrets ? (
|
||||
<NoPermission resourceText="view this project's edge function secrets" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2">
|
||||
<Input
|
||||
size="small"
|
||||
className="w-full md:w-80"
|
||||
placeholder="Search for a secret"
|
||||
value={searchString}
|
||||
onChange={(e: any) => setSearchString(e.target.value)}
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="secret-name">Name</Table.th>,
|
||||
<Table.th key="secret-value" className="flex items-center gap-x-2">
|
||||
Digest{' '}
|
||||
<Badge color="scale" className="font-mono">
|
||||
SHA256
|
||||
</Badge>
|
||||
</Table.th>,
|
||||
<Table.th key="actions" />,
|
||||
]}
|
||||
body={
|
||||
secrets.length > 0 ? (
|
||||
secrets.map((secret) => (
|
||||
<EdgeFunctionSecret
|
||||
key={secret.name}
|
||||
secret={secret}
|
||||
onSelectDelete={() => setSelectedSecret(secret)}
|
||||
/>
|
||||
))
|
||||
) : secrets.length === 0 && searchString.length > 0 ? (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{searchString}" did not return any results
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
) : (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No secrets created</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are no secrets associated with your project yet
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="secret-name">Name</Table.th>,
|
||||
<Table.th key="secret-value" className="flex items-center gap-x-2">
|
||||
Digest{' '}
|
||||
<Badge color="scale" className="font-mono">
|
||||
SHA256
|
||||
</Badge>
|
||||
</Table.th>,
|
||||
<Table.th key="actions" />,
|
||||
]}
|
||||
body={
|
||||
secrets.length > 0 ? (
|
||||
secrets.map((secret) => (
|
||||
<EdgeFunctionSecret
|
||||
key={secret.name}
|
||||
secret={secret}
|
||||
onSelectDelete={() => setSelectedSecret(secret)}
|
||||
/>
|
||||
))
|
||||
) : secrets.length === 0 && searchString.length > 0 ? (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{searchString}" did not return any results
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
) : (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No secrets created</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are no secrets associated with your project yet
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AddNewSecretModal visible={showCreateSecret} onClose={() => setShowCreateSecret(false)} />
|
||||
|
||||
<ConfirmationModal
|
||||
variant="warning"
|
||||
variant="destructive"
|
||||
loading={isDeleting}
|
||||
visible={selectedSecret !== undefined}
|
||||
confirmLabel="Delete secret"
|
||||
@@ -147,8 +130,8 @@ const EdgeFunctionSecrets = () => {
|
||||
}}
|
||||
>
|
||||
<p className="text-sm">
|
||||
Before removing this secret, do ensure that none of your edge functions are currently
|
||||
actively using this secret. This action cannot be undone.
|
||||
Before removing this secret, ensure none of your Edge Functions are actively using it.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
|
||||
@@ -8,16 +8,22 @@ import {
|
||||
ScaffoldTitle,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader>
|
||||
<ScaffoldTitle>Edge Function Secrets Management</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Manage the secrets for your project's edge functions
|
||||
</ScaffoldDescription>
|
||||
<ScaffoldHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<ScaffoldTitle>Edge Function Secrets</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Manage the secrets (environment variables) for your project's Edge Functions
|
||||
</ScaffoldDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<DocsButton href="https://supabase.com/docs/guides/functions/secrets" />
|
||||
</div>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer bottomPadding>
|
||||
|
||||
Reference in New Issue
Block a user