Merge branch 'main' into 'refactor-terminologies'.

This commit is contained in:
Darshan
2025-07-20 18:30:08 +05:30
112 changed files with 1306 additions and 803 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PUBLIC_CONSOLE_MODE=self-hosted
PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths
PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths
PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
+3 -3
View File
@@ -79,7 +79,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
@@ -118,7 +118,7 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=self-hosted"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
publish-cloud-no-regions:
@@ -156,6 +156,6 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
+2 -2
View File
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+25 -9
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -261,8 +261,8 @@ packages:
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f}
version: 1.9.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -282,8 +282,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -1266,6 +1266,14 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tanstack/svelte-virtual@3.13.12':
resolution: {integrity: sha512-SsOinCktvpMFbdSW61X6jeUNaJvD1tmt30AFVSLe1FQxGxjdPkZo0gAsoVPqM9FFZqidvvXQ4gpadDaTz/yGxA==}
peerDependencies:
svelte: ^3.48.0 || ^4.0.0 || ^5.0.0
'@tanstack/virtual-core@3.13.12':
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -3638,7 +3646,7 @@ snapshots:
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3655,12 +3663,13 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
'@tanstack/svelte-virtual': 3.13.12(svelte@5.25.3)
ansicolor: 2.0.3
d3: 7.9.0
fuse.js: 7.1.0
@@ -4711,6 +4720,13 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tanstack/svelte-virtual@3.13.12(svelte@5.25.3)':
dependencies:
'@tanstack/virtual-core': 3.13.12
svelte: 5.25.3
'@tanstack/virtual-core@3.13.12': {}
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
+21 -4
View File
@@ -148,7 +148,12 @@
})}
clearOnCallback={false}
on:keydown={(e) => {
if (e.detail.key !== 'Escape') {
const showingExamples = !$isLoading && !answer;
if (e.detail.key === 'Enter' && $input.trim()) {
e.detail.cancel();
return;
}
if (e.detail.key !== 'Escape' && !showingExamples) {
e.detail.cancel();
}
}}
@@ -302,15 +307,27 @@
}
}
:global(.answer ul),
:global(.answer ol) {
gap: 1rem;
:global(.answer ul) {
padding-inline-start: 1rem;
list-style-type: disc;
display: grid;
gap: 1rem;
}
:global(.answer ol) {
padding-inline-start: 1.1rem;
list-style-type: decimal;
display: grid;
gap: 1rem;
}
:global(.answer a) {
text-decoration: underline;
}
:global(.answer a:hover) {
opacity: 0.8;
}
}
.experimental {
@@ -12,7 +12,7 @@
$organization.$id,
$paymentMissingMandate.$id
);
await confirmSetup(method.clientSecret, method.$id);
await confirmSetup(method.clientSecret, method.providerMethodId);
}
</script>
@@ -0,0 +1,51 @@
<script lang="ts">
import { page } from '$app/state';
import { Click } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import {
billingProjectsLimitDate,
hideBillingHeaderRoutes,
upgradeURL
} from '$lib/stores/billing';
import { currentPlan } from '$lib/stores/organization';
import { onMount } from 'svelte';
import SelectProjectCloud from './selectProjectCloud.svelte';
import { toLocaleDate } from '$lib/helpers/date';
let showSelectProject: boolean = $state(false);
let selectedProjects: string[] = $state([]);
onMount(() => {
selectedProjects = page.data.organization?.projects || [];
});
</script>
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
<HeaderAlert
type="warning"
title="Action required: You have more than {$currentPlan.projects} projects.">
<svelte:fragment>
Choose which projects to keep before {toLocaleDate(billingProjectsLimitDate)} or upgrade
to Pro. Projects over the limit will be blocked after this date.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
compact
on:click={() => {
showSelectProject = true;
}}>Manage projects</Button>
<Button
href={$upgradeURL}
event={Click.OrganizationClickUpgrade}
eventData={{
from: 'button',
source: 'projects_limit_banner'
}}
secondary
fullWidthMobile>
<span class="text">Upgrade</span>
</Button>
</svelte:fragment>
</HeaderAlert>
{/if}
@@ -0,0 +1,117 @@
<script lang="ts">
import { type Models } from '@appwrite.io/console';
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
import { Modal } from '$lib/components';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { billingProjectsLimitDate } from '$lib/stores/billing';
import { page } from '$app/state';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { currentPlan } from '$lib/stores/organization';
let {
showSelectProject = $bindable(false),
selectedProjects = $bindable([])
}: {
showSelectProject: boolean;
selectedProjects: string[];
} = $props();
let projects = $state<Array<Models.Project>>([]);
let error = $state<string | null>(null);
onMount(() => {
projects = page.data.projects?.projects || [];
});
let projectsToArchive = $derived(
projects.filter((project) => !selectedProjects.includes(project.$id))
);
async function updateSelected() {
try {
await sdk.forConsole.billing.updateSelectedProjects(
projects[0].teamId,
selectedProjects
);
showSelectProject = false;
invalidate(Dependencies.ORGANIZATION);
addNotification({
type: 'success',
message: `Projects updated for archiving`
});
} catch (e) {
error = e.message;
}
}
function formatProjectsToArchive() {
let result = '';
projectsToArchive.forEach((project, index) => {
const text = `${index === 0 ? '' : ' '}<b>${project.name}</b> `;
result += text;
if (index < projectsToArchive.length - 1) {
if (index == projectsToArchive.length - 2) {
result += 'and ';
}
if (index < projectsToArchive.length - 2) {
result += ', ';
}
}
});
return result;
}
</script>
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
<svelte:fragment slot="description">
Choose which two projects to keep. Projects over the limit will be blocked after this date.
</svelte:fragment>
{#if error}
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
{/if}
<Table.Root
let:root
allowSelection
bind:selectedRows={selectedProjects}
columns={[{ id: 'name' }, { id: 'created' }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each projects as project}
<Table.Row.Base {root} id={project.$id}>
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
<Table.Cell column="created" {root}
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if selectedProjects.length > $currentPlan?.projects}
<div class="u-text-warning u-mb-4">
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
</div>
{/if}
{#if selectedProjects.length === $currentPlan?.projects}
<Alert.Inline
status="warning"
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
<span>
{@html formatProjectsToArchive()}
will be archived.
</span>
</Alert.Inline>
{/if}
<svelte:fragment slot="footer">
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
>Cancel</Button.Button>
<Button.Button size="s" disabled={selectedProjects.length !== $currentPlan?.projects}
>Save</Button.Button>
</svelte:fragment>
</Modal>
+59 -34
View File
@@ -3,7 +3,6 @@
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { app } from '$lib/stores/app';
import {
type BottomModalAlertAction,
type BottomModalAlertItem,
bottomModalAlertsConfig,
dismissBottomModalAlert,
@@ -20,18 +19,18 @@
import { goto } from '$app/navigation';
import { Typography } from '@appwrite.io/pink-svelte';
let currentIndex = 0;
let openModalOnMobile = false;
let currentIndex = $state(0);
let openModalOnMobile = $state(false);
function getPageScope(pathname: string) {
const isProjectPage = pathname.includes('project-[region]-[project]');
const isOrganizationPage = pathname.includes('organization-[organization]');
function getPageScope(route: string) {
const isProjectPage = route.includes('project-[region]-[project]');
const isOrganizationPage = route.includes('organization-[organization]');
return { isProjectPage, isOrganizationPage };
}
function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) {
const { isProjectPage, isOrganizationPage } = getPageScope(pathname);
function filterModalAlerts(alerts: BottomModalAlertItem[], route: string) {
const { isProjectPage, isOrganizationPage } = getPageScope(route);
return alerts
.sort((a, b) => b.importance - a.importance)
@@ -51,19 +50,32 @@
});
}
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
const bottomModalAlerts = $derived($bottomModalAlertsConfig.alerts);
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
const filteredModalAlerts = $derived(filterModalAlerts(bottomModalAlerts, page.route.id));
$: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined';
const currentModalAlert = $derived(filteredModalAlerts[currentIndex] as BottomModalAlertItem);
function handleClose() {
filteredModalAlerts.forEach((alert) => {
const hasOnlyPrimaryCta = $derived(typeof currentModalAlert?.learnMore === 'undefined');
const isOnOnboarding = $derived(page.route.id.includes('(console)/onboarding'));
function handleClose(id: string | null = null) {
const alerts = !id
? filteredModalAlerts
: filteredModalAlerts.filter((alert) => alert.id === id);
alerts.forEach((alert) => {
const modalAlert = alert;
dismissBottomModalAlert(modalAlert.id);
hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 });
if (modalAlert.closed) modalAlert.closed();
});
// reset `currentIndex` if we removed the last item
if (currentIndex >= filteredModalAlerts.length - 1) {
currentIndex = Math.max(0, filteredModalAlerts.length - 2);
}
}
function showNext() {
@@ -114,8 +126,13 @@
}
// the button component cannot have both href and on:click!
function triggerWindowLink(alertAction: BottomModalAlertAction, event?: string) {
function triggerWindowLink(alert: BottomModalAlertItem, event?: string) {
const alertAction = alert.cta;
const shouldShowUpgrade = showUpgrade();
// for correct event tracking after removal
const currentModalId = currentModalAlert.id;
const url = shouldShowUpgrade
? $upgradeURL
: alertAction.link({
@@ -130,14 +147,12 @@
}
if (alertAction?.hideOnClick === true) {
// be careful of this one.
// once clicked, its gone!
handleClose();
handleClose(alert.id); // gone!
}
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalAlert.id}`)
promo: currentModalId,
type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalId}`)
});
}
@@ -156,12 +171,10 @@
}
}
onMount(() => {
addBottomModalAlerts();
});
onMount(addBottomModalAlerts);
</script>
{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')}
{#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert}
{@const shouldShowUpgrade = showUpgrade()}
<div class="main-alert-wrapper is-not-mobile">
<div class="alert-container">
@@ -170,7 +183,7 @@
<button
aria-label="Close modal"
class="icon-inline-tag"
on:click={() => handleClose()}>
onclick={() => handleClose()}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
@@ -208,14 +221,19 @@
<button
aria-label="Previous"
class="icon-cheveron-left"
on:click={showPrevious}
style:cursor={currentIndex !== 0 ? 'pointer' : undefined}
onclick={showPrevious}
disabled={currentIndex === 0}
class:active={currentIndex > 0}></button>
<button
aria-label="Next"
class="icon-cheveron-right"
on:click={showNext}
onclick={showNext}
style:cursor={currentIndex !==
filteredModalAlerts.length - 1
? 'pointer'
: undefined}
disabled={currentIndex === filteredModalAlerts.length - 1}
class:active={currentIndex !==
filteredModalAlerts.length - 1}></button>
@@ -243,7 +261,7 @@
fullWidthMobile
secondary={!hasOnlyPrimaryCta}
class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
on:click={() => triggerWindowLink(currentModalAlert.cta)}>
on:click={() => triggerWindowLink(currentModalAlert)}>
{currentModalAlert.cta.text}
</Button>
@@ -276,7 +294,7 @@
<button
aria-label="Close modal"
class="icon-inline-tag"
on:click={() => handleClose()}>
onclick={() => handleClose()}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
@@ -314,14 +332,21 @@
<button
aria-label="Previous"
class="icon-cheveron-left"
on:click={showPrevious}
style:cursor={currentIndex !== 0
? 'pointer'
: undefined}
onclick={showPrevious}
disabled={currentIndex === 0}
class:active={currentIndex > 0}></button>
<button
aria-label="Next"
class="icon-cheveron-right"
on:click={showNext}
onclick={showNext}
style:cursor={currentIndex !==
filteredModalAlerts.length - 1
? 'pointer'
: undefined}
disabled={currentIndex ===
filteredModalAlerts.length - 1}
class:active={currentIndex !==
@@ -352,7 +377,7 @@
fullWidthMobile
on:click={() => {
openModalOnMobile = false;
triggerWindowLink(currentModalAlert.cta);
triggerWindowLink(currentModalAlert);
}}>
{shouldShowUpgrade
? 'Upgrade plan'
@@ -382,13 +407,13 @@
{:else}
{@const mobileConfig = getMobileWindowConfig()}
<!-- we don't need keydown because we show this only on mobile -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
tabindex="0"
role="button"
class:showing={!openModalOnMobile}
class="card notification-card u-width-full-line"
on:click={() => {
onclick={() => {
if (mobileConfig.cta) {
// navigate manually!
triggerMobileWindowLink();
@@ -401,7 +426,7 @@
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{currentModalAlert.title}
</Typography.Text>
<button on:click={hideAllModalAlerts} aria-label="Close">
<button onclick={hideAllModalAlerts} aria-label="Close">
<span class="icon-x"></span>
</button>
</div>
+14 -13
View File
@@ -32,7 +32,7 @@
};
const saveColumnPreferences = () => {
const shownColumns = $columns.filter((n) => n.hide !== true).map((n) => n.id);
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
if (isCustomTable) {
preferences.setCustomTableColumns(page.params.table, shownColumns);
@@ -45,22 +45,22 @@
if (isCustomTable) {
const shownColumns = preferences.getCustomTableColumns(page.params.table);
columns.update((columns) => {
return columns.map((column) => {
column.hide = !shownColumns.includes(column.id);
columns.update((n) =>
n.map((column) => {
column.hide = shownColumns?.includes(column.id) ?? false;
return column;
});
});
})
);
} else {
const prefs = preferences.get(page.route);
if (prefs?.columns && prefs.columns.length > 0) {
columns.update((cols) => {
return cols.map((column) => {
column.hide = !prefs.columns.includes(column.id);
if (prefs?.columns) {
columns.update((n) =>
n.map((column) => {
column.hide = prefs.columns?.includes(column.id) ?? false;
return column;
});
});
})
);
}
}
@@ -79,7 +79,7 @@
columns.update((cols) =>
cols.map((col) => {
if (col.id === column.id) {
col.hide = !column.hide;
column.hide = !column.hide;
}
return col;
})
@@ -90,6 +90,7 @@
</script>
<svelte:window on:resize={calcMaxHeight} />
{#if $columns?.length}
<Popover let:toggle placement="bottom-end" padding="none">
{@render children(toggle, selectedColumnsNumber)}
+60 -56
View File
@@ -10,7 +10,7 @@
import { Badge, InteractiveText, Layout, Popover, Typography } from '@appwrite.io/pink-svelte';
import { menuOpen } from '$lib/components/menu/store';
export let time: string = '';
export let time: string | null = null;
export let placement: ComponentProps<Popover>['placement'] = 'bottom';
function timeDifference(dateString: string): string {
@@ -75,64 +75,68 @@
$: timeToString = time ? timeDifference(time) : 'Invalid time';
</script>
<Popover let:show let:hide {placement} portal>
<button
on:mouseenter={() => {
if (!$menuOpen) {
setTimeout(show, 150);
}
}}
on:mouseleave={() => hidePopover(hide)}>
<slot>{capitalize(timeFromNow(time))}</slot>
</button>
{#if time}
<Popover let:show let:hide {placement} portal>
<button
on:mouseenter={() => {
if (!$menuOpen) {
setTimeout(show, 150);
}
}}
on:mouseleave={() => hidePopover(hide)}>
<slot>{capitalize(timeFromNow(time))}</slot>
</button>
<div
let:hide
slot="tooltip"
role="tooltip"
style:padding="1rem"
style:margin="-1rem"
on:mouseenter={() => (isMouseOverTooltip = true)}
on:mouseleave={() => hidePopover(hide, false)}>
<Layout.Stack gap="s" alignContent="flex-start">
<!-- `Raw time` as per design -->
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
{#if $$slots.title}
<slot name="title" />
{:else}
{timeToString}
{/if}
</Typography.Caption>
<div
let:hide
slot="tooltip"
role="tooltip"
style:padding="1rem"
style:margin="-1rem"
on:mouseenter={() => (isMouseOverTooltip = true)}
on:mouseleave={() => hidePopover(hide, false)}>
<Layout.Stack gap="s" alignContent="flex-start">
<!-- `Raw time` as per design -->
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
{#if $$slots.title}
<slot name="title" />
{:else}
{timeToString}
{/if}
</Typography.Caption>
<!-- `Absolute time` as per design -->
<Layout.Stack gap="xxs">
<Layout.Stack
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<InteractiveText
isVisible
variant="copy"
text={toLocaleDateTime(time, false, 'UTC')}
value={toISOString(time)} />
<!-- `Absolute time` as per design -->
<Layout.Stack gap="xxs">
<Layout.Stack
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<InteractiveText
isVisible
variant="copy"
text={toLocaleDateTime(time, false, 'UTC')}
value={toISOString(time)} />
<Badge variant="secondary" content="UTC" size="xs" />
</Layout.Stack>
<Badge variant="secondary" content="UTC" size="xs" />
</Layout.Stack>
<Layout.Stack
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<InteractiveText
isVisible
variant="copy"
text={toLocaleDateTime(time)}
value={toLocalDateTimeISO(time)} />
<Badge variant="secondary" content="Local" size="xs" />
<Layout.Stack
direction="row"
alignItems="center"
alignContent="center"
justifyContent="space-between">
<InteractiveText
isVisible
variant="copy"
text={toLocaleDateTime(time)}
value={toLocalDateTimeISO(time)} />
<Badge variant="secondary" content="Local" size="xs" />
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</div>
</Popover>
</div>
</Popover>
{:else}
<Typography.Text>{time === null || time === undefined ? 'null' : time}</Typography.Text>
{/if}
@@ -90,5 +90,5 @@
</Fieldset>
{#if show}
<SelectRootModal bind:show bind:rootDir {product} />
<SelectRootModal bind:show bind:rootDir {product} {branch} />
{/if}
+129 -146
View File
@@ -23,7 +23,8 @@
import { page } from '$app/state';
import Card from '../card.svelte';
import SkeletonRepoList from './skeletonRepoList.svelte';
import { onMount, untrack } from 'svelte';
import { onMount, untrack, onDestroy } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
let {
action = $bindable('select'),
@@ -52,6 +53,28 @@
loadInstallations();
});
const debouncedLoadRepositories = debounce(
async (installationId: string, searchTerm: string) => {
isLoadingRepositories = true;
try {
await loadRepositories(installationId, searchTerm);
} finally {
isLoadingRepositories = false;
}
},
300
);
$effect(() => {
if (selectedInstallation && search !== undefined) {
debouncedLoadRepositories(selectedInstallation, search);
}
});
onDestroy(() => {
debouncedLoadRepositories.cancel();
});
async function loadInstallations() {
if (installationList) {
if (installationList.installations.length) {
@@ -71,7 +94,7 @@
.vcs.listInstallations();
if (installations.length) {
if (!selectedInstallation) {
untrack(() => (selectedInstallation = installationList.installations[0].$id));
untrack(() => (selectedInstallation = installations[0].$id));
}
installation.set(installations.find((entry) => entry.$id === selectedInstallation));
}
@@ -80,22 +103,6 @@
}
async function loadRepositories(installationId: string, search: string) {
if (
!$repositories ||
$repositories.installationId !== installationId ||
$repositories.search !== search
) {
await fetchRepos(installationId, search);
}
if ($repositories.repositories.length && action === 'select') {
selectedRepository = $repositories.repositories[0].id;
$repository = $repositories.repositories[0];
}
return $repositories.repositories;
}
async function fetchRepos(installationId: string, search: string) {
if (product === 'functions') {
$repositories.repositories = (
(await sdk
@@ -117,9 +124,9 @@
)) as unknown as Models.ProviderRepositoryFrameworkList
).frameworkProviderRepositories;
}
$repositories.search = search;
$repositories.installationId = installationId;
return $repositories.repositories;
}
selectedRepository;
@@ -168,10 +175,7 @@
installationsMap.find((entry) => entry.$id === selectedInstallation)
);
isLoadingRepositories = true;
loadRepositories(selectedInstallation, search).then(() => {
isLoadingRepositories = false;
});
debouncedLoadRepositories.cancel();
}}
bind:value={selectedInstallation} />
<InputSearch
@@ -185,130 +189,109 @@
<!-- manual installation change -->
{#if isLoadingRepositories}
<SkeletonRepoList />
{:else if $repositories?.repositories?.length}
<Paginator
items={$repositories.repositories}
hideFooter={$repositories.repositories?.length <= 6}
limit={6}>
{#snippet children(
paginatedItems: Models.ProviderRepositoryRuntime[] &
Models.ProviderRepositoryFramework[]
)}
<Table.Root columns={1} let:root>
{#each paginatedItems as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(repo.framework)}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar size="xs" alt={repo.name} empty={!iconName}>
{#if iconName}
<SvgIcon name={iconName} iconSize="small" />
{/if}
</Avatar>
{/if}
<Layout.Stack
direction="row"
alignItems="center"
gap="s"
style="flex: 1; min-width: 0;">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary"
style="flex: 1; min-width: 0;">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
{#if !$isSmallViewport}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
{/if}
</Layout.Stack>
{#if action === 'button'}
<PinkButton.Button
size="xs"
variant="secondary"
style="flex-shrink: 0;"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else if search}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>Clear search</Button>
{/if}
</svelte:fragment>
</EmptySearch>
{:else}
{#await loadRepositories(selectedInstallation, search)}
<SkeletonRepoList />
{:then response}
{#if response?.length}
<Paginator items={response} hideFooter={response?.length <= 6} limit={6}>
{#snippet children(
paginatedItems: Models.ProviderRepositoryRuntime[] &
Models.ProviderRepositoryFramework[]
)}
<Table.Root columns={1} let:root>
{#each paginatedItems as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack
direction="row"
alignItems="center"
gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(
repo.framework
)}
iconSize="small" />
</Avatar>
{:else}
<Avatar
size="xs"
alt={repo.name}
empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar
size="xs"
alt={repo.name}
empty={!iconName}>
<SvgIcon
name={iconName}
iconSize="small" />
</Avatar>
{/if}
<Layout.Stack
gap="s"
direction="row"
alignItems="center"
justifyContent="space-between">
<Layout.Stack
direction="row"
gap="s"
alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
{#if !$isSmallViewport}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
{/if}
</Layout.Stack>
{#if action === 'button'}
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
{/if}
</Layout.Stack>
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
{:else if search}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>
Clear search
</Button>
{/if}
</svelte:fragment>
</EmptySearch>
{:else}
<Card>
<Layout.Stack alignItems="center" justifyContent="center">
<Typography.Text
variation="m-500"
color="--fgcolor-neutral-tertiary">
No repositories available
</Typography.Text>
</Layout.Stack>
</Card>
{/if}
{/await}
<Card>
<Layout.Stack alignItems="center" justifyContent="center">
<Typography.Text variation="m-500" color="--fgcolor-neutral-tertiary">
No repositories available
</Typography.Text>
</Layout.Stack>
</Card>
{/if}
{/if}
</Layout.Stack>
@@ -22,6 +22,7 @@
export let show = false;
export let rootDir: string;
export let product: 'sites' | 'functions' = 'functions';
export let branch: string;
let isLoading = true;
let directories: Directory[] = [
@@ -42,7 +43,7 @@
try {
const content = await sdk
.forProject(page.params.region, page.params.project)
.vcs.getRepositoryContents($installation.$id, $repository.id, currentPath);
.vcs.getRepositoryContents($installation.$id, $repository.id, currentPath, branch);
directories[0].fileCount = content.contents?.length ?? 0;
directories[0].children = content.contents
.filter((e) => e.isDirectory)
@@ -82,7 +83,7 @@
try {
const content = await sdk
.forProject(page.params.region, page.params.project)
.vcs.getRepositoryContents($installation.$id, $repository.id, path);
.vcs.getRepositoryContents($installation.$id, $repository.id, path, branch);
const fileCount = content.contents?.length ?? 0;
const contentDirectories = content.contents.filter((e) => e.isDirectory);
+1 -1
View File
@@ -14,7 +14,7 @@
<slot name="subtitle" />
</div>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center">
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="flex-start">
<slot name="status" />
</Layout.Stack>
</Layout.Stack>
@@ -12,7 +12,6 @@
export let showTeam: boolean;
export let showLabel: boolean;
export let showCustom: boolean;
export let hideOnClick: boolean = false;
export let groups: Writable<Map<string, Permission>>;
const dispatch = createEventDispatcher();
@@ -25,7 +24,7 @@
<ActionMenu.Item.Button
disabled={$groups.has('any')}
on:click={(e) => {
if (hideOnClick) hide(e);
hide(e);
dispatch('create', ['any']);
}}>
Any
@@ -33,7 +32,7 @@
<ActionMenu.Item.Button
disabled={$groups.has('guests')}
on:click={(e) => {
if (hideOnClick) hide(e);
hide(e);
dispatch('create', ['guests']);
}}>
All guests
@@ -41,7 +40,7 @@
<ActionMenu.Item.Button
disabled={$groups.has('users')}
on:click={(e) => {
if (hideOnClick) hide(e);
hide(e);
dispatch('create', ['users']);
}}>
All users
@@ -49,22 +48,22 @@
<ActionMenu.Item.Button
on:click={(e) => {
showUser = true;
if (hideOnClick) hide(e);
hide(e);
}}>Select users</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showTeam = true;
if (hideOnClick) hide(e);
hide(e);
}}>Select teams</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showLabel = true;
if (hideOnClick) hide(e);
hide(e);
}}>Label</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
showCustom = true;
if (hideOnClick) hide(e);
hide(e);
}}>Custom permission</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
@@ -22,7 +22,6 @@
import { Card } from '$lib/components';
export let withCreate = false;
export let hideOnClick = false;
export let permissions: string[] = [];
let showUser = false;
@@ -197,7 +196,6 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button secondary on:click={toggle}>
@@ -215,7 +213,6 @@
bind:showTeam
bind:showUser
{groups}
{hideOnClick}
on:create={create}
let:toggle>
<Button secondary icon on:click={toggle}>
+8 -5
View File
@@ -34,6 +34,7 @@
offset = 0;
search = '';
selected.clear();
show = false;
}
function create() {
@@ -88,11 +89,13 @@
{@const exists = $groups.has(role)}
<Table.Row.Button {root} on:click={() => onSelection(role)} disabled={exists}>
<Table.Cell column="checkbox" {root}>
<Selector.Checkbox
size="s"
id={team.$id}
disabled={exists}
checked={exists || selected.has(role)} />
<div style:pointer-events="none">
<Selector.Checkbox
size="s"
id={team.$id}
disabled={exists}
checked={exists || selected.has(role)} />
</div>
</Table.Cell>
<Table.Cell column="team" {root}>
<Layout.Stack direction="row" alignItems="center" gap="s">
+8 -5
View File
@@ -37,6 +37,7 @@
offset = 0;
search = '';
selected.clear();
show = false;
}
function create() {
@@ -90,11 +91,13 @@
{@const exists = $groups.has(role)}
<Table.Row.Button {root} on:click={() => onSelection(role)} disabled={exists}>
<Table.Cell column="checkbox" {root}>
<Selector.Checkbox
size="s"
id={user.$id}
disabled={exists}
checked={exists || selected.has(role)} />
<div style:pointer-events="none">
<Selector.Checkbox
size="s"
id={user.$id}
disabled={exists}
checked={exists || selected.has(role)} />
</div>
</Table.Cell>
<Table.Cell column="user" {root}>
<Layout.Stack direction="row" alignItems="center" gap="s">
+7 -1
View File
@@ -79,7 +79,13 @@
{ name: 'Functions', icon: IconLightningBolt, slug: 'functions', category: 'build' },
{ name: 'Messaging', icon: IconChatBubble, slug: 'messaging', category: 'build' },
{ name: 'Storage', icon: IconFolder, slug: 'storage', category: 'build' },
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy', badge: 'New' }
{
name: 'Sites',
icon: IconGlobeAlt,
slug: 'sites',
category: 'deploy',
badge: 'Early access'
}
];
const isSelected = (service: string): boolean => {
+1 -1
View File
@@ -18,7 +18,7 @@ export enum Dependencies {
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
UPGRADE_PLAN = 'dependency:upgrade_plan',
CREATE_ORGANIZATION = 'dependency:create_organization',
ORGANIZATIONS = 'dependency:organizations',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
-2
View File
@@ -20,7 +20,5 @@ function isFlagEnabled(name: string) {
export const flags = {
showSites: isFlagEnabled('sites'),
showCsvImport: isFlagEnabled('csv-import'),
showAttributeEncrypt: isFlagEnabled('column-encrypt'),
showIndexLengths: isFlagEnabled('index-lengths')
};
+2 -3
View File
@@ -60,9 +60,8 @@ export type TableRootProp = {
selectedAll: boolean;
selectedNone: boolean;
selectedSome: boolean;
// columns: Array<Column> | number;
columns: Record<PinkColumn['id'], PinkColumn>;
// columnsMap: Record<PinkColumn['id'], PinkColumn>;
columns: Array<PinkColumn> | number;
columnsMap: Record<PinkColumn['id'], PinkColumn>;
toggle: (id: string) => void;
toggleAll: () => void;
addAvailableId: (id: string) => void;
Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

+75 -42
View File
@@ -1,19 +1,39 @@
<script lang="ts">
import { Layout, Typography, Input, Tag, Icon } from '@appwrite.io/pink-svelte';
import { Layout, Typography, Input, Tag, Icon, Alert } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { CustomId } from '$lib/components/index.js';
import { getFlagUrl } from '$lib/helpers/flag';
import { isCloud } from '$lib/system.js';
import { currentPlan } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { filterRegions } from '$lib/helpers/regions';
import type { Snippet } from 'svelte';
export let projectName: string;
export let id: string;
export let regions: Array<Models.ConsoleRegion> = [];
export let region: string;
export let showTitle = true;
let {
projectName = $bindable(''),
id = $bindable(''),
regions = [],
region = $bindable(''),
showTitle = true,
projects = undefined,
submit
}: {
projectName: string;
id: string;
regions: Array<Models.ConsoleRegion>;
region: string;
showTitle: boolean;
projects?: number;
submit?: Snippet;
} = $props();
let showCustomId = false;
let showCustomId = $state(false);
let projectsLimited = $derived(
$currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
);
</script>
<svelte:head>
@@ -22,44 +42,57 @@
{/each}
</svelte:head>
<form on:submit|preventDefault>
<Layout.Stack direction="column" gap="xxl">
{#if showTitle}
<Typography.Title size="l">Create your project</Typography.Title>
{/if}
{#if projectsLimited}
<Alert.Inline status="warning" title="You've reached your limit of 2 projects">
Extra projects are available on paid plans for an additional fee
<svelte:fragment slot="actions">
<Button
compact
size="s"
href={`${base}/organization-${page.params.organization}/billing`}
external
text>Upgrade</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
<Layout.Stack direction="column" gap="xxl">
{#if showTitle}
<Typography.Title size="l">Create your project</Typography.Title>
{/if}
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="s">
<Input.Text
label="Name"
placeholder="Project name"
required
autofocus
bind:value={projectName} />
{#if !showCustomId}
<div>
<Tag
size="s"
on:click={() => {
showCustomId = true;
}}><Icon slot="start" icon={IconPencil} size="s" /> Project ID</Tag>
</div>
{/if}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
</Layout.Stack>
{#if isCloud && regions.length > 0}
<Layout.Stack gap="xs">
<Input.Select
required
bind:value={region}
placeholder="Select a region"
options={filterRegions(regions)}
label="Region" />
<Typography.Text>Region cannot be changed after creation</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="column" gap="s">
<Input.Text
disabled={projectsLimited}
label="Name"
placeholder="Project name"
required
autofocus
bind:value={projectName} />
{#if !showCustomId}
<div>
<Tag
size="s"
on:click={() => {
showCustomId = true;
}}><Icon icon={IconPencil} slot="start" size="s" /> Project ID</Tag>
</div>
{/if}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
</Layout.Stack>
{#if isCloud && regions.length > 0}
<Layout.Stack gap="xs">
<Input.Select
disabled={projectsLimited}
required
bind:value={region}
placeholder="Select a region"
options={filterRegions(regions)}
label="Region" />
<Typography.Text>Region cannot be changed after creation</Typography.Text>
</Layout.Stack>
{/if}
</Layout.Stack>
<slot name="submit"></slot>
</Layout.Stack>
</form>
{@render submit?.()}
</Layout.Stack>
+20
View File
@@ -313,6 +313,7 @@ export type Plan = {
webhooks: number;
users: number;
teams: number;
projects: number;
databases: number;
databasesAllowEncrypt: boolean;
buckets: number;
@@ -592,6 +593,25 @@ export class Billing {
);
}
async updateSelectedProjects(
organizationId: string,
projects: string[]
): Promise<Organization> {
const path = `/organizations/${organizationId}/projects`;
const params = {
projects
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'patch',
uri,
{
'content-type': 'application/json'
},
params
);
}
async setOrganizationPaymentMethod(
organizationId: string,
paymentMethodId: string
+21
View File
@@ -39,6 +39,7 @@ import { sdk } from './sdk';
import { user } from './user';
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
@@ -68,6 +69,7 @@ export const roles = [
export const teamStatusReadonly = 'readonly';
export const billingLimitOutstandingInvoice = 'outstanding_invoice';
export const billingProjectsLimitDate = '2024-09-01';
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
@@ -287,6 +289,7 @@ export function checkForEnterpriseTrial(org: Organization) {
}
export function calculateEnterpriseTrial(org: Organization) {
if (!org || !org.billingNextInvoiceDate) return 0;
const endDate = new Date(org.billingNextInvoiceDate);
const startDate = new Date(org.billingCurrentInvoiceDate);
const today = new Date();
@@ -314,6 +317,24 @@ export function calculateTrialDay(org: Organization) {
return days;
}
export async function checkForProjectsLimit(org: Organization, projects: number) {
if (!isCloud) return;
if (!org || !projects) return;
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
if (!plan) return;
if (plan.$id !== BillingPlan.FREE) return;
if (org.projects?.length > 0) return;
if (plan.projects > 0 && projects > plan.projects) {
headerAlert.add({
id: 'projectsLimitReached',
component: ProjectsLimit,
show: true,
importance: 12
});
}
}
export async function checkForUsageLimit(org: Organization) {
if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) {
headerAlert.add({
+1 -1
View File
@@ -34,6 +34,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
billingInvoiceId: string;
status: string;
remarks: string;
projects: string[];
};
export type OrganizationList = {
@@ -61,5 +62,4 @@ 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);
export const regions = writable<Models.ConsoleRegionList>({ total: 0, regions: [] });
+1 -1
View File
@@ -180,7 +180,7 @@ function createPreferences() {
names: TeamPreferences['names']
) => {
let teamPrefs: Models.Preferences;
update((n) => {
await updateAndSync((n) => {
if (!n?.displayNames) {
n ??= {};
n.displayNames ??= {};
+2
View File
@@ -18,6 +18,7 @@
checkForMarkedForDeletion,
checkForMissingPaymentMethod,
checkForNewDevUpgradePro,
checkForProjectsLimit,
checkForUsageLimit,
checkPaymentAuthorizationRequired,
paymentExpired,
@@ -296,6 +297,7 @@
if (currentOrganizationId === org.$id) return;
if (isCloud) {
currentOrganizationId = org.$id;
checkForProjectsLimit(org, data.projects?.projects?.length || 0);
checkForEnterpriseTrial(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
+17 -1
View File
@@ -4,6 +4,7 @@ import type { LayoutLoad } from './$types';
import { Dependencies } from '$lib/constants';
import type { Tier } from '$lib/stores/billing';
import type { Plan, PlanList } from '$lib/sdk/billing';
import { Query } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ depends, parent }) => {
const { organizations } = await parent();
@@ -28,6 +29,20 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
preferences.organization ??
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
// Load projects for the current organization if one is selected
let projects = null;
if (currentOrgId) {
try {
projects = await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(1000) // Get all projects for organization
]);
} catch (e) {
// Handle error silently - projects might not be accessible
projects = {};
}
}
return {
plansInfo,
roles: [],
@@ -36,7 +51,8 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
currentOrgId,
organizations,
consoleVariables,
version: versionData?.version ?? null
version: versionData?.version ?? null,
projects
};
};
@@ -56,7 +56,7 @@
if (organization) {
org = await sdk.forConsole.billing.setBillingAddress(organization, response.$id);
trackEvent(Submit.OrganizationBillingAddressUpdate);
await invalidate(Dependencies.ORGANIZATION);
await invalidate(Dependencies.ORGANIZATIONS);
}
await invalidate(Dependencies.ADDRESS);
+66 -1
View File
@@ -1,11 +1,76 @@
import { isCloud } from '$lib/system';
import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import AutoIncrementDark from '$lib/images/promos/auto-increment-dark.png';
import AutoIncrementLight from '$lib/images/promos/auto-increment-light.png';
import EncryptedDatabasesDark from '$lib/images/promos/encrypted-attribute-dark.png';
import EncryptedDatabasesLight from '$lib/images/promos/encrypted-attribute-light.png';
import OptInRelationDark from '$lib/images/promos/opt-relation-dark.png';
import OptInRelationLight from '$lib/images/promos/opt-relation-light.png';
const listOfPromotions: BottomModalAlertItem[] = [];
if (isCloud) {
/* add promos here */
const encryptedAttributePromo: BottomModalAlertItem = {
id: 'modal:encrypted_attribute_announcement',
src: {
dark: EncryptedDatabasesDark,
light: EncryptedDatabasesLight
},
title: 'Introducing Encrypted string attribute support',
message: 'Encrypt string attributes at rest, directly from the Appwrite Console.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://appwrite.io/blog/post/announcing-encrypted-string-attributes',
external: true,
hideOnClick: true
},
show: true
};
const autoIncrementPromo: BottomModalAlertItem = {
id: 'modal:auto_increment_announcement',
src: {
dark: AutoIncrementDark,
light: AutoIncrementLight
},
title: 'Announcing Auto-increment support',
message:
'Get a built-in numeric identifier that increases predictably with every new document added.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://appwrite.io/blog/post/announcing-auto-increment-support',
external: true,
hideOnClick: true
},
show: true
};
const optInRelationPromo: BottomModalAlertItem = {
id: 'modal:opt_in_relation_announcement',
src: {
dark: OptInRelationDark,
light: OptInRelationLight
},
title: 'Introducing Opt-in relationship loading',
message:
'Gain full control over which related documents to fetch and drastically reduce payload sizes.',
plan: 'free',
importance: 8,
scope: 'project',
cta: {
text: 'Read announcement',
link: () => 'https://appwrite.io/blog/post/announcing-opt-in-relationship-loading',
external: true,
hideOnClick: true
},
show: false
};
listOfPromotions.push(optInRelationPromo, autoIncrementPromo, encryptedAttributePromo);
}
export function addBottomModalAlerts() {
@@ -6,7 +6,7 @@ import type { Organization } from '$lib/stores/organization';
export const load: PageLoad = async ({ url, parent, depends }) => {
const { organizations } = await parent();
depends(Dependencies.CREATE_ORGANIZATION);
depends(Dependencies.ORGANIZATIONS);
const [coupon, paymentMethods] = await Promise.all([
getCoupon(url),
@@ -50,7 +50,7 @@
// fixes an edge case where
// the org is not available for some reason!
await invalidate(Dependencies.CREATE_ORGANIZATION);
await invalidate(Dependencies.ORGANIZATIONS);
}
isLoading = false;
}
@@ -15,7 +15,7 @@
import { user } from '$lib/stores/user';
let isLoading = false;
let id: string;
let id: string = ID.unique();
let startAnimation = false;
let projectName = 'Appwrite project';
let region = Region.Fra;
@@ -99,14 +99,18 @@
bind:projectName
bind:id
bind:region
on:submit={createProject}>
<svelte:fragment slot="submit">
showTitle={true}>
{#snippet submit()}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button.Button type="submit" variant="primary" size="s">
<Button.Button
on:click={createProject}
type="submit"
variant="primary"
size="s">
Create
</Button.Button>
</Layout.Stack>
</svelte:fragment>
{/snippet}
</CreateProject>
</Card.Base>
{/if}
@@ -21,11 +21,11 @@
import { loading } from '$routes/store';
import { ID, Region, type Models } from '@appwrite.io/console';
import { openImportWizard } from '../project-[region]-[project]/settings/migrations/(import)';
import { readOnly } from '$lib/stores/billing';
import { billingProjectsLimitDate, readOnly } from '$lib/stores/billing';
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { Badge, Icon, Typography } from '@appwrite.io/pink-svelte';
import { Badge, Icon, Typography, Alert, Tag, Tooltip } from '@appwrite.io/pink-svelte';
import {
IconAndroid,
IconApple,
@@ -37,13 +37,16 @@
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
import CreateProjectCloud from './createProjectCloud.svelte';
import { regions as regionsStore } from '$lib/stores/organization';
import { currentPlan, regions as regionsStore } from '$lib/stores/organization';
import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte';
import { toLocaleDate } from '$lib/helpers/date';
export let data;
let showCreate = false;
let showCreateProjectCloud = false;
let addOrganization = false;
let showSelectProject = false;
function filterPlatforms(platforms: { name: string; icon: string }[]) {
return platforms.filter(
@@ -117,8 +120,25 @@
function findRegion(project: Models.Project) {
return $regionsStore.regions.find((region) => region.$id === project.region);
}
function isSetToArchive(project: Models.Project): boolean {
if (!isCloud) return false;
if (data.organization.projects?.length === 0) return false;
if (!project || !project.$id) return false;
return !data.organization.projects?.includes(project.$id);
}
function formatName(name: string, limit: number = 19) {
return name ? (name.length > limit ? `${name.slice(0, limit)}...` : name) : '-';
}
$: projectsToArchive = data.projects.projects.filter(
(project) => !data.organization.projects?.includes(project.$id)
);
</script>
<SelectProjectCloud selectedProjects={data.organization.projects || []} bind:showSelectProject />
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Typography.Title>Projects</Typography.Title>
@@ -144,6 +164,27 @@
</DropList>
</div>
{#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects}
<Alert.Inline
title={`${data.projects.total - data.organization.projects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
<Typography.Text>
{#each projectsToArchive as project, index}{@const text = `<b>${project.name}</b>`}
{@html text}{index == projectsToArchive.length - 2
? ', and '
: index < projectsToArchive.length - 1
? ', '
: ''}
{/each}
will be archived
</Typography.Text>
<svelte:fragment slot="actions">
<Button secondary size="s" on:click={() => (showSelectProject = true)}>
Manage projects
</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
{#if data.projects.total}
<CardContainer
disableEmpty={!$canWriteProjects}
@@ -154,13 +195,35 @@
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{@const formatted = isSetToArchive(project)
? formatName(project.name)
: project.name}
<GridItem1
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
<Tooltip
maxWidth={project.name.length.toString()}
placement="top"
disabled={!isSetToArchive(project)}>
{formatted}
<span slot="tooltip">
{project.name}
</span>
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="status">
{#if isSetToArchive(project)}
<Tag
size="s"
on:click={(event) => {
event.preventDefault();
showSelectProject = true;
}}>Set to archive</Tag>
{/if}
</svelte:fragment>
{#each platforms.slice(0, 2) as platform}
@@ -210,4 +273,8 @@
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
<CreateProjectCloud bind:showCreateProjectCloud regions={$regionsStore.regions} />
<CreateProjectCloud
projects={data.projects.total}
bind:showCreateProjectCloud
regions={$regionsStore.regions}
teamId={page.params.organization} />
@@ -1,6 +1,5 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { organization } from '$lib/stores/organization';
import BudgetCap from './budgetCap.svelte';
import PlanSummary from './planSummary.svelte';
import BillingAddress from './billingAddress.svelte';
@@ -25,14 +24,15 @@
import type { PageData } from './$types';
export let data: PageData;
let organization = data.organization;
// why are these reactive?
$: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
(method: PaymentMethodData) => method.$id === organization?.paymentMethodId
);
$: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
(method: PaymentMethodData) => method.$id === $organization?.backupPaymentMethodId
(method: PaymentMethodData) => method.$id === organization?.backupPaymentMethodId
);
onMount(async () => {
@@ -52,10 +52,10 @@
);
await confirmPayment(
$organization.$id,
organization.$id,
invoice.clientSecret,
$organization.paymentMethodId,
`${base}/organization-${$organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}`
organization.paymentMethodId,
`${base}/organization-${organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}`
);
}
@@ -64,7 +64,7 @@
page.url.searchParams.get('type') === 'validate-invoice'
) {
const invoiceId = page.url.searchParams.get('invoice');
await sdk.forConsole.billing.updateInvoiceStatus($organization.$id, invoiceId);
await sdk.forConsole.billing.updateInvoiceStatus(organization.$id, invoiceId);
invalidate(Dependencies.INVOICES);
invalidate(Dependencies.ORGANIZATION);
}
@@ -84,7 +84,7 @@
}
if (page.url.searchParams.has('clientSecret')) {
const clientSecret = page.url.searchParams.get('clientSecret');
await confirmPayment($organization.$id, clientSecret, $organization.paymentMethodId);
await confirmPayment(organization.$id, clientSecret, organization.paymentMethodId);
}
});
</script>
@@ -93,7 +93,7 @@
{#if $failedInvoice}
{#if $failedInvoice?.lastError}
<Alert.Inline status="error">
The scheduled payment for {$organization.name} failed due to following error: {$failedInvoice.lastError}
The scheduled payment for {organization.name} failed due to following error: {$failedInvoice.lastError}
<svelte:fragment slot="actions">
<Button
text
@@ -106,7 +106,7 @@
{:else}
<Alert.Inline
status="error"
title={`The scheduled payment for ${$organization.name} failed`}>
title={`The scheduled payment for ${organization.name} failed`}>
To avoid service disruptions in organization's your projects, please verify your
payment details and update them if necessary.
</Alert.Inline>
@@ -115,29 +115,33 @@
{#if defaultPaymentMethod?.failed && !backupPaymentMethod}
<Alert.Inline
status="error"
title={`The default payment method for ${$organization.name} has expired`}>
title={`The default payment method for ${organization.name} has expired`}>
To avoid service disruptions in your organization's projects, please update your payment
details.
</Alert.Inline>
{/if}
{#if $organization?.billingPlanDowngrade}
{#if organization?.billingPlanDowngrade}
<Alert.Inline status="info">
Your organization has changed to {tierToPlan($organization?.billingPlanDowngrade).name} plan.
You will continue to have access to {tierToPlan($organization?.billingPlan).name} plan features
until your billing period ends on {toLocaleDate($organization.billingNextInvoiceDate)}.
Your organization has changed to {tierToPlan(organization?.billingPlanDowngrade).name} plan.
You will continue to have access to {tierToPlan(organization?.billingPlan).name} plan features
until your billing period ends on {toLocaleDate(organization.billingNextInvoiceDate)}.
</Alert.Inline>
{/if}
<Typography.Title>Billing</Typography.Title>
<PlanSummary
availableCredit={data?.availableCredit}
currentPlan={data?.aggregationBillingPlan}
currentPlan={data?.currentPlan}
currentAggregation={data?.billingAggregation}
currentInvoice={data?.billingInvoice} />
<PaymentHistory />
<PaymentMethods methods={data?.paymentMethods} />
<BillingAddress {data} />
<PaymentMethods organization={data?.organization} methods={data?.paymentMethods} />
<BillingAddress
organization={data?.organization}
billingAddress={data?.billingAddress}
locale={data?.locale}
countryList={data?.countryList} />
<TaxId />
<BudgetCap />
<BudgetCap organization={data?.organization} currentPlan={data?.currentPlan} />
<AvailableCredit areCreditsSupported={data.areCreditsSupported} />
</Container>
@@ -7,7 +7,7 @@ import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes, currentPlan, countryList, locale, plansInfo } = await parent();
const { organization, scopes, currentPlan, countryList, locale } = await parent();
if (!scopes.includes('billing.read')) {
return redirect(301, `/console/organization-${organization.$id}`);
@@ -64,10 +64,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
areCreditsSupported ? sdk.forConsole.billing.getAvailableCredit(organization.$id) : null
]);
const aggregationBillingPlan = plansInfo.get(
billingAggregation?.plan ?? organization.billingPlan
);
// make number
const credits = availableCredit ? availableCredit.available : null;
@@ -75,7 +71,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
paymentMethods,
addressList,
billingAddress,
aggregationBillingPlan,
availableCredit: credits,
billingAggregation,
billingInvoice,
@@ -7,7 +7,7 @@
import type { Address } from '$lib/sdk/billing';
import { addressList } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import RemoveAddress from './removeAddress.svelte';
import { user } from '$lib/stores/user';
@@ -23,14 +23,11 @@
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
export let data: PageData;
const locale: Models.Locale = data.locale;
const countryList: Models.CountryList = data.countryList;
let billingAddress: Address = data?.billingAddress;
export let organization: Organization;
export let locale: Models.Locale;
export let countryList: Models.CountryList;
export let billingAddress: Address;
let showCreate = false;
let showEdit = false;
@@ -39,16 +36,16 @@
async function addAddress(addressId: string) {
try {
await sdk.forConsole.billing.setBillingAddress($organization.$id, addressId);
await sdk.forConsole.billing.setBillingAddress(organization.$id, addressId);
addNotification({
type: 'success',
message: `A new billing address has been added to ${$organization.name}`
message: `A new billing address has been added to ${organization.name}`
});
trackEvent(Submit.OrganizationBillingAddressUpdate);
invalidate(Dependencies.ADDRESS);
invalidate(Dependencies.ORGANIZATION);
invalidate(Dependencies.ORGANIZATIONS);
} catch (error) {
addNotification({
type: 'error',
@@ -63,7 +60,7 @@
<svelte:fragment slot="title">Billing address</svelte:fragment>
View or update your billing address. This address will be included in your invoices from Appwrite.
<svelte:fragment slot="aside">
{#if $organization?.billingAddressId && billingAddress}
{#if organization?.billingAddressId && billingAddress}
<Card.Base variant="secondary" padding="s">
<Layout.Stack direction="row" justifyContent="space-between">
<div>
@@ -140,7 +137,7 @@
</CardGrid>
{#if showCreate}
<AddressModal bind:show={showCreate} organization={$organization?.$id} {countryList} {locale} />
<AddressModal bind:show={showCreate} organization={organization?.$id} {countryList} {locale} />
{/if}
{#if showEdit}
<EditAddressModal
@@ -7,18 +7,20 @@
import { Button, Form } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { currentPlan, organization } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { Alert, Icon, Table } from '@appwrite.io/pink-svelte';
import { IconTrash } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import type { Plan } from '$lib/sdk/billing';
export let organization: Organization;
export let currentPlan: Plan;
export let alertsEnabled = false;
let search: string;
let selectedAlert: number;
let alerts: number[] = [];
let alerts: number[] = organization?.budgetAlerts ?? [];
$: options = [
{ value: 25, label: '25%', disabled: false },
@@ -33,10 +35,6 @@
};
});
onMount(() => {
alerts = $organization?.budgetAlerts ?? [];
});
function addAlert() {
if (alerts.some((alert) => alert === selectedAlert)) {
return;
@@ -50,8 +48,8 @@
async function updateBudget() {
try {
await sdk.forConsole.billing.updateBudget(
$organization.$id,
$organization.billingBudget,
organization.$id,
organization.billingBudget,
alerts
);
@@ -60,7 +58,7 @@
addNotification({
type: 'success',
isHtml: true,
message: `<span> ${alerts.length === 0 ? 'Budget alerts removed from' : alerts.length > 1 ? `Budget alerts added to` : 'A budget alert has been added to'} <b>${$organization.name}</b> </span>`
message: `<span> ${alerts.length === 0 ? 'Budget alerts removed from' : alerts.length > 1 ? `Budget alerts added to` : 'A budget alert has been added to'} <b>${organization.name}</b> </span>`
});
trackEvent(Submit.BudgetAlertsUpdate, {
alerts
@@ -75,26 +73,26 @@
}
$: isButtonDisabled =
symmetricDifference(alerts, $organization.budgetAlerts).length === 0 || !alertsEnabled;
symmetricDifference(alerts, organization.budgetAlerts).length === 0 || !alertsEnabled;
</script>
<CardGrid>
<svelte:fragment slot="title">Billing alerts</svelte:fragment>
{#if !$currentPlan.budgeting}
{#if !currentPlan.budgeting}
Get notified by email when your organization meets a percentage of your budget cap. <b
>{tierToPlan($organization.billingPlan).name} organizations will receive one notification
>{tierToPlan(organization.billingPlan).name} organizations will receive one notification
at 75% resource usage.</b>
{:else}
Get notified by email when your organization meets or exceeds a percentage of your specified
billing alert(s).
{/if}
<svelte:fragment slot="aside">
{#if !$currentPlan.budgeting}
{#if !currentPlan.budgeting}
<Alert.Inline status="info" title="Billing alerts are a Pro plan feature">
Upgrade to a Pro plan to manage when you receive billing alerts for your
organization.
</Alert.Inline>
{:else if !$currentPlan.budgetCapEnabled}
{:else if !currentPlan.budgetCapEnabled}
<Alert.Inline status="info" title="Budget cap disabled">
Budget caps are not supported on your current plan. For more information, please
reach out to your customer success manager.
@@ -150,7 +148,7 @@
<svelte:fragment slot="actions">
<Form onSubmit={updateBudget}>
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
{#if organization?.billingPlan === BillingPlan.FREE || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<Button
secondary
href={$upgradeURL}
@@ -6,31 +6,28 @@
import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms';
import { showUsageRatesModal, upgradeURL } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization, currentPlan } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { Alert, Link } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import BudgetAlert from './budgetAlert.svelte';
import type { Plan } from '$lib/sdk/billing';
let capActive = false;
let budget: number;
onMount(() => {
budget = $organization?.billingBudget;
capActive = $organization?.billingBudget !== null;
});
export let currentPlan: Plan;
export let organization: Organization;
let capActive = organization?.billingBudget !== null;
let budget = organization.billingBudget;
async function updateBudget() {
try {
await sdk.forConsole.billing.updateBudget(
$organization.$id,
organization.$id,
budget,
$organization.budgetAlerts
organization.budgetAlerts
);
await invalidate(Dependencies.ORGANIZATION);
addNotification({
type: 'success',
message: `Budget cap enabled for ${$organization.name}`
message: `Budget cap enabled for ${organization.name}`
});
trackEvent(Submit.BudgetCapUpdate, {
budget: capActive ? budget : undefined
@@ -55,7 +52,7 @@
Restrict your resource usage by setting a budget cap. Cap usage is reset at the beginning of
each billing cycle.
<svelte:fragment slot="aside">
{#if !$currentPlan.budgeting}
{#if !currentPlan.budgeting}
<Alert.Inline status="info" title="Budget caps are a Pro plan feature">
Upgrade to a Pro plan to set a budget cap for your organization. For more
information on what you can do with a Pro plan,
@@ -64,7 +61,7 @@
target="_blank"
rel="noopener noreferrer">view our pricing guide.</Link.Anchor>
</Alert.Inline>
{:else if !$currentPlan.budgetCapEnabled}
{:else if !currentPlan.budgetCapEnabled}
<Alert.Inline status="info" title="Budget cap disabled">
Budget caps are not supported on your current plan. For more information, please
reach out to your customer success manager.
@@ -92,7 +89,7 @@
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if organization?.billingPlan === BillingPlan.FREE}
<Button
secondary
href={$upgradeURL}
@@ -105,10 +102,10 @@
>Upgrade to Pro
</Button>
{:else}
<Button disabled={$organization?.billingBudget === budget} submit>Update</Button>
<Button disabled={organization?.billingBudget === budget} submit>Update</Button>
{/if}
</svelte:fragment>
</CardGrid>
</Form>
<BudgetAlert alertsEnabled={capActive && budget > 0} />
<BudgetAlert {organization} {currentPlan} alertsEnabled={capActive && budget > 0} />
@@ -5,7 +5,7 @@
import { CardGrid, CreditCardBrandImage, CreditCardInfo } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { hasStripePublicKey, isCloud } from '$lib/system';
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
@@ -34,6 +34,7 @@
IconTrash
} from '@appwrite.io/pink-icons-svelte';
export let organization: Organization;
export let methods: PaymentList;
let showPayment = false;
@@ -47,12 +48,12 @@
async function addPaymentMethod(paymentMethodId: string) {
try {
await sdk.forConsole.billing.setOrganizationPaymentMethod(
$organization.$id,
organization.$id,
paymentMethodId
);
addNotification({
type: 'success',
message: `A new payment method has been added to ${$organization.name}`
message: `A new payment method has been added to ${organization.name}`
});
trackEvent(Submit.OrganizationPaymentUpdate);
invalidate(Dependencies.ORGANIZATION);
@@ -68,12 +69,12 @@
async function addBackupPaymentMethod(paymentMethodId: string) {
try {
await sdk.forConsole.billing.setOrganizationPaymentMethodBackup(
$organization.$id,
organization.$id,
paymentMethodId
);
addNotification({
type: 'success',
message: `A new payment method has been added to ${$organization.name}`
message: `A new payment method has been added to ${organization.name}`
});
invalidate(Dependencies.ORGANIZATION);
} catch (error) {
@@ -85,15 +86,15 @@
}
}
$: if ($organization?.backupPaymentMethodId) {
$: if (organization?.backupPaymentMethodId) {
sdk.forConsole.billing
.getOrganizationPaymentMethod($organization.$id, $organization.backupPaymentMethodId)
.getOrganizationPaymentMethod(organization.$id, organization.backupPaymentMethodId)
.then((res) => (backupPaymentMethod = res));
}
$: if ($organization?.paymentMethodId) {
$: if (organization?.paymentMethodId) {
sdk.forConsole.billing
.getOrganizationPaymentMethod($organization.$id, $organization.paymentMethodId)
.getOrganizationPaymentMethod(organization.$id, organization.paymentMethodId)
.then((res) => (defaultPaymentMethod = res));
}
@@ -112,7 +113,7 @@
<svelte:fragment slot="title">Payment methods</svelte:fragment>
View or update your organization payment methods here.
<svelte:fragment slot="aside">
{#if $organization?.paymentMethodId}
{#if organization?.paymentMethodId}
<Table.Root
let:root
columns={[
@@ -168,7 +169,7 @@
</Popover>
</Table.Cell>
</Table.Row.Base>
{#if $organization?.backupPaymentMethodId}
{#if organization?.backupPaymentMethodId}
<Table.Row.Base {root}>
<CreditCardInfo {root} isBackup paymentMethod={backupPaymentMethod} />
<Table.Cell column="actions" {root}>
@@ -209,9 +210,9 @@
</Table.Row.Base>
{/if}
</Table.Root>
{#if !$organization?.backupPaymentMethodId}
{#if !organization?.backupPaymentMethodId}
{@const filteredPaymentMethods = methods.paymentMethods.filter(
(o) => !!o.last4 && o.$id !== $organization?.paymentMethodId
(o) => !!o.last4 && o.$id !== organization?.paymentMethodId
)}
<div>
<Popover let:toggle placement="bottom-start" padding="none">
@@ -264,7 +265,7 @@
{/if}
{:else}
{@const filteredPaymentMethods = methods.paymentMethods.filter(
(o) => !!o.last4 && o.$id !== $organization?.backupPaymentMethodId
(o) => !!o.last4 && o.$id !== organization?.backupPaymentMethodId
)}
<Card.Base>
<Layout.Stack justifyContent="center" alignItems="center" gap="m">
@@ -319,15 +320,15 @@
bind:show={showEdit} />
{/if}
{#if isCloud && hasStripePublicKey}
<ReplaceCard {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
<ReplaceCard {organization} {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
{/if}
{#if showDelete && isCloud && hasStripePublicKey}
{@const hasOtherMethod = isSelectedBackup
? !!$organization?.paymentMethodId
: !!$organization?.backupPaymentMethodId}
? !!organization?.paymentMethodId
: !!organization?.backupPaymentMethodId}
<DeleteOrgPayment
bind:showDelete
{hasOtherMethod}
isBackup={isSelectedBackup}
disabled={$organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
disabled={organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
{/if}
@@ -20,7 +20,7 @@
message: `The billing address has been removed from ${$organization.name}`
});
trackEvent(Submit.OrganizationBillingAddressDelete);
invalidate(Dependencies.ORGANIZATION);
invalidate(Dependencies.ORGANIZATIONS);
show = false;
} catch (e) {
error = e.message;
@@ -3,7 +3,7 @@
import { FakeModal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
import type { Organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { submitStripeCard } from '$lib/stores/stripe';
import { onMount } from 'svelte';
@@ -12,6 +12,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { PaymentBoxes } from '$lib/components/billing';
export let organization: Organization;
export let show = false;
export let isBackup = false;
export let methods: PaymentList;
@@ -21,12 +22,12 @@
let selectedPaymentMethodId: string;
onMount(async () => {
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
if (!organization.paymentMethodId && !organization.backupPaymentMethodId) {
selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null;
} else {
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;
? organization.backupPaymentMethodId
: organization.paymentMethodId;
// If the selected payment method does not belong to the current user, select the first one.
if (
methods?.total &&
@@ -40,7 +41,7 @@
async function handleSubmit() {
try {
if (selectedPaymentMethodId === '$new') {
const method = await submitStripeCard(name, $organization.$id);
const method = await submitStripeCard(name, organization.$id);
selectedPaymentMethodId = method.$id;
}
isBackup
@@ -68,7 +69,7 @@
async function addPaymentMethod(paymentMethodId: string) {
try {
await sdk.forConsole.billing.setOrganizationPaymentMethod(
$organization.$id,
organization.$id,
paymentMethodId
);
} catch (e) {
@@ -79,7 +80,7 @@
async function addBackupPaymentMethod(paymentMethodId: string) {
try {
await sdk.forConsole.billing.setOrganizationPaymentMethodBackup(
$organization.$id,
organization.$id,
paymentMethodId
);
} catch (e) {
@@ -97,18 +98,18 @@
methods={filteredMethods}
bind:name
bind:group={selectedPaymentMethodId}
defaultMethod={$organization?.paymentMethodId}
backupMethod={$organization?.backupPaymentMethodId}
defaultMethod={organization?.paymentMethodId}
backupMethod={organization?.backupPaymentMethodId}
disabledCondition={isBackup
? $organization.paymentMethodId
: $organization.backupPaymentMethodId} />
? organization.paymentMethodId
: organization.backupPaymentMethodId} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button
secondary
submit
disabled={selectedPaymentMethodId ===
(isBackup ? $organization.backupPaymentMethodId : $organization.paymentMethodId)}>
(isBackup ? organization.backupPaymentMethodId : organization.paymentMethodId)}>
Save
</Button>
</svelte:fragment>
@@ -21,10 +21,12 @@
let showCustomId = false;
let disabled: boolean = false;
let name: string = 'New project';
let showSubmissionLoader = false;
async function create() {
try {
disabled = true;
showSubmissionLoader = true;
const project = await sdk.forConsole.projects.create(id ?? ID.unique(), name, teamId);
show = false;
dispatch('created', project);
@@ -40,7 +42,9 @@
} catch (e) {
error = e.message;
trackError(e, Submit.ProjectCreate);
} finally {
disabled = false;
showSubmissionLoader = false;
}
}
</script>
@@ -62,6 +66,10 @@
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit {disabled}>Create</Button>
<Button
submit
{disabled}
forceShowLoader={showSubmissionLoader}
submissionLoader={showSubmissionLoader}>Create</Button>
</svelte:fragment>
</Modal>
@@ -2,16 +2,18 @@
import { sdk } from '$lib/stores/sdk';
import { onDestroy } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { ID, type Models, Region as ConsoleRegion, Region } from '@appwrite.io/console';
import { base } from '$app/paths';
import CreateProject from '$lib/layout/createProject.svelte';
import { Modal } from '$lib/components';
import { currentPlan } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { Dependencies } from '$lib/constants';
export let teamId: string;
export let showCreateProjectCloud: boolean;
export let projects: number;
export let regions: Array<Models.ConsoleRegion> = [];
let id: string = null;
@@ -20,7 +22,6 @@
let region: ConsoleRegion = Region.Fra;
let showSubmissionLoader = false;
const teamId = page.params.organization;
async function create() {
let project: Models.Project;
@@ -60,11 +61,18 @@
title={'Create project'}
onSubmit={create}
bind:error>
<CreateProject showTitle={false} bind:id bind:projectName={name} bind:region {regions} />
<CreateProject
{projects}
showTitle={false}
bind:id
bind:projectName={name}
bind:region
{regions} />
<svelte:fragment slot="footer">
<Button
submit
size="s"
disabled={$currentPlan.projects > 0 && projects && projects >= $currentPlan?.projects}
forceShowLoader={showSubmissionLoader}
submissionLoader={showSubmissionLoader}>Create</Button>
</svelte:fragment>
@@ -196,7 +196,7 @@
<svelte:fragment slot="actions">
<Button
external
href="#/"
href="https://appwrite.io/docs/products/network/dns"
text
event="empty_documentation"
size="s"
@@ -7,7 +7,13 @@
import { toLocaleDate } from '$lib/helpers/date';
import { isTabSelected } from '$lib/helpers/load';
import { Cover } from '$lib/layout';
import { daysLeftInTrial, getServiceLimit, plansInfo, readOnly } from '$lib/stores/billing';
import {
daysLeftInTrial,
getServiceLimit,
plansInfo,
readOnly,
tierToPlan
} from '$lib/stores/billing';
import { members, newMemberModal, type Organization } from '$lib/stores/organization';
import {
canSeeBilling,
@@ -114,14 +120,25 @@
{/if}
{#if $isOwner}
<Button
secondary
size="s"
on:click={() => newMemberModal.set(true)}
disabled={areMembersLimited}>
<Icon icon={IconPlus} size="s" slot="start" />
Invite
</Button>
<Tooltip disabled={!areMembersLimited} placement="bottom-end">
<div>
<Button
secondary
size="s"
on:click={() => newMemberModal.set(true)}
disabled={areMembersLimited}>
<Icon icon={IconPlus} size="s" slot="start" />
Invite
</Button>
</div>
<div slot="tooltip">
{organization?.billingPlan === BillingPlan.FREE
? 'Upgrade to add more members'
: `You've reached the members limit for the ${
tierToPlan(organization?.billingPlan)?.name
} plan`}
</div>
</Tooltip>
{/if}
</Layout.Stack>
</div>
@@ -31,8 +31,11 @@
Icon,
Typography,
Popover,
ActionMenu
ActionMenu,
Tooltip
} from '@appwrite.io/pink-svelte';
import { BillingPlan } from '$lib/constants';
import { tierToPlan } from '$lib/stores/billing';
export let data;
@@ -41,6 +44,13 @@
let showEdit = false;
let showDropdown = [];
// Calculate if button should be disabled and tooltip should show
$: memberCount = data.organizationMembers?.total ?? 0;
$: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1;
$: isButtonDisabled = isCloud
? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported
: false;
const resend = async (member: Models.Membership) => {
try {
await sdk.forConsole.teams.createMembership(
@@ -70,14 +80,25 @@
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Members</Typography.Title>
<ConsoleButton
size="s"
event="create_user"
on:click={() => newMemberModal.set(true)}
disabled={isCloud ? !$currentPlan?.addons?.seats?.supported : false}>
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Invite</span>
</ConsoleButton>
<Tooltip disabled={!isButtonDisabled} placement="bottom-end">
<div>
<ConsoleButton
size="s"
event="create_user"
on:click={() => newMemberModal.set(true)}
disabled={isButtonDisabled}>
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Invite</span>
</ConsoleButton>
</div>
<div slot="tooltip">
{$organization?.billingPlan === BillingPlan.FREE
? 'Upgrade to add more members'
: `You've reached the members limit for the ${
tierToPlan($organization?.billingPlan)?.name
} plan`}
</div>
</Tooltip>
</Layout.Stack>
<Table.Root
@@ -67,11 +67,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -71,11 +71,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -75,11 +75,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -70,11 +70,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -65,11 +65,9 @@
To complete the setup, create an OAuth2 client ID with "Web application" as the application
type, then add this redirect URI to your {provider.name} configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
@@ -65,11 +65,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -72,11 +72,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -109,11 +109,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -82,11 +82,9 @@
<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
<div>
<p>URI</p>
<CopyInput
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
</div>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
@@ -17,7 +17,7 @@
async function updateSessionLength() {
try {
await sdk.forConsole.projects.updateAuthDuration($project.$id, $baseValue);
await sdk.forConsole.projects.updateAuthDuration(project.$id, $baseValue);
await invalidate(Dependencies.PROJECT);
addNotification({
@@ -24,7 +24,6 @@
let name = '';
let id: string = null;
let showCustomId = false;
let showPlanUpgradeAlert = true;
const trackEvents = (policies) => {
policies.forEach((policy) => {
@@ -104,10 +103,6 @@
trackError(error, Submit.DatabaseCreate);
}
};
$: if (!showCreate) {
showPlanUpgradeAlert = true;
}
</script>
<Modal title="Create database" onSubmit={create} bind:show={showCreate}>
@@ -133,18 +128,12 @@
{#if isCloud}
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if showPlanUpgradeAlert}
<Alert.Inline
dismissible
title="This database won't be backed up"
status="warning"
on:dismiss={() => (showPlanUpgradeAlert = false)}>
Upgrade your plan to ensure your data stays safe and backed up.
<svelte:fragment slot="actions">
<Button compact href={$upgradeURL}>Upgrade plan</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
<Alert.Inline title="This database won't be backed up" status="warning">
Upgrade your plan to ensure your data stays safe and backed up.
<svelte:fragment slot="actions">
<Button compact href={$upgradeURL}>Upgrade plan</Button>
</svelte:fragment>
</Alert.Inline>
{:else}
<CreatePolicy
bind:totalPolicies
@@ -24,7 +24,6 @@
import { base } from '$app/paths';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import { flags } from '$lib/flags';
export let data: PageData;
@@ -35,12 +34,13 @@
const filterColumns = writable<Column[]>([]);
$: selected = preferences.getCustomTableColumns(page.params.table);
$: tableColumns.set(
$table.columns.map((column) => ({
id: column.key,
title: column.key,
type: column.type as ColumnType,
hide: !selected?.includes(column.key),
hide: !!selected?.includes(column.key),
array: column?.array,
format: 'format' in column && column?.format === 'enum' ? column.format : null,
elements: 'elements' in column ? column.elements : null
@@ -51,13 +51,13 @@
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
id,
title: id,
hide: false,
show: true,
type: (id === '$id' ? 'string' : 'datetime') as ColumnType
}))
]);
$: hasColumns = !!$table.columns.length;
$: hasValidColumns = $table?.columns?.some((attr) => attr.status === 'available');
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
async function onSelect(file: Models.File) {
$isCsvImportInProgress = true;
@@ -100,15 +100,13 @@
analyticsSource="database_rows" />
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={data.view} columns={tableColumns} hideView isCustomTable />
{#if flags.showCsvImport(data)}
<Button
secondary
event={Click.DatabaseImportCsv}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => (showImportCSV = true)}>
Import CSV
</Button>
{/if}
<Button
secondary
event={Click.DatabaseImportCsv}
disabled={!(hasColumns && hasValidColumns)}
on:click={() => (showImportCSV = true)}>
Import CSV
</Button>
{#if !$isSmallViewport}
<Button
disabled={!(hasColumns && hasValidColumns)}
@@ -1,5 +1,5 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { InputChoice } from '$lib/elements/forms';
@@ -7,11 +7,9 @@
import { table } from '../store';
import type { Columns } from '../store';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isRelationship } from '../row-[row]/columns/store';
import Confirm from '$lib/components/confirm.svelte';
import { preferences } from '$lib/stores/preferences';
export let showDelete = false;
export let selectedColumn: Columns;
@@ -21,24 +19,12 @@
let error: string;
let checked = false;
async function updateTableColumns() {
const selectedColumns = preferences
.getCustomTableColumns($table.$id)
.filter((column) => column != selectedColumn.key);
// todo: change method name to table
await preferences.setCustomTableColumns($table.$id, selectedColumns);
await invalidate(Dependencies.TABLE);
}
async function handleDelete() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.tables.deleteColumn(databaseId, $table.$id, selectedColumn.key);
await updateTableColumns();
showDelete = false;
addNotification({
type: 'success',
@@ -50,7 +50,6 @@
import { ActionMenu, Selector } from '@appwrite.io/pink-svelte';
import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte';
import { flags } from '$lib/flags';
export let data: Partial<Models.ColumnString> = {
required: false,
@@ -63,8 +62,6 @@
let savedDefault = data.default;
const showEncrypt = flags.showAttributeEncrypt(page.data);
function handleDefaultState(hideDefault: boolean) {
if (hideDefault) {
savedDefault = data.default;
@@ -128,53 +125,55 @@
disabled={data.required || editing}
description="Indicate whether this column is an array. Defaults to an empty array." />
{#if showEncrypt}
<Layout.Stack gap="xs" direction="column">
<div class="popover-holder" class:disabled-checkbox={!supportsStringEncryption || editing}>
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
<Popover let:toggle placement="bottom-start">
<Selector.Checkbox
size="s"
id="encrypt"
bind:checked={data.encrypt}
disabled={!supportsStringEncryption || editing} />
<Layout.Stack gap="xs" direction="column">
<div
class="popover-holder"
class:cursor-not-allowed={editing}
class:disabled-checkbox={!supportsStringEncryption || editing}>
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
<Popover let:toggle placement="bottom-start">
<Selector.Checkbox
size="s"
id="encrypt"
bind:checked={data.encrypt}
disabled={!supportsStringEncryption || editing} />
<Layout.Stack gap="xxs" direction="column">
<button
type="button"
class="u-cursor-pointer"
on:click={(e) => {
if (!supportsStringEncryption) {
toggle(e);
} else {
data.encrypt = !data.encrypt;
}
}}>
<Layout.Stack inline gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">Encrypted</Typography.Text>
{#if !supportsStringEncryption}
<Tag variant="default" size="xs" on:click={toggle}>Pro</Tag>
{/if}
</Layout.Stack>
</button>
<Typography.Text color="--fgcolor-neutral-tertiary">
Indicate whether this column is encrypted. Encrypted columns cannot be
queried.
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xxs" direction="column">
<button
type="button"
disabled={editing}
class:cursor-pointer={!editing}
class:cursor-not-allowed={editing}
on:click={(e) => {
if (!supportsStringEncryption) {
toggle(e);
} else {
data.encrypt = !data.encrypt;
}
}}>
<Layout.Stack inline gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">Encrypted</Typography.Text>
{#if !supportsStringEncryption}
<Tag variant="default" size="xs" on:click={toggle}>Pro</Tag>
{/if}
</Layout.Stack>
</button>
<Typography.Text color="--fgcolor-neutral-tertiary">
Protect attribute against data leaks for best privacy compliance. Encrypted
attributes cannot be queried.
</Typography.Text>
</Layout.Stack>
<ActionMenu.Root width="180px" slot="tooltip">
<Typography.Text variant="m-500">
Available on Pro plan. <Link.Anchor href={$upgradeURL}
>Upgrade</Link.Anchor>
to enable encrypted columns.
</Typography.Text>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</div>
</Layout.Stack>
{/if}
<ActionMenu.Root width="180px" slot="tooltip">
<Typography.Text variant="m-500">
Available on Pro plan. <Link.Anchor href={$upgradeURL}>Upgrade</Link.Anchor>
to enable encrypted columns.
</Typography.Text>
</ActionMenu.Root>
</Popover>
</Layout.Stack>
</div>
</Layout.Stack>
<style lang="scss">
.popover-holder {
@@ -185,7 +184,16 @@
// no cursor when disabled
&.disabled-checkbox :global(button) {
cursor: unset !important;
cursor: unset;
}
}
.cursor-pointer {
// !important is needed
cursor: pointer !important;
}
.cursor-not-allowed {
cursor: not-allowed;
}
</style>
@@ -13,7 +13,6 @@
import { base } from '$app/paths';
import type { Columns } from './store';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { preferences } from '$lib/stores/preferences';
export let showCreate = false;
export let selectedOption: Option['name'] = null;
@@ -29,17 +28,10 @@
};
let error: string;
async function updateTableColumns() {
const selectedColumns = preferences.getCustomTableColumns(tableId);
selectedColumns.push(key ?? data?.key);
await preferences.setCustomTableColumns(tableId, selectedColumns);
await invalidate(Dependencies.TABLE);
}
async function submit() {
try {
await $option.create(databaseId, tableId, key, data);
await updateTableColumns();
await invalidate(Dependencies.TABLE);
if (!page.url.pathname.includes('columns')) {
await goto(
@@ -22,18 +22,22 @@
data = null;
selectedRelationship = null;
}
$: tableColumns = [
// take full width if no display names set
{ id: '$id', width: args.length ? 200 : undefined },
...args.map((id) => ({ id }))
];
</script>
<Modal bind:show title={selectedRelationship?.key} hideFooter>
{#if data?.length}
<Paginator items={data} {limit}>
{#snippet children(paginatedItems: typeof data)}
<Table.Root
let:root
columns={[{ id: '$id', width: 150 }, ...args.map((id) => ({ id }))]}>
<Table.Root let:root columns={tableColumns}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="$id" {root}>Row ID</Table.Header.Cell>
{#if args?.length}
{#if args.length}
{#each args as arg}
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
{/each}
@@ -47,7 +51,7 @@
<Table.Cell column="$id" {root}>
<Id value={doc.$id}>{doc.$id}</Id>
</Table.Cell>
{#if args?.length}
{#if args.length}
{#each args as arg}
<Table.Cell column={arg} {root}>{doc[arg]}</Table.Cell>
{/each}
@@ -12,6 +12,7 @@
<InputSelect
{id}
{label}
bind:value
{optionalText}
placeholder="Select a value"
required={column.required}
@@ -19,5 +20,4 @@
!column.required && { label: 'NULL', value: null },
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)}
bind:value />
].filter(Boolean)} />
@@ -6,14 +6,12 @@
export let label: string;
export let value: string | null;
export let column: Models.ColumnEmail;
$: nullable = column.required ? false : !value;
</script>
<InputEmail
{id}
{label}
{nullable}
placeholder="Enter Email"
bind:value
placeholder="Enter URL"
required={column.required}
bind:value />
nullable={!column.required} />
@@ -6,16 +6,14 @@
export let label: string;
export let value: number;
export let column: Models.ColumnInteger;
$: nullable = column.required ? false : !value;
</script>
<InputNumber
{id}
{label}
{nullable}
required={column.required}
bind:value
min={column.min}
max={column.max}
step={column.type === 'double' ? 'any' : 1}
bind:value />
required={column.required}
nullable={!column.required}
step={column.type === 'double' ? 'any' : 1} />
@@ -116,9 +116,29 @@
$: totalCount = relatedList?.length ?? 0;
$: options =
rowList?.rows?.map((n) => {
const data = displayNames.filter((name) => name !== '$id').map((name) => n?.[name]);
return { value: n.$id, label: n.$id, data };
rowList?.rows?.map((row) => {
const names = displayNames.filter((name) => name !== '$id');
const values = names
.map((name) => row?.[name])
// always supposed to be a string but just being a bit safe here
.filter((value) => value != null && typeof value === 'string' && value !== '');
const displayValues = !editing
? values
: // on non edit routes like create, there's enough space!
values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
const label = !values.length
? row.$id
: // values are in `$id (a | b)` format
// previously used to have a `$id a | b`.
`${row.$id} (${displayValues.join(' | ')})`;
return {
label,
value: row.$id,
data: names.map((name) => row?.[name])
};
}) ?? [];
$: hasItems = totalCount > 0;
@@ -6,26 +6,24 @@
export let label: string;
export let value: string;
export let column: Models.ColumnString;
$: nullable = column.required ? false : !value;
</script>
{#if column.size >= 50}
<InputTextarea
{id}
{label}
{nullable}
bind:value
placeholder="Enter string"
required={column.required}
maxlength={column.size}
bind:value />
required={column.required}
nullable={!column.required} />
{:else}
<InputText
{id}
{label}
{nullable}
bind:value
placeholder="Enter string"
required={column.required}
maxlength={column.size}
bind:value />
required={column.required}
nullable={!column.required} />
{/if}
@@ -6,8 +6,12 @@
export let label: string;
export let value: string | null;
export let column: Models.ColumnUrl;
$: nullable = column.required ? false : !value;
</script>
<InputURL {id} {label} {nullable} placeholder="Enter URL" required={column.required} bind:value />
<InputURL
{id}
{label}
bind:value
placeholder="Enter URL"
required={column.required}
nullable={!column.required} />
@@ -17,10 +17,23 @@
const tableId = page.params.table;
let names: string[] = [...(preferences.getDisplayNames()?.[tableId] ?? [])];
function getDisplayNames() {
return [...(preferences.getDisplayNames()?.[tableId] ?? [])].filter(
(name) => name !== '$id'
); // edge case with `$id`? got saved during tests!
}
let names: string[] = $state(getDisplayNames());
async function updateDisplayName() {
try {
await preferences.setDisplayNames($organization.$id, tableId, names);
names = [...(preferences.getDisplayNames()?.[tableId] ?? [])];
// $state makes proxy,
// structuredClone doesn't work
const regularArray = [...names];
await preferences.setDisplayNames($organization.$id, tableId, regularArray);
names = getDisplayNames();
await invalidate(Dependencies.TEAM);
addNotification({
message: 'Display names have been updated',
@@ -36,23 +49,34 @@
}
}
$: options = ($columns as Models.ColumnString[])
.filter(
(attr) =>
attr.type === 'string' && !attr?.array && !names?.some((name) => name === attr.key)
)
.map((attr) => {
return {
function getValidAttributes() {
return ($attributes as Models.AttributeString[]).filter(
(attr) => attr.type === 'string' && !attr?.array
);
}
function getOptions(index: number) {
const current = names?.[index];
return getValidAttributes()
.filter((attr) => !names?.includes(attr.key) || attr.key === current)
.map((attr) => ({
value: attr.key,
label: attr.key
};
});
}));
}
$: addAttributeDisabled = names?.length >= 5 || (names?.length && !names[names?.length - 1]);
const addAttributeDisabled = $derived(
names?.length >= 5 || (names?.length && !names[names?.length - 1])
);
$: updateBtnDisabled =
const updateBtnDisabled = $derived(
!symmetricDifference(names, preferences.getDisplayNames()?.[tableId] ?? [])?.length ||
(names?.length && !last(names));
(names?.length && !last(names))
);
const hasExhaustedOptions = $derived(
getValidAttributes().length === names.filter(Boolean).length
);
</script>
<Form onSubmit={updateDisplayName}>
@@ -72,19 +96,22 @@
</span>
</Layout.Stack>
{#if names?.length}
{#each names as name, i}
{#each names as name, index}
<Layout.Stack direction="row" gap="xxs">
{@const options = getOptions(index)}
{@const disabled =
(!!names[index] && names.length > index + 1) || hasExhaustedOptions}
<InputSelect
id={name}
placeholder="Select column"
bind:value={names[i]}
disabled={!!names[i] && names.length > i + 1}
{options} />
{options}
{disabled}
bind:value={names[index]}
placeholder="Select column" />
<Button
icon
extraCompact
on:click={() => {
names.splice(i, 1);
names.splice(index, 1);
names = names;
}}>
<Icon icon={IconX} />
@@ -92,18 +119,22 @@
</Layout.Stack>
{/each}
{/if}
<div>
<Button
compact
disabled={addAttributeDisabled}
on:click={() => {
names[names.length] = null;
names = names;
}}>
<Icon icon={IconPlus} slot="start" size="s" />
Add column
</Button>
</div>
<!-- show only when options don't have all the attributes -->
{#if !hasExhaustedOptions}
<div>
<Button
compact
disabled={addAttributeDisabled}
on:click={() => {
names[names.length] = null;
names = names;
}}>
<Icon icon={IconPlus} slot="start" size="s" />
Add column
</Button>
</div>
{/if}
</Layout.Stack>
</svelte:fragment>
@@ -10,7 +10,7 @@
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { afterUpdate, onMount } from 'svelte';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { isRelationship, isRelationshipToMany, isString } from './row-[row]/columns/store';
import RelationshipsModal from './relationshipsModal.svelte';
@@ -28,36 +28,21 @@
} from '@appwrite.io/pink-svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { flags } from '$lib/flags';
export let data: PageData;
const databaseId = page.params.database;
const tableId = page.params.table;
let displayNames = {};
let showRelationships = false;
let selectedRelationship: Models.ColumnRelationship = null;
let relationshipData: Partial<Models.Document>[];
let displayNames = {};
onMount(async () => {
displayNames = preferences.getDisplayNames();
updateMaxWidth();
});
afterUpdate(() => updateMaxWidth());
function updateMaxWidth() {
const tableCells = Array.from(document.querySelectorAll('.less-width-truncated'));
const visibleColumnsCount = $tableColumns.filter((col) => !col.hide).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 '[ ]';
@@ -104,7 +89,7 @@
id: column.key,
title: column.key,
type: column.type as ColumnType,
hide: !selected?.includes(column.key),
hide: !!selected?.includes(column.key),
array: column?.array,
width: { min: 168 },
format: 'format' in column && column?.format === 'enum' ? column.format : null,
@@ -164,8 +149,6 @@
) as Models.ColumnRelationship[];
let checked = false;
const showEncrypt = flags.showAttributeEncrypt(data);
</script>
<Table.Root
@@ -257,7 +240,7 @@
<span slot="title">Timestamp</span>
{toLocaleDateTime(datetime, true)}
</DualTimeView>
{:else if isEncryptedAttribute && showEncrypt}
{:else if isEncryptedAttribute}
<button on:click={(e) => e.preventDefault()}>
<InteractiveText
copy={false}
@@ -370,8 +353,12 @@
</Confirm>
<style>
.floating-action-bar :global(div:first-of-type) {
/* 50% > 60% because we have sub-navigation */
left: calc(60% - var(--p-floating-action-bar-width) / 2);
.floating-action-bar {
position: fixed;
:global(div:first-of-type) {
/* 50% > 60% because we have sub-navigation */
left: calc(60% - var(--p-floating-action-bar-width) / 2);
}
}
</style>
@@ -214,9 +214,11 @@
>Only .tar.gz files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
{#if maxSize > 0}
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
{/if}
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
@@ -101,9 +101,11 @@
>Only .tar.gz files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
{#if maxSize > 0}
<Typography.Caption variant="400"
>Max file size: {readableMaxSize.value +
readableMaxSize.unit}</Typography.Caption>
{/if}
</Layout.Stack>
</Layout.Stack>
</Upload.Dropzone>
@@ -64,7 +64,7 @@
<svelte:fragment slot="actions">
<Button
external
href="#/"
href="https://appwrite.io/docs/products/functions/domains"
text
event="empty_documentation"
size="s"
@@ -83,14 +83,16 @@
let path = '/';
let method = ExecutionMethod.GET;
let body = '';
let headers: [string, string][] = [[null, '']];
let headers: [string, string][] = [['', '']];
async function handleSubmit() {
try {
const headersObject = {};
for (const [name, value] of headers) {
headersObject[name] = value;
if (name && name.trim() !== '') {
headersObject[name] = value;
}
}
await sdk
@@ -134,7 +136,8 @@
$: dateTime = new Date(`${date}T${time}`);
$: filteredKeyList = keyList.filter((key) => {
const name = last(headers)[0];
const lastHeader = last(headers);
const name = lastHeader ? lastHeader[0] : '';
if (!name) return true;
return key.value.toLowerCase().includes(name?.toLowerCase());
});
@@ -274,5 +274,9 @@
<DisconnectRepo bind:show={showDisconnect} on:success={loadRepository} />
{/if}
{#if showSelectRoot}
<SelectRootModal bind:show={showSelectRoot} product="sites" bind:rootDir={selectedDir} />
<SelectRootModal
bind:show={showSelectRoot}
product="sites"
bind:rootDir={selectedDir}
branch={selectedBranch} />
{/if}
@@ -18,6 +18,8 @@
export let func: Models.Function;
export let specs: Models.SpecificationList;
let specification = func.specification;
let originalSpecification = func.specification;
$: originalSpecification = func.specification;
async function updateLogging() {
try {
@@ -47,6 +49,9 @@
specification || undefined
);
await invalidate(Dependencies.FUNCTION);
originalSpecification = specification;
addNotification({
type: 'success',
message: 'Resource limits have been updated'
@@ -96,7 +101,7 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={func.specification === specification} submit>Update</Button>
<Button disabled={originalSpecification === specification} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -11,7 +11,7 @@
import { func } from '../store';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Runtime } from '@appwrite.io/console';
import Scopes from '$routes/(console)/project-[region]-[project]/overview/keys/scopes.svelte';
import Scopes from '$routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { Link } from '$lib/elements';
@@ -24,8 +24,7 @@
APNSProviderParams
} from './store';
import TooltipPopover from './tooltipPopover.svelte';
import { Icon, Tooltip, Upload, Layout, Typography } from '@appwrite.io/pink-svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { Upload, Layout, Typography } from '@appwrite.io/pink-svelte';
import { InvalidFileType, removeFile } from '$lib/helpers/files';
import { addNotification } from '$lib/stores/notifications';
@@ -44,18 +43,6 @@
| FCMProviderParams
| APNSProviderParams
>;
const popover = input.popover ? PopoverContent : null;
const popoverProps = getPopoverProps(input);
function getPopoverProps(input: ProviderInput) {
if (!input.popover) {
return {};
}
return {
lines: input.popover,
image: input.popoverImage
};
}
function handleInvalid(e: CustomEvent) {
const reason = e.detail?.reason ?? '';
@@ -72,6 +59,14 @@
});
}
}
$: popover = input.popover ? PopoverContent : null;
$: popoverProps = !input.popover
? {}
: {
lines: input.popover,
image: input.popoverImage
};
</script>
{#if input.type === 'text'}
@@ -122,25 +117,15 @@
{:else if input.type === 'file'}
<Upload.Dropzone
on:invalid={handleInvalid}
extensions={['json']}
extensions={input.allowedFileExtensions}
bind:files={files[input.name]}
maxSize={10000000}
required={!input.optional}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" justifyContent="center" direction="row" gap="s">
<Typography.Text variant="l-500">
Drag and drop service account JSON here or click to upload
</Typography.Text>
<Tooltip>
<Layout.Stack alignItems="center" justifyContent="center" inline>
<Icon icon={IconInfo} size="s" />
</Layout.Stack>
<svelte:fragment slot="tooltip">Only .json files allowed</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400">Max file size 10MB</Typography.Caption>
</Layout.Stack>
<Layout.Stack alignItems="center" justifyContent="center" direction="row" gap="xxs">
<Typography.Text variant="l-500">
Drag and drop your {input.label} here or click to upload
</Typography.Text>
<TooltipPopover {popover} {popoverProps} tooltip={input.tooltip} />
</Layout.Stack>
</Upload.Dropzone>
{#if files[input.name]?.length}
@@ -85,7 +85,7 @@ export const providers: ProvidersMap = {
],
configure: [
{
label: 'Service account JSON (.json file)',
label: 'service account JSON (.json file)',
name: 'serviceAccountJSON',
type: 'file',
allowedFileExtensions: ['json'],
@@ -164,7 +164,7 @@ export const providers: ProvidersMap = {
}
},
{
label: 'Auth key (.p8 file)',
label: 'auth key (.p8 file)',
name: 'authKey',
type: 'file',
allowedFileExtensions: ['p8'],
@@ -13,7 +13,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import Scopes from '../keys/scopes.svelte';
import Scopes from '../api-keys/scopes.svelte';
import { page } from '$app/state';
const projectId = page.params.project;
@@ -41,7 +41,7 @@
trackEvent(Submit.KeyCreate);
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$id}`
`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$id}`
);
addNotification({
message: `API key has been created`,
@@ -59,7 +59,7 @@
<Wizard
title="Create API key"
href={`${base}/project-${page.params.region}-${page.params.project}/overview/keys/`}
href={`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/`}
bind:showExitModal
column
columnSize="s"
@@ -14,7 +14,7 @@
import UpdateExpirationDate from './updateExpirationDate.svelte';
import type { Models } from '@appwrite.io/console';
import { symmetricDifference } from '$lib/helpers/array';
import Scopes from '../keys/scopes.svelte';
import Scopes from '../api-keys/scopes.svelte';
import { InteractiveText, Layout, Typography } from '@appwrite.io/pink-svelte';
export let key: Models.DevKey | Models.Key;
@@ -20,7 +20,7 @@
const isApiKey = keyType === 'api';
const label = isApiKey ? 'API' : 'dev';
const slug = isApiKey ? 'keys' : 'dev-keys';
const slug = isApiKey ? 'api-keys' : 'dev-keys';
function asApiKey(key: Models.Key | Models.DevKey) {
return key as Models.Key;
@@ -70,7 +70,7 @@
label: 'Create API Key',
icon: IconPlus,
callback() {
goto(`${path}/keys/create`);
goto(`${path}/api-keys/create`);
},
keys: ['c', 'k'],
group: 'integrations',
@@ -229,12 +229,12 @@
selected={isTabSelected('platforms')}>Platforms</Tab>
<Tab
noscroll
event="keys"
href={`${path}/keys`}
selected={isTabSelected('keys')}>API keys</Tab>
event="api-keys"
href={`${path}/api-keys`}
selected={isTabSelected('api-keys')}>API keys</Tab>
<Tab
noscroll
event="keys"
event="dev-keys"
href={`${path}/dev-keys`}
selected={isTabSelected('dev-keys')}>Dev keys</Tab>
</Tabs>
@@ -16,11 +16,11 @@
title: $project?.name
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys`,
href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys`,
title: 'API keys'
},
{
href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$key?.$id}`,
href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$key?.$id}`,
title: $key?.name
}
];
@@ -9,7 +9,7 @@
<Cover>
<svelte:fragment slot="header">
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/overview/keys`}>
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/overview/api-keys`}>
{$key?.name}
</CoverTitle>
</svelte:fragment>
@@ -9,7 +9,7 @@
{#if $canWriteKeys}
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/overview/keys/create`}>
href={`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/create`}>
<Icon icon={IconPlus} slot="start" size="s" />
Create API key
</Button>

Some files were not shown because too many files have changed in this diff Show More