mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: add paused project modal and update console access tracking logic
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7dc3a8f",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c1feb89",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
@@ -24,6 +24,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deep-equal": "^2.2.3",
|
||||
"echarts": "^5.6.0",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478",
|
||||
"ignore": "^6.0.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"nanotar": "^0.1.1",
|
||||
@@ -107,7 +108,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@7dc3a8f", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }],
|
||||
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }],
|
||||
|
||||
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7dc3a8f",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c1feb89",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
|
||||
@@ -25,6 +25,34 @@
|
||||
canWriteSites
|
||||
} from '$lib/stores/roles';
|
||||
import CsvImportBox from '$lib/components/csvImportBox.svelte';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { isCloud } from '$lib/system';
|
||||
import PausedProjectModal from './pausedProjectModal.svelte';
|
||||
|
||||
/**
|
||||
* Calculate if the project is paused based on console access date and plan's inactivity threshold.
|
||||
*/
|
||||
function isProjectPaused(
|
||||
consoleAccessedAt: string | null | undefined,
|
||||
projectInactivityDays: number | undefined
|
||||
): boolean {
|
||||
if (!isCloud) return false;
|
||||
if (!projectInactivityDays || projectInactivityDays <= 0) return false;
|
||||
if (!consoleAccessedAt) return false;
|
||||
|
||||
const lastAccess = new Date(consoleAccessedAt);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - lastAccess.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
return diffDays >= projectInactivityDays;
|
||||
}
|
||||
|
||||
let showPausedModal: boolean;
|
||||
$: showPausedModal = isProjectPaused(
|
||||
($project as { consoleAccessedAt?: string })?.consoleAccessedAt,
|
||||
$currentPlan?.projectInactivityDays
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
|
||||
@@ -114,6 +142,10 @@
|
||||
|
||||
<slot />
|
||||
|
||||
{#if isCloud}
|
||||
<PausedProjectModal bind:show={showPausedModal} projectId={$project.$id} />
|
||||
{/if}
|
||||
|
||||
<div class="layout-level-progress-bars">
|
||||
<UploadBox />
|
||||
<MigrationBox />
|
||||
|
||||
@@ -21,16 +21,6 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
const project = await sdk.forConsole.projects.get({ projectId: params.project });
|
||||
project.region ??= 'default';
|
||||
|
||||
// Track console access for cloud only (fire-and-forget, backend has 6-day cooldown)
|
||||
if (isCloud) {
|
||||
generateFingerprintToken()
|
||||
.then((fingerprint) => {
|
||||
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
|
||||
return sdk.forConsole.projects.updateConsoleAccess({ projectId: params.project });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// fast path without a network call!
|
||||
let organization = (organizations as Models.OrganizationList)?.teams?.find(
|
||||
(org) => org.$id === project.teamId
|
||||
@@ -103,6 +93,30 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
plansInfo.set(organization.billingPlanId, organizationPlan);
|
||||
}
|
||||
|
||||
// Track console access for cloud only (fire-and-forget, backend has 6-day cooldown)
|
||||
// Don't call if project is paused - user must explicitly resume via createConsoleAccess
|
||||
if (isCloud) {
|
||||
const projectInactivityDays = organizationPlan?.projectInactivityDays ?? 0;
|
||||
const consoleAccessedAt = (project as { consoleAccessedAt?: string }).consoleAccessedAt;
|
||||
|
||||
let isPaused = false;
|
||||
if (projectInactivityDays > 0 && consoleAccessedAt) {
|
||||
const lastAccess = new Date(consoleAccessedAt);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now.getTime() - lastAccess.getTime()) / (1000 * 60 * 60 * 24));
|
||||
isPaused = diffDays >= projectInactivityDays;
|
||||
}
|
||||
|
||||
if (!isPaused) {
|
||||
generateFingerprintToken()
|
||||
.then((fingerprint) => {
|
||||
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
|
||||
return sdk.forConsole.projects.updateConsoleAccess({ projectId: params.project });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
organization,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { trackError } from '$lib/actions/analytics';
|
||||
import { generateFingerprintToken } from '$lib/helpers/fingerprint';
|
||||
import { Alert, Layout, Modal, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
export let projectId: string;
|
||||
|
||||
let loading = false;
|
||||
let error: string | null = null;
|
||||
|
||||
async function handleResume() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const fingerprint = await generateFingerprintToken();
|
||||
sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint;
|
||||
|
||||
await sdk.forConsole.projects.createConsoleAccess({ projectId });
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Project resumed successfully'
|
||||
});
|
||||
|
||||
// Reload project data to get updated consoleAccessedAt
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
|
||||
show = false;
|
||||
} catch (e) {
|
||||
const message =
|
||||
e && typeof e === 'object' && 'message' in e
|
||||
? String((e as { message: string }).message)
|
||||
: 'Failed to resume project. Please try again.';
|
||||
error = message;
|
||||
trackError(e, 'resume_paused_project');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Project paused" bind:open={show} size="s" dismissible={false}>
|
||||
<Layout.Stack gap="m">
|
||||
<Typography.Text>
|
||||
This project has been paused due to inactivity on the free plan.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
Your data is safe and will remain intact. Resume the project to continue using it.
|
||||
</Typography.Text>
|
||||
|
||||
{#if error}
|
||||
<Alert.Inline status="error" dismissible on:dismiss={() => (error = null)}>
|
||||
{error}
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button disabled={loading} on:click={handleResume}>
|
||||
{#if loading}
|
||||
Resuming...
|
||||
{:else}
|
||||
Resume project
|
||||
{/if}
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user