mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
This commit is contained in:
@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
PUBLIC_APPWRITE_ENDPOINT: 'https://console-testing.appwrite.org/v1',
|
||||
PUBLIC_APPWRITE_ENDPOINT: 'https://console-testing-2.appwrite.org/v1',
|
||||
PUBLIC_CONSOLE_MODE: 'cloud',
|
||||
PUBLIC_STRIPE_KEY:
|
||||
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
export let noBoxPadding = false;
|
||||
export let allowScroll = false;
|
||||
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
Prism.plugins.customClass.prefix('prism-');
|
||||
|
||||
afterUpdate(async () => {
|
||||
@@ -39,7 +42,10 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="box u-overflow-hidden" class:common-section={!noMargin} class:noBoxPadding>
|
||||
<section
|
||||
class="box u-overflow-hidden {classes}"
|
||||
class:common-section={!noMargin}
|
||||
class:noBoxPadding>
|
||||
<div
|
||||
class="controls u-position-absolute u-inset-inline-end-8 u-inset-block-start-8 u-flex u-gap-8">
|
||||
{#if label}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let disabled = false;
|
||||
export let external = false;
|
||||
export let event: string = null;
|
||||
export let iconStyle: string = '';
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
@@ -29,7 +30,7 @@
|
||||
rel={external ? 'noopener noreferrer' : ''}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
<span class={`icon-${icon}`} style={iconStyle} aria-hidden="true" />
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -5,17 +5,14 @@
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
import { showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { localeTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: isPaid =
|
||||
$organization?.billingPlan === BillingPlan.PRO ||
|
||||
$organization?.billingPlan === BillingPlan.SCALE;
|
||||
$: hasPremiumSupport = $currentPlan?.premiumSupport ?? false;
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeTimezoneName()}`;
|
||||
</script>
|
||||
@@ -24,13 +21,13 @@
|
||||
<section class="drop-section u-grid u-gap-24 u-padding-24">
|
||||
<div>
|
||||
<h4 class="eyebrow-heading-3">Premium support</h4>
|
||||
{#if isPaid}
|
||||
{#if hasPremiumSupport}
|
||||
<p class="u-line-height-1-5 u-margin-block-start-8">
|
||||
Get personalized support from the Appwrite team from <b>{supportTimings}</b>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if !hasPremiumSupport}
|
||||
<Button
|
||||
fullWidth
|
||||
href={$upgradeURL}
|
||||
|
||||
+55
-11
@@ -156,8 +156,9 @@
|
||||
|
||||
<div class="u-stretch u-margin-block-start-32 u-overflow-hidden">
|
||||
<section class="code-panel">
|
||||
<header class="code-panel-header u-flex u-main-space-between u-width-full-line">
|
||||
<div class="u-flex u-gap-24">
|
||||
<header
|
||||
class="code-panel-header code-panel-compact-header u-main-space-between u-width-full-line">
|
||||
<div class="u-flex">
|
||||
<div class="u-flex u-gap-16">
|
||||
<h4 class="text u-bold">Method:</h4>
|
||||
<span class="u-text-color-gray">{execution.requestMethod}</span>
|
||||
@@ -197,8 +198,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="code-panel-content grid-1-2" style="u-grid">
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<div class="code-panel-content grid-1-2">
|
||||
<div
|
||||
class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16 mobile-only-inline-20-padding">
|
||||
<Heading tag="h3" size="6">Request</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
@@ -281,7 +283,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
{execution.requestHeaders?.length
|
||||
? 'Not all header data is'
|
||||
: 'Header data is not'}
|
||||
@@ -296,7 +298,7 @@
|
||||
>.
|
||||
</p>
|
||||
{:else if selectedRequest === 'body'}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
Body data is not captured by Appwrite for your user's security
|
||||
and privacy. To display body data in the Logs tab, use
|
||||
<b>context.log()</b>.
|
||||
@@ -309,7 +311,9 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<div class="u-sep-block-end is-only-mobile u-padding-block-start-16" />
|
||||
<div
|
||||
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0 mobile-only-inline-20-padding mobile-only-block-start-20-padding">
|
||||
<Heading tag="h3" size="6">Response</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
@@ -348,10 +352,12 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
<Code
|
||||
allowScroll
|
||||
withCopy
|
||||
noMargin
|
||||
code={unescapeNewLines(execution.logs)}
|
||||
language="sh" />
|
||||
language="sh"
|
||||
class="limited-code-height" />
|
||||
{:else}
|
||||
<Card isDashed isTile>
|
||||
<p class="text u-text-center">No response was recorded.</p>
|
||||
@@ -360,10 +366,12 @@
|
||||
{:else if selectedResponse === 'errors'}
|
||||
{#if execution?.errors}
|
||||
<Code
|
||||
allowScroll
|
||||
withCopy
|
||||
noMargin
|
||||
code={unescapeNewLines(execution.errors)}
|
||||
language="sh" />
|
||||
language="sh"
|
||||
class="limited-code-height" />
|
||||
{:else}
|
||||
<Card isDashed isTile>
|
||||
<p class="text u-text-center">No response was recorded.</p>
|
||||
@@ -389,7 +397,7 @@
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
{/if}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
{execution.responseHeaders?.length
|
||||
? 'Not all header data is'
|
||||
: 'Header data is not'}
|
||||
@@ -404,7 +412,7 @@
|
||||
>.
|
||||
</p>
|
||||
{:else if selectedResponse === 'body'}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
Body data is not captured by Appwrite for your user's security
|
||||
and privacy. To display body data in the Logs tab, use
|
||||
<b>context.log()</b>.
|
||||
@@ -423,3 +431,39 @@
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.limited-code-height) {
|
||||
overflow: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.code-panel-compact-header .u-flex {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.code-panel-compact-header {
|
||||
row-gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code-panel-content {
|
||||
padding: unset;
|
||||
padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-only-inline-20-padding {
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-only-block-start-20-padding {
|
||||
padding-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
.code-panel-content.grid-1-2 {
|
||||
display: unset;
|
||||
row-gap: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
import { showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let isOpen = false;
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
$: project = $page.params.project;
|
||||
$: projectPath = `${base}/project-${project}`;
|
||||
@@ -44,12 +43,12 @@
|
||||
}
|
||||
|
||||
function openFeedback() {
|
||||
isOpen = false;
|
||||
$showSubNavigation = false;
|
||||
wizard.start(Create);
|
||||
}
|
||||
|
||||
function openSupport() {
|
||||
isOpen = false;
|
||||
$showSubNavigation = false;
|
||||
$showSupportModal = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
export let isOpen = false;
|
||||
export let showSideNavigation = false;
|
||||
|
||||
let y: number;
|
||||
|
||||
page.subscribe(({ url }) => {
|
||||
isOpen = url.searchParams.get('openNavbar') === 'true';
|
||||
$showSubNavigation = url.searchParams.get('openNavbar') === 'true';
|
||||
});
|
||||
|
||||
const toggleMenu = () => {
|
||||
y = 0;
|
||||
isOpen = !isOpen;
|
||||
$showSubNavigation = !$showSubNavigation;
|
||||
if (browser) {
|
||||
if (isOpen) {
|
||||
if ($showSubNavigation) {
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
} else {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<main
|
||||
class:grid-with-side={showSideNavigation}
|
||||
class:is-open={isOpen}
|
||||
class:is-open={$showSubNavigation}
|
||||
class:u-hide={$wizard.show || $log.show || $wizard.cover}
|
||||
class:is-fixed-layout={$activeHeaderAlert?.show}
|
||||
style:--p-side-size={sideSize}>
|
||||
@@ -65,7 +65,10 @@
|
||||
class="icon-button is-not-desktop"
|
||||
aria-label="Open Menu"
|
||||
on:click={toggleMenu}>
|
||||
<span class:icon-x={isOpen} class:icon-menu={!isOpen} aria-hidden="true" />
|
||||
<span
|
||||
class:icon-x={$showSubNavigation}
|
||||
class:icon-menu={!$showSubNavigation}
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
|
||||
@@ -285,6 +285,12 @@ export type Plan = {
|
||||
trialDays: number;
|
||||
isAvailable: boolean;
|
||||
selfService: boolean;
|
||||
premiumSupport: boolean;
|
||||
budgeting: boolean;
|
||||
supportsMockNumbers: boolean;
|
||||
backupsEnabled: boolean;
|
||||
backupPolicies: number;
|
||||
emailBranding: boolean;
|
||||
};
|
||||
|
||||
export type PlansInfo = {
|
||||
|
||||
@@ -25,3 +25,5 @@ export function updateLayout(args: updateLayoutArguments) {
|
||||
header.set(args.header ?? null);
|
||||
breadcrumb.set(args.breadcrumb ?? null);
|
||||
}
|
||||
|
||||
export const showSubNavigation = writable(false);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { page } from '$app/stores';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Tier } from './billing';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export type OrganizationError = {
|
||||
status: number;
|
||||
@@ -54,4 +55,5 @@ export const organizationList = derived(
|
||||
);
|
||||
|
||||
export const organization = derived(page, ($page) => $page.data?.organization as Organization);
|
||||
export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Plan);
|
||||
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</p>
|
||||
{#if selectedInstallation}
|
||||
{#await loadRepositories(selectedInstallation, search)}
|
||||
<div class="u-flex u-gap-8 u-cross-center u-main-center">
|
||||
<div class="u-flex u-gap-8 u-cross-center u-main-center" style:height="50vh">
|
||||
<div class="loader u-margin-32" />
|
||||
</div>
|
||||
{:then response}
|
||||
@@ -225,3 +225,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon-lock-closed {
|
||||
color: hsl(var(--color-neutral-50));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
{/each}
|
||||
{:then templatesListWithoutStarter}
|
||||
{#each templatesListWithoutStarter.templates as template}
|
||||
<li class="clickable-list-item">
|
||||
<li class="clickable-list-item u-padding-block-8">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -295,6 +295,22 @@
|
||||
);
|
||||
}
|
||||
|
||||
.u-sep-block-start {
|
||||
border-block-start: solid 0.0625rem hsl(var(--color-neutral)) !important;
|
||||
}
|
||||
|
||||
.clickable-list-item:not(:last-child) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--color-neutral)) !important;
|
||||
}
|
||||
|
||||
:global(.theme-light) {
|
||||
--color-neutral: var(--color-neutral-10);
|
||||
}
|
||||
|
||||
:global(.theme-dark) {
|
||||
--color-neutral: var(--color-neutral-85);
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
$choices.branch = event.detail.value;
|
||||
}}
|
||||
interactiveOutput
|
||||
name="branch"
|
||||
name="branches"
|
||||
{options} />
|
||||
<InputText
|
||||
id="root"
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
rank: -1
|
||||
}
|
||||
]);
|
||||
let isOpen = false;
|
||||
onMount(async () => {
|
||||
loading.set(false);
|
||||
if (!localStorage.getItem('feedbackElapsed')) {
|
||||
@@ -319,14 +318,13 @@
|
||||
<CommandCenter />
|
||||
|
||||
<Shell
|
||||
bind:isOpen
|
||||
showSideNavigation={$page.url.pathname !== '/console' &&
|
||||
!$page?.params.organization &&
|
||||
!$page.url.pathname.includes('/console/account') &&
|
||||
!$page.url.pathname.includes('/console/card') &&
|
||||
!$page.url.pathname.includes('/console/onboarding')}>
|
||||
<Header slot="header" />
|
||||
<SideNavigation slot="side" bind:isOpen />
|
||||
<SideNavigation slot="side" />
|
||||
<slot />
|
||||
<Footer slot="footer" />
|
||||
</Shell>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { get } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
@@ -19,12 +20,14 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
let currentPlan: Plan = null;
|
||||
|
||||
try {
|
||||
if (isCloud) {
|
||||
const res = await sdk.forConsole.billing.getRoles(params.organization);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
currentPlan = await sdk.forConsole.billing.getPlan(params.organization);
|
||||
if (scopes.includes('billing.read')) {
|
||||
await failedInvoice.load(params.organization);
|
||||
if (get(failedInvoice)) {
|
||||
@@ -53,6 +56,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
organization,
|
||||
currentPlan,
|
||||
members,
|
||||
roles,
|
||||
scopes
|
||||
|
||||
@@ -29,14 +29,12 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
const billingInvoiceId = (organization as Organization)?.billingInvoiceId;
|
||||
|
||||
const billingAggregation = await sdk.forConsole.billing.getAggregation(organization.$id, billingAggregationId);
|
||||
console.log(billingAggregation);
|
||||
|
||||
const [
|
||||
paymentMethods,
|
||||
addressList,
|
||||
aggregationList,
|
||||
billingAddress,
|
||||
currentPlan,
|
||||
creditList,
|
||||
invoices,
|
||||
aggregationBillingPlan,
|
||||
@@ -46,7 +44,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
sdk.forConsole.billing.listAggregation(organization.$id),
|
||||
billingAddressPromise,
|
||||
sdk.forConsole.billing.getOrganizationPlan(organization.$id),
|
||||
sdk.forConsole.billing.listCredits(organization.$id),
|
||||
sdk.forConsole.billing.listInvoices(organization.$id, [
|
||||
Query.limit(1),
|
||||
@@ -61,7 +58,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
addressList,
|
||||
aggregationList,
|
||||
billingAddress,
|
||||
currentPlan,
|
||||
aggregationBillingPlan,
|
||||
creditList,
|
||||
invoices,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Button, Form, FormList, InputNumber, InputSwitch } from '$lib/elements/forms';
|
||||
import { showUsageRatesModal, upgradeURL } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { organization, currentPlan } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
class="link">Learn more about usage rates.</button>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if !$currentPlan.budgeting}
|
||||
<Alert type="info">
|
||||
<svelte:fragment slot="title">
|
||||
Budget caps are a Pro plan feature
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal, RadioBoxes } from '$lib/components';
|
||||
import { Alert, Modal, RadioBoxes } from '$lib/components';
|
||||
import { Button, FormItem, FormList, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
@@ -10,6 +10,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let show = false;
|
||||
let addresses: AddressesList;
|
||||
@@ -185,10 +186,21 @@
|
||||
</FormList>
|
||||
</RadioBoxes>
|
||||
</FormList>
|
||||
{:else}
|
||||
<Alert
|
||||
buttons={[
|
||||
{
|
||||
slot: 'Add address',
|
||||
href: `${base}/account/payments`
|
||||
}
|
||||
]}>There are no billing addresses linked to your account.</Alert>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit disabled={selectedAddress === $organization.billingAddressId}>
|
||||
<Button
|
||||
secondary
|
||||
submit
|
||||
disabled={selectedAddress === $organization.billingAddressId || !addresses?.total}>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import {
|
||||
currentPlan,
|
||||
organization,
|
||||
organizationList,
|
||||
type Organization,
|
||||
type OrganizationError
|
||||
type OrganizationError,
|
||||
type Organization
|
||||
} from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe.js';
|
||||
@@ -107,8 +108,7 @@
|
||||
billingPlan = BillingPlan.PRO;
|
||||
}
|
||||
|
||||
const currentPlan = await sdk.forConsole.billing.getOrganizationPlan($organization?.$id);
|
||||
selfService = currentPlan.selfService;
|
||||
selfService = $currentPlan?.selfService ?? true;
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
|
||||
@@ -7,9 +7,11 @@ import { failedInvoice } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.PROJECT);
|
||||
let currentPlan: Plan = null;
|
||||
|
||||
try {
|
||||
const project = await sdk.forConsole.projects.get(params.project);
|
||||
@@ -27,6 +29,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
if (isCloud) {
|
||||
currentPlan = await sdk.forConsole.billing.getPlan(project.teamId);
|
||||
const res = await sdk.forConsole.billing.getRoles(project.teamId);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
@@ -39,7 +42,8 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
project,
|
||||
organization,
|
||||
roles,
|
||||
scopes
|
||||
scopes,
|
||||
currentPlan
|
||||
};
|
||||
} catch (e) {
|
||||
error(e.code, e.message);
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import MockNumbersLight from './mock-numbers-light.png';
|
||||
import MockNumbersDark from './mock-numbers-dark.png';
|
||||
@@ -27,7 +26,7 @@
|
||||
$: isSubmitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
|
||||
|
||||
let isComponentDisabled: boolean =
|
||||
isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE);
|
||||
isSelfHosted || (isCloud && !$currentPlan.supportsMockNumbers);
|
||||
let emptyStateTitle: string = isSelfHosted
|
||||
? 'Available on Appwrite Cloud'
|
||||
: 'Upgrade to add mock phone numbers';
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
// import { baseEmailTemplate, baseSmsTemplate, emailTemplate, smsTemplate } from './store';
|
||||
import { baseEmailTemplate, emailTemplate } from './store';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import EmailSignature from './emailSignature.svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type {
|
||||
@@ -268,7 +267,7 @@
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</CardGrid>-->
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if isCloud && $currentPlan.emailBranding}
|
||||
<EmailSignature />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
+5
-14
@@ -11,9 +11,9 @@
|
||||
import { addNotification, dismissAllNotifications } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups';
|
||||
@@ -24,19 +24,10 @@
|
||||
|
||||
let policyCreateError: string;
|
||||
let totalPolicies: UserBackupPolicy[] = [];
|
||||
let isDisabled = isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE);
|
||||
let isDisabled = isSelfHosted || (isCloud && !$currentPlan.backupsEnabled);
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: hasPolicyCreationLimitations = () => {
|
||||
// allow when on Pro and no policy exists
|
||||
if ($organization?.billingPlan === BillingPlan.PRO) {
|
||||
return data.policies.total > 0;
|
||||
} else if ($organization?.billingPlan === BillingPlan.SCALE) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showFeedbackNotification = () => {
|
||||
let counter = localStorage.getItem('createBackupsCounter');
|
||||
const parsedCounter = counter ? parseInt(counter, 10) : 0;
|
||||
@@ -184,7 +175,8 @@
|
||||
buttonEvent="create_backup"
|
||||
buttonType="secondary"
|
||||
buttonDisabled={isDisabled}
|
||||
hasLimitations={hasPolicyCreationLimitations()}
|
||||
maxPolicies={$currentPlan.backupPolicies}
|
||||
policiesCreated={data.policies.total}
|
||||
buttonMethod={() => {
|
||||
$showCreatePolicy = true;
|
||||
trackEvent('click_policy_create');
|
||||
@@ -202,7 +194,6 @@
|
||||
buttonText="Manual backup"
|
||||
buttonEvent="create_backup"
|
||||
buttonType="secondary"
|
||||
hasLimitations={false}
|
||||
buttonDisabled={isDisabled}
|
||||
buttonMethod={() => {
|
||||
$showCreateBackup = true;
|
||||
|
||||
+7
-12
@@ -4,14 +4,13 @@
|
||||
import { Pill } from '$lib/elements';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
|
||||
export let buttonText: string = null;
|
||||
export let hasLimitations: boolean = true;
|
||||
export let policiesCreated: number = 0;
|
||||
export let maxPolicies: number = 0;
|
||||
export let buttonMethod: () => void = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let buttonDisabled = false;
|
||||
@@ -27,16 +26,16 @@
|
||||
<div class="u-flex u-cross-child-center u-cross-center u-gap-12">
|
||||
<div class="body-text-1 u-bold backups-title">{title}</div>
|
||||
|
||||
{#if hasLimitations && $organization.billingPlan === BillingPlan.PRO}
|
||||
{#if title === 'Policies' && policiesCreated >= maxPolicies}
|
||||
<div style="height: 40px; padding-block-start: 4px">
|
||||
<DropList bind:show={showDropdown} width="16">
|
||||
<Pill button on:click={() => (showDropdown = true)}>
|
||||
<span class="icon-info" />1/1 created
|
||||
<span class="icon-info" />{policiesCreated}/{maxPolicies} created
|
||||
</Pill>
|
||||
<svelte:fragment slot="list">
|
||||
<slot name="tooltip">
|
||||
<span>
|
||||
You are limited to one policy on Pro plan.
|
||||
You are limited to {maxPolicies} policy on your plan.
|
||||
<button
|
||||
class="u-underline"
|
||||
on:click={() => {
|
||||
@@ -52,14 +51,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !hasLimitations}
|
||||
{#if title === 'Backups' || policiesCreated < maxPolicies}
|
||||
<Button
|
||||
event={buttonEvent}
|
||||
on:click={hasLimitations
|
||||
? () => {
|
||||
showDropdown = true;
|
||||
}
|
||||
: buttonMethod}
|
||||
on:click={buttonMethod}
|
||||
disabled={buttonDisabled}
|
||||
text={buttonType === 'text'}
|
||||
secondary={buttonType === 'secondary'}>
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { showCreate } from '../store';
|
||||
import type { PageData } from './$types';
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
$: data = $page.data as PageData;
|
||||
$: project = $page.params.project;
|
||||
@@ -24,7 +25,10 @@
|
||||
<h5 class="eyebrow-heading-3 u-padding-block-12 is-not-mobile">Collections</h5>
|
||||
<button
|
||||
class="button is-text is-full-width u-main-start u-padding-inline-0"
|
||||
on:click={() => ($showCreate = true)}>
|
||||
on:click={() => {
|
||||
$showCreate = true;
|
||||
$showSubNavigation = false;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</button>
|
||||
|
||||
+21
-1
@@ -98,7 +98,7 @@
|
||||
>.
|
||||
</Alert>
|
||||
|
||||
<div class="editor-border">
|
||||
<div class="editor-border box">
|
||||
<SecondaryTabs large class="u-sep-block-end u-padding-8">
|
||||
{#each ['Unix', 'CMD', 'PowerShell'] as cat}
|
||||
<SecondaryTabsItem
|
||||
@@ -118,6 +118,7 @@
|
||||
withLineNumbers
|
||||
withCopy
|
||||
language="sh"
|
||||
class="cli-commands-code-box-no-outline"
|
||||
label={codeSnippets[cat].language}
|
||||
code={codeSnippets[cat].code} />
|
||||
{/if}
|
||||
@@ -127,3 +128,22 @@
|
||||
<Button secondary on:click={() => (show = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
padding: unset;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.editor-border :global(.cli-commands-code-box-no-outline) {
|
||||
margin: 1rem 0;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
background-color: unset;
|
||||
padding: 0 var(--box-padding, 1.5rem) 0 var(--box-padding, 1.5rem);
|
||||
}
|
||||
|
||||
:global(.editor-border .cli-commands-code-box-no-outline pre) {
|
||||
background-color: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
+45
-11
@@ -25,6 +25,9 @@
|
||||
let showCancel = false;
|
||||
let showActivate = false;
|
||||
|
||||
let enableScrollButton = false;
|
||||
let codePanelContent: HTMLElement;
|
||||
|
||||
function handleActivate() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
@@ -36,23 +39,41 @@
|
||||
);
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const threshold = codePanelContent.clientHeight * 0.25;
|
||||
enableScrollButton = codePanelContent.scrollTop > threshold;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
logs = $deployment.buildLogs;
|
||||
|
||||
codePanelContent = document.querySelector('.code-panel-content');
|
||||
codePanelContent.addEventListener('scroll', handleScroll);
|
||||
|
||||
if ($deployment.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
return sdk.forConsole.client.subscribe<Models.Deployment>('console', (message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${$page.params.function}.deployments.${$page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
logs = message.payload['logs'];
|
||||
if (message.payload.status === 'ready') {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
|
||||
const unsubscribe = sdk.forConsole.client.subscribe<Models.Deployment>(
|
||||
'console',
|
||||
(message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${$page.params.function}.deployments.${$page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
logs = message.payload['logs'];
|
||||
if (message.payload.status === 'ready') {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
codePanelContent.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -103,7 +124,8 @@
|
||||
<span class="icon-external-link" aria-hidden="true" /> Raw data</Button> -->
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
disabled={!enableScrollButton}
|
||||
on:click={() => codePanelContent?.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
<span class="icon-arrow-sm-up" aria-hidden="true" /> Scroll to top</Button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -135,3 +157,15 @@
|
||||
<Delete selectedDeployment={$deployment} bind:showDelete />
|
||||
<Cancel selectedDeployment={$deployment} bind:showCancel />
|
||||
<Activate selectedDeployment={$deployment} bind:showActivate on:activated={handleActivate} />
|
||||
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.code-panel-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.code-panel-content {
|
||||
max-height: 50vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
+24
-6
@@ -15,17 +15,35 @@
|
||||
<span class="icon-github" aria-hidden="true" /> <span class="link">GitHub</span>
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListLink href={deployment.providerRepositoryUrl} external icon="github">
|
||||
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
|
||||
<DropListLink
|
||||
href={deployment.providerRepositoryUrl}
|
||||
external
|
||||
icon="github"
|
||||
iconStyle="font-size:20px">
|
||||
<Trim alternativeTrim>
|
||||
<span style:white-space="pre-wrap">
|
||||
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
|
||||
</span>
|
||||
</Trim>
|
||||
</DropListLink>
|
||||
<DropListLink href={deployment.providerBranchUrl} external icon="git-branch">
|
||||
<DropListLink
|
||||
href={deployment.providerBranchUrl}
|
||||
external
|
||||
icon="git-branch"
|
||||
iconStyle="font-size:20px">
|
||||
{deployment.providerBranch}
|
||||
</DropListLink>
|
||||
{#if deployment?.providerCommitMessage && deployment?.providerCommitHash && deployment?.providerCommitUrl}
|
||||
<DropListLink href={deployment.providerCommitUrl} external icon="git-commit">
|
||||
<DropListLink
|
||||
href={deployment.providerCommitUrl}
|
||||
external
|
||||
icon="git-commit"
|
||||
iconStyle="font-size:20px">
|
||||
<Trim alternativeTrim>
|
||||
{deployment?.providerCommitHash?.substring(0, 7)}
|
||||
{deployment.providerCommitMessage}
|
||||
<span style="white-space: pre-wrap">
|
||||
{deployment?.providerCommitHash?.substring(0, 7)}
|
||||
{deployment.providerCommitMessage}
|
||||
</span>
|
||||
</Trim>
|
||||
</DropListLink>
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { functionsList } from '../store';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -148,7 +149,7 @@
|
||||
class="is-small"
|
||||
checked={isChecked(useCase)}
|
||||
on:change={(e) => applyFilter('useCase', useCase, e)} />
|
||||
<span class="u-trim-1">{useCase}</span>
|
||||
<span class="u-trim-1">{capitalize(useCase)}</span>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -168,7 +169,8 @@
|
||||
.getAll('runtime')
|
||||
.includes(runtime)}
|
||||
on:change={(e) => applyFilter('runtime', runtime, e)} />
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<div
|
||||
class="u-flex u-cross-center u-gap-8 functions-avatar-holder">
|
||||
<div class="avatar is-size-x-small">
|
||||
<SvgIcon name={icon} iconSize="small" />
|
||||
</div>
|
||||
@@ -207,14 +209,16 @@
|
||||
<li>
|
||||
<article class="card u-min-height-100-percent">
|
||||
<div class="u-flex u-gap-16 u-cross-center u-main-space-between">
|
||||
<h2 class="body-text-1 u-bold u-trim-1">
|
||||
<h2
|
||||
class="body-text-1 u-bold u-trim-1 functions-template-title">
|
||||
{template.name}
|
||||
</h2>
|
||||
<ul class="avatars-group is-with-border">
|
||||
{#each displayed as runtime}
|
||||
{@const icon = getIconFromRuntime(runtime.name)}
|
||||
{#if icon}
|
||||
<li class="avatars-group-item">
|
||||
<li
|
||||
class="avatars-group-item functions-avatar-holder">
|
||||
<div class="avatar is-size-small">
|
||||
<img
|
||||
style:--p-text-size="20px"
|
||||
@@ -227,7 +231,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hidden.length}
|
||||
<li class="avatars-group-item">
|
||||
<li class="avatars-group-item functions-avatar-holder">
|
||||
<div
|
||||
class="avatar is-size-small"
|
||||
use:tooltip={{
|
||||
@@ -287,3 +291,18 @@
|
||||
</section>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
:global(.theme-light .functions-avatar-holder .avatar) {
|
||||
background-color: var(--p-avatar-bg-color-default);
|
||||
--p-avatar-bg-color-default: hsl(var(--color-neutral-0));
|
||||
}
|
||||
|
||||
:global(.theme-light .functions-template-title) {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
}
|
||||
|
||||
:global(.theme-dark .functions-template-title) {
|
||||
color: hsl(var(--color-neutral-15));
|
||||
}
|
||||
</style>
|
||||
|
||||
+21
-9
@@ -22,9 +22,10 @@
|
||||
<div class="grid-300px-1fr">
|
||||
<section>
|
||||
<Collapsible>
|
||||
<li class="collapsible-item">
|
||||
<h3 class="body-text-2 u-bold u-padding-block-12">
|
||||
Use cases <span class="inline-tag">{$template.useCases.length}</span>
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
|
||||
Use cases
|
||||
<span class="inline-tag">{$template.useCases.length}</span>
|
||||
</h3>
|
||||
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
|
||||
{#each $template.useCases as useCase}
|
||||
@@ -32,9 +33,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
<li class="collapsible-item">
|
||||
<h3 class="body-text-2 u-bold u-padding-block-12">
|
||||
Runtimes <span class="inline-tag">{$template.runtimes.length}</span>
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
|
||||
Runtimes
|
||||
<span class="inline-tag">{$template.runtimes.length}</span>
|
||||
</h3>
|
||||
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
|
||||
{#each $template.runtimes as runtime}
|
||||
@@ -42,12 +44,12 @@
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
<li class="collapsible-item">
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<section class="card u-margin-block-start-24">
|
||||
<h4 class="body-text-1 u-bold">Published by</h4>
|
||||
<img
|
||||
class="u-margin-block-start-8"
|
||||
src={$app.themeInUse == 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
|
||||
src={$app.themeInUse === 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite" />
|
||||
@@ -58,7 +60,7 @@
|
||||
<section>
|
||||
<Card>
|
||||
<Heading size="7" tag="h3">
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
<span class="u-flex u-cross-center u-gap-16 functions-avatar-holder">
|
||||
<div class="avatar is-size-small">
|
||||
<span
|
||||
style:--p-text-size="20px"
|
||||
@@ -92,3 +94,13 @@
|
||||
</section>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
:global(.theme-dark .collapsible-item-divider:where(:not(:last-child))) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
:global(.theme-light .collapsible-item-divider:where(:not(:last-child))) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--color-neutral-10));
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user