Merge branch 'main' into fix-support.

This commit is contained in:
Darshan
2025-01-02 18:13:39 +05:30
76 changed files with 687 additions and 296 deletions
+2
View File
@@ -36,6 +36,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"
},
@@ -52,6 +53,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
@@ -59,6 +59,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)
@@ -102,6 +105,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.6.3))(eslint@8.57.1)(typescript@5.6.3)
@@ -1327,6 +1333,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==}
@@ -1559,6 +1568,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'}
@@ -3062,6 +3074,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'}
@@ -5143,6 +5160,8 @@ snapshots:
'@types/pug@2.0.10': {}
'@types/remarkable@2.0.8': {}
'@types/shimmer@1.2.0': {}
'@types/stack-utils@2.0.3': {}
@@ -5435,6 +5454,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
@@ -7144,6 +7167,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
@@ -56,9 +56,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>
+3 -6
View File
@@ -3,19 +3,16 @@
import { wizard } from '$lib/stores/wizard';
import SupportWizard from '$routes/(console)/supportWizard.svelte';
import { isSupportOnline, showSupportModal } from '$routes/(console)/wizard/support/store';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
import { localeShortTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
import { upgradeURL } from '$lib/stores/billing';
import { Card } from '$lib/components/index';
import { app } from '$lib/stores/app';
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')} ${localeShortTimezoneName()}`;
@@ -73,7 +70,7 @@
{#if option.showSupport}
<div class="u-flex u-gap-12 u-cross-center">
{#if !isPaid}
{#if !hasPremiumSupport}
<Button
href={$upgradeURL}
on:click={() => {
+8 -2
View File
@@ -24,11 +24,17 @@
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
if (autofocus) {
addInputFocus();
}
});
export function addInputFocus() {
if (element) {
element.focus();
}
}
const handleInvalid = (event: Event) => {
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>
@@ -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"
@@ -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}
@@ -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() {
@@ -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
};
};
@@ -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">
@@ -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>
@@ -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'}>
@@ -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;
@@ -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>
@@ -71,10 +71,10 @@
</div>
{:else}
<CustomId
autofocus
bind:show={showCustomId}
name="Collection"
bind:id
autofocus={false}
fullWidth={true} />
{/if}
</FormList>
@@ -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>
@@ -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>
@@ -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>
@@ -16,6 +16,11 @@
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(
@@ -64,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">