update: optimize projects loading.

This commit is contained in:
Darshan
2025-08-24 12:58:59 +05:30
parent f86b824936
commit b51f745be3
10 changed files with 171 additions and 110 deletions
@@ -9,17 +9,22 @@
upgradeURL
} from '$lib/stores/billing';
import { currentPlan } from '$lib/stores/organization';
import { onMount } from 'svelte';
import SelectProjectCloud from './selectProjectCloud.svelte';
import { toLocaleDate } from '$lib/helpers/date';
let showSelectProject: boolean = $state(false);
let selectedProjects: string[] = $state([]);
onMount(() => {
selectedProjects = page.data.organization?.projects || [];
let showSelectProject: boolean = $state(false);
const organizationId = $derived(page.data.organization?.$id);
$effect(() => {
if (organizationId) {
selectedProjects = page.data.organization?.projects || [];
}
});
</script>
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
<SelectProjectCloud bind:showSelectProject bind:selectedProjects {organizationId} />
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
<HeaderAlert
@@ -1,33 +1,30 @@
<script lang="ts">
import { type Models } from '@appwrite.io/console';
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
import { type Models, Query } from '@appwrite.io/console';
import { Alert, Button, Skeleton, Table } from '@appwrite.io/pink-svelte';
import { Modal } from '$lib/components';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { billingProjectsLimitDate } from '$lib/stores/billing';
import { page } from '$app/state';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { currentPlan } from '$lib/stores/organization';
let {
showSelectProject = $bindable(false),
selectedProjects = $bindable([])
selectedProjects = $bindable([]),
organizationId
}: {
showSelectProject: boolean;
selectedProjects: string[];
organizationId: string;
} = $props();
let projects = $state<Array<Models.Project>>([]);
let error = $state<string | null>(null);
let loading = $state(false);
let projectsLoadingError = $state<string | null>(null);
onMount(() => {
const currentOrgId = page.data.organization?.$id;
projects =
page.data.currentOrgId === currentOrgId ? page.data.allProjects?.projects || [] : [];
});
let error = $state<string | null>(null);
let projects = $state<Array<Models.Project>>([]);
let projectsToArchive = $derived(
projects.filter((project) => !selectedProjects.includes(project.$id))
@@ -39,6 +36,7 @@
projects[0].teamId,
selectedProjects
);
showSelectProject = false;
invalidate(Dependencies.ORGANIZATION);
addNotification({
@@ -69,47 +67,95 @@
return result;
}
$effect(() => {
if (!showSelectProject) return;
const areProjectsLoaded = projects.length > 0;
const teamIdInLoadedProjects = areProjectsLoaded ? projects[0].teamId : null;
if (organizationId != teamIdInLoadedProjects) {
loading = true;
sdk.forConsole.projects
.list([
Query.equal('teamId', organizationId),
Query.limit(1000) // Get all projects for organization
])
.then((loadedProjects) => (projects = loadedProjects.projects))
.catch((err) => (projectsLoadingError = err.message))
.finally(() => (loading = false));
}
});
</script>
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
<Modal bind:show={showSelectProject} title="Manage projects" onSubmit={updateSelected}>
<svelte:fragment slot="description">
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be
blocked after this date.
</svelte:fragment>
{#if error}
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
{/if}
<Table.Root
let:root
allowSelection
bind:selectedRows={selectedProjects}
columns={[{ id: 'name' }, { id: 'created' }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each projects as project}
<Table.Row.Base {root} id={project.$id}>
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
<Table.Cell column="created" {root}
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if selectedProjects.length > $currentPlan?.projects}
<div class="u-text-warning u-mb-4">
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
{#if loading}
<div class="skeleton-projects">
<Table.Root let:root allowSelection columns={[{ id: 'name' }, { id: 'created' }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each Array.from({ length: 5 }) as _}
<Table.Row.Base {root} select="disabled">
<Table.Cell column="name" {root}>
<Skeleton variant="line" height={20} width="80%" />
</Table.Cell>
<Table.Cell column="created" {root}>
<Skeleton variant="line" height={20} width="80%" />
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
</div>
{/if}
{#if selectedProjects.length === $currentPlan?.projects}
<Alert.Inline
status="warning"
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
<span>
{@html formatProjectsToArchive()}
will be archived.
</span>
</Alert.Inline>
{:else if projectsLoadingError}
<Alert.Inline status="error" title="Error">{projectsLoadingError}</Alert.Inline>
{:else}
{#if error}
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
{/if}
<Table.Root
let:root
allowSelection
bind:selectedRows={selectedProjects}
columns={[{ id: 'name' }, { id: 'created' }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each projects as project}
<Table.Row.Base {root} id={project.$id}>
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
<Table.Cell column="created" {root}
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if selectedProjects.length > $currentPlan?.projects}
<div class="u-text-warning u-mb-4">
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
</div>
{/if}
{#if selectedProjects.length === $currentPlan?.projects}
<Alert.Inline
status="warning"
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
<span>
{@html formatProjectsToArchive()}
will be archived.
</span>
</Alert.Inline>
{/if}
{/if}
<svelte:fragment slot="footer">
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
@@ -118,3 +164,13 @@
>Save</Button.Button>
</svelte:fragment>
</Modal>
<style lang="scss">
/* disable the top select all selector button */
.skeleton-projects :global([role='rowheader'] button) {
opacity: 0.4;
cursor: default;
pointer-events: none;
background: var(--bgcolor-neutral-tertiary);
}
</style>
+36 -8
View File
@@ -22,7 +22,9 @@
import { base } from '$app/paths';
import { currentPlan, newOrgModal } from '$lib/stores/organization';
import { Click, trackEvent } from '$lib/actions/analytics';
import type { Models } from '@appwrite.io/console';
import { type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
type Organization = {
name: string;
@@ -63,15 +65,15 @@
}
} = createMenu();
let isLoadingProjects = true;
let isLoadingProjects = false;
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
export let organizations: Organization[] = [];
export let currentProject: Models.Project | null = null;
export let projects: Promise<Models.ProjectList> = Promise.resolve(loadedProjects);
let projectsBottomSheetOpen = false;
let organisationBottomSheetOpen = false;
let projectsBottomSheet: Promise<SheetMenu | null> = null;
function createOrg() {
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
@@ -99,13 +101,25 @@
}
};
async function createProjectsBottomSheet(organization: Organization): Promise<SheetMenu> {
async function createProjectsBottomSheet(
organization: Organization
): Promise<SheetMenu | null> {
if (!isOnProjects) return null;
isLoadingProjects = true;
// null on non-org/project path like `onboarding`.
loadedProjects = (await projects) ?? loadedProjects;
loadedProjects =
(await sdk.forConsole.projects.list([
Query.equal('teamId', organizationId ?? page.data.currentOrgId),
Query.limit(5),
Query.orderDesc('$updatedAt')
])) ?? loadedProjects;
for (const project of loadedProjects.projects) {
project.region ??= 'default';
}
isLoadingProjects = false;
const createProjectItem = {
@@ -185,8 +199,6 @@
$: selectedOrg = organizations.find((org) => org.isSelected);
$: projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
$: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg);
$: correctPlanName =
@@ -198,6 +210,20 @@
: selectedOrg?.tierName; // fallback
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
$: organizationId = currentProject?.teamId;
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
$: shouldReloadProjects = isLoadingProjects
? false
: currentProject && loadedProjects.projects.length
? loadedProjects.projects[0].teamId != currentProject.teamId
: !loadedProjects.projects.length;
$: if (shouldReloadProjects) {
projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
}
</script>
<svelte:window on:resize={onResize} />
@@ -380,7 +406,9 @@
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet} />
{#await projectsBottomSheet then menu}
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
{#if menu}
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
{/if}
{/await}
{/key}
+3 -11
View File
@@ -1,15 +1,6 @@
<script lang="ts" context="module">
import type { HTMLAttributes } from 'svelte/elements';
export type NavbarProject = {
name: string;
$id: string;
region: string;
isSelected: boolean;
platformCount: number;
pingCount: number;
};
export type BaseNavbarProps = HTMLAttributes<HTMLHeadElement> & {
logo: {
src: string;
@@ -106,7 +97,6 @@
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
export let showAccountMenu = false;
export let currentProject: Models.Project = undefined;
export let projects: Promise<Models.ProjectList> = undefined;
let activeTheme = $app.theme;
let shouldAnimateThemeToggle = false;
@@ -138,7 +128,9 @@
class="only-desktop">
<img src={logo.src} alt={logo.alt} />
</a>
<Breadcrumbs {organizations} {projects} {currentProject} />
<Breadcrumbs {organizations} {currentProject} />
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
<div class="only-desktop" style:margin-inline-start="-16px">
<Button.Anchor
-3
View File
@@ -22,7 +22,6 @@
export let showFooter = true;
export let showSideNavigation = false;
export let selectedProject: Models.Project = null;
export let projects: Promise<Models.ProjectList> = undefined;
let yOnMenuOpen: number;
let showContentTransition = false;
@@ -104,8 +103,6 @@
};
}),
projects: projects,
currentProject: selectedProject
};
+2
View File
@@ -320,8 +320,10 @@ export function calculateTrialDay(org: Organization) {
export async function checkForProjectsLimit(org: Organization, orgProjectCount?: number) {
if (!isCloud) return;
if (!org) return;
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
if (!plan) return;
if (plan.$id !== BillingPlan.FREE) return;
if (org.projects?.length > 0) return;
+2 -21
View File
@@ -55,8 +55,6 @@
IconSwitchHorizontal
} from '@appwrite.io/pink-icons-svelte';
import type { LayoutData } from './$types';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
export let data: LayoutData;
@@ -298,10 +296,8 @@
if (isCloud) {
currentOrganizationId = org.$id;
const orgProjectCount =
data.allProjects && data.currentOrgId === org.$id
? data.allProjects.projects.length
: undefined;
checkForProjectsLimit(org, orgProjectCount);
data.currentOrgId === org.$id ? data.allProjectsCount : undefined;
await checkForProjectsLimit(org, orgProjectCount);
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
@@ -322,20 +318,6 @@
$: checkForUsageLimits($organization);
$: isOnOnboarding = page.route?.id?.includes('/(console)/onboarding');
$: projects = isOnOnboarding
? null
: sdk.forConsole.projects.list([
Query.equal(
'teamId',
// id from page params ?? id from store ?? id from preferences
page.params.organization ?? currentOrganizationId ?? data.currentOrgId
),
Query.limit(5),
Query.orderDesc('$updatedAt')
]);
$: if ($requestedMigration) {
openMigrationWizard();
}
@@ -356,7 +338,6 @@
!page.url.pathname.includes('/console/onboarding')}
showHeader={!page.url.pathname.includes('/console/onboarding/create-project')}
showFooter={!page.url.pathname.includes('/console/onboarding/create-project')}
{projects}
selectedProject={page.data?.project}>
<!-- <Header slot="header" />-->
<slot />
+9 -11
View File
@@ -30,19 +30,17 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
// Load projects for the current organization if one is selected
let projects = null;
let projectsCount = 0;
if (currentOrgId) {
try {
projects = await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(1000) // Get all projects for organization
]);
for (const project of projects.projects) {
project.region ??= 'default';
}
projectsCount = (
await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(1)
])
).total;
} catch (e) {
// Handle error silently - projects might not be accessible
projects = {};
projectsCount = 0;
}
}
@@ -55,7 +53,7 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
organizations,
consoleVariables,
version: versionData?.version ?? null,
allProjects: projects
allProjectsCount: projectsCount
};
};
@@ -114,9 +114,8 @@
trackError(e, Submit.ProjectCreate);
}
};
onMount(async () => {
checkPricingRefAndRedirect(page.url.searchParams);
});
onMount(async () => checkPricingRefAndRedirect(page.url.searchParams));
function findRegion(project: Models.Project) {
return $regionsStore.regions.find((region) => region.$id === project.region);
@@ -140,7 +139,10 @@
);
</script>
<SelectProjectCloud selectedProjects={data.organization.projects || []} bind:showSelectProject />
<SelectProjectCloud
bind:showSelectProject
organizationId={page.params.organization}
selectedProjects={data.organization.projects || []} />
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
@@ -12,8 +12,8 @@
import { Dependencies } from '$lib/constants';
export let teamId: string;
export let showCreateProjectCloud: boolean;
export let projects: number;
export let showCreateProjectCloud: boolean;
export let regions: Array<Models.ConsoleRegion> = [];
let id: string = null;