mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1010 from appwrite/feat-org-limit-banner-alert
feat: oraganization limit banner
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { readOnly, tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.STARTER && $readOnly && !$page.url.pathname.includes('/console/account')}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
|
||||
<svelte:fragment>
|
||||
Usage for the <b>{$organization.name}</b> organization has reached the limits of the Starter
|
||||
plan. Consider upgrading to increase your resource usage.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href={`${base}/console/organization-${$organization?.$id}/usage`}
|
||||
text
|
||||
fullWidthMobile>
|
||||
<span class="text">View usage</span>
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => {
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'limit_reached_banner'
|
||||
});
|
||||
}}
|
||||
secondary
|
||||
fullWidthMobile>
|
||||
<span class="text">Upgrade plan</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -24,9 +24,6 @@
|
||||
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
|
||||
import { Feedback } from '$lib/components/feedback';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { showExcess } from '$routes/console/organization-[organization]/store';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
let showDropdown = false;
|
||||
@@ -105,17 +102,6 @@
|
||||
<svelte:component this={$page.data.breadcrumbs} />
|
||||
{/if}
|
||||
|
||||
{#if !$page.url.pathname.includes('/console/account') && $readOnly}
|
||||
<div style="min-inline-size: fit-content">
|
||||
<Pill danger button on:click={() => ($showExcess = true)}>
|
||||
<div>
|
||||
<span class="icon-exclamation-circle" aria-hidden="true" />
|
||||
<span>limit reached</span>
|
||||
</div>
|
||||
</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="main-header-end">
|
||||
<nav class="u-flex is-only-desktop u-cross-center">
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
|
||||
|
||||
@@ -23,6 +23,10 @@ import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
|
||||
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
|
||||
import LimitReached from '$lib/components/billing/alerts/limitReached.svelte';
|
||||
import { wizard } from './wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
|
||||
|
||||
@@ -179,26 +183,74 @@ export function calculateTrialDay(org: Organization) {
|
||||
}
|
||||
|
||||
export async function checkForUsageLimit(org: Organization) {
|
||||
if (org.billingPlan !== BillingPlan.STARTER) {
|
||||
readOnly.set(false);
|
||||
return;
|
||||
}
|
||||
if (!org?.billingLimits) {
|
||||
readOnly.set(false);
|
||||
return;
|
||||
}
|
||||
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
|
||||
const resources = [
|
||||
{ value: bandwidth, name: 'bandwidth' },
|
||||
{ value: documents, name: 'documents' },
|
||||
{ value: executions, name: 'executions' },
|
||||
{ value: storage, name: 'storage' },
|
||||
{ value: users, name: 'users' }
|
||||
];
|
||||
|
||||
const members = await sdk.forConsole.teams.listMemberships(org.$id);
|
||||
const plan = get(plansInfo)?.get(org.billingPlan);
|
||||
const membersOverflow =
|
||||
members?.total > plan.members ? members.total - (plan.members || members.total) : 0;
|
||||
|
||||
if (
|
||||
bandwidth >= 100 ||
|
||||
documents >= 100 ||
|
||||
executions >= 100 ||
|
||||
storage >= 100 ||
|
||||
users >= 100 ||
|
||||
membersOverflow > 0
|
||||
) {
|
||||
if (resources.some((r) => r.value >= 100) || membersOverflow > 0) {
|
||||
readOnly.set(true);
|
||||
} else readOnly.set(false);
|
||||
headerAlert.add({
|
||||
id: 'limitReached',
|
||||
component: LimitReached,
|
||||
show: true,
|
||||
importance: 7
|
||||
});
|
||||
} else if (resources.some((r) => r.value >= 75)) {
|
||||
readOnly.set(false);
|
||||
const lastNotification = parseInt(localStorage.getItem('limitReachedNotification')) ?? 0;
|
||||
const now = new Date().getTime();
|
||||
if (now - lastNotification < 1000 * 60 * 60 * 24) return;
|
||||
|
||||
localStorage.setItem('limitReachedNotification', now.toString());
|
||||
let message = `<b>${org.name}</b> has reached <b>75%</b> of the Starter plan's ${resources.find((r) => r.value >= 75).name} limit. Upgrade to ensure there are no service disruptions.`;
|
||||
if (resources.filter((r) => r.value >= 75)?.length > 1) {
|
||||
message = `Usage for <b>${org.name}</b> has reached 75% of the Starter plan limit. Upgrade to ensure there are no service disruptions.`;
|
||||
}
|
||||
addNotification({
|
||||
type: 'warning',
|
||||
isHtml: true,
|
||||
timeout: 0,
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
name: 'View usage',
|
||||
method: () => {
|
||||
goto(`${base}/console/organization-${org.$id}/usage`);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Upgrade plan',
|
||||
method: () => {
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'limit_reached_notification'
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
readOnly.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkPaymentAuthorizationRequired(org: Organization) {
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
import { stripe } from '$lib/stores/stripe';
|
||||
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
|
||||
import { showSupportModal } from './wizard/support/store';
|
||||
import ExcesLimitModal from './organization-[organization]/excesLimitModal.svelte';
|
||||
import { showExcess } from './organization-[organization]/store';
|
||||
|
||||
import UsageRates from './wizard/cloudOrganization/usageRates.svelte';
|
||||
import { activeHeaderAlert, consoleVariables } from './store';
|
||||
import { headerAlert } from '$lib/stores/headerAlert';
|
||||
@@ -325,9 +324,6 @@
|
||||
<MobileSupportModal bind:show={$showSupportModal}></MobileSupportModal>
|
||||
{/if}
|
||||
|
||||
{#if isCloud && $showExcess}
|
||||
<ExcesLimitModal bind:show={$showExcess}></ExcesLimitModal>
|
||||
{/if}
|
||||
{#if isCloud && $showUsageRatesModal}
|
||||
<UsageRates bind:show={$showUsageRatesModal} tier={$organization?.billingPlan} />
|
||||
{/if}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { sizeToBytes } from '$lib/helpers/sizeConvertion';
|
||||
import { plansInfo, tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import PlanExcess from '../wizard/cloudOrganizationChangeTier/planExcess.svelte';
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '../changeOrganizationTierCloud.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let show = false;
|
||||
const plan = $plansInfo?.get($organization.billingPlan);
|
||||
let usage: OrganizationUsage = null;
|
||||
let members: Models.MembershipList = null;
|
||||
let excess: Record<string, number> = null;
|
||||
|
||||
onMount(async () => {
|
||||
usage = await sdk.forConsole.billing.listUsage(
|
||||
$organization.$id,
|
||||
$organization.billingCurrentInvoiceDate,
|
||||
new Date().toISOString()
|
||||
);
|
||||
members = await sdk.forConsole.teams.listMemberships($organization.$id);
|
||||
calculateExcess();
|
||||
});
|
||||
|
||||
function calculateExcess() {
|
||||
const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0;
|
||||
const totUsers = usage?.users?.length > 0 ? last(usage.users).value : 0;
|
||||
|
||||
excess = {
|
||||
bandwidth: totBandwidth > plan.bandwidth ? totBandwidth - plan.bandwidth : 0,
|
||||
storage:
|
||||
usage?.storageTotal > sizeToBytes(plan.storage, 'GB')
|
||||
? usage.storageTotal - sizeToBytes(plan.storage, 'GB')
|
||||
: 0,
|
||||
users: totUsers > (plan.users || Infinity) ? totUsers - plan.users : 0,
|
||||
executions:
|
||||
usage?.executionsTotal > sizeToBytes(plan.executions, 'GB')
|
||||
? usage.executionsTotal - sizeToBytes(plan.executions, 'GB')
|
||||
: 0,
|
||||
members:
|
||||
members?.total > (plan.members || Infinity)
|
||||
? members.total - (plan.members || Infinity)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
$: if (show) calculateExcess();
|
||||
</script>
|
||||
|
||||
<Modal bind:show title="Limit reached">
|
||||
<svelte:fragment slot="title">
|
||||
Your usage exceeds the {tierToPlan($organization.billingPlan).name} plan limits
|
||||
</svelte:fragment>
|
||||
|
||||
{#if $organization.billingPlan === BillingPlan.STARTER}
|
||||
<p class="text">
|
||||
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan. Consider upgrading to increase your resource usage.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">
|
||||
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan. Excess usage fees will apply.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<PlanExcess {excess} currentTier={$organization.billingPlan} />
|
||||
<svelte:fragment slot="footer">
|
||||
<div class="u-flex u-main-space-between u-width-full-line">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
show = false;
|
||||
goto(`/console/organization-${$organization.$id}/usage`);
|
||||
}}>
|
||||
View usage
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => {
|
||||
show = false;
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'limit_reached_modal'
|
||||
});
|
||||
}}>
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const showExcess = writable<boolean>(false);
|
||||
export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList);
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
<Heading tag="h6" size="7">Bandwidth</Heading>
|
||||
|
||||
<p class="text">
|
||||
Calculated for all bandwidth used across all projects in your organization.
|
||||
Calculated for all bandwidth used across all projects in your organization. Resets at
|
||||
the start of each billing cycle.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
|
||||
@@ -104,7 +104,10 @@
|
||||
</div>
|
||||
<CardGrid>
|
||||
<Heading tag="h4" size="7">Bandwidth</Heading>
|
||||
<p class="text">Calculated for all bandwidth used in your project.</p>
|
||||
<p class="text">
|
||||
Calculated for all bandwidth used across your project. Resets at the start of each
|
||||
billing cycle.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if network}
|
||||
{@const humanized = humanFileSize(total(network))}
|
||||
|
||||
Reference in New Issue
Block a user