Merge remote-tracking branch 'origin/main' into fix-SER-428-Quality-Check-root-drectory-modal-issue

This commit is contained in:
Harsh Mahajan
2026-02-19 19:12:29 +05:30
32 changed files with 453 additions and 333 deletions
+2 -4
View File
@@ -6,13 +6,11 @@
"name": "@appwrite/console",
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@c6f60aa",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@de65a99",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@df765cc",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@df765cc",
"@faker-js/faker": "^9.9.0",
"@plausible-analytics/tracker": "^0.4.4",
"@popperjs/core": "^2.11.8",
@@ -109,7 +107,7 @@
"@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="],
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@c6f60aa", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }],
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@de65a99", { "dependencies": { "json-bigint": "1.0.0" } }],
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
+1 -3
View File
@@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@c6f60aa",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@de65a99",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc",
"@appwrite.io/pink-legacy": "^1.0.3",
@@ -38,8 +38,6 @@
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"echarts": "^5.6.0",
"https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@a4067bf": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@a4067bf",
"https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@a4067bf": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@a4067bf",
"ignore": "^6.0.2",
"nanoid": "^5.1.5",
"nanotar": "^0.1.1",
@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { currentPlan, organization } from '$lib/stores/organization';
import { HeaderAlert } from '$lib/layout';
import { isCloud } from '$lib/system';
import { getChangePlanUrl } from '$lib/stores/billing';
@@ -17,7 +17,7 @@
</script>
{#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)}
{@const areBackupsAvailable = $organization?.billingPlanDetails.backupsEnabled}
{@const areBackupsAvailable = $currentPlan?.backupsEnabled}
{@const subtitle = !areBackupsAvailable
? 'Upgrade your plan to ensure your data stays safe and backed up'
@@ -54,8 +54,6 @@
case 'cname':
if (service === 'sites') {
return $regionalConsoleVariables._APP_DOMAIN_SITES;
} else if (service === 'functions') {
return $regionalConsoleVariables._APP_DOMAIN_FUNCTIONS;
} else {
return $regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME;
}
+9 -9
View File
@@ -3,10 +3,7 @@
import { Uniform, MeshBasicMaterial, Vector2 } from 'three';
import noise from './threlte/shaders/noise.glsl?raw';
import { onMount } from 'svelte';
import { useViewport } from '@threlte/extras';
const { invalidate } = useThrelte();
const viewport = useViewport();
const { invalidate, size } = useThrelte();
let uAspect: Uniform;
let uOpacity: Uniform;
@@ -14,8 +11,8 @@
let uViewportSize: Uniform;
$effect(() => {
const width = $viewport.width;
const height = $viewport.height;
const width = $size.width || 1;
const height = $size.height || 1;
if (uAspect) {
uAspect.value = width / height;
invalidate();
@@ -30,10 +27,13 @@
let material = $state<MeshBasicMaterial>(new MeshBasicMaterial());
onMount(() => {
uAspect = new Uniform($viewport.width / $viewport.height);
const width = $size.width || 1;
const height = $size.height || 1;
uAspect = new Uniform(width / height);
uOpacity = new Uniform(0);
uTime = new Uniform(0);
uViewportSize = new Uniform(new Vector2($viewport.width, $viewport.height));
uViewportSize = new Uniform(new Vector2(width, height));
material.onBeforeCompile = (shader) => {
shader.uniforms.uTime = uTime;
@@ -104,6 +104,6 @@
</script>
<T.OrthographicCamera position={[0, 0, 10]} fov={10} near={0.1} far={1000} makeDefault />
<T.Mesh scale={[$viewport.width, $viewport.height, 1]} {material}>
<T.Mesh scale={[$size.width || 1, $size.height || 1, 1]} {material}>
<T.PlaneGeometry args={[1, 1]} />
</T.Mesh>
@@ -9,12 +9,29 @@
import { page } from '$app/state';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
export let show = false;
export let variables: Partial<Models.Variable>[];
export type ProductLabel = 'site' | 'function';
let newVariables: Partial<Models.Variable>[] = [{ key: '', value: '' }];
let secret = false;
let error = '';
let {
show = $bindable(false),
variables = $bindable(),
productLabel = 'site'
}: {
show: boolean;
variables: Partial<Models.Variable>[];
productLabel?: ProductLabel;
} = $props();
let newVariables = $state<Partial<Models.Variable>[]>([{ key: '', value: '' }]);
let secret = $state(false);
let error = $state('');
$effect(() => {
if (!show) {
newVariables = [{ key: '', value: '' }];
secret = false;
error = '';
}
});
function handleVariable() {
try {
@@ -54,19 +71,17 @@
function removeVariable(index: number) {
if (newVariables.length === 1) {
newVariables[0].key = '';
newVariables[0].value = '';
newVariables = [{ key: '', value: '' }];
} else {
newVariables.splice(index, 1);
newVariables = [...newVariables];
newVariables = newVariables.filter((_, i) => i !== index);
}
}
</script>
<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
@@ -95,7 +110,7 @@
type="button"
size="s"
disabled={newVariables.length === 1 && !pair.key && !pair.value}
on:click={() => removeVariable(i)}>
onclick={() => removeVariable(i)}>
<Icon icon={IconX} />
</PinkButton.Button>
</Layout.Stack>
@@ -0,0 +1,271 @@
<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, { type ProductLabel } from './createVariableModal.svelte';
import DeleteVariableModal from './deleteVariableModal.svelte';
import UpdateVariableModal from './updateVariableModal.svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
const DOCS_LINKS: Record<ProductLabel, string> = {
site: 'https://appwrite.io/docs/products/sites/develop#accessing-environment-variables',
function: 'https://appwrite.io/docs/products/functions/develop#environment-variables'
};
let {
variables = $bindable([]),
productLabel = 'site',
analyticsSource = 'site_configuration',
analyticsCreateSource = 'site_settings',
isLoading = false
}: {
variables: Partial<Models.Variable>[];
productLabel?: ProductLabel;
analyticsSource?: string;
analyticsCreateSource?: string;
isLoading?: boolean;
} = $props();
let showEditorModal = $state(false);
let showImportModal = $state(false);
let showSecretModal = $state(false);
let showCreate = $state(false);
let showUpdate = $state(false);
let showDelete = $state(false);
let currentVariable = $state<Partial<Models.Variable>>(undefined);
const createSource = $derived(analyticsCreateSource || analyticsSource);
const docsLink = $derived(DOCS_LINKS[productLabel]);
const tableColumns = [
{ id: 'key', width: { min: 300 } },
{ id: 'value', width: { min: 280 } },
{ id: 'actions', width: 40 }
];
</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:click={() => {
showEditorModal = true;
trackEvent(Click.VariablesUpdateClick, {
source: analyticsSource
});
}}>
<Icon slot="start" icon={IconCode} /> Editor
</Button>
<Button
secondary
size="s"
on:click={() => {
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:click={() => {
showCreate = true;
trackEvent(Click.VariablesCreateClick, {
source: createSource
});
}}>
<Icon slot="start" icon={IconPlus} /> Create variable
</Button>
{/if}
</Layout.Stack>
{#if isLoading && !variables?.length}
<Table.Root class="responsive-table" let:root columns={tableColumns}>
<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={tableColumns}>
<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: 100%">
{#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={true}
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"
onclick={(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}
onclick={(e) => {
toggle(e);
currentVariable = variable;
showUpdate = true;
}}>
Update
</ActionMenu.Item.Button>
{/if}
{#if !variable?.secret}
<ActionMenu.Item.Button
leadingIcon={IconEyeOff}
onclick={(e) => {
toggle(e);
currentVariable = variable;
showSecretModal = true;
}}>
Secret
</ActionMenu.Item.Button>
{/if}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
onclick={(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;
trackEvent(Click.VariablesCreateClick, {
source: createSource
});
}}>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}
+7
View File
@@ -0,0 +1,7 @@
export { default as CreateVariableModal, type ProductLabel } from './createVariableModal.svelte';
export { default as DeleteVariableModal } from './deleteVariableModal.svelte';
export { default as EnvironmentVariables } from './environmentVariables.svelte';
export { default as ImportVariablesModal } from './importVariablesModal.svelte';
export { default as SecretVariableModal } from './secretVariableModal.svelte';
export { default as UpdateVariableModal } from './updateVariableModal.svelte';
export { default as VariableEditorModal } from './variableEditorModal.svelte';
@@ -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">
+35
View File
@@ -0,0 +1,35 @@
import type { Models } from '@appwrite.io/console';
export function normalizeDetectedVariables(
detected: Models.DetectionVariable[] = []
): Partial<Models.Variable>[] {
const normalized: Partial<Models.Variable>[] = [];
for (const variable of detected) {
const key = variable.name?.trim();
if (!key) {
continue;
}
normalized.push({
key,
value: variable.value ?? '',
secret: false
});
}
return normalized;
}
export 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());
}
+3 -15
View File
@@ -105,8 +105,6 @@ function createFeedbackStore() {
return feedback;
});
},
// TODO: update growth server to accept `billingPlan`.
// TODO: update growth server to accept `source` key to know the feedback source area.
submitFeedback: async (
subject: string,
message: string,
@@ -122,18 +120,6 @@ function createFeedbackStore() {
if (!VARS.GROWTH_ENDPOINT) return;
trackEvent(Submit.FeedbackSubmit);
const customFields: Array<{ id: string; value: string | number }> = [
{ id: '47364', value: currentPage }
];
if (value) {
customFields.push({ id: '40655', value });
}
if (billingPlan) {
customFields.push({ id: '56109', value: billingPlan });
}
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, {
method: 'POST',
headers: {
@@ -143,9 +129,11 @@ function createFeedbackStore() {
subject,
message,
email,
customFields,
firstname: (name || 'Unknown').slice(0, 40),
metaFields: {
currentPage,
npsScore: value,
billingPlan,
source: get(feedback).source,
orgId,
projectId,
@@ -6,7 +6,7 @@
import { base } from '$app/paths';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import Upgrade from '$lib/components/roles/upgrade.svelte';
import { getRoleLabel } from '$lib/stores/billing';
import { getServiceLimit, readOnly, getRoleLabel } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { currentPlan, newMemberModal, organization } from '$lib/stores/organization';
import { isOwner } from '$lib/stores/roles';
@@ -14,7 +14,7 @@
import type { Models } from '@appwrite.io/console';
import Delete from '../deleteMember.svelte';
import Edit from './edit.svelte';
import { isCloud } from '$lib/system';
import { isCloud, GRACE_PERIOD_OVERRIDE } from '$lib/system';
import {
IconDotsHorizontal,
IconInfo,
@@ -45,8 +45,10 @@
// Calculate if button should be disabled and tooltip should show
$: memberCount = data.organizationMembers?.total ?? 0;
$: supportsMembers = $organization?.billingPlanDetails?.addons?.seats;
$: isFreeWithMembers = !supportsMembers && memberCount >= 1;
$: isButtonDisabled = isCloud ? isFreeWithMembers : false;
$: limit = getServiceLimit('members', null, $currentPlan) || Infinity;
$: isLimited = limit !== 0 && limit < Infinity;
$: isButtonDisabled =
isCloud && (($readOnly && !GRACE_PERIOD_OVERRIDE) || (isLimited && memberCount >= limit));
const resend = async (member: Models.Membership) => {
try {
@@ -67,13 +67,11 @@
firstName: ($user?.name ?? '').slice(0, 40),
message: `BAA request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`,
tags: ['cloud'],
customFields: [
{ id: '41612', value: 'BAA' },
{ id: '48493', value: $user?.name ?? '' },
{ id: '48492', value: $organization?.$id ?? '' },
{ id: '48490', value: $user?.$id ?? '' }
],
metaFields: {
category: 'BAA',
userName: $user?.name ?? '',
orgId: $organization?.$id ?? '',
userId: $user?.$id ?? '',
employees: employees,
country: country,
role: role,
@@ -68,13 +68,11 @@
firstName: ($user?.name ?? '').slice(0, 40),
message: `SOC-2 request for ${$organization?.name ?? ''} (${$organization?.$id ?? ''})`,
tags: ['cloud'],
customFields: [
{ id: '41612', value: 'SOC-2' },
{ id: '48493', value: $user?.name ?? '' },
{ id: '48492', value: $organization?.$id ?? '' },
{ id: '48490', value: $user?.$id ?? '' }
],
metaFields: {
category: 'SOC-2',
userName: $user?.name ?? '',
orgId: $organization?.$id ?? '',
userId: $user?.$id ?? '',
employees: employees,
country: country,
role: role,
@@ -23,12 +23,12 @@
try {
const template = await loadEmailTemplate(
project.$id,
EmailTemplateType.Mfachallenge,
EmailTemplateType.MfaChallenge,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Mfachallenge });
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MfaChallenge });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -23,12 +23,12 @@
try {
const template = await loadEmailTemplate(
project.$id,
EmailTemplateType.Magicsession,
EmailTemplateType.MagicSession,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Magicsession });
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.MagicSession });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -23,12 +23,12 @@
try {
const template = await loadEmailTemplate(
project.$id,
EmailTemplateType.Otpsession,
EmailTemplateType.OtpSession,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Otpsession });
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.OtpSession });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -23,12 +23,12 @@
try {
const template = await loadEmailTemplate(
project.$id,
EmailTemplateType.Sessionalert,
EmailTemplateType.SessionAlert,
locale
);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.Sessionalert });
trackEvent(Submit.EmailChangeLocale, { locale, type: EmailTemplateType.SessionAlert });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
@@ -51,13 +51,13 @@ export const templates = [
component: EmailVerificationTemplate
},
{
key: EmailTemplateType.Magicsession,
key: EmailTemplateType.MagicSession,
title: 'Magic URL',
description: 'Send an email to users that sign in with a magic URL.',
component: EmailMagicUrlTemplate
},
{
key: EmailTemplateType.Otpsession,
key: EmailTemplateType.OtpSession,
title: 'OTP session',
description: 'Send an email to users that sign in with a email OTP.',
component: EmailOtpSessionTemplate
@@ -75,13 +75,13 @@ export const templates = [
component: EmailInviteTemplate
},
{
key: EmailTemplateType.Mfachallenge,
key: EmailTemplateType.MfaChallenge,
title: '2FA verification',
description: 'Send a two-factor authentication email to a user.',
component: Email2FaTemplate
},
{
key: EmailTemplateType.Sessionalert,
key: EmailTemplateType.SessionAlert,
title: 'Session alert',
description: 'Send an email to users when a new session is created.',
component: EmailSessionAlertTemplate,
@@ -9,7 +9,7 @@
import deepEqual from 'deep-equal';
import { addNotification } from '$lib/stores/notifications';
import { type Columns, columnsOrder, databaseColumnSheetOptions } from '../store';
import { columnOptions, type Option } from './store';
import { columnOptions, STRING_COLUMN_NAME, type Option } from './store';
import { onMount } from 'svelte';
import { Layout } from '@appwrite.io/pink-svelte';
import { preferences } from '$lib/stores/preferences';
@@ -34,14 +34,21 @@
}
});
$: option = columnOptions.find((option) => {
if (selectedColumn) {
if ('format' in selectedColumn && selectedColumn.format) {
return option?.format === selectedColumn?.format;
} else {
return option?.type === selectedColumn?.type;
}
$: option = columnOptions.find((opt) => {
if (!selectedColumn) return false;
// format match when present
if ('format' in selectedColumn && selectedColumn.format) {
return opt?.format === selectedColumn.format;
}
// Legacy string columns (no format)
if (selectedColumn.type === 'string') {
return opt.name === STRING_COLUMN_NAME;
}
// Fallback: match by type
return opt.type === selectedColumn.type;
}) as Option;
export async function submit() {
@@ -85,6 +85,8 @@ export type Option = {
icon: ComponentType;
};
export const STRING_COLUMN_NAME = 'String (deprecated)';
export const columnOptions: Option[] = [
{
name: 'Text',
@@ -235,7 +237,7 @@ export const columnOptions: Option[] = [
icon: IconRelationship
},
{
name: 'String (deprecated)',
name: STRING_COLUMN_NAME,
sentenceName: 'string',
component: String,
type: 'string',
@@ -22,6 +22,7 @@
import RepoCard from './repoCard.svelte';
import { getIconFromRuntime } from '$lib/stores/runtimes';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { normalizeDetectedVariables, mergeVariables } from '$lib/helpers/variables';
export let data;
@@ -82,6 +83,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 +194,11 @@
installationId={data.installation.$id}
repositoryId={data.repository.id} />
<Configuration bind:buildCommand bind:roles />
<Configuration
bind:buildCommand
bind:roles
bind:variables
isLoading={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';
export let buildCommand = '';
export let roles: string[] = [];
export let variables: Partial<Models.Variable>[] = [];
export let isLoading = false;
</script>
<Fieldset legend="Settings">
@@ -31,5 +35,11 @@
<Roles bind:roles />
</Layout.Stack>
</Accordion>
<EnvironmentVariables
bind:variables
productLabel="function"
analyticsSource="function_configuration"
analyticsCreateSource="function_configuration"
{isLoading} />
</Layout.Stack>
</Fieldset>
@@ -29,8 +29,8 @@
const ruleId = page.url.searchParams.get('rule');
const showCNAMETab = $derived(
Boolean($regionalConsoleVariables._APP_DOMAIN_FUNCTIONS) &&
$regionalConsoleVariables._APP_DOMAIN_FUNCTIONS !== 'localhost'
Boolean($regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME) &&
$regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost'
);
const showATab = $derived(
!isCloud &&
@@ -26,8 +26,8 @@
} = $props();
const showCNAMETab = $derived(
Boolean($regionalConsoleVariables._APP_DOMAIN_FUNCTIONS) &&
$regionalConsoleVariables._APP_DOMAIN_FUNCTIONS !== 'localhost'
Boolean($regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME) &&
$regionalConsoleVariables._APP_DOMAIN_TARGET_CNAME !== 'localhost'
);
const showATab = $derived(
!isCloud &&
@@ -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';
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 isLoading = 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} />
</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}
@@ -27,6 +27,7 @@
import Configuration from '../../configuration.svelte';
import Domain from '../../domain.svelte';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { normalizeDetectedVariables, mergeVariables } from '$lib/helpers/variables';
export let data;
let showExitModal = false;
@@ -47,6 +48,7 @@
let silentMode = false;
let domain = data.domain;
let domainIsValid = true;
let isVariablesLoading = true;
onMount(async () => {
installation.set(data.installation);
@@ -58,6 +60,7 @@
async function detectFramework() {
try {
isVariablesLoading = true;
const response = await sdk
.forProject(page.params.region, page.params.project)
.vcs.createRepositoryDetection({
@@ -71,6 +74,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 +85,8 @@
} catch (error) {
framework = data.frameworks.frameworks.find((f) => f.key === 'other');
trackError(error, Submit.FrameworkDetect);
} finally {
isVariablesLoading = false;
}
}
@@ -201,6 +210,7 @@
bind:outputDirectory
bind:selectedFramework={framework}
bind:variables
isLoading={isVariablesLoading}
frameworks={data.frameworks.frameworks} />
{/key}
+8 -8
View File
@@ -136,14 +136,14 @@
formData.append('message', $supportData.message);
formData.append('tags[]', categoryTopicTag);
formData.append(
'customFields',
JSON.stringify([
{ id: '41612', value: $supportData.category },
{ id: '48492', value: $organization?.$id ?? '' },
{ id: '48491', value: $supportData?.project ?? '' },
{ id: '56023', value: $supportData?.severity ?? '' },
{ id: '56024', value: $organization?.billingPlanId ?? '' }
])
'metaFields',
JSON.stringify({
category: $supportData.category,
orgId: $organization?.$id ?? '',
projectId: $supportData?.project ?? '',
severity: $supportData?.severity ?? '',
billingPlan: $organization?.billingPlanId ?? ''
})
);
if (files && files.length > 0) {
formData.append('attachment', files[0]);