From 3f8ca05f7ea947fe8d4f98900afe1aa561d1eef7 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan Date: Fri, 27 Mar 2026 16:33:18 +0530 Subject: [PATCH] feat:env var pagination --- .../project-[region]-[project]/+layout.svelte | 2 + .../rawVariableEditor.svelte | 229 +++++++++++++++--- .../settings/+page.svelte | 4 + .../settings/+page.ts | 34 ++- .../updateVariables.svelte | 70 +++++- .../uploadVariablesModal.svelte | 79 ++++-- .../variablesImportBox.svelte | 193 +++++++++++++++ .../variablesOperation.ts | 40 +++ 8 files changed, 588 insertions(+), 63 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/variablesImportBox.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/variablesOperation.ts diff --git a/src/routes/(console)/project-[region]-[project]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/+layout.svelte index 4f358189d..0f49a7a36 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/+layout.svelte @@ -1,5 +1,6 @@ @@ -138,10 +255,10 @@

- (tab = 'env')} active={tab === 'env'}> + changeTab('env')} active={tab === 'env'}> ENV - (tab = 'json')} active={tab === 'json'}> + changeTab('json')} active={tab === 'json'}> JSON @@ -151,23 +268,53 @@ {:else if tab === 'json'} {/if} + {#if totalEntries > EDITOR_PAGE_LIMIT} + +

+ Variables {pageOffset + 1}-{Math.min( + pageOffset + EDITOR_PAGE_LIMIT, + totalEntries + )} of {totalEntries} +

+ + + + +
+ {/if} - + - + +
diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index 2738caf61..a49c3fbfc 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -97,6 +97,10 @@ {sdkDeleteVariable} isGlobal variableList={data.variables} + allVariableList={data.allVariables} + backendPagination + variablesOffset={data.variablesOffset} + variablesLimit={data.limit} project={data.project} analyticsSource="project_settings" /> diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/+page.ts index 33685ac28..31f719abe 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.ts @@ -8,10 +8,36 @@ export const load: PageLoad = async ({ depends, url, params }) => { depends(Dependencies.PROJECT_INSTALLATIONS); const limit = PAGE_LIMIT; const offset = Number(url.searchParams.get('offset') ?? 0); + const variablesOffset = Number(url.searchParams.get('variablesOffset') ?? 0); + const projectSdk = sdk.forProject(params.region, params.project); - const [variables, installations] = await Promise.all([ - sdk.forProject(params.region, params.project).projectApi.listVariables(), - sdk.forProject(params.region, params.project).vcs.listInstallations({ + async function listAllVariables() { + const allVariables = []; + let nextOffset = 0; + let total = 0; + + do { + const response = await projectSdk.projectApi.listVariables({ + queries: [Query.limit(limit), Query.offset(nextOffset)] + }); + + allVariables.push(...response.variables); + total = response.total; + nextOffset += response.variables.length; + } while (nextOffset < total); + + return { + total, + variables: allVariables + }; + } + + const [variables, allVariables, installations] = await Promise.all([ + projectSdk.projectApi.listVariables({ + queries: [Query.limit(limit), Query.offset(variablesOffset)] + }), + listAllVariables(), + projectSdk.vcs.listInstallations({ queries: [Query.limit(limit), Query.offset(offset)] }) ]); @@ -19,7 +45,9 @@ export const load: PageLoad = async ({ depends, url, params }) => { return { limit, offset, + variablesOffset, variables, + allVariables, installations }; }; diff --git a/src/routes/(console)/project-[region]-[project]/updateVariables.svelte b/src/routes/(console)/project-[region]-[project]/updateVariables.svelte index a2052f86a..168aad860 100644 --- a/src/routes/(console)/project-[region]-[project]/updateVariables.svelte +++ b/src/routes/(console)/project-[region]-[project]/updateVariables.svelte @@ -4,6 +4,7 @@ import { Button } from '$lib/elements/forms'; import { CardGrid, Empty, Output, PaginationInline } from '$lib/components'; import UploadVariables from './uploadVariablesModal.svelte'; + import { variablesOperation, type VariablesOperationItem } from './variablesOperation'; import { goto, invalidate } from '$app/navigation'; import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { Dependencies } from '$lib/constants'; @@ -43,6 +44,7 @@ export let project: Models.Project; export let variableList: Models.VariableList; + export let allVariableList: Models.VariableList | undefined = undefined; export let globalVariableList: Models.VariableList | undefined = undefined; export let analyticsSource = ''; export let isGlobal: boolean; @@ -59,6 +61,9 @@ ) => Promise; export let sdkDeleteVariable: (variableId: string) => Promise; export let product: 'function' | 'site' = 'function'; + export let backendPagination = false; + export let variablesOffset = 0; + export let variablesLimit = 10; let selectedVar: Models.Variable = null; let showVariablesUpload = false; @@ -71,6 +76,9 @@ let deleteError: string; let offset = 0; const limit = 10; + function handleVariablesImportStatus(detail: VariablesOperationItem) { + variablesOperation.set(detail); + } async function handleVariableCreated(event: CustomEvent) { const variables = event.detail; @@ -140,10 +148,25 @@ } async function handleVariableDeleted() { + const deleteId = selectedVar.$id; + try { + variablesOperation.set({ + id: deleteId, + count: 1, + mode: 'delete', + status: 'deleting' + }); + await sdkDeleteVariable(selectedVar.$id); showDeleteModal = false; selectedVar = null; + variablesOperation.set({ + id: deleteId, + count: 1, + mode: 'delete', + status: 'completed' + }); addNotification({ type: 'success', message: `${project.name} ${ @@ -153,6 +176,13 @@ trackEvent(Submit.VariableDelete); } catch (error) { deleteError = error.message; + variablesOperation.set({ + id: deleteId, + count: 1, + mode: 'delete', + status: 'failed', + error: error.message + }); trackError(error, Submit.VariableDelete); } } @@ -260,8 +290,13 @@ }) : []; + $: editorVariableList = allVariableList ?? variableList; + $: displayedVariables = backendPagination + ? variableList.variables + : variableList.variables.slice(offset, offset + limit); + $: hasConflictOnPage = globalVariableList - ? variableList.variables.slice(offset, offset + limit).filter((variable) => { + ? displayedVariables.filter((variable) => { return globalVariableList.variables.find((globalVariable) => { return variable.key === globalVariable.key; }); @@ -279,6 +314,17 @@ { id: 'value', width: { min: 200, max: 400 } }, { id: 'actions', width: 50 } ]; + + async function handleVariablesPageChange() { + const nextUrl = new URL(page.url); + + nextUrl.searchParams.set('variablesOffset', String(variablesOffset)); + + await goto(nextUrl, { + keepFocus: true, + noScroll: true + }); + } @@ -364,7 +410,7 @@ Value - {#each variableList.variables.slice(offset, offset + limit) as variable} + {#each displayedVariables as variable} {@const isConflicting = globalVariableList @@ -461,10 +507,19 @@ {/each} - {#if sum > limit} + {#if sum > (backendPagination ? variablesLimit : limit)}

Total variables: {sum}

- + {#if backendPagination} + + {:else} + + {/if}
{/if} @@ -504,7 +559,7 @@ {sdkCreateVariable} {sdkUpdateVariable} {sdkDeleteVariable} - {variableList} + variableList={editorVariableList} bind:showEditor={showEditorModal} /> {/if} @@ -524,8 +579,9 @@ + variableList={editorVariableList} + bind:show={showVariablesUpload} + onStatusChange={handleVariablesImportStatus} /> {/if} {#if showDeleteModal} diff --git a/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte b/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte index 5cf19fe9d..edbb7613c 100644 --- a/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/uploadVariablesModal.svelte @@ -16,6 +16,7 @@ } from '@appwrite.io/pink-svelte'; import { parse } from '$lib/helpers/envfile'; import { removeFile } from '$lib/helpers/files'; + import type { VariablesOperationItem } from './variablesOperation'; export let show = false; export let variableList: Models.VariableList; @@ -30,10 +31,12 @@ value: string, secret?: boolean ) => Promise; + export let onStatusChange: (detail: VariablesOperationItem) => void = () => {}; let files: FileList; let secret = false; let error: string; + let isSubmitting = false; $: filesList = files?.length ? Array.from(files).map((file) => ({ ...file, @@ -45,6 +48,9 @@ : []; async function handleSubmit() { + let importId = ''; + let uploadCount = 0; + try { if (!files?.length) { throw new Error('No file selected'); @@ -64,27 +70,70 @@ } } + const filteredEntries = entries.filter(([, value]) => !!value); + + if (!filteredEntries.length) { + throw new Error('No variables found'); + } + + if (filteredEntries.length > 100) { + throw new Error('Please upload a file with fewer than 100 environment variables.'); + } + + importId = crypto.randomUUID(); + uploadCount = filteredEntries.length; + isSubmitting = true; + + onStatusChange({ + id: importId, + count: uploadCount, + mode: 'import', + status: 'uploading' + }); + + show = false; + await Promise.all( - entries - .filter(([, value]) => !!value) - .map(([key, value]) => { - const found = variableList.variables.find( - (variable) => variable.key === key - ); - return found - ? sdkUpdateVariable(found.$id, key, value, secret) - : sdkCreateVariable(key, value, secret); - }) + filteredEntries.map(([key, value]) => { + const found = variableList.variables.find((variable) => variable.key === key); + return found + ? sdkUpdateVariable(found.$id, key, value, secret) + : sdkCreateVariable(key, value, secret); + }) ); + onStatusChange({ + id: importId, + count: uploadCount, + mode: 'import', + status: 'completed' + }); + addNotification({ type: 'success', message: `Variables have been uploaded.` }); - - show = false; } catch (e) { error = e.message; + + if (importId) { + onStatusChange({ + id: importId, + count: uploadCount, + mode: 'import', + status: 'failed', + error + }); + } + + if (!show) { + addNotification({ + type: 'error', + message: error + }); + } + } finally { + isSubmitting = false; } } @@ -138,7 +187,9 @@ bind:checked={secret} description="If selected, you and your team won't be able to read the values after creation." /> - - + + diff --git a/src/routes/(console)/project-[region]-[project]/variablesImportBox.svelte b/src/routes/(console)/project-[region]-[project]/variablesImportBox.svelte new file mode 100644 index 000000000..8314b7fa4 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/variablesImportBox.svelte @@ -0,0 +1,193 @@ + + +{#if currentItem} + +
+
+

+ + {currentItem.mode === 'delete' + ? 'Deleting variables' + : 'Importing variables'} + +

+ + +
+ +
+
+
    +
  • +
    +
    + {text(currentItem)} +
    +
    +
    + {#if currentItem.status === 'failed'} + + + + There was an issue + {currentItem.mode === 'delete' + ? ' deleting' + : ' importing'} + variables. + {#if currentItem.error} + (showDetails = true)}> + View details + + {/if} + + + {/if} +
    +
  • +
+ {#if $variablesOperation.length > 1} + + + {offset + 1} of {$variablesOperation.length} + + + + {/if} +
+
+
+
+{/if} + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/variablesOperation.ts b/src/routes/(console)/project-[region]-[project]/variablesOperation.ts new file mode 100644 index 000000000..2f45df2a5 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/variablesOperation.ts @@ -0,0 +1,40 @@ +import { writable } from 'svelte/store'; + +export type VariablesOperationItem = { + id: string; + count: number; + mode: 'import' | 'delete'; + status: 'uploading' | 'deleting' | 'completed' | 'failed'; + error?: string; +}; + +function createVariablesOperation() { + const { subscribe, update, set } = writable([]); + + return { + subscribe, + set: (item: VariablesOperationItem) => + update((items) => { + const index = items.findIndex((current) => current.id === item.id); + + if (index !== -1) { + const next = [...items]; + next[index] = item; + return next; + } + + return [item, ...items]; + }), + clear: (id?: string) => + update((items) => { + if (!id) { + return []; + } + + return items.filter((item) => item.id !== id); + }), + reset: () => set([]) + }; +} + +export const variablesOperation = createVariablesOperation();