Merge branch 'main' into feat-pink-v2

This commit is contained in:
Darshan
2025-01-03 15:47:42 +05:30
committed by GitHub
100 changed files with 1108 additions and 618 deletions
+2
View File
@@ -40,6 +40,7 @@
"plausible-tracker": "^0.3.9",
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
"remarkable": "^2.0.1",
"svelte-confetti": "^1.4.0",
"tippy.js": "^6.3.7"
},
@@ -56,6 +57,7 @@
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.5",
"@types/remarkable": "^2.0.8",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/ui": "^1.6.0",
+1 -1
View File
@@ -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'
+28
View File
@@ -71,6 +71,9 @@ importers:
prismjs:
specifier: ^1.29.0
version: 1.29.0
remarkable:
specifier: ^2.0.1
version: 2.0.1
svelte-confetti:
specifier: ^1.4.0
version: 1.4.0(svelte@4.2.19)
@@ -114,6 +117,9 @@ importers:
'@types/prismjs':
specifier: ^1.26.5
version: 1.26.5
'@types/remarkable':
specifier: ^2.0.8
version: 2.0.8
'@typescript-eslint/eslint-plugin':
specifier: ^7.18.0
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)
@@ -1382,6 +1388,9 @@ packages:
'@types/pug@2.0.10':
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
'@types/remarkable@2.0.8':
resolution: {integrity: sha512-eKXqPZfpQl1kOADjdKchHrp2gwn9qMnGXhH/AtZe0UrklzhGJkawJo/Y/D0AlWcdWoWamFNIum8+/nkAISQVGg==}
'@types/shimmer@1.2.0':
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
@@ -1620,6 +1629,9 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autolinker@3.16.2:
resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -3196,6 +3208,11 @@ packages:
resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==}
engines: {node: '>= 0.4'}
remarkable@2.0.1:
resolution: {integrity: sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==}
engines: {node: '>= 6.0.0'}
hasBin: true
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -5357,6 +5374,8 @@ snapshots:
'@types/pug@2.0.10': {}
'@types/remarkable@2.0.8': {}
'@types/shimmer@1.2.0': {}
'@types/stack-utils@2.0.3': {}
@@ -5653,6 +5672,10 @@ snapshots:
asynckit@0.4.0: {}
autolinker@3.16.2:
dependencies:
tslib: 2.8.1
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
@@ -7459,6 +7482,11 @@ snapshots:
es-errors: 1.3.0
set-function-name: 2.0.2
remarkable@2.0.1:
dependencies:
argparse: 1.0.10
autolinker: 3.16.2
require-directory@2.1.1: {}
require-in-the-middle@7.4.0:
+53 -6
View File
@@ -1,6 +1,9 @@
<script lang="ts">
import { Remarkable } from 'remarkable';
import Template from './template.svelte';
const markdownInstance = new Remarkable();
import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { user } from '$lib/stores/user';
import { useCompletion } from 'ai/svelte';
@@ -19,8 +22,6 @@
credentials: 'include'
});
let question = $input;
const examples = [
'How to add platform in the console?',
'How can I manage users, permissions, and access control in Appwrite?',
@@ -87,10 +88,47 @@
$: answer = parseCompletion($completion);
function renderMarkdown(answer: string): string {
const trimmedAnswer = answer
.trim()
.replace(/[ \t]+/g, ' ')
.replace(/\n[ \t]+/g, '\n')
.replace(/\n+/g, '\n');
// targeting links in plain text.
const processedAnswer = trimmedAnswer
.replace(/(\[(.*?)]\((.*?)\))|https?:\/\/\S+/g, (match, fullMarkdownLink, _, __) =>
fullMarkdownLink ? match : `[${match}](${match})`
)
.replace(/https?:\/\/\S+##/g, (url) => url.replace(/##/, '#'));
const formattedAnswer = processedAnswer.replace(
/(^|\n)Sources:/g,
(_, prefix) => `${prefix}\nSources:`
);
let renderedHTML = markdownInstance.render(formattedAnswer);
// add target blank to open links in a new tab.
renderedHTML = renderedHTML.replace(/<a\s+href="([^"]+)"/g, '<a href="$1" target="_blank"');
return renderedHTML;
}
function getInitials(name: string) {
const [first, last] = name.split(' ');
return `${first?.[0] ?? ''}${last?.[0] ?? ''}`;
}
let previousQuestion = '';
$: if ($input) {
previousQuestion = $input;
}
$: if (!$isLoading && answer) {
// reset input if answer received.
$input = '';
}
</script>
<Template
@@ -142,7 +180,7 @@
<div class="content">
<div class="u-flex u-gap-8 u-cross-center">
<div class="avatar is-size-x-small">{getInitials($user.name)}</div>
<p class="u-opacity-75">{question}</p>
<p class="u-opacity-75">{previousQuestion}</p>
</div>
<div class="u-flex u-gap-8 u-margin-block-start-24">
<div class="logo">
@@ -154,7 +192,7 @@
{:else}
{#each answer as part}
{#if part.type === 'text'}
<p>{part.value.trimStart()}</p>
<p>{@html renderMarkdown(part.value.trim())}</p>
{:else if part.type === 'code'}
{#key part.value}
<div
@@ -196,7 +234,6 @@
class="input-text-wrapper u-width-full-line"
style="--amount-of-buttons: 1;"
on:submit|preventDefault={(e) => {
question = $input;
handleSubmit(e);
}}>
<!-- svelte-ignore a11y-autofocus -->
@@ -269,10 +306,20 @@
.answer {
overflow: hidden;
p {
p:first-of-type {
white-space: pre-wrap;
}
}
:global(.answer ul),
:global(.answer ol) {
gap: 1rem;
display: grid;
}
:global(.answer a) {
text-decoration: underline;
}
}
.footer {
+4 -1
View File
@@ -9,6 +9,9 @@
export let bordered = false;
export let color = '';
let classes = '';
export { classes as class };
enum Sizes {
xsmall = 'is-size-x-small',
small = 'is-size-small',
@@ -18,7 +21,7 @@
}
</script>
<ul class="avatars-group" class:is-with-border={bordered}>
<ul class="avatars-group {classes}" class:is-with-border={bordered}>
{#each avatars as name, index}
{#if index < 2}
<li class="avatars-group-item">
@@ -64,7 +64,7 @@
</span>
<p class="text u-margin-block-start-16">
You'll pay <span class="u-bold">{formatCurrency(estimatedTotal)}</span> now, with our first
You'll pay <span class="u-bold">{formatCurrency(estimatedTotal)}</span> now, with your first
billing cycle starting on
<span class="u-bold"
>{!currentPlan.trialDays
@@ -140,6 +140,7 @@
<InputText
id="taxId"
label="Tax ID"
autofocus
placeholder="Tax ID"
bind:value={taxId} />
</div>
+7 -1
View File
@@ -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}
+2 -2
View File
@@ -57,9 +57,9 @@
<style lang="scss">
// TODO: remove once pink is updated
.collapsible-item {
.collapsible-item:not(.is-info) {
.collapsible-wrapper {
padding-left: 0;
padding-left: 0.5rem;
}
.collapsible-wrapper.is-disabled {
cursor: not-allowed;
+2 -1
View File
@@ -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>
+1 -1
View File
@@ -73,7 +73,7 @@
</button>
</header>
<div class="upload-box-content is-open">
<section class="progress-bar">
<section class="progress-bar u-padding-inline-16 u-padding-block-16">
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span>{percentage}%</span>
</div>
+4 -7
View File
@@ -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}
+12
View File
@@ -21,6 +21,18 @@
let error: string;
onMount(() => {
if (autofocus) {
addInputFocus();
}
});
export function addInputFocus() {
if (element) {
element.focus();
}
}
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
+55 -11
View File
@@ -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>
+3 -4
View File
@@ -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>
+9 -6
View File
@@ -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>
+6 -2
View File
@@ -75,7 +75,11 @@
<TableBody>
{#each rules.rules as domain, i}
<TableRow>
<TableCellLink title="Domain" href={`http://${domain.domain}`} external noStyle>
<TableCellLink
title="Domain"
href={`https://${domain.domain}`}
external
noStyle>
<span class="u-flex u-gap-4 u-cross-center">
<Trim>
<span class="link">{domain.domain}</span>
@@ -135,7 +139,7 @@
{:else}
<Pill warning>
<span class="icon-clock u-text-color-gray" aria-hidden="true" />
<p class="text">Blocked by verification</p>
<p class="text">blocked by verification</p>
</Pill>
{/if}
</TableCell>
+5 -1
View File
@@ -11,6 +11,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
let retrying = false;
export let showTitle = true;
const dispatch = createEventDispatcher();
@@ -36,7 +37,10 @@
}
</script>
<Trim alternativeTrim><b>{$domain.domain}</b></Trim>
{#if showTitle}
<Trim alternativeTrim><b>{$domain.domain}</b></Trim>
{/if}
{#if $domain.status === 'created'}
<Box radius="small">
<div class="u-flex u-gap-8 u-cross-center">
+1 -1
View File
@@ -18,7 +18,7 @@
<div class="boxes-wrapper u-margin-block-start-24">
{#if $domain.status === 'created'}
<Retry on:error={onRetryError} />
<Retry on:error={onRetryError} showTitle={false} />
{:else}
<div class="u-flex u-gap-8 u-cross-center">
<span class="icon-check u-color-text-success" aria-hidden="true" />
+6
View File
@@ -281,6 +281,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 = {
+2
View File
@@ -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
View File
@@ -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 Organization = Models.Team<Record<string, unknown>> & {
billingBudget: number;
@@ -43,4 +44,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>
+17 -1
View File
@@ -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;
@@ -71,6 +71,7 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Function"
bind:id={$createFunction.$id}
@@ -67,7 +67,7 @@
$choices.branch = event.detail.value;
}}
interactiveOutput
name="branch"
name="branches"
{options} />
<InputText
id="root"
@@ -48,6 +48,7 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Function"
bind:id={$createFunction.$id}
@@ -73,6 +73,7 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Function"
bind:id={$templateConfig.$id}
File diff suppressed because one or more lines are too long
+1 -3
View File
@@ -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>
@@ -91,7 +91,7 @@
{#each linkedOrgs as org}
<a
class="u-underline u-trim"
href={`${base}/console/organization-${org.$id}/billing`}>
href={`${base}/organization-${org.$id}/billing`}>
{org.name}
</a>
{/each}
@@ -87,7 +87,7 @@
{#each linkedOrgs as org}
<a
class="u-underline u-trim"
href={`${base}/console/organization-${org.$id}/billing`}>
href={`${base}/organization-${org.$id}/billing`}>
{org.name}
</a>
{/each}
@@ -189,6 +189,11 @@
$: selectedOrg = $organizationList?.teams?.find(
(team) => team.$id === selectedOrgId
) as Organization;
$: billingPlan =
selectedOrg?.billingPlan === BillingPlan.SCALE
? BillingPlan.SCALE
: (campaign?.plan ?? BillingPlan.PRO);
</script>
<svelte:head>
@@ -268,7 +273,7 @@
</div>
</div>
{/if}
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE}
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
<section
class="card u-margin-block-start-24"
style:--p-card-padding="1.5rem"
+7 -4
View File
@@ -3,9 +3,12 @@ import BackupsDark from '$lib/images/backups/promo/backups-dark.png';
import BackupsLight from '$lib/images/backups/promo/backups-light.png';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import { isCloud } from '$lib/system';
const listOfPromotions: BottomModalAlertItem[] = [
{
const listOfPromotions: BottomModalAlertItem[] = [];
if (isCloud) {
listOfPromotions.push({
id: 'modal:databaseBackups',
src: {
dark: BackupsDark,
@@ -24,8 +27,8 @@ const listOfPromotions: BottomModalAlertItem[] = [
text: 'Learn more',
link: () => 'http://appwrite.io/docs/products/databases/backups'
}
}
];
});
}
export function addBottomModalAlerts() {
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
@@ -75,7 +75,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Organization" bind:id />
<CustomId autofocus bind:show={showCustomId} name="Organization" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
+8 -3
View File
@@ -27,11 +27,11 @@
? [
{
value: BillingPlan.FREE,
label: `${tierToPlan(BillingPlan.FREE).name} - ${formatCurrency($plansInfo.get(BillingPlan.FREE).price)}/month`
label: `${tierToPlan(BillingPlan.FREE).name} - ${formatCurrency($plansInfo.get(BillingPlan.FREE).price)} / month`
},
{
value: BillingPlan.PRO,
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)} / month + add-ons`
}
// {
// value: BillingPlan.SCALE,
@@ -125,7 +125,12 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Organization" isProject bind:id />
<CustomId
autofocus
bind:show={showCustomId}
name="Organization"
isProject
bind:id />
{/if}
{#if isCloud}
<div class="u-margin-block-start-8">
@@ -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
@@ -1,6 +1,6 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { organization } from '$lib/stores/organization';
import { currentPlan, organization } from '$lib/stores/organization';
import BudgetAlert from './budgetAlert.svelte';
import BudgetCap from './budgetCap.svelte';
import PlanSummary from './planSummary.svelte';
@@ -123,7 +123,7 @@
<PlanSummary
creditList={data?.creditList}
members={data?.members}
currentPlan={data?.currentPlan}
currentPlan={$currentPlan}
invoices={data?.invoices.invoices} />
<PaymentHistory />
<PaymentMethods />
@@ -25,33 +25,24 @@ export const load: PageLoad = async ({ parent, depends }) => {
.catch(() => null)
: null;
const [
paymentMethods,
addressList,
aggregationList,
billingAddress,
currentPlan,
creditList,
invoices
] = await Promise.all([
sdk.forConsole.billing.listPaymentMethods(),
sdk.forConsole.billing.listAddresses(),
sdk.forConsole.billing.listAggregation(organization.$id),
billingAddressPromise,
sdk.forConsole.billing.getPlan(organization.$id),
sdk.forConsole.billing.listCredits(organization.$id),
sdk.forConsole.billing.listInvoices(organization.$id, [
Query.limit(1),
Query.equal('from', organization.billingCurrentInvoiceDate)
])
]);
const [paymentMethods, addressList, aggregationList, billingAddress, creditList, invoices] =
await Promise.all([
sdk.forConsole.billing.listPaymentMethods(),
sdk.forConsole.billing.listAddresses(),
sdk.forConsole.billing.listAggregation(organization.$id),
billingAddressPromise,
sdk.forConsole.billing.listCredits(organization.$id),
sdk.forConsole.billing.listInvoices(organization.$id, [
Query.limit(1),
Query.equal('from', organization.billingCurrentInvoiceDate)
])
]);
return {
paymentMethods,
addressList,
aggregationList,
billingAddress,
currentPlan,
creditList,
invoices
};
@@ -40,7 +40,12 @@
Apply Appwrite credits to your organization.
<FormList>
<InputText placeholder="Promo code" id="code" label="Add promo code" bind:value={coupon} />
<InputText
placeholder="Promo code"
id="code"
autofocus
label="Add promo code"
bind:value={coupon} />
</FormList>
<svelte:fragment slot="footer">
@@ -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';
@@ -37,7 +37,7 @@
} catch (error) {
addNotification({
type: 'error',
message: `There was an error enabling your budget cap`
message: error.message
});
trackError(error, Submit.BudgetCapUpdate);
}
@@ -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
@@ -84,6 +84,7 @@
<InputNumber
placeholder="Add budget cap"
id="cap"
autofocus
label="Budget cap (USD)"
bind:value={budget} />
{/if}
@@ -18,15 +18,15 @@
export let currentPlan: Plan;
export let creditList: CreditList;
const currentInvoice: Invoice = invoices[0];
const currentInvoice: Invoice | undefined = invoices.length > 0 ? invoices[0] : undefined;
const extraMembers = members.total > 1 ? members.total - 1 : 0;
const availableCredit = creditList.available;
const today = new Date();
const isTrial =
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
$plansInfo.get($organization.billingPlan)?.trialDays;
const extraUsage = currentInvoice.amount - currentPlan?.price;
const extraAddons = currentInvoice.usage?.length;
const extraUsage = currentInvoice ? currentInvoice.amount - currentPlan?.price : 0;
const extraAddons = currentInvoice ? currentInvoice.usage?.length : 0;
</script>
{#if $organization}
@@ -186,7 +186,7 @@
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div class="u-flex u-gap-16 u-flex-wrap">
<Button text href={`${base}/organization-${$organization?.$id}/usage`}>
View estimated usage
@@ -202,7 +202,7 @@
Upgrade
</Button>
</div>
{:else if $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
{:else}
<div class="u-flex u-gap-16 u-flex-wrap">
<Button
text
@@ -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>
@@ -32,7 +32,12 @@
import { type Coupon, type PaymentList } from '$lib/sdk/billing';
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization, organizationList, type Organization } from '$lib/stores/organization';
import {
currentPlan,
organization,
organizationList,
type Organization
} from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { VARS } from '$lib/system';
@@ -101,8 +106,7 @@
billingPlan = BillingPlan.PRO;
}
const currentPlan = await sdk.forConsole.billing.getPlan($organization?.$id);
selfService = currentPlan.selfService;
selfService = $currentPlan?.selfService ?? true;
});
async function loadPaymentMethods() {
@@ -1,16 +1,16 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Alert, Modal } from '$lib/components';
import { Modal } from '$lib/components';
import { InputText, InputEmail, Button, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { organization } from '$lib/stores/organization';
import { invalidate } from '$app/navigation';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isCloud } from '$lib/system';
import { isCloud, isSelfHosted } from '$lib/system';
import { roles } from '$lib/stores/billing';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import Roles from '$lib/components/roles/roles.svelte';
@@ -22,7 +22,7 @@
let email: string,
name: string,
error: string,
role: string = 'developer';
role: string = isSelfHosted ? 'owner' : 'developer';
async function create() {
try {
@@ -60,22 +60,6 @@
</script>
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
{#if isCloud}
{#if $organization?.billingPlan === BillingPlan.PRO}
<!-- <Alert type="info">
You can add unlimited organization members on the {plan.name} plan for
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
</Alert> -->
<Alert type="info">
New roles are free until 1st January 2025. <a
class="link"
href="https://appwrite.io/docs/advanced/platform/roles"
target="_blank"
rel="noopener noreferrer">Learn more</a
>.
</Alert>
{/if}
{/if}
<FormList>
<InputEmail
required
@@ -89,7 +73,9 @@
label="Name (optional)"
placeholder="Enter name"
bind:value={name} />
<InputSelect popover={Roles} id="role" label="Role" options={roles} bind:value={role} />
{#if isCloud}
<InputSelect popover={Roles} id="role" label="Role" options={roles} bind:value={role} />
{/if}
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
@@ -61,7 +61,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
<CustomId autofocus bind:show={showCustomId} name="Project" isProject bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
@@ -143,7 +143,6 @@
<Heading tag="h6" size="7">Users</Heading>
<p class="text">The total number of users across all projects in your organization.</p>
<svelte:fragment slot="aside">
{#if data.organizationUsage.users}
{@const current = data.organizationUsage.usersTotal}
@@ -169,7 +168,7 @@
{
name: 'Users',
data: accumulateFromEndingTotal(
data.organizationUsage.users,
data.usersUsageToDate,
data.organizationUsage.usersTotal
)
}
@@ -72,12 +72,15 @@ export const load: PageLoad = async ({ params, parent }) => {
}
}
const usersUsageToDate = usage.users.filter((user) => new Date(user.date) < new Date());
return {
organizationUsage: usage,
projectNames,
invoices,
currentInvoice,
organizationMembers,
plan
plan,
usersUsageToDate
};
};
@@ -3,12 +3,12 @@
import { Collapsible, CollapsibleItem } from '$lib/components';
import {
TableBody,
TableCellLink,
TableCell,
TableCellHead,
TableHeader,
TableRow,
TableScroll,
TableCellText
TableRowLink,
TableScroll
} from '$lib/elements/table';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
@@ -53,31 +53,43 @@
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">Project breakdown</svelte:fragment>
<TableScroll noMargin>
<TableScroll noMargin style="table-layout: auto">
<TableHeader>
<TableCellHead width={185} style="padding-left: 0;">Project</TableCellHead>
<TableCellHead width={100}>Usage</TableCellHead>
<TableCellHead>Project</TableCellHead>
<TableCellHead>Usage</TableCellHead>
{#if $canSeeProjects}
<TableCellHead width={140} />
<TableCellHead />
{/if}
</TableHeader>
<TableBody>
{#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project}
<TableRow>
<TableCellText title="Project" style="padding-left: 0;">
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
</TableCellText>
<TableCellText title="Usage">{format(project.usage)}</TableCellText>
{#if $canSeeProjects}
<TableCellLink
title="Go to project usage"
href={getProjectUsageLink(project.projectId)}>
View project usage
</TableCellLink>
{/if}
</TableRow>
{#if !$canSeeProjects}
<TableRow>
<TableCell title="Project">
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
</TableCell>
<TableCell title="Usage">{format(project.usage)}</TableCell>
</TableRow>
{:else}
<TableRowLink href={getProjectUsageLink(project.projectId)}>
<TableCell title="Project">
{data.projectNames[project.projectId]?.name ?? 'Unknown'}
</TableCell>
<TableCell title="Usage">{format(project.usage)}</TableCell>
<TableCell right={true}>
<span
class="icon-cheveron-right u-cross-child-center ignore-icon-rotate" />
</TableCell>
</TableRowLink>
{/if}
{/each}
</TableBody>
</TableScroll>
</CollapsibleItem>
</Collapsible>
<style>
.ignore-icon-rotate {
rotate: unset !important;
}
</style>
@@ -31,6 +31,7 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Project"
isProject={true}
@@ -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);
@@ -58,7 +58,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Team" bind:id />
<CustomId autofocus bind:show={showCustomId} name="Team" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
@@ -84,7 +84,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showDropdown} name="User" bind:id />
<CustomId autofocus bind:show={showDropdown} name="User" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
@@ -1,8 +1,5 @@
<script lang="ts">
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import { Container } from '$lib/layout';
import { isCloud } from '$lib/system';
import UpdateMockNumbers from './updateMockNumbers.svelte';
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
@@ -23,7 +20,5 @@
<UpdatePersonalDataCheck />
<UpdateSessionAlerts />
<UpdateMockNumbers />
{#if isCloud && $organization.billingPlan === BillingPlan.SCALE}
<UpdateMembershipPrivacy />
{/if}
<UpdateMembershipPrivacy />
</Container>
@@ -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';
@@ -8,6 +8,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { tick } from 'svelte';
let passwordHistory = $project?.authPasswordHistory < 1 ? 5 : $project?.authPasswordHistory;
let passwordHistoryEnabled = ($project?.authPasswordHistory ?? 0) !== 0;
@@ -34,6 +35,14 @@
trackError(error, Submit.AuthPasswordHistoryUpdate);
}
}
let maxSessionInputField: InputNumber | null = null;
$: if (passwordHistoryEnabled && maxSessionInputField) {
tick().then(() => {
maxSessionInputField.addInputFocus();
});
}
</script>
<Form onSubmit={updatePasswordHistoryLimit}>
@@ -59,6 +68,7 @@
min={1}
id="max-session"
label="Limit"
bind:this={maxSessionInputField}
disabled={!passwordHistoryEnabled}
bind:value={passwordHistory} />
</FormList>
@@ -9,6 +9,7 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { tick } from 'svelte';
let isLimited = $project?.authLimit !== 0;
let newLimit = isLimited ? $project?.authLimit : 100;
@@ -41,6 +42,14 @@
trackError(error, Submit.AuthLimitUpdate);
}
}
let maxUsersInputField: HTMLInputElement | null = null;
$: if (isLimited && maxUsersInputField) {
tick().then(() => {
maxUsersInputField.focus();
});
}
</script>
<CardGrid>
@@ -96,7 +105,8 @@
class="input-text"
max="10000"
disabled={!isLimited}
bind:value={newLimit} />
bind:value={newLimit}
bind:this={maxUsersInputField} />
</div>
</li>
</ul>
@@ -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>
@@ -10,15 +10,16 @@ export const load: PageLoad = async ({ params, url, route, depends }) => {
const limit = getLimit(url, route, CARD_LIMIT);
const view = getView(url, route, View.Grid);
const offset = pageToOffset(page, limit);
const collections = await sdk.forProject.databases.listCollections(params.database, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc('')
]);
return {
offset,
limit,
view,
collections: await sdk.forProject.databases.listCollections(params.database, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc('')
])
collections
};
};
@@ -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;
@@ -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'}>
@@ -77,20 +77,28 @@
$: handleDefaultState($required || $array);
</script>
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
step="any"
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
step="any"
required={editing} />
<div>
<ul class="u-flex u-gap-16">
<li class="u-flex-basis-50-percent">
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
step="any"
required={editing} />
</li>
<li class="u-flex-basis-50-percent">
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
step="any"
required={editing} />
</li>
</ul>
</div>
<InputNumber
id="default"
label="Default value"
@@ -77,18 +77,27 @@
$: handleDefaultState($required || $array);
</script>
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
required={editing} />
<div>
<ul class="u-flex u-gap-16">
<li class="u-flex-basis-50-percent">
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
required={editing} />
</li>
<li class="u-flex-basis-50-percent">
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
required={editing} />
</li>
</ul>
</div>
<InputNumber
id="default"
label="Default value"
@@ -157,30 +157,32 @@
</ul>
</li>
<InputSelectSearch
id="related"
label="Related Collection"
name="collections"
bind:search
bind:value={data.relatedCollection}
required
interactiveOutput={!editing}
placeholder="Select a collection"
disabled={editing}
options={collections?.map((n) => ({ value: n.$id, label: n.$id, data: [n.name] })) ?? []}
on:select={updateKeyName}
let:option={o}>
<SelectSearchItem data={o.data}>
{o.label}
</SelectSearchItem>
<svelte:fragment slot="output" let:option={o}>
<output class="input-text" class:is-read-only={editing}>
<SelectSearchItem data={o.data}>
{o.label}
</SelectSearchItem>
</output>
</svelte:fragment>
</InputSelectSearch>
<div>
<InputSelectSearch
id="related"
label="Related Collection"
name="collections"
bind:search
bind:value={data.relatedCollection}
required
interactiveOutput={!editing}
placeholder="Select a collection"
disabled={editing}
options={collections?.map((n) => ({ value: n.$id, label: n.$id, data: [n.name] })) ?? []}
on:select={updateKeyName}
let:option={o}>
<SelectSearchItem data={o.data}>
{o.label}
</SelectSearchItem>
<svelte:fragment slot="output" let:option={o}>
<output class="input-text" class:is-read-only={editing}>
<SelectSearchItem data={o.data}>
{o.label}
</SelectSearchItem>
</output>
</svelte:fragment>
</InputSelectSearch>
</div>
{#if data?.relatedCollection}
<div>
@@ -192,13 +194,11 @@
autofocus
required />
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center">
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
<span
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
</span>
<span class="text u-line-height-1-5">Allowed characters: a-z, A-Z, 0-9, -, .</span>
</div>
</div>
{#if way === 'two'}
@@ -211,12 +211,13 @@
required
readonly={editing} />
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center">
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
<span
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
Allowed characters: a-z, A-Z, 0-9, -, . Once created, attribute key cannot be
adjusted to maintain data integrity.
</span>
</div>
</div>
@@ -90,12 +90,12 @@
autofocus
required />
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center">
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
<span
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
Allowed characters: alphanumeric, hyphen, non-leading underscore, period.
</span>
</div>
</div>
@@ -29,7 +29,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Document" bind:id={customId} />
<CustomId autofocus bind:show={showCustomId} name="Document" bind:id={customId} />
{/if}
{/if}
</ul>
@@ -2,7 +2,7 @@
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputSelect, InputText } from '$lib/elements/forms';
@@ -11,8 +11,7 @@
import { sdk } from '$lib/stores/sdk';
import { IndexType } from '@appwrite.io/console';
import { isRelationship } from '../document-[document]/attributes/store';
import { indexes, type Attributes } from '../store';
import { collection } from '../store';
import { type Attributes, collection, indexes } from '../store';
import Select from './select.svelte';
export let showCreateIndex = false;
@@ -20,8 +19,8 @@
const databaseId = $page.params.database;
let key = '';
let error: string;
let key = `index_${$indexes.length + 1}`;
let types = [
{ value: IndexType.Key, label: 'Key' },
{ value: IndexType.Unique, label: 'Unique' },
@@ -38,6 +37,17 @@
let attributeList = [{ value: '', order: '' }];
function generateIndexKey() {
let indexKeys = $indexes.map((index) => index.key);
let highestIndex = indexKeys.reduce((max, key) => {
const match = key.match(/^index_(\d+)$/);
return match ? Math.max(max, parseInt(match[1], 10)) : max;
}, indexKeys.length);
return `index_${highestIndex + 1}`;
}
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC' }]
@@ -49,6 +59,7 @@
$: if (showCreateIndex) {
error = null;
initialize();
key = generateIndexKey();
}
$: addAttributeDisabled = !attributeList.at(-1)?.value || !attributeList.at(-1)?.order;
@@ -83,12 +94,9 @@
});
trackEvent(Submit.IndexCreate);
showCreateIndex = false;
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.IndexCreate);
} catch (err) {
error = err.message;
trackError(err, Submit.IndexCreate);
}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, InputText, FormList } from '$lib/elements/forms';
import { InputText, FormList } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
export let showOverview = false;
@@ -50,11 +50,6 @@
readonly />
</div>
</div>
<div class="form-item-part u-cross-child-end">
<Button text disabled>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
{/if}
@@ -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>
@@ -23,7 +23,7 @@
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { afterUpdate, onMount } from 'svelte';
import type { PageData } from './$types';
import { isRelationship, isRelationshipToMany } from './document-[document]/attributes/store';
import RelationshipsModal from './relationshipsModal.svelte';
@@ -44,8 +44,23 @@
onMount(async () => {
displayNames = preferences.getDisplayNames();
updateMaxWidth();
});
afterUpdate(() => updateMaxWidth());
function updateMaxWidth() {
const tableCells = Array.from(document.querySelectorAll('.less-width-truncated'));
const visibleColumnsCount = $columns.filter((col) => col.show).length;
const newMaxWidth = Math.max(50 - (visibleColumnsCount - 1) * 5, 25);
tableCells.forEach((cell) => {
const cellItem = cell as HTMLElement;
cellItem.style.maxWidth = `${newMaxWidth}vw`;
});
}
function formatArray(array: unknown[]) {
if (array.length === 0) return '[ ]';
@@ -220,15 +235,19 @@
</TableCell>
{:else}
{@const formatted = formatColumn(document[column.id])}
<TableCell>
<TableCell class="truncated-content-table-cell">
<div
class="u-width-fit-content"
class:truncated={formatted.whole.length >
formatted.value.length}
class:less-width-truncated={$columns.filter((col) => col.show)
.length > 1}
use:tooltip={{
content: formatted.whole,
disabled: !formatted.truncated
}}
data-private>
{formatted.value}
data-private
data-content={formatted.whole}>
{formatted.whole}
</div>
</TableCell>
{/if}
@@ -345,4 +364,16 @@
display: inline-block;
}
}
:global(.truncated-content-table-cell .truncated) {
max-width: 50vw;
overflow: hidden;
white-space: nowrap;
display: inline-block;
text-overflow: ellipsis;
}
:global(.truncated-content-table-cell:has(.truncated)) {
inline-size: 0;
}
</style>
@@ -71,10 +71,10 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Collection"
bind:id
autofocus={false}
fullWidth={true} />
{/if}
</FormList>
@@ -4,17 +4,76 @@
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Button, InputCheckbox } from '$lib/elements/forms';
import { Button, FormList, InputCheckbox } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { database } from './store';
import { FormList } from '$lib/elements/forms/index.js';
import {
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import { type Models, Query } from '@appwrite.io/console';
const databaseId = $page.params.database;
export let showDelete = false;
let confirmedDeletion = false;
let error = null;
let isLoadingDocumentsCount = false;
/* enable overflow-x */
const columnWidth = 135;
$: collectionItems = [];
let collections: Models.CollectionList = null;
function buildQueries(): string[] {
const queries = [Query.orderDesc('$updatedAt')];
if (collectionItems.length > 0) {
queries.push(Query.limit(25));
queries.push(Query.offset(collectionItems.length));
} else {
queries.push(Query.limit(3));
}
return queries;
}
async function listCollections() {
// let's just wait...
if (isLoadingDocumentsCount) return;
isLoadingDocumentsCount = true;
try {
const queries = buildQueries();
collections = await sdk.forProject.databases.listCollections(databaseId, queries);
const collectionPromises = collections.collections.map(async (collection) => {
return {
id: collection.$id,
name: collection.name,
updatedAt: collection.$updatedAt
};
});
collectionItems = [...collectionItems, ...(await Promise.all(collectionPromises))];
} catch (err) {
error = true;
} finally {
isLoadingDocumentsCount = false;
}
}
const handleDelete = async () => {
try {
await sdk.forProject.databases.delete(databaseId);
@@ -33,40 +92,115 @@
trackError(error, Submit.DatabaseDelete);
}
};
/* reset data on modal close */
$: if (!showDelete) {
collections = null;
collectionItems = [];
}
</script>
<Modal
title="Delete database"
icon="exclamation"
state="warning"
size="small"
bind:show={showDelete}
onSubmit={handleDelete}
headerDivider={false}>
<FormList>
<p class="text" data-private>
Are you sure you want to delete <b>{$database.name}</b>?
</p>
<div class="max-height-dialog">
<Modal
title="Delete database"
icon="exclamation"
size="big"
state="warning"
bind:show={showDelete}
onSubmit={handleDelete}
headerDivider={false}>
{#await listCollections()}
<div class="u-flex u-main-center">
<div class="loader" />
</div>
{:then}
{#if error}
<p class="text" data-private>
Are you sure you want to delete <b>{$database.name}</b>?
</p>
{:else if collectionItems.length > 0}
<p class="text" data-private>
The following collections and all data associated with <b>{$database.name}</b>,
will be permanently deleted.
</p>
<p class="text">
<b>
Once deleted, this database and its backups cannot be restored. This action is
irreversible.
</b>
</p>
<div class="u-flex-vertical u-gap-16">
<TableScroll dense noMargin class="table">
<TableHeader>
<TableCellHead width={columnWidth}>Collection</TableCellHead>
<!-- small spacer placeholder -->
<TableCellHead width={columnWidth / 4} />
<TableCellHead width={columnWidth}>Last Updated</TableCellHead>
</TableHeader>
<TableBody>
{#each collectionItems as collection}
<TableRow>
<TableCell title="Collection">{collection.name}</TableCell>
<TableCell />
<TableCell title="Last Updated"
>{toLocaleDate(collection.updatedAt)}</TableCell>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<div class="input-check-box-friction">
<InputCheckbox
required
size="small"
id="delete_policy"
bind:checked={confirmedDeletion}
label="I understand and confirm" />
</div>
</FormList>
{#if collectionItems.length < collections.total}
<div class="u-flex u-gap-16 u-cross-center">
<button class="u-underline" on:click={listCollections} type="button">
Show more
</button>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit disabled={!confirmedDeletion}>Delete</Button>
</svelte:fragment>
</Modal>
{#if isLoadingDocumentsCount}
<div class="loader is-small" />
{/if}
</div>
{:else if collectionItems.length > 25}
<button
class="u-underline"
on:click={() => {
collectionItems = collectionItems.slice(0, 3);
}}
type="button">
Show less
</button>
{/if}
</div>
{:else}
<p class="text" data-private>
Are you sure you want to delete <b>{$database.name}</b>?
</p>
{/if}
{/await}
{#if !isLoadingDocumentsCount}
<p class="text" data-private>
<b>
Once deleted, this database and its backups cannot be restored. This action is
irreversible.
</b>
</p>
<FormList>
<div class="input-check-box-friction">
<InputCheckbox
required
size="small"
id="delete_policy"
bind:checked={confirmedDeletion}
label="I understand and confirm" />
</div>
</FormList>
{/if}
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit disabled={!confirmedDeletion}>Delete</Button>
</svelte:fragment>
</Modal>
</div>
<style>
:global(.max-height-dialog dialog) {
max-height: 650px;
}
</style>
@@ -12,6 +12,7 @@
import { onMount } from 'svelte';
import Delete from '../delete.svelte';
import { database } from '../store';
import { Query } from '@appwrite.io/console';
let showDelete = false;
let showError: false | 'name' | 'email' | 'password' = false;
@@ -23,6 +24,13 @@
databaseName ??= $database.name;
});
async function loadCollectionCount() {
const { total } = await sdk.forProject.databases.listCollections($database.$id, [
Query.limit(1)
]);
return total;
}
function addError(location: typeof showError, message: string, type: typeof errorType) {
showError = location;
errorMessage = message;
@@ -82,7 +90,8 @@
<svelte:fragment slot="actions">
<Button disabled={databaseName === $database.name || !databaseName} submit
>Update</Button>
>Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -93,14 +102,22 @@
</div>
<p>
The database will be permanently deleted, including all data associated with this
team. This action is irreversible.
The database will be permanently deleted, including all collections within it. This
action is irreversible.
</p>
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$database.name}</h6>
<span>Last updated: {toLocaleDateTime($database.$updatedAt)}</span>
<span class="u-flex u-gap-8">
{#await loadCollectionCount()}
<div class="loader is-small" />
{:then count}
{count}
{/await}
Collections
</span>
</svelte:fragment>
</BoxAvatar>
</svelte:fragment>
@@ -74,7 +74,7 @@
{/each}
</TableHeader>
<TableBody>
{#each data.collections.collections as collection}
{#each data.collections.collections as collection (collection.$id)}
<TableRowLink
href={`${base}/project-${projectId}/databases/database-${databaseId}/collection-${collection.$id}`}>
{#if $canWriteCollections}
@@ -2,10 +2,19 @@ import type { Column } from '$lib/helpers/types';
import { isCloud } from '$lib/system';
import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Database ID', type: 'string', show: true, width: 150 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 120 },
isCloud && { id: 'backup', title: 'Backups', type: 'string', show: true, width: 120 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 120 },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', show: true, width: 120 }
]);
export const columns = writable<Column[]>(
isCloud
? [
{ id: '$id', title: 'Database ID', type: 'string', show: true, width: 150 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 120 },
{ id: 'backup', title: 'Backups', type: 'string', show: true, width: 120 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 120 },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', show: true, width: 120 }
]
: [
{ id: '$id', title: 'Database ID', type: 'string', show: true, width: 150 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 120 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 120 },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', show: true, width: 120 }
]
);
@@ -1,28 +1,18 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { tooltip } from '$lib/actions/tooltip';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Id, Modal } from '$lib/components';
import FloatingActionBar from '$lib/components/floatingActionBar.svelte';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputCheckbox } from '$lib/elements/forms';
import { Id } from '$lib/components';
import {
TableBody,
TableCell,
TableCellCheck,
TableCellHead,
TableCellHeadCheck,
TableCellText,
TableHeader,
TableRowLink,
TableScroll
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { canWriteDatabases } from '$lib/stores/roles';
import { sdk } from '$lib/stores/sdk';
import type { PageData } from './$types';
import { columns } from './store';
import Cell from '$lib/elements/table/cell.svelte';
@@ -30,36 +20,6 @@
export let data: PageData;
const projectId = $page.params.project;
let selected: string[] = [];
let showDelete = false;
let deleting = false;
let confirmedDeletion = false;
async function handleDelete() {
showDelete = false;
const promises = selected.map((databaseId) => sdk.forProject.databases.delete(databaseId));
try {
await Promise.all(promises);
trackEvent(Submit.DatabaseDelete);
addNotification({
type: 'success',
message: `${selected.length} database${selected.length > 1 ? 's' : ''} deleted`
});
invalidate(Dependencies.DATABASES);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DatabaseDelete);
} finally {
selected = [];
showDelete = false;
confirmedDeletion = false;
}
}
function getPolicyDescription(cron: string): string {
const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' ');
@@ -72,11 +32,6 @@
<TableScroll>
<TableHeader>
{#if $canWriteDatabases}
<TableCellHeadCheck
bind:selected
pageItemsIds={data.databases.databases.map((c) => c.$id)} />
{/if}
{#each $columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
@@ -84,11 +39,8 @@
{/each}
</TableHeader>
<TableBody>
{#each data.databases.databases as database}
{#each data.databases.databases as database (database.$id)}
<TableRowLink href={`${base}/project-${projectId}/databases/database-${database.$id}`}>
{#if $canWriteDatabases}
<TableCellCheck bind:selectedIds={selected} id={database.$id} />
{/if}
{#each $columns as column}
{#if column.show}
{#if column.id === '$id'}
@@ -137,76 +89,7 @@
</TableBody>
</TableScroll>
<FloatingActionBar show={selected.length > 0}>
<div class="u-flex u-cross-center u-main-space-between actions">
<div class="u-flex u-cross-center u-gap-8">
<span class="indicator body-text-2 u-bold">{selected.length}</span>
<p>
<span class="is-only-desktop">
{selected.length > 1 ? 'databases' : 'database'}
</span>
selected
</p>
</div>
<div class="u-flex u-cross-center u-gap-8">
<Button text on:click={() => (selected = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>
<p>Delete</p>
</Button>
</div>
</div>
</FloatingActionBar>
<Modal
title="Delete Database"
icon="exclamation"
state="warning"
size="small"
bind:show={showDelete}
onSubmit={handleDelete}
headerDivider={false}
closable={!deleting}>
<FormList>
<p class="text" data-private>
Are you sure you want to delete <b>{selected.length}</b>
{selected.length > 1 ? 'databases' : 'database'}?
</p>
<p class="text">
<b>
Once deleted, {selected.length > 1 ? 'these databases' : 'this database'} and its backups
cannot be restored. This action is irreversible.
</b>
</p>
<div class="input-check-box-friction">
<InputCheckbox
required
size="small"
id="delete_policy"
bind:checked={confirmedDeletion}
label="I understand and confirm" />
</div>
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)} disabled={deleting}>Cancel</Button>
<Button secondary submit disabled={deleting || !confirmedDeletion}>Delete</Button>
</svelte:fragment>
</Modal>
<style lang="scss">
.actions {
.indicator {
border-radius: 0.25rem;
background: hsl(var(--color-information-100));
color: hsl(var(--color-neutral-0));
padding: 0rem 0.375rem;
display: inline-block;
}
}
.icon-exclamation {
color: hsl(var(--color-warning-100)) !important;
}
@@ -21,7 +21,7 @@
showCancel = false;
addNotification({
type: 'success',
message: `Deployment has been deleted`
message: `Deployment has been cancelled`
});
trackEvent(Submit.DeploymentCancel);
} catch (error) {
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -52,6 +52,7 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Function"
bind:id={$createFunction.id}
@@ -63,7 +63,7 @@
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<CardGrid hideFooter={message.status !== 'draft'}>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<Heading tag="h6" size="7">Message</Heading>
</div>
@@ -72,12 +72,12 @@
<InputText
id="subject"
label="Subject"
disabled={message.status != 'draft'}
disabled={message.status !== 'draft'}
bind:value={subject}></InputText>
<InputTextarea
id="message"
label="Message"
disabled={message.status != 'draft'}
disabled={message.status !== 'draft'}
bind:value={content}></InputTextarea>
<InputSwitch label="HTML mode" id="html" bind:value={html}>
<svelte:fragment slot="description">
@@ -58,7 +58,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Topic" bind:id />
<CustomId autofocus bind:show={showCustomId} name="Topic" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
@@ -3,7 +3,7 @@
import { Button, FormList, InputCheckbox, InputSearch } from '$lib/elements/forms';
import { Table, TableBody, TableCell, TableRow } from '$lib/elements/table';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models, MessagingProviderType } from '@appwrite.io/console';
import { MessagingProviderType, type Models, Query } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { getTotal } from './wizard/store';
import ProviderType from './providerType.svelte';
@@ -22,6 +22,8 @@
let selected: Record<string, Models.Topic> = {};
let hasSelection = false;
let emptyTopicsExists = false;
function reset() {
offset = 0;
search = '';
@@ -36,16 +38,22 @@
if (!show) return;
const queries = [Query.limit(5), Query.offset(offset)];
if (providerType === MessagingProviderType.Email) {
queries.push(Query.greaterThan('emailTotal', 0));
} else if (providerType === MessagingProviderType.Sms) {
queries.push(Query.greaterThan('smsTotal', 0));
} else if (providerType === MessagingProviderType.Push) {
queries.push(Query.greaterThan('pushTotal', 0));
}
const response = await sdk.forProject.messaging.listTopics(queries, search || undefined);
if (response.total !== 0) {
switch (providerType) {
case MessagingProviderType.Email:
emptyTopicsExists = response.topics.every((topic) => topic.emailTotal === 0);
break;
case MessagingProviderType.Sms:
emptyTopicsExists = response.topics.every((topic) => topic.smsTotal === 0);
break;
case MessagingProviderType.Push:
emptyTopicsExists = response.topics.every((topic) => topic.pushTotal === 0);
break;
}
}
totalResults = response.total;
topicResultsById = {};
response.topics.forEach((topic) => {
@@ -94,7 +102,9 @@
<div class="u-flex u-flex-vertical u-gap-32">
<p class="text">
Select existing topics you want to send this message to its targets. The message will be
sent only to <ProviderType type={providerType} noIcon /> targets.
sent only to
<ProviderType type={providerType} noIcon />
targets.
</p>
<div class="u-flex u-flex-vertical u-gap-16">
<InputSearch
@@ -103,7 +113,7 @@
placeholder="Search for topics"
bind:value={search} />
<div>
{#if Object.keys(topicResultsById).length > 0}
{#if Object.keys(topicResultsById).length > 0 && !emptyTopicsExists}
<div class="u-flex-vertical u-gap-8">
<FormList>
<Table noMargin noStyles>
@@ -155,9 +165,12 @@
<Button
external
href="https://appwrite.io/docs/products/messaging/topics"
text>Documentation</Button>
text
>Documentation
</Button>
<Button secondary on:click={() => (search = '')}
>Clear search</Button>
>Clear search
</Button>
</div>
</div>
</EmptySearch>
@@ -166,15 +179,19 @@
<div class="common-section">
<div class="u-text-center common-section">
<p class="text u-line-height-1-5">
You have no topics. Create a topic to see them here.
You have no topics{emptyTopicsExists
? ` with ${providerType.toUpperCase()} targets`
: ''}. Create a topic to see them here.
</p>
<p class="text u-line-height-1-5">
Need a hand? Learn more in our <Button
Need a hand? Learn more in our
<Button
link
external
href="https://appwrite.io/docs/products/messaging/topics"
>documentation</Button
>.
>documentation
</Button>
.
</p>
</div>
</div>
@@ -10,7 +10,7 @@
InputTextarea
} from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Modal } from '$lib/components';
import { CustomId, Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -113,5 +113,12 @@
Message ID
</span></Pill>
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
fullWidth />
{/if}
</FormList>
@@ -38,7 +38,7 @@
Label
} from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Modal } from '$lib/components';
import { CustomId, Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -232,6 +232,13 @@
Message ID
</span></Pill>
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
fullWidth />
{/if}
</FormList>
<PushPhone
@@ -2,7 +2,7 @@
import { messageParams, providerType } from './store';
import { Button, FormList, InputEmail, InputRadio, InputTextarea } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Modal } from '$lib/components';
import { CustomId, Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -91,6 +91,13 @@
Message ID
</span></Pill>
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
fullWidth />
{/if}
</FormList>
<SMSPhone content={$messageParams[$providerType]['content']} classes="is-only-desktop" />
@@ -51,11 +51,6 @@
bind:show={showGitDisconnect}
onSubmit={handleSubmit}
size="big">
<p>
Are you sure you want to disconnect this git installation? This will affect future
deployments to the following functions:
</p>
{#await loadFunctions()}
<div class="u-flex u-main-center">
<div class="avatar is-size-x-small">
@@ -64,6 +59,10 @@
</div>
{:then functions}
{#if functions.total}
<p>
Are you sure you want to disconnect this git installation? This will affect future
deployments to the following functions:
</p>
<div class="u-flex u-flex-vertical u-gap-12">
{#each functions.functions as func}
<div class="u-flex u-main-start u-gap-8">
@@ -73,7 +72,9 @@
<div
class="u-cross-child-center u-flex u-main-space-between u-flex-wrap u-gap-8 u-width-full-line">
<h6>{func.name}</h6>
<p class="u-x-small" style="color: hsl(var(--color-neutral-70));">
<p
class="u-x-small u-cross-child-center"
style="color: hsl(var(--color-neutral-70));">
Last deployed: {toLocaleDateTime(func.$updatedAt)}
</p>
</div>
@@ -19,7 +19,7 @@
</script>
<Modal title="Install" headerDivider={false} bind:show={showGitInstall} size="big">
<p>Select a provider to import an existing git repositories.</p>
<p>Select a provider to import an existing git repository.</p>
<div class="u-flex u-cross-center u-flex-vertical u-gap-16">
<Button href={connectGitHub().toString()} fullWidth secondary>
@@ -191,7 +191,7 @@
{capitalize(status)}
</Status>
</TableCell>
<TableCell title="">
<TableCell title="" showOverflow>
<div class="u-flex u-main-end">
<Button secondary on:click={() => showDetails(entry)}
>Details</Button>
@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import {
Alert,
Arrow,
AvatarGroup,
CardGrid,
DropList,
@@ -89,7 +90,7 @@
<svelte:fragment slot="aside">
{#if total > 0}
<div>
<div class="u-flex u-flex-vertical-mobile u-main-end">
<div class="u-flex u-flex-vertical-mobile u-main-end u-padding-block-end-16">
<ul class="buttons-list">
<li class="buttons-list-item">
<Button secondary on:click={() => (showGitIstall = true)}>
@@ -100,77 +101,81 @@
</ul>
</div>
<Table noMargin noStyles>
<TableHeader>
<TableCellHead width={150}>Installation ID</TableCellHead>
<TableCellHead>Repository</TableCellHead>
<TableCellHead>Updated</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each installations as installation, i}
<TableRow>
<TableCell title="installations">
<Id value={installation.$id}>{installation.$id}</Id>
</TableCell>
<TableCell title="repository">
<div class="u-flex u-gap-8 u-cross-center">
<div class="avatar is-size-small">
<span
class={getProviderIcon(installation.provider)}
style="font-size: var(--icon-size-medium)!important" />
<div style="overflow: auto">
<Table noMargin noStyles isAutoLayout>
<TableHeader>
<TableCellHead width={150}>Installation ID</TableCellHead>
<TableCellHead>Repository</TableCellHead>
<TableCellHead>Updated</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each installations as installation, i}
<TableRow>
<TableCell title="installations">
<Id value={installation.$id}>{installation.$id}</Id>
</TableCell>
<TableCell title="repository">
<div class="u-flex u-gap-8 u-cross-center">
<div class="avatar is-size-small">
<span
class={getProviderIcon(installation.provider)}
style="font-size: var(--icon-size-medium)!important" />
</div>
<a
href={getInstallationLink(installation)}
target="_blank"
class="u-flex u-gap-4 u-cross-center">
<span>{installation.organization}</span><span
style="font-size: 1rem; color: hsl(var(--color-neutral-70));"
class="icon-external-link" /></a>
</div>
<a
href={getInstallationLink(installation)}
target="_blank"
class="u-flex u-gap-4 u-cross-center">
<span>{installation.organization}</span><span
style="font-size: 1rem; color: hsl(var(--color-neutral-70));"
class="icon-external-link" /></a>
</div>
</TableCell>
<TableCellText title="updated">
{dayjs().to(installation.$updatedAt)}
</TableCellText>
</TableCell>
<TableCellText title="updated">
{dayjs().to(installation.$updatedAt)}
</TableCellText>
<TableCell>
<DropList
bind:show={showInstallationDropdown[i]}
placement="bottom-start"
noArrow>
<button
class="button is-text is-only-icon"
aria-label="more options"
on:click|preventDefault={() =>
(showInstallationDropdown[i] =
!showInstallationDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListLink
href={configureGitHub()}
icon="external-link"
on:click={() => {
showInstallationDropdown[i] = false;
}}>
Configure
</DropListLink>
<DropListItem
icon="x-circle"
on:click={async () => {
showInstallationDropdown[i] = false;
showGitDisconnect = true;
selectedInstallation = installation;
}}>
Disconnect
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<TableCell>
<DropList
bind:show={showInstallationDropdown[i]}
placement="bottom-start"
noArrow>
<button
class="button is-text is-only-icon"
aria-label="more options"
on:click|preventDefault={() =>
(showInstallationDropdown[i] =
!showInstallationDropdown[i])}>
<span
class="icon-dots-horizontal"
aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListLink
href={configureGitHub()}
icon="external-link"
on:click={() => {
showInstallationDropdown[i] = false;
}}>
Configure
</DropListLink>
<DropListItem
icon="x-circle"
on:click={async () => {
showInstallationDropdown[i] = false;
showGitDisconnect = true;
selectedInstallation = installation;
}}>
Disconnect
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</div>
</div>
<div class="u-flex u-main-space-between">
<p class="text">Total installations: {total}</p>
@@ -201,8 +206,12 @@
<article class="card-git card is-border-dashed is-no-shadow">
<div class="u-flex u-cross-center u-flex-vertical u-gap-32">
<div class="u-flex u-cross-center u-flex-vertical u-gap-8">
<AvatarGroup bordered icons={['github', 'gitlab', 'bitBucket', 'azure']} />
<span class="icon-arrow-narrow-down" />
<AvatarGroup
class="git-installation-avatar-group"
bordered
icons={['github', 'gitlab', 'bitBucket', 'azure']} />
<Arrow direction="down" />
<div class="avatar"><SvgIcon name="appwrite" type="color" size={80} /></div>
</div>
@@ -225,3 +234,9 @@
{#if showGitDisconnect}
<GitDisconnectModal bind:showGitDisconnect {selectedInstallation} />
{/if}
<style>
:global(.git-installation-avatar-group .icon-gitlab, .icon-bitBucket, .icon-azure) {
color: hsl(var(--color-neutral-50));
}
</style>
@@ -4,11 +4,10 @@
import {
TableBody,
TableCell,
TableCellLink,
TableCellHead,
TableHeader,
TableRow,
Table
Table,
TableRowLink
} from '$lib/elements/table';
import { showUsageRatesModal, tierToPlan, upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
@@ -225,27 +224,26 @@
}
]} />
{#if data.usage.executionsBreakdown.length > 0}
<Table noMargin noStyles>
<Table noMargin noStyles style="table-layout: auto">
<TableHeader>
<TableCellHead width={285}>Function</TableCellHead>
<TableCellHead>Function</TableCellHead>
<TableCellHead>Usage</TableCellHead>
<TableCellHead width={140} />
<TableCellHead />
</TableHeader>
<TableBody>
{#each data.usage.executionsBreakdown as func}
<TableRow>
<TableRowLink
href={`${baseRoute}/functions/function-${func.resourceId}`}>
<TableCell title="Function">
{func.name ?? func.resourceId}
</TableCell>
<TableCell title="Usage">
{formatNum(func.value)} executions
</TableCell>
<TableCellLink
href={`${baseRoute}/functions/function-${func.resourceId}`}
title="View function">
View function
</TableCellLink>
</TableRow>
<TableCell right={true}>
<span class="icon-cheveron-right u-cross-child-center" />
</TableCell>
</TableRowLink>
{/each}
</TableBody>
</Table>
@@ -62,7 +62,7 @@
</div>
{:else}
<div class="custom-id-wrapper">
<CustomId bind:show={showCustomId} name="File" bind:id={$createFile.id} />
<CustomId autofocus bind:show={showCustomId} name="File" bind:id={$createFile.id} />
</div>
{/if}
</FormList>
@@ -87,6 +87,9 @@
import { readOnly } from '$lib/stores/billing';
import { GRACE_PERIOD_OVERRIDE } from '$lib/system';
import { isValueOfStringEnum } from '$lib/helpers/types';
import type { PageData } from './$types';
export let data: PageData;
let showDelete = false;
@@ -397,7 +400,7 @@
</CardGrid>
</Form>
<UpdateMaxFileSize />
<UpdateMaxFileSize currentPlan={data.currentPlan} />
<Form onSubmit={updateAllowedExtensions}>
<CardGrid hideOverflow>
@@ -0,0 +1,10 @@
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async ({ parent }) => {
const { organization } = await parent();
const currentPlan = await sdk.forConsole.billing.getPlan(organization.$id);
return {
currentPlan
};
};
@@ -5,15 +5,22 @@
import { Button, Form, FormItem, InputNumber, InputSelect } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
import { createByteUnitPair } from '$lib/helpers/unit';
import { getServiceLimit, readOnly, tierToPlan, upgradeURL } from '$lib/stores/billing';
import { readOnly, upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { bucket } from '../store';
import { updateBucket } from './+page.svelte';
import type { Plan } from '$lib/sdk/billing';
export let currentPlan: Plan;
const service = getServiceLimit('fileSize');
const service = currentPlan['fileSize'];
const { value, unit, baseValue, units } = createByteUnitPair($bucket.maximumFileSize, 1000);
const options = units.map((v) => ({ label: v.name, value: v.name }));
$: selectedUnit = $unit;
$: maxValue = function formMaxFileSize() {
return (service * 1000 * 1000) / units.find((unit) => unit.name === selectedUnit).value;
};
function updateMaxSize() {
updateBucket(
@@ -35,10 +42,9 @@
<svelte:fragment slot="aside">
{#if isCloud}
{@const size = humanFileSize(sizeToBytes(service, 'MB', 1000))}
{@const plan = tierToPlan($organization?.billingPlan)}
<Alert type="info">
<p class="text">
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
The {currentPlan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === BillingPlan.FREE}
@@ -63,7 +69,7 @@
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}
placeholder={$bucket.maximumFileSize.toString()}
min={0}
max={isCloud ? service : Infinity}
max={isCloud ? maxValue() : Infinity}
bind:value={$value} />
<InputSelect
id="bytes"
@@ -63,7 +63,7 @@
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Bucket" bind:id />
<CustomId autofocus bind:show={showCustomId} name="Bucket" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
+2 -2
View File
@@ -2,6 +2,7 @@ import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
import { VARS } from '$lib/system';
const handleGithubEducationMembership = async (name: string, email: string) => {
const result = await sdk.forConsole.billing.setMembership('github-student-developer');
@@ -46,9 +47,8 @@ export const load: PageLoad = async ({ parent, url }) => {
};
const setToGhStudentMailingList = async (name: string, email: string) => {
const path = `/mailinglists/gh-student`;
const body = name !== '' ? { name, email } : { email };
return fetch('https://growth.appwrite.io/v1' + path, {
return fetch(`${VARS.GROWTH_ENDPOINT}/mailinglists/gh-student`, {
method: 'POST',
body: JSON.stringify(body),
headers: {