feat: auto-fill detected env variables for sites/functions

This commit is contained in:
Harsh Mahajan
2026-02-12 18:27:18 +05:30
parent 3daf2d44cf
commit 2f317447fb
11 changed files with 380 additions and 239 deletions
@@ -11,6 +11,7 @@
export let show = false;
export let variables: Partial<Models.Variable>[];
export let productLabel = 'site';
let newVariables: Partial<Models.Variable>[] = [{ key: '', value: '' }];
let secret = false;
@@ -65,8 +66,8 @@
<Modal bind:show onSubmit={handleVariable} title="Create variables" bind:error>
<span slot="description">
Set the environment variables or secret that will be passed to your site. Global variables
can be set in <Link
Set the environment variables or secret that will be passed to your {productLabel}. Global
variables can be set in <Link
variant="muted"
href={`${base}/project-${page.params.region}-${page.params.project}/settings`}
>project settings</Link
@@ -0,0 +1,260 @@
<script lang="ts">
import { Empty, Paginator } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
ActionMenu,
Accordion,
Badge,
InteractiveText,
Icon,
Layout,
Popover,
Skeleton,
Table,
Tooltip,
Button as PinkButton
} from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
IconCode,
IconUpload,
IconPlus,
IconTrash,
IconEyeOff,
IconPencil
} from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import VariableEditorModal from './variableEditorModal.svelte';
import SecretVariableModal from './secretVariableModal.svelte';
import ImportVariablesModal from './importVariablesModal.svelte';
import CreateVariableModal from './createVariableModal.svelte';
import DeleteVariableModal from './deleteVariableModal.svelte';
import UpdateVariableModal from './updateVariableModal.svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
export let variables: Partial<Models.Variable>[] = [];
export let productLabel = 'site';
export let docsLink =
'https://appwrite.io/docs/products/sites/develop#accessing-environment-variables';
export let analyticsSource = 'site_configuration';
export let analyticsCreateSource = 'site_settings';
export let isLoading = false;
let showEditorModal = false;
let showImportModal = false;
let showSecretModal = false;
let showCreate = false;
let showUpdate = false;
let showDelete = false;
let currentVariable: Partial<Models.Variable>;
$: createSource = analyticsCreateSource || analyticsSource;
</script>
<Accordion title="Environment variables" badge="Optional" hideDivider>
<Layout.Stack gap="xl">
Set up environment variables to securely manage keys and settings for your project.
<Layout.Stack gap="l">
<Layout.Stack direction="row">
<Layout.Stack direction="row" gap="s">
<Button
secondary
size="s"
on:mousedown={() => {
showEditorModal = true;
trackEvent(Click.VariablesUpdateClick, {
source: analyticsSource
});
}}>
<Icon slot="start" icon={IconCode} /> Editor
</Button>
<Button
secondary
size="s"
on:mousedown={() => {
showImportModal = true;
trackEvent(Click.VariablesImportClick, {
source: analyticsSource
});
}}>
<Icon slot="start" icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
{#if variables?.length}
<Button
secondary
size="s"
on:mousedown={() => {
showCreate = true;
trackEvent(Click.VariablesCreateClick, {
source: createSource
});
}}>
<Icon slot="start" icon={IconPlus} /> Create variable
</Button>
{/if}
</Layout.Stack>
{#if isLoading && !variables?.length}
<Table.Root
let:root
columns={[
{ id: 'key', width: 200 },
{ id: 'value' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="value" {root}>Value</Table.Header.Cell>
<Table.Header.Cell column="actions" {root}></Table.Header.Cell>
</svelte:fragment>
{#each Array(3) as _}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>
<Skeleton variant="line" width={120} height={14} />
</Table.Cell>
<Table.Cell column="value" {root}>
<Skeleton variant="line" width="100%" height={14} />
</Table.Cell>
<Table.Cell column="actions" {root}>
<Skeleton variant="line" width={24} height={14} />
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{:else if variables?.length}
<Paginator items={variables} limit={6} hideFooter={variables.length <= 6}>
{#snippet children(paginatedItems)}
<Table.Root
let:root
columns={[
{ id: 'key', width: 200 },
{ id: 'value' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="value" {root}>Value</Table.Header.Cell>
<Table.Header.Cell column="actions" {root}></Table.Header.Cell>
</svelte:fragment>
{#each paginatedItems as variable}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>{variable.key}</Table.Cell>
<Table.Cell column="value" {root}>
<!-- TODO: fix max width -->
<div style="max-width: 20rem">
{#if variable.secret}
<Tooltip maxWidth="26rem">
<Badge
content="Secret"
variant="secondary"
size="s" />
<svelte:fragment slot="tooltip">
This value is secret, you cannot see its
value.
</svelte:fragment>
</Tooltip>
{:else}
<InteractiveText
variant="secret"
isVisible={false}
text={variable.value} />
{/if}
</div>
</Table.Cell>
<Table.Cell column="actions" {root}>
<div style="margin-inline-start: auto">
<Popover
padding="none"
placement="bottom-end"
let:toggle>
<PinkButton.Button
icon
variant="text"
size="s"
aria-label="More options"
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</PinkButton.Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#if !variable?.secret}
<ActionMenu.Item.Button
leadingIcon={IconPencil}
on:click={(e) => {
toggle(e);
currentVariable = variable;
showUpdate = true;
}}>
Update
</ActionMenu.Item.Button>
{/if}
{#if !variable?.secret}
<ActionMenu.Item.Button
leadingIcon={IconEyeOff}
on:click={(e) => {
toggle(e);
currentVariable = variable;
showSecretModal = true;
}}>
Secret
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
toggle(e);
currentVariable = variable;
showDelete = true;
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</div>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else}
<Empty on:click={() => (showCreate = true)}>Create variables to get started</Empty>
{/if}
</Layout.Stack>
</Layout.Stack>
</Accordion>
{#if showEditorModal}
<VariableEditorModal bind:variables bind:showEditor={showEditorModal} {docsLink} />
{/if}
{#if showSecretModal}
<SecretVariableModal bind:show={showSecretModal} bind:currentVariable bind:variables />
{/if}
{#if showImportModal}
<ImportVariablesModal bind:show={showImportModal} bind:variables />
{/if}
{#if showCreate}
<CreateVariableModal bind:show={showCreate} bind:variables {productLabel} />
{/if}
{#if showUpdate}
<UpdateVariableModal
bind:show={showUpdate}
bind:variables
bind:selectedVar={currentVariable}
{productLabel} />
{/if}
{#if showDelete}
<DeleteVariableModal bind:show={showDelete} bind:variables bind:currentVariable />
{/if}
@@ -11,6 +11,7 @@
export let show = false;
export let selectedVar: Partial<Models.Variable>;
export let variables: Partial<Models.Variable>[];
export let productLabel = 'site';
let pair = {
$id: selectedVar?.$id,
@@ -40,7 +41,8 @@
<Modal bind:show onSubmit={handleVariable} title="Update variable">
<span slot="description">
Update the environment variable for your site. Global variables can be set in <Link
Update the environment variable for your {productLabel}. Global variables can be set in
<Link
variant="muted"
href={`${base}/project-${page.params.region}-${page.params.project}/settings`}
>project settings</Link
@@ -13,6 +13,8 @@
export let showEditor = false;
export let variables: Partial<Models.Variable>[];
export let docsLink =
'https://appwrite.io/docs/products/sites/develop#accessing-environment-variables';
const editableVariables = variables.filter((variable) => !variable.secret);
const secretVariables = variables.filter((variable) => variable.secret);
@@ -122,11 +124,7 @@
{#if secretVariables?.length > 0}
<Alert.Inline status="info">
{secretVariables.length} secret variables are hidden from the editor. Their values will
remain unchanged. <Link
href="https://appwrite.io/docs/products/sites/develop#accessing-environment-variables"
external
variant="muted">Learn more</Link
>.
remain unchanged. <Link href={docsLink} external variant="muted">Learn more</Link>.
</Alert.Inline>
{/if}
<Layout.Stack gap="s">
@@ -59,6 +59,45 @@
let detectingRuntime = true;
type DetectedVariable = {
key?: string;
name?: string;
value?: string;
secret?: boolean;
};
function normalizeDetectedVariables(detected: DetectedVariable[] = []) {
const normalized: Partial<Models.Variable>[] = [];
detected.forEach((variable) => {
const key = variable.key ?? variable.name;
if (!key) {
return;
}
normalized.push({
key,
value: variable.value ?? '',
secret: variable.secret ?? false
});
});
return normalized;
}
function mergeVariables(
existing: Partial<Models.Variable>[],
detected: Partial<Models.Variable>[]
) {
const map = new Map(existing.map((variable) => [variable.key, variable]));
detected.forEach((variable) => {
if (!variable.key) {
return;
}
if (!map.has(variable.key)) {
map.set(variable.key, variable);
}
});
return Array.from(map.values());
}
onMount(async () => {
installation.set(data.installation);
repository.set(data.repository);
@@ -82,6 +121,10 @@
entrypoint = detections.entrypoint;
buildCommand = detections.commands;
runtime = detections.runtime as Runtime;
const detectedVariables = normalizeDetectedVariables(detections?.variables);
if (detectedVariables.length) {
variables = mergeVariables(variables, detectedVariables);
}
trackEvent(Submit.FrameworkDetect, { runtime, source: 'repository' });
} catch (error) {
@@ -189,7 +232,11 @@
installationId={data.installation.$id}
repositoryId={data.repository.id} />
<Configuration bind:buildCommand bind:roles />
<Configuration
bind:buildCommand
bind:roles
bind:variables
isVariablesLoading={detectingRuntime} />
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -3,9 +3,13 @@
import { Link } from '$lib/elements';
import { InputText } from '$lib/elements/forms';
import { Accordion, Fieldset, Layout } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
import EnvironmentVariables from '$lib/components/variables/environmentVariables.svelte';
export let buildCommand = '';
export let roles: string[] = [];
export let variables: Partial<Models.Variable>[] = [];
export let isVariablesLoading = false;
</script>
<Fieldset legend="Settings">
@@ -31,5 +35,12 @@
<Roles bind:roles />
</Layout.Stack>
</Accordion>
<EnvironmentVariables
bind:variables
productLabel="function"
docsLink="https://appwrite.io/docs/products/functions/develop"
analyticsSource="function_configuration"
analyticsCreateSource="function_configuration"
isLoading={isVariablesLoading} />
</Layout.Stack>
</Fieldset>
@@ -1,38 +1,10 @@
<script lang="ts">
import { Empty, Paginator } from '$lib/components';
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import {
Fieldset,
Layout,
Popover,
Icon,
Table,
Badge,
InteractiveText,
ActionMenu,
Accordion,
Tooltip,
Button as PinkButton
} from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
IconCode,
IconUpload,
IconPlus,
IconTrash,
IconEyeOff,
IconPencil
} from '@appwrite.io/pink-icons-svelte';
import { Fieldset, Layout, Accordion } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
import { iconPath } from '$lib/stores/app';
import VariableEditorModal from './variableEditorModal.svelte';
import SecretVariableModal from './secretVariableModal.svelte';
import ImportSiteVariablesModal from './importSiteVariablesModal.svelte';
import CreateVariableModal from './createVariableModal.svelte';
import DeleteVariableModal from './deleteVariableModal.svelte';
import UpdateVariableModal from './updateVariableModal.svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
import { getFrameworkIcon } from '$lib/stores/sites';
import EnvironmentVariables from '$lib/components/variables/environmentVariables.svelte';
export let frameworks: Models.Framework[];
export let selectedFramework: Models.Framework;
@@ -42,18 +14,11 @@
frameworkData?.adapters.find((adapter) => adapter.key === 'static');
export let variables: Partial<Models.Variable>[] = [];
export let isVariablesLoading = false;
export let installCommand = '';
export let buildCommand = '';
export let outputDirectory = '';
let showEditorModal = false;
let showImportModal = false;
let showSecretModal = false;
let showCreate = false;
let showUpdate = false;
let showDelete = false;
let currentVariable: Partial<Models.Variable>;
let frameworkId = selectedFramework.key;
$: if (!installCommand || !buildCommand || !outputDirectory) {
@@ -138,198 +103,7 @@
</Layout.Stack>
</Accordion>
<Accordion title="Environment variables" badge="Optional" hideDivider>
<Layout.Stack gap="xl">
Set up environment variables to securely manage keys and settings for your
project.
<Layout.Stack gap="l">
<Layout.Stack direction="row">
<Layout.Stack direction="row" gap="s">
<Button
secondary
size="s"
on:mousedown={() => {
showEditorModal = true;
trackEvent(Click.VariablesUpdateClick, {
source: 'site_configuration'
});
}}>
<Icon slot="start" icon={IconCode} /> Editor
</Button>
<Button
secondary
size="s"
on:mousedown={() => {
showImportModal = true;
trackEvent(Click.VariablesImportClick, {
source: 'site_configuration'
});
}}>
<Icon slot="start" icon={IconUpload} /> Import .env
</Button>
</Layout.Stack>
{#if variables?.length}
<Button
secondary
size="s"
on:mousedown={() => {
showCreate = true;
trackEvent(Click.VariablesCreateClick, {
source: 'site_settings'
});
}}>
<Icon slot="start" icon={IconPlus} /> Create variable
</Button>
{/if}
</Layout.Stack>
{#if variables?.length}
<Paginator
items={variables}
limit={6}
hideFooter={variables.length <= 6}>
{#snippet children(paginatedItems)}
<Table.Root
let:root
columns={[
{ id: 'key', width: 200 },
{ id: 'value' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}
>Key</Table.Header.Cell>
<Table.Header.Cell column="value" {root}
>Value</Table.Header.Cell>
<Table.Header.Cell column="actions" {root}
></Table.Header.Cell>
</svelte:fragment>
{#each paginatedItems as variable}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}
>{variable.key}</Table.Cell>
<Table.Cell column="value" {root}>
<!-- TODO: fix max width -->
<div style="max-width: 20rem">
{#if variable.secret}
<Tooltip maxWidth="26rem">
<Badge
content="Secret"
variant="secondary"
size="s" />
<svelte:fragment slot="tooltip">
This value is secret, you cannot
see its value.
</svelte:fragment>
</Tooltip>
{:else}
<InteractiveText
variant="secret"
isVisible={false}
text={variable.value} />
{/if}
</div>
</Table.Cell>
<Table.Cell column="actions" {root}>
<div style="margin-inline-start: auto">
<Popover
padding="none"
placement="bottom-end"
let:toggle>
<PinkButton.Button
icon
variant="text"
size="s"
aria-label="More options"
on:click={(e) => {
e.preventDefault();
toggle(e);
}}>
<Icon
icon={IconDotsHorizontal}
size="s" />
</PinkButton.Button>
<svelte:fragment
slot="tooltip"
let:toggle>
<ActionMenu.Root>
{#if !variable?.secret}
<ActionMenu.Item.Button
leadingIcon={IconPencil}
on:click={(e) => {
toggle(e);
currentVariable =
variable;
showUpdate = true;
}}>
Update
</ActionMenu.Item.Button>
{/if}
{#if !variable?.secret}
<ActionMenu.Item.Button
leadingIcon={IconEyeOff}
on:click={(e) => {
toggle(e);
currentVariable =
variable;
showSecretModal = true;
}}>
Secret
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={(e) => {
toggle(e);
currentVariable =
variable;
showDelete = true;
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</div>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else}
<Empty on:click={() => (showCreate = true)}
>Create variables to get started</Empty>
{/if}
</Layout.Stack>
</Layout.Stack>
</Accordion>
<EnvironmentVariables bind:variables isLoading={isVariablesLoading} />
</Layout.Stack>
</Layout.Stack>
</Fieldset>
{#if showEditorModal}
<VariableEditorModal bind:variables bind:showEditor={showEditorModal} />
{/if}
{#if showSecretModal}
<SecretVariableModal bind:show={showSecretModal} bind:currentVariable bind:variables />
{/if}
{#if showImportModal}
<ImportSiteVariablesModal bind:show={showImportModal} bind:variables />
{/if}
{#if showCreate}
<CreateVariableModal bind:show={showCreate} bind:variables />
{/if}
{#if showUpdate}
<UpdateVariableModal bind:show={showUpdate} bind:variables bind:selectedVar={currentVariable} />
{/if}
{#if showDelete}
<DeleteVariableModal bind:show={showDelete} bind:variables bind:currentVariable />
{/if}
@@ -47,6 +47,46 @@
let silentMode = false;
let domain = data.domain;
let domainIsValid = true;
let isVariablesLoading = true;
type DetectedVariable = {
key?: string;
name?: string;
value?: string;
secret?: boolean;
};
function normalizeDetectedVariables(detected: DetectedVariable[] = []) {
const normalized: Partial<Models.Variable>[] = [];
detected.forEach((variable) => {
const key = variable.key ?? variable.name;
if (!key) {
return;
}
normalized.push({
key,
value: variable.value ?? '',
secret: variable.secret ?? false
});
});
return normalized;
}
function mergeVariables(
existing: Partial<Models.Variable>[],
detected: Partial<Models.Variable>[]
) {
const map = new Map(existing.map((variable) => [variable.key, variable]));
detected.forEach((variable) => {
if (!variable.key) {
return;
}
if (!map.has(variable.key)) {
map.set(variable.key, variable);
}
});
return Array.from(map.values());
}
onMount(async () => {
installation.set(data.installation);
@@ -58,6 +98,7 @@
async function detectFramework() {
try {
isVariablesLoading = true;
const response = await sdk
.forProject(page.params.region, page.params.project)
.vcs.createRepositoryDetection({
@@ -71,6 +112,10 @@
installCommand = adapter?.installCommand;
buildCommand = adapter?.buildCommand;
outputDirectory = adapter?.outputDirectory;
const detectedVariables = normalizeDetectedVariables(response?.variables);
if (detectedVariables.length) {
variables = mergeVariables(variables, detectedVariables);
}
trackEvent(Submit.FrameworkDetect, {
source: 'repository',
framework: framework.key
@@ -78,6 +123,8 @@
} catch (error) {
framework = data.frameworks.frameworks.find((f) => f.key === 'other');
trackError(error, Submit.FrameworkDetect);
} finally {
isVariablesLoading = false;
}
}
@@ -201,6 +248,7 @@
bind:outputDirectory
bind:selectedFramework={framework}
bind:variables
{isVariablesLoading}
frameworks={data.frameworks.frameworks} />
{/key}