mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-pink-v2
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
Generated
+28
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -156,8 +156,9 @@
|
||||
|
||||
<div class="u-stretch u-margin-block-start-32 u-overflow-hidden">
|
||||
<section class="code-panel">
|
||||
<header class="code-panel-header u-flex u-main-space-between u-width-full-line">
|
||||
<div class="u-flex u-gap-24">
|
||||
<header
|
||||
class="code-panel-header code-panel-compact-header u-main-space-between u-width-full-line">
|
||||
<div class="u-flex">
|
||||
<div class="u-flex u-gap-16">
|
||||
<h4 class="text u-bold">Method:</h4>
|
||||
<span class="u-text-color-gray">{execution.requestMethod}</span>
|
||||
@@ -197,8 +198,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="code-panel-content grid-1-2" style="u-grid">
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<div class="code-panel-content grid-1-2">
|
||||
<div
|
||||
class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16 mobile-only-inline-20-padding">
|
||||
<Heading tag="h3" size="6">Request</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
@@ -281,7 +283,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
{execution.requestHeaders?.length
|
||||
? 'Not all header data is'
|
||||
: 'Header data is not'}
|
||||
@@ -296,7 +298,7 @@
|
||||
>.
|
||||
</p>
|
||||
{:else if selectedRequest === 'body'}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
Body data is not captured by Appwrite for your user's security
|
||||
and privacy. To display body data in the Logs tab, use
|
||||
<b>context.log()</b>.
|
||||
@@ -309,7 +311,9 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<div class="u-sep-block-end is-only-mobile u-padding-block-start-16" />
|
||||
<div
|
||||
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0 mobile-only-inline-20-padding mobile-only-block-start-20-padding">
|
||||
<Heading tag="h3" size="6">Response</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
@@ -348,10 +352,12 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
<Code
|
||||
allowScroll
|
||||
withCopy
|
||||
noMargin
|
||||
code={unescapeNewLines(execution.logs)}
|
||||
language="sh" />
|
||||
language="sh"
|
||||
class="limited-code-height" />
|
||||
{:else}
|
||||
<Card isDashed isTile>
|
||||
<p class="text u-text-center">No response was recorded.</p>
|
||||
@@ -360,10 +366,12 @@
|
||||
{:else if selectedResponse === 'errors'}
|
||||
{#if execution?.errors}
|
||||
<Code
|
||||
allowScroll
|
||||
withCopy
|
||||
noMargin
|
||||
code={unescapeNewLines(execution.errors)}
|
||||
language="sh" />
|
||||
language="sh"
|
||||
class="limited-code-height" />
|
||||
{:else}
|
||||
<Card isDashed isTile>
|
||||
<p class="text u-text-center">No response was recorded.</p>
|
||||
@@ -389,7 +397,7 @@
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
{/if}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
{execution.responseHeaders?.length
|
||||
? 'Not all header data is'
|
||||
: 'Header data is not'}
|
||||
@@ -404,7 +412,7 @@
|
||||
>.
|
||||
</p>
|
||||
{:else if selectedResponse === 'body'}
|
||||
<p class="text u-text-center u-padding-24">
|
||||
<p class="text u-text-center u-padding-16">
|
||||
Body data is not captured by Appwrite for your user's security
|
||||
and privacy. To display body data in the Logs tab, use
|
||||
<b>context.log()</b>.
|
||||
@@ -423,3 +431,39 @@
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.limited-code-height) {
|
||||
overflow: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.code-panel-compact-header .u-flex {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.code-panel-compact-header {
|
||||
row-gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code-panel-content {
|
||||
padding: unset;
|
||||
padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-only-inline-20-padding {
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-only-block-start-20-padding {
|
||||
padding-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
.code-panel-content.grid-1-2 {
|
||||
display: unset;
|
||||
row-gap: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
import { showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let isOpen = false;
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
$: project = $page.params.project;
|
||||
$: projectPath = `${base}/project-${project}`;
|
||||
@@ -44,12 +43,12 @@
|
||||
}
|
||||
|
||||
function openFeedback() {
|
||||
isOpen = false;
|
||||
$showSubNavigation = false;
|
||||
wizard.start(Create);
|
||||
}
|
||||
|
||||
function openSupport() {
|
||||
isOpen = false;
|
||||
$showSubNavigation = false;
|
||||
$showSupportModal = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
export let isOpen = false;
|
||||
export let showSideNavigation = false;
|
||||
|
||||
let y: number;
|
||||
|
||||
page.subscribe(({ url }) => {
|
||||
isOpen = url.searchParams.get('openNavbar') === 'true';
|
||||
$showSubNavigation = url.searchParams.get('openNavbar') === 'true';
|
||||
});
|
||||
|
||||
const toggleMenu = () => {
|
||||
y = 0;
|
||||
isOpen = !isOpen;
|
||||
$showSubNavigation = !$showSubNavigation;
|
||||
if (browser) {
|
||||
if (isOpen) {
|
||||
if ($showSubNavigation) {
|
||||
document.body.classList.add('u-overflow-hidden');
|
||||
} else {
|
||||
document.body.classList.remove('u-overflow-hidden');
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<main
|
||||
class:grid-with-side={showSideNavigation}
|
||||
class:is-open={isOpen}
|
||||
class:is-open={$showSubNavigation}
|
||||
class:u-hide={$wizard.show || $log.show || $wizard.cover}
|
||||
class:is-fixed-layout={$activeHeaderAlert?.show}
|
||||
style:--p-side-size={sideSize}>
|
||||
@@ -65,7 +65,10 @@
|
||||
class="icon-button is-not-desktop"
|
||||
aria-label="Open Menu"
|
||||
on:click={toggleMenu}>
|
||||
<span class:icon-x={isOpen} class:icon-menu={!isOpen} aria-hidden="true" />
|
||||
<span
|
||||
class:icon-x={$showSubNavigation}
|
||||
class:icon-menu={!$showSubNavigation}
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -25,3 +25,5 @@ export function updateLayout(args: updateLayoutArguments) {
|
||||
header.set(args.header ?? null);
|
||||
breadcrumb.set(args.breadcrumb ?? null);
|
||||
}
|
||||
|
||||
export const showSubNavigation = writable(false);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { page } from '$app/stores';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Tier } from './billing';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export type 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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
+32
-20
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
+5
-14
@@ -11,9 +11,9 @@
|
||||
import { addNotification, dismissAllNotifications } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups';
|
||||
@@ -24,19 +24,10 @@
|
||||
|
||||
let policyCreateError: string;
|
||||
let totalPolicies: UserBackupPolicy[] = [];
|
||||
let isDisabled = isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE);
|
||||
let isDisabled = isSelfHosted || (isCloud && !$currentPlan.backupsEnabled);
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: hasPolicyCreationLimitations = () => {
|
||||
// allow when on Pro and no policy exists
|
||||
if ($organization?.billingPlan === BillingPlan.PRO) {
|
||||
return data.policies.total > 0;
|
||||
} else if ($organization?.billingPlan === BillingPlan.SCALE) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showFeedbackNotification = () => {
|
||||
let counter = localStorage.getItem('createBackupsCounter');
|
||||
const parsedCounter = counter ? parseInt(counter, 10) : 0;
|
||||
@@ -184,7 +175,8 @@
|
||||
buttonEvent="create_backup"
|
||||
buttonType="secondary"
|
||||
buttonDisabled={isDisabled}
|
||||
hasLimitations={hasPolicyCreationLimitations()}
|
||||
maxPolicies={$currentPlan.backupPolicies}
|
||||
policiesCreated={data.policies.total}
|
||||
buttonMethod={() => {
|
||||
$showCreatePolicy = true;
|
||||
trackEvent('click_policy_create');
|
||||
@@ -202,7 +194,6 @@
|
||||
buttonText="Manual backup"
|
||||
buttonEvent="create_backup"
|
||||
buttonType="secondary"
|
||||
hasLimitations={false}
|
||||
buttonDisabled={isDisabled}
|
||||
buttonMethod={() => {
|
||||
$showCreateBackup = true;
|
||||
|
||||
+7
-12
@@ -4,14 +4,13 @@
|
||||
import { Pill } from '$lib/elements';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
|
||||
export let buttonText: string = null;
|
||||
export let hasLimitations: boolean = true;
|
||||
export let policiesCreated: number = 0;
|
||||
export let maxPolicies: number = 0;
|
||||
export let buttonMethod: () => void = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let buttonDisabled = false;
|
||||
@@ -27,16 +26,16 @@
|
||||
<div class="u-flex u-cross-child-center u-cross-center u-gap-12">
|
||||
<div class="body-text-1 u-bold backups-title">{title}</div>
|
||||
|
||||
{#if hasLimitations && $organization.billingPlan === BillingPlan.PRO}
|
||||
{#if title === 'Policies' && policiesCreated >= maxPolicies}
|
||||
<div style="height: 40px; padding-block-start: 4px">
|
||||
<DropList bind:show={showDropdown} width="16">
|
||||
<Pill button on:click={() => (showDropdown = true)}>
|
||||
<span class="icon-info" />1/1 created
|
||||
<span class="icon-info" />{policiesCreated}/{maxPolicies} created
|
||||
</Pill>
|
||||
<svelte:fragment slot="list">
|
||||
<slot name="tooltip">
|
||||
<span>
|
||||
You are limited to one policy on Pro plan.
|
||||
You are limited to {maxPolicies} policy on your plan.
|
||||
<button
|
||||
class="u-underline"
|
||||
on:click={() => {
|
||||
@@ -52,14 +51,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !hasLimitations}
|
||||
{#if title === 'Backups' || policiesCreated < maxPolicies}
|
||||
<Button
|
||||
event={buttonEvent}
|
||||
on:click={hasLimitations
|
||||
? () => {
|
||||
showDropdown = true;
|
||||
}
|
||||
: buttonMethod}
|
||||
on:click={buttonMethod}
|
||||
disabled={buttonDisabled}
|
||||
text={buttonType === 'text'}
|
||||
secondary={buttonType === 'secondary'}>
|
||||
|
||||
+22
-14
@@ -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"
|
||||
|
||||
+21
-12
@@ -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"
|
||||
|
||||
+31
-30
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+18
-10
@@ -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 +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}
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { showCreate } from '../store';
|
||||
import type { PageData } from './$types';
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
|
||||
$: data = $page.data as PageData;
|
||||
$: project = $page.params.project;
|
||||
@@ -24,7 +25,10 @@
|
||||
<h5 class="eyebrow-heading-3 u-padding-block-12 is-not-mobile">Collections</h5>
|
||||
<button
|
||||
class="button is-text is-full-width u-main-start u-padding-inline-0"
|
||||
on:click={() => ($showCreate = true)}>
|
||||
on:click={() => {
|
||||
$showCreate = true;
|
||||
$showSubNavigation = false;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</button>
|
||||
|
||||
+36
-5
@@ -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>
|
||||
|
||||
+1
-1
@@ -71,10 +71,10 @@
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
autofocus
|
||||
bind:show={showCustomId}
|
||||
name="Collection"
|
||||
bind:id
|
||||
autofocus={false}
|
||||
fullWidth={true} />
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
+168
-34
@@ -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>
|
||||
|
||||
+21
-4
@@ -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) {
|
||||
|
||||
+21
-1
@@ -98,7 +98,7 @@
|
||||
>.
|
||||
</Alert>
|
||||
|
||||
<div class="editor-border">
|
||||
<div class="editor-border box">
|
||||
<SecondaryTabs large class="u-sep-block-end u-padding-8">
|
||||
{#each ['Unix', 'CMD', 'PowerShell'] as cat}
|
||||
<SecondaryTabsItem
|
||||
@@ -118,6 +118,7 @@
|
||||
withLineNumbers
|
||||
withCopy
|
||||
language="sh"
|
||||
class="cli-commands-code-box-no-outline"
|
||||
label={codeSnippets[cat].language}
|
||||
code={codeSnippets[cat].code} />
|
||||
{/if}
|
||||
@@ -127,3 +128,22 @@
|
||||
<Button secondary on:click={() => (show = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
padding: unset;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.editor-border :global(.cli-commands-code-box-no-outline) {
|
||||
margin: 1rem 0;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
background-color: unset;
|
||||
padding: 0 var(--box-padding, 1.5rem) 0 var(--box-padding, 1.5rem);
|
||||
}
|
||||
|
||||
:global(.editor-border .cli-commands-code-box-no-outline pre) {
|
||||
background-color: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
+45
-11
@@ -25,6 +25,9 @@
|
||||
let showCancel = false;
|
||||
let showActivate = false;
|
||||
|
||||
let enableScrollButton = false;
|
||||
let codePanelContent: HTMLElement;
|
||||
|
||||
function handleActivate() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
@@ -36,23 +39,41 @@
|
||||
);
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const threshold = codePanelContent.clientHeight * 0.25;
|
||||
enableScrollButton = codePanelContent.scrollTop > threshold;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
logs = $deployment.buildLogs;
|
||||
|
||||
codePanelContent = document.querySelector('.code-panel-content');
|
||||
codePanelContent.addEventListener('scroll', handleScroll);
|
||||
|
||||
if ($deployment.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
return sdk.forConsole.client.subscribe<Models.Deployment>('console', (message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${$page.params.function}.deployments.${$page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
logs = message.payload['logs'];
|
||||
if (message.payload.status === 'ready') {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
|
||||
const unsubscribe = sdk.forConsole.client.subscribe<Models.Deployment>(
|
||||
'console',
|
||||
(message) => {
|
||||
if (
|
||||
message.events.includes(
|
||||
`functions.${$page.params.function}.deployments.${$page.params.deployment}.update`
|
||||
)
|
||||
) {
|
||||
logs = message.payload['logs'];
|
||||
if (message.payload.status === 'ready') {
|
||||
invalidate(Dependencies.DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
codePanelContent.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -103,7 +124,8 @@
|
||||
<span class="icon-external-link" aria-hidden="true" /> Raw data</Button> -->
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
disabled={!enableScrollButton}
|
||||
on:click={() => codePanelContent?.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
<span class="icon-arrow-sm-up" aria-hidden="true" /> Scroll to top</Button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -135,3 +157,15 @@
|
||||
<Delete selectedDeployment={$deployment} bind:showDelete />
|
||||
<Cancel selectedDeployment={$deployment} bind:showCancel />
|
||||
<Activate selectedDeployment={$deployment} bind:showActivate on:activated={handleActivate} />
|
||||
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.code-panel-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.code-panel-content {
|
||||
max-height: 50vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
+24
-6
@@ -15,17 +15,35 @@
|
||||
<span class="icon-github" aria-hidden="true" /> <span class="link">GitHub</span>
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListLink href={deployment.providerRepositoryUrl} external icon="github">
|
||||
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
|
||||
<DropListLink
|
||||
href={deployment.providerRepositoryUrl}
|
||||
external
|
||||
icon="github"
|
||||
iconStyle="font-size:20px">
|
||||
<Trim alternativeTrim>
|
||||
<span style:white-space="pre-wrap">
|
||||
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
|
||||
</span>
|
||||
</Trim>
|
||||
</DropListLink>
|
||||
<DropListLink href={deployment.providerBranchUrl} external icon="git-branch">
|
||||
<DropListLink
|
||||
href={deployment.providerBranchUrl}
|
||||
external
|
||||
icon="git-branch"
|
||||
iconStyle="font-size:20px">
|
||||
{deployment.providerBranch}
|
||||
</DropListLink>
|
||||
{#if deployment?.providerCommitMessage && deployment?.providerCommitHash && deployment?.providerCommitUrl}
|
||||
<DropListLink href={deployment.providerCommitUrl} external icon="git-commit">
|
||||
<DropListLink
|
||||
href={deployment.providerCommitUrl}
|
||||
external
|
||||
icon="git-commit"
|
||||
iconStyle="font-size:20px">
|
||||
<Trim alternativeTrim>
|
||||
{deployment?.providerCommitHash?.substring(0, 7)}
|
||||
{deployment.providerCommitMessage}
|
||||
<span style="white-space: pre-wrap">
|
||||
{deployment?.providerCommitHash?.substring(0, 7)}
|
||||
{deployment.providerCommitMessage}
|
||||
</span>
|
||||
</Trim>
|
||||
</DropListLink>
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { functionsList } from '../store';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -148,7 +149,7 @@
|
||||
class="is-small"
|
||||
checked={isChecked(useCase)}
|
||||
on:change={(e) => applyFilter('useCase', useCase, e)} />
|
||||
<span class="u-trim-1">{useCase}</span>
|
||||
<span class="u-trim-1">{capitalize(useCase)}</span>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -168,7 +169,8 @@
|
||||
.getAll('runtime')
|
||||
.includes(runtime)}
|
||||
on:change={(e) => applyFilter('runtime', runtime, e)} />
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<div
|
||||
class="u-flex u-cross-center u-gap-8 functions-avatar-holder">
|
||||
<div class="avatar is-size-x-small">
|
||||
<SvgIcon name={icon} iconSize="small" />
|
||||
</div>
|
||||
@@ -207,14 +209,16 @@
|
||||
<li>
|
||||
<article class="card u-min-height-100-percent">
|
||||
<div class="u-flex u-gap-16 u-cross-center u-main-space-between">
|
||||
<h2 class="body-text-1 u-bold u-trim-1">
|
||||
<h2
|
||||
class="body-text-1 u-bold u-trim-1 functions-template-title">
|
||||
{template.name}
|
||||
</h2>
|
||||
<ul class="avatars-group is-with-border">
|
||||
{#each displayed as runtime}
|
||||
{@const icon = getIconFromRuntime(runtime.name)}
|
||||
{#if icon}
|
||||
<li class="avatars-group-item">
|
||||
<li
|
||||
class="avatars-group-item functions-avatar-holder">
|
||||
<div class="avatar is-size-small">
|
||||
<img
|
||||
style:--p-text-size="20px"
|
||||
@@ -227,7 +231,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hidden.length}
|
||||
<li class="avatars-group-item">
|
||||
<li class="avatars-group-item functions-avatar-holder">
|
||||
<div
|
||||
class="avatar is-size-small"
|
||||
use:tooltip={{
|
||||
@@ -287,3 +291,18 @@
|
||||
</section>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
:global(.theme-light .functions-avatar-holder .avatar) {
|
||||
background-color: var(--p-avatar-bg-color-default);
|
||||
--p-avatar-bg-color-default: hsl(var(--color-neutral-0));
|
||||
}
|
||||
|
||||
:global(.theme-light .functions-template-title) {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
}
|
||||
|
||||
:global(.theme-dark .functions-template-title) {
|
||||
color: hsl(var(--color-neutral-15));
|
||||
}
|
||||
</style>
|
||||
|
||||
+21
-9
@@ -22,9 +22,10 @@
|
||||
<div class="grid-300px-1fr">
|
||||
<section>
|
||||
<Collapsible>
|
||||
<li class="collapsible-item">
|
||||
<h3 class="body-text-2 u-bold u-padding-block-12">
|
||||
Use cases <span class="inline-tag">{$template.useCases.length}</span>
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
|
||||
Use cases
|
||||
<span class="inline-tag">{$template.useCases.length}</span>
|
||||
</h3>
|
||||
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
|
||||
{#each $template.useCases as useCase}
|
||||
@@ -32,9 +33,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
<li class="collapsible-item">
|
||||
<h3 class="body-text-2 u-bold u-padding-block-12">
|
||||
Runtimes <span class="inline-tag">{$template.runtimes.length}</span>
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
|
||||
Runtimes
|
||||
<span class="inline-tag">{$template.runtimes.length}</span>
|
||||
</h3>
|
||||
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
|
||||
{#each $template.runtimes as runtime}
|
||||
@@ -42,12 +44,12 @@
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
<li class="collapsible-item">
|
||||
<li class="collapsible-item-divider collapsible-item">
|
||||
<section class="card u-margin-block-start-24">
|
||||
<h4 class="body-text-1 u-bold">Published by</h4>
|
||||
<img
|
||||
class="u-margin-block-start-8"
|
||||
src={$app.themeInUse == 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
|
||||
src={$app.themeInUse === 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite" />
|
||||
@@ -58,7 +60,7 @@
|
||||
<section>
|
||||
<Card>
|
||||
<Heading size="7" tag="h3">
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
<span class="u-flex u-cross-center u-gap-16 functions-avatar-holder">
|
||||
<div class="avatar is-size-small">
|
||||
<span
|
||||
style:--p-text-size="20px"
|
||||
@@ -92,3 +94,13 @@
|
||||
</section>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
:global(.theme-dark .collapsible-item-divider:where(:not(:last-child))) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
:global(.theme-light .collapsible-item-divider:where(:not(:last-child))) {
|
||||
border-block-end: solid 0.0625rem hsl(var(--color-neutral-10));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
autofocus
|
||||
bind:show={showCustomId}
|
||||
name="Function"
|
||||
bind:id={$createFunction.id}
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+4
-1
@@ -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
|
||||
};
|
||||
};
|
||||
+11
-5
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user