Merge branch 'feat-custom-signup' of github.com:appwrite/console into db-pools-support

This commit is contained in:
Arman
2023-01-18 19:10:40 +01:00
70 changed files with 1759 additions and 900 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_SENTRY_DSN=
VITE_CONSOLE_MODE=self-hosted
VITE_CONSOLE_MODE=self-hosted
VITE_CONSOLE_MODE=cloud
VITE_CONSOLE_TIER=base
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v2
with:
+13 -13
View File
@@ -9,7 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@aw-labs/appwrite-console": "^13.0.0",
"@aw-labs/appwrite-console": "^13.1.0",
"@aw-labs/icons": "0.0.0-77",
"@aw-labs/ui": "0.0.0-77",
"@popperjs/core": "^2.11.6",
@@ -145,9 +145,9 @@
"integrity": "sha512-1Yw7u/COtxx06BfwlI+kVhsa/upKYzmCNrT4c8QDeCY2KMYlnijkUjtHiPU08HxyTIVB5j6d75O0YWVIHwQS8g=="
},
"node_modules/@aw-labs/appwrite-console": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@aw-labs/appwrite-console/-/appwrite-console-13.0.0.tgz",
"integrity": "sha512-a5illDoFZAhgLP8Ae+KHPzBIUozKXzjyHUw6jTwNPZaL+b2RnTe3oRLZU8l8Kt5pB5m7NZx6kAcZm+NMf8NeCw==",
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@aw-labs/appwrite-console/-/appwrite-console-13.1.0.tgz",
"integrity": "sha512-1FyKke5b3vx+D3VfNYd3YnFhzUOlsj9bD+KTRLWA4WRItb55thFVZJMWQQ4XnZtDiLPuJAn2XjbMSfMr+9XYwg==",
"dependencies": {
"cross-fetch": "3.1.5",
"isomorphic-form-data": "2.0.0"
@@ -5399,9 +5399,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"peer": true,
"bin": {
@@ -7740,9 +7740,9 @@
"integrity": "sha512-1Yw7u/COtxx06BfwlI+kVhsa/upKYzmCNrT4c8QDeCY2KMYlnijkUjtHiPU08HxyTIVB5j6d75O0YWVIHwQS8g=="
},
"@aw-labs/appwrite-console": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@aw-labs/appwrite-console/-/appwrite-console-13.0.0.tgz",
"integrity": "sha512-a5illDoFZAhgLP8Ae+KHPzBIUozKXzjyHUw6jTwNPZaL+b2RnTe3oRLZU8l8Kt5pB5m7NZx6kAcZm+NMf8NeCw==",
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@aw-labs/appwrite-console/-/appwrite-console-13.1.0.tgz",
"integrity": "sha512-1FyKke5b3vx+D3VfNYd3YnFhzUOlsj9bD+KTRLWA4WRItb55thFVZJMWQQ4XnZtDiLPuJAn2XjbMSfMr+9XYwg==",
"requires": {
"cross-fetch": "3.1.5",
"isomorphic-form-data": "2.0.0"
@@ -11827,9 +11827,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"peer": true
},
+1 -1
View File
@@ -21,7 +21,7 @@
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"analytics": "^0.8.1",
"@aw-labs/appwrite-console": "^13.0.0",
"@aw-labs/appwrite-console": "^13.1.0",
"@aw-labs/icons": "0.0.0-77",
"@aw-labs/ui": "0.0.0-77",
"@popperjs/core": "^2.11.6",
+2 -2
View File
@@ -3,7 +3,7 @@ import googleAnalytics from '@analytics/google-analytics';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { Mode } from '$lib/constants';
import { growthEndpoint, Mode } from '$lib/constants';
const analytics = Analytics({
app: 'appwrite',
@@ -40,7 +40,7 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
email = userStore.email;
name = userStore.name;
}
fetch('https://growth.appwrite.io/v1/analytics', {
fetch(`${growthEndpoint}/analytics`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+15 -13
View File
@@ -2,6 +2,7 @@
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { clickOnEnter } from '$lib/helpers/a11y';
import { copy } from '$lib/helpers/copy';
import { addNotification } from '$lib/stores/notifications';
export let value: string;
@@ -9,27 +10,28 @@
let content = 'Click to copy';
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
async function handleClick() {
const success = await copy(value);
if (success) {
content = 'Copied';
} catch (error) {
} else {
addNotification({
message: error.message,
message: 'Unable to copy to clipboard',
type: 'error'
});
} finally {
if (event) {
trackEvent('click_id_tag', {
name: event
});
}
}
};
if (event) {
trackEvent('click_id_tag', {
name: event
});
}
}
</script>
<span
on:click|preventDefault={copy}
on:click|preventDefault={handleClick}
on:keyup={clickOnEnter}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
+8 -6
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { copy } from '$lib/helpers/copy';
import { addNotification } from '$lib/stores/notifications';
@@ -9,13 +10,14 @@
let content = 'Click to copy';
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
const handleCopy = async () => {
const success = await copy(value);
if (success) {
content = 'Copied';
} catch (error) {
} else {
addNotification({
message: error.message,
message: 'Unable to copy to clipboard',
type: 'error'
});
}
@@ -31,7 +33,7 @@
type="button"
class="input-button"
aria-label="Click to copy."
on:click={copy}
on:click={handleCopy}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
content,
@@ -142,9 +142,9 @@
</script>
{#if [...$groups]?.length}
<Table noMargin noStyles noMobile>
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>Role</TableCellHead>
<TableCellHead width={70}>Role</TableCellHead>
{#if withCreate}
<TableCellHead width={70}>Create</TableCellHead>
{/if}
+2 -2
View File
@@ -92,9 +92,9 @@
</script>
{#if [...$groups.keys()]?.length}
<Table noMargin noStyles noMobile>
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>Role</TableCellHead>
<TableCellHead width={70}>Role</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
+47 -45
View File
@@ -53,59 +53,61 @@
})
.finally(() => {
tick().then(() => {
instance.setContent(content.innerHTML);
instance.setContent(content);
});
});
}
}}>
{role}
</div>
<div class="u-hide" bind:this={content}>
{#if data}
{@const isUser = role.startsWith('user')}
{@const isTeam = role.startsWith('team')}
{@const isAnonymous = !data.email && !data.phone}
<div class="user-profile">
{#if isAnonymous}
<div class="avatar is-size-small ">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{:else if data.name}
<AvatarInitials name={data.name} size={40} />
{:else}
<div class="avatar is-size-small ">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
<span class="user-profile-info is-only-desktop">
<span class="name">
{data.name
? data.name
: data?.email
? data?.email
: data?.phone
? data?.phone
: '-'}
</span>
<Output value={data.$id}>{role}</Output>
</span>
{#if (isUser && (data?.email || data?.phone)) || isTeam}
<span class="user-profile-sep" />
<span class="user-profile-empty-column" />
<div class="u-hide">
<div bind:this={content}>
{#if data}
{@const isUser = role.startsWith('user')}
{@const isTeam = role.startsWith('team')}
{@const isAnonymous = !data.email && !data.phone}
<div class="user-profile">
{#if isAnonymous}
<div class="avatar is-size-small ">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{:else if data.name}
<AvatarInitials name={data.name} size={40} />
{:else}
<div class="avatar is-size-small ">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
<span class="user-profile-info is-only-desktop">
{#if isUser}
<p class="text u-x-small">{data?.email}</p>
<p class="text u-x-small">{data?.phone}</p>
{:else if isTeam}
<p class="text u-x-small">Members: {data?.total}</p>
{/if}
<span class="name">
{data.name
? data.name
: data?.email
? data?.email
: data?.phone
? data?.phone
: '-'}
</span>
<Output value={data.$id}>{role}</Output>
</span>
{/if}
</div>
{:else}
Not found.
{/if}
{#if (isUser && (data?.email || data?.phone)) || isTeam}
<span class="user-profile-sep" />
<span class="user-profile-empty-column" />
<span class="user-profile-info is-only-desktop">
{#if isUser}
<p class="text u-x-small">{data?.email}</p>
<p class="text u-x-small">{data?.phone}</p>
{:else if isTeam}
<p class="text u-x-small">Members: {data?.total}</p>
{/if}
</span>
{/if}
</div>
{:else}
Not found.
{/if}
</div>
</div>
{/if}
</div>
+2
View File
@@ -7,6 +7,8 @@ export enum Mode {
SELF_HOSTED = 'self-hosted'
}
export const growthEndpoint = import.meta.env.VITE_APPWRITE_GROWTH_ENDPOINT;
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
+11 -4
View File
@@ -10,9 +10,11 @@
export let autofocus = false;
export let disabled = false;
export let readonly = false;
export let required = false;
let value = '';
let element: HTMLInputElement;
let hiddenEl: HTMLInputElement;
let error: string;
onMount(() => {
@@ -54,11 +56,11 @@
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
if (hiddenEl.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
error = hiddenEl.validationMessage;
};
$: if (value) {
@@ -67,6 +69,12 @@
</script>
<FormItem>
<input
class="u-hide"
bind:this={hiddenEl}
value={tags.join(',')}
{required}
on:invalid={handleInvalid} />
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<div class="input-text-wrapper">
<div class="tags-input">
@@ -98,8 +106,7 @@
bind:value
bind:this={element}
on:keydown={handleInput}
on:blur={addValue}
on:invalid={handleInvalid} />
on:blur={addValue} />
</div>
</div>
{#if error}
+2
View File
@@ -1,9 +1,11 @@
<script lang="ts">
export let width: number = null;
export let onlyDesktop = false;
</script>
<div
style={width ? `--p-col-width:${width?.toString()}` : ''}
class:is-only-desktop={onlyDesktop}
class="table-thead-col"
role="columnheader">
<span class="eyebrow-heading-3">
+7 -1
View File
@@ -2,8 +2,14 @@
import { Trim } from '$lib/components';
export let title: string;
export let showOverflow = false;
export let onlyDesktop = false;
</script>
<div class="table-col " class:u-overflow-visible={showOverflow} data-title={title} role="cell">
<div
class="table-col"
class:is-only-desktop={onlyDesktop}
class:u-overflow-visible={showOverflow}
data-title={title}
role="cell">
<Trim><slot /></Trim>
</div>
+1 -3
View File
@@ -1,12 +1,10 @@
<script lang="ts">
export let noMargin = false;
export let noStyles = false;
export let noMobile = false;
</script>
<div
class="table is-vertical"
class:is-vertical={!noMobile}
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
role="table">
+6 -2
View File
@@ -1,7 +1,11 @@
<script lang="ts">
export let isSticky = false;
</script>
<div class="table-with-scroll u-margin-block-start-32">
<div class="table-wrapper">
<div class="table is-sticky-scroll" role="table">
<table class="table" class:is-sticky-scroll={isSticky}>
<slot />
</div>
</table>
</div>
</div>
+36
View File
@@ -0,0 +1,36 @@
async function securedCopy(value: string) {
try {
await navigator.clipboard.writeText(value);
} catch {
return false;
}
return true;
}
function unsecuredCopy(value: string) {
const textArea = document.createElement('textarea');
textArea.value = value;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let success = true;
try {
document.execCommand('copy');
} catch {
success = false;
} finally {
document.body.removeChild(textArea);
}
return success;
}
export async function copy(value: string) {
// securedCopy works only in HTTPS environment.
// unsecuredCopy works in HTTP and only runs if securedCopy fails.
const success = (await securedCopy(value)) || unsecuredCopy(value);
return success;
}
+9 -9
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import { EmptySearch, Pagination, Trim } from '$lib/components';
import {
Table,
TableBody,
TableHeader,
TableRow,
TableCellHead,
TableCell,
TableCellText
TableCellText,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
@@ -26,13 +26,13 @@
<Container>
{#if logs.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead>Client</TableCellHead>
<TableCellHead>Event</TableCellHead>
<TableCellHead>Location</TableCellHead>
<TableCellHead>IP</TableCellHead>
<TableCellHead>Date</TableCellHead>
<TableCellHead width={150}>Client</TableCellHead>
<TableCellHead width={100}>Event</TableCellHead>
<TableCellHead width={80}>Location</TableCellHead>
<TableCellHead width={90}>IP</TableCellHead>
<TableCellHead width={140}>Date</TableCellHead>
</TableHeader>
<TableBody>
{#each logs.logs as log}
@@ -75,7 +75,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {logs.total}</p>
<Pagination limit={PAGE_LIMIT} {path} {offset} sum={logs.total} />
+10 -6
View File
@@ -1,9 +1,13 @@
<script lang="ts">
import AppwriteLogo from '$lib/images/appwrite.svg';
import LoginLight from '$lib/images/login/login-light-mode.svg';
import LoginDark from '$lib/images/login/login-dark-mode.svg';
import { app } from '$lib/stores/app';
import { base } from '$app/paths';
import { user } from '$lib/stores/user';
import LoginLight from '$lib/images/login/login-light-mode.svg';
import LoginDark from '$lib/images/login/login-dark-mode.svg';
export let imgLight = LoginLight;
export let imgDark = LoginDark;
const technologies = [
'js',
@@ -25,7 +29,7 @@
<div
class="container u-margin-block-start-20"
style="--p-container-max-size: var(--container-size-medium);">
<a href="/">
<a href={user ? '/console' : '/'}>
<img src={AppwriteLogo} width="196" height="47" class="u-block" alt="Appwrite" />
</a>
</div>
@@ -34,12 +38,12 @@
<div class="u-margin-block-start-auto is-no-mobile" />
<div
class="container u-margin-block-start-20 is-no-mobile"
class="container u-margin-block-start-20 is-no-mobile u-flex u-main-center"
style="--p-container-max-size: var(--container-size-large);">
{#if $app.themeInUse === 'dark'}
<img src={LoginDark} alt="" class="u-only-dark" />
<img src={imgDark} alt="" class="u-only-dark" />
{:else}
<img src={LoginLight} alt="" class="u-only-light" />
<img src={imgLight} alt="" class="u-only-light" />
{/if}
</div>
+6 -1
View File
@@ -1,4 +1,5 @@
import { browser } from '$app/environment';
import { growthEndpoint } from '$lib/constants';
import { writable } from 'svelte/store';
export type AppStore = {
@@ -18,6 +19,10 @@ export const app = writable<AppStore>({
themeInUse: 'light'
});
//TODO: remove cloud always set to true
export const isCloud =
(import.meta.env.VITE_CONSOLE_MODE?.toString() ?? 'cloud') === 'cloud' ? true : false;
function createFeedbackStore() {
const { subscribe, update } = writable<Feedback>({
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
@@ -59,7 +64,7 @@ function createFeedbackStore() {
email?: string,
value?: number
) => {
const response = await fetch('https://growth.appwrite.io/v1/feedback', {
const response = await fetch(`${growthEndpoint}/feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+10
View File
@@ -54,6 +54,11 @@ function createServices() {
label: 'Users',
method: 'users',
value: null
},
{
label: 'GraphQL',
method: 'graphql',
value: null
}
]
});
@@ -107,6 +112,11 @@ function createServices() {
label: 'Users',
method: 'users',
value: project.serviceStatusForUsers
},
{
label: 'GraphQL',
method: 'graphql',
value: project.serviceStatusForGraphql
}
];
set({ list });
+1 -1
View File
@@ -51,4 +51,4 @@ const sdkForProject = {
users: new Users(clientProject)
};
export { sdkForConsole, sdkForProject, setProject };
export { sdkForConsole, sdkForProject, setProject, endpoint };
+10 -2
View File
@@ -49,13 +49,21 @@
if (!$page.url.pathname.startsWith('/auth')) {
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
if ($user) {
if (!$page.url.pathname.startsWith('/console')) {
if (
!$page.url.pathname.startsWith('/console') &&
!$page.url.pathname.startsWith('/invite')
) {
console.log('test');
await goto(`${base}/console`, {
replaceState: true
});
}
loading.set(false);
} else {
if (acceptedRoutes.includes($page.url.pathname)) {
if (
acceptedRoutes.includes($page.url.pathname) ||
$page.url.pathname.startsWith('/register/invite')
) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
} else {
await goto(`${base}/login`, {
+4 -1
View File
@@ -27,7 +27,10 @@ export const load: LayoutLoad = async ({ depends, url }) => {
'/auth/oauth2/failure'
];
if (!acceptedRoutes.includes(url.pathname)) {
if (
!acceptedRoutes.includes(url.pathname) &&
!url.pathname.startsWith('/register/invite')
) {
throw redirect(303, '/login');
}
}
@@ -1,15 +1,15 @@
<script lang="ts">
import { Empty, Heading } from '$lib/components';
import { Empty, Heading, Trim } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import {
Table,
TableBody,
TableHeader,
TableRow,
TableCellHead,
TableCell,
TableCellText
TableCellText,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
@@ -52,40 +52,39 @@
</div>
{#if data.sessions.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead>Client</TableCellHead>
<TableCellHead width={100}>Location</TableCellHead>
<TableCellHead width={150}>IP</TableCellHead>
<TableCellHead width={100} />
<TableCellHead width={250}>Client</TableCellHead>
<TableCellHead width={70}>Location</TableCellHead>
<TableCellHead width={90}>IP</TableCellHead>
<TableCellHead width={60} />
</TableHeader>
<TableBody>
{#each data.sessions.sessions as session}
<TableRow>
<TableCell title="Client">
<div class="u-flex u-gap-12 u-cross-center">
<div class="u-flex u-cross-center u-gap-12">
{#if session.clientName}
<div class="avatar is-small">
<img
height="20"
width="20"
src={getBrowser(session.clientCode).toString()}
alt={session.clientName} />
</div>
<p class="text u-trim">
{session.clientName}
{session.clientVersion}
on {session.osName}
{session.osVersion}
</p>
{:else}
<span class="avatar is-color-empty" />
<p class="text u-trim">Unknown</p>
{/if}
{#if session.clientName}
<div class="avatar is-small">
<img
height="20"
width="20"
src={getBrowser(session.clientCode).toString()}
alt={session.clientName} />
</div>
<Trim>
{session.clientName}
{session.clientVersion}
on {session.osName}
{session.osVersion}
</Trim>
{:else}
<span class="avatar is-color-empty" />
<p class="text u-trim">Unknown</p>
{/if}
<div class="is-only-desktop">
<Pill>{session.provider}</Pill>
</div>
<Pill>{session.provider}</Pill>
{#if session.current}
<Pill success>current session</Pill>
{/if}
@@ -105,7 +104,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
{:else}
<Empty>
<div class="u-flex u-flex-vertical u-cross-center">
@@ -1,12 +1,12 @@
<script lang="ts">
import {
Table,
TableHeader,
TableBody,
TableCellHead,
TableCell,
TableCellText,
TableRow
TableRow,
TableScroll
} from '$lib/elements/table';
import { AvatarInitials, Heading, Pagination } from '$lib/components';
import { Pill } from '$lib/elements';
@@ -64,11 +64,11 @@
</Button>
</div>
<Table>
<TableScroll>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Email</TableCellHead>
<TableCellHead width={100}>Role</TableCellHead>
<TableCellHead width={140}>Name</TableCellHead>
<TableCellHead width={120}>Email</TableCellHead>
<TableCellHead width={90}>Role</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -112,7 +112,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {data.organizationMembers.total}</p>
<Pagination
@@ -47,10 +47,10 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Identifiers</TableCellHead>
<TableCellHead width={130}>Status</TableCellHead>
<TableCellHead width={100}>ID</TableCellHead>
<TableCellHead>Joined</TableCellHead>
<TableCellHead onlyDesktop>Identifiers</TableCellHead>
<TableCellHead onlyDesktop width={130}>Status</TableCellHead>
<TableCellHead onlyDesktop width={100}>ID</TableCellHead>
<TableCellHead onlyDesktop>Joined</TableCellHead>
</TableHeader>
<TableBody>
{#each data.users.users as user}
@@ -75,12 +75,12 @@
{/if}
</div>
</TableCell>
<TableCellText title="Identifiers">
<TableCellText onlyDesktop title="Identifiers">
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
</TableCellText>
<TableCell title="Status">
<TableCell onlyDesktop title="Status">
{#if user.status}
<Pill success={user.emailVerification || user.phoneVerification}>
{user.emailVerification && user.phoneVerification
@@ -95,7 +95,7 @@
<Pill danger>blocked</Pill>
{/if}
</TableCell>
<TableCell showOverflow title="ID">
<TableCell onlyDesktop showOverflow title="ID">
<Copy value={user.$id} event="user">
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
@@ -103,7 +103,7 @@
</Pill>
</Copy>
</TableCell>
<TableCellText title="Joined">
<TableCellText onlyDesktop title="Joined">
{toLocaleDateTime(user.registration)}
</TableCellText>
</TableRowLink>
@@ -1,7 +1,7 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Pill } from '$lib/elements/';
import { Button, InputNumber, InputSelect } from '$lib/elements/forms';
import { Button, InputNumber, InputSelect, Form } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { project } from '../../store';
import { addNotification } from '$lib/stores/notifications';
@@ -20,6 +20,7 @@
let timeInSec = time;
let period = writable('s');
let btnActive = false;
let maxSessions = $project.authSessionsLimit;
let options = [
{ label: 'days', value: 'd' },
@@ -81,6 +82,22 @@
});
}
}
async function updateSessionsLimit() {
try {
await sdkForConsole.projects.updateAuthSessionsLimit(projectId, maxSessions);
addNotification({
type: 'success',
message: 'Sessions limit has been updated'
});
trackEvent('submit_sessions_limit_update');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
period.subscribe((p) => {
if (p === 'd') {
@@ -198,4 +215,20 @@
</Button>
</svelte:fragment>
</CardGrid>
<Form on:submit={updateSessionsLimit}>
<CardGrid>
<Heading tag="h2" size="6">Session Limit</Heading>
<p>Maximum number of active sessions allowed per user.</p>
<svelte:fragment slot="aside">
<ul>
<InputNumber id="max-session" label="Limit" bind:value={maxSessions} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={maxSessions === $project.authSessionsLimit} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
</Container>
@@ -40,21 +40,22 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Members</TableCellHead>
<TableCellHead>Created</TableCellHead>
<TableCellHead onlyDesktop>Members</TableCellHead>
<TableCellHead onlyDesktop>Created</TableCellHead>
</TableHeader>
<TableBody>
{#each data.teams.teams as team}
<TableRowLink
href={`${base}/console/project-${project}/auth/teams/team-${team.$id}`}>
<TableCell title="ID">
<TableCell title="Name">
<div class="u-flex u-gap-12">
<AvatarInitials size={32} name={team.name} />
<span class="text u-trim">{team.name}</span>
</div>
</TableCell>
<TableCellText title="Members">{team.total} members</TableCellText>
<TableCellText title="Members">
<TableCellText onlyDesktop title="Members"
>{team.total} members</TableCellText>
<TableCellText onlyDesktop title="Created">
{toLocaleDateTime(team.$createdAt)}
</TableCellText>
</TableRowLink>
@@ -46,8 +46,8 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Roles</TableCellHead>
<TableCellHead>Joined</TableCellHead>
<TableCellHead onlyDesktop>Roles</TableCellHead>
<TableCellHead onlyDesktop>Joined</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -60,8 +60,8 @@
<span>{membership.userName ? membership.userName : 'n/a'}</span>
</div>
</TableCellText>
<TableCellText title="Roles">{membership.roles}</TableCellText>
<TableCellText title="Joined">
<TableCellText onlyDesktop title="Roles">{membership.roles}</TableCellText>
<TableCellText onlyDesktop title="Joined">
{toLocaleDateTime(membership.joined)}
</TableCellText>
<TableCell>
@@ -32,8 +32,8 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Roles</TableCellHead>
<TableCellHead>Joined</TableCellHead>
<TableCellHead onlyDesktop>Roles</TableCellHead>
<TableCellHead onlyDesktop>Joined</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -46,8 +46,8 @@
<span>{membership.teamName ? membership.teamName : 'n/a'}</span>
</div>
</TableCellText>
<TableCellText title="Roles">{membership.roles}</TableCellText>
<TableCellText title="Joined">
<TableCellText onlyDesktop title="Roles">{membership.roles}</TableCellText>
<TableCellText onlyDesktop title="Joined">
{toLocaleDateTime(membership.joined)}
</TableCellText>
<TableCell width={30}>
@@ -35,7 +35,7 @@
</div>
<Table>
<TableHeader>
<TableCellHead>Browser and Device</TableCellHead>
<TableCellHead width={140}>Browser and Device</TableCellHead>
<TableCellHead width={140}>Session</TableCellHead>
<TableCellHead width={140}>Location</TableCellHead>
<TableCellHead width={140}>IP</TableCellHead>
@@ -53,10 +53,10 @@
async function updateStatus() {
try {
await sdkForProject.users.updateStatus($user.$id, !$user.status);
invalidate(Dependencies.USER);
await invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.status ? 'blocked' : 'unblocked'
$user.status ? 'unblocked' : 'blocked'
}`,
type: 'success'
});
@@ -50,7 +50,7 @@
{#if $collection?.attributes?.length}
{#if data.documents.total}
<TableScroll>
<TableScroll isSticky>
<TableHeader>
<TableCellHead>Document ID</TableCellHead>
{#each columns as column}
@@ -40,8 +40,8 @@
<Table>
<TableHeader>
<TableCellHead>Key</TableCellHead>
<TableCellHead>Type</TableCellHead>
<TableCellHead>Default Value</TableCellHead>
<TableCellHead onlyDesktop>Type</TableCellHead>
<TableCellHead onlyDesktop>Default Value</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -63,9 +63,11 @@
{/if}
</div>
</TableCell>
<TableCellText title="Type">{attribute.type}</TableCellText>
<TableCellText title="Default Value">
{attribute.default ? attribute.default : '-'}
<TableCellText onlyDesktop title="Type">
{`${attribute.type}${attribute.array ? '[]' : ''}`}
</TableCellText>
<TableCellText onlyDesktop title="Default Value">
{attribute.default !== null ? attribute.default : '-'}
</TableCellText>
<TableCell showOverflow>
<DropList
@@ -86,12 +88,14 @@
on:click={() => {
selectedAttribute = attribute;
showOverview = true;
showDropdown[index] = false;
}}>Overview</DropListItem>
<DropListItem
icon="plus"
on:click={() => {
selectedAttribute = attribute;
showCreateIndex = true;
showDropdown[index] = false;
}}>Create Index</DropListItem>
<DropListItem
icon="trash"
@@ -40,6 +40,7 @@
default: data.default
} = selectedAttribute);
}
$: if (data.required || data.array) {
data.default = null;
}
@@ -50,7 +51,8 @@
label="Elements"
bind:tags={data.elements}
placeholder="Add elements here"
readonly={!!selectedAttribute} />
readonly={!!selectedAttribute}
required />
<InputSelect
id="default"
label="Default value"
@@ -15,7 +15,7 @@
data.required,
data.min,
data.max,
data.default ? data.default : undefined,
typeof data.default === 'number' ? data.default : undefined,
data.array
);
}
@@ -15,7 +15,7 @@
data.required,
data.min,
data.max,
data.default ? data.default : undefined,
typeof data.default === 'number' ? data.default : undefined,
data.array
);
}
@@ -50,9 +50,9 @@
<Table>
<TableHeader>
<TableCellHead>Key</TableCellHead>
<TableCellHead>Type</TableCellHead>
<TableCellHead>Attributes</TableCellHead>
<TableCellHead>Asc/Desc</TableCellHead>
<TableCellHead onlyDesktop>Type</TableCellHead>
<TableCellHead onlyDesktop>Attributes</TableCellHead>
<TableCellHead onlyDesktop>Asc/Desc</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -72,11 +72,11 @@
{/if}
</div>
</TableCell>
<TableCellText title="Type">{index.type}</TableCellText>
<TableCellText title="Attributes">
<TableCellText title="Type" onlyDesktop>{index.type}</TableCellText>
<TableCellText title="Attributes" onlyDesktop>
{index.attributes}
</TableCellText>
<TableCellText title="ASC/DESC">
<TableCellText title="ASC/DESC" onlyDesktop>
{index.orders}
</TableCellText>
<TableCell showOverflow>
@@ -1,17 +1,17 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, InputText, FormList, InputSelect } from '$lib/elements/forms';
import { collection } from '../store';
import { onMount } from 'svelte';
import { sdkForProject } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import type { Attributes } from '../store';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import Select from './select.svelte';
import { trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputSelect, InputText } from '$lib/elements/forms';
import { remove } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import type { Attributes } from '../store';
import { collection } from '../store';
import Select from './select.svelte';
export let showCreateIndex = false;
export let externalAttribute: Attributes = null;
@@ -32,26 +32,31 @@
label: attribute.key
}));
let attributeList = [{ value: '', order: '' }];
let creating = false;
$: addAttributeDisabled = !attributeList.at(-1)?.value || !attributeList.at(-1)?.order;
onMount(() => {
if (!externalAttribute) return;
attributeList = [{ value: externalAttribute.key, order: 'ASC' }];
});
$: if (showCreateIndex) {
attributeList = [{ value: '', order: '' }];
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC' }]
: [{ value: '', order: '' }];
selectedType = 'key';
key = null;
}
$: if (showCreateIndex) {
initialize();
}
$: addAttributeDisabled =
!attributeList.at(-1)?.value || !attributeList.at(-1)?.order || creating;
async function create() {
if (!(key && selectedType && !addAttributeDisabled)) {
error = 'All fields are required';
return;
}
creating = true;
try {
await sdkForProject.databases.createIndex(
databaseId,
@@ -61,7 +66,15 @@
attributeList.map((a) => a.value),
attributeList.map((a) => a.order)
);
invalidate(Dependencies.COLLECTION);
await Promise.allSettled([
invalidate(Dependencies.COLLECTION),
invalidate(Dependencies.DATABASE)
]);
goto(
`${base}/console/project-${$page.params.project}/databases/database-${databaseId}/collection-${$collection.$id}/indexes`
);
addNotification({
message: 'Index has been created',
type: 'success'
@@ -74,6 +87,7 @@
});
} finally {
showCreateIndex = false;
creating = false;
}
}
@@ -140,6 +154,6 @@
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreateIndex = false)}>Cancel</Button>
<Button submit>Create</Button>
<Button submit disabled={creating}>Create</Button>
</svelte:fragment>
</Modal>
@@ -1,263 +1,19 @@
<script lang="ts">
import { CardGrid, Box, Heading } from '$lib/components';
import { Container } from '$lib/layout';
import { Button, InputText, InputSwitch, Helper } from '$lib/elements/forms';
import { Permissions } from '$lib/components/permissions';
import { collection } from '../store';
import { toLocaleDateTime } from '$lib/helpers/date';
import { sdkForProject } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { symmetricDifference } from '$lib/helpers/array';
import Delete from './deleteCollection.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import FormList from '$lib/elements/forms/formList.svelte';
import { trackEvent } from '$lib/actions/analytics';
const databaseId = $page.params.database;
let showDelete = false;
let showError: false | 'name' | 'size' = false,
errorMessage = 'Something went wrong',
errorType: 'error' | 'warning' | 'success' = 'error';
let enabled: boolean = null,
collectionName: string = null,
collectionDocumentSecurity: boolean = null,
collectionPermissions: string[] = null,
arePermsDisabled = true;
onMount(() => {
enabled ??= $collection.enabled;
collectionName ??= $collection.name;
collectionPermissions ??= $collection.$permissions;
collectionDocumentSecurity ??= $collection.documentSecurity;
});
$: if (
collectionPermissions &&
symmetricDifference(collectionPermissions, $collection.$permissions).length
) {
arePermsDisabled = false;
} else arePermsDisabled = true;
function addError(location: typeof showError, message: string, type: typeof errorType) {
showError = location;
errorMessage = message;
errorType = type;
}
async function toggleCollection() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
$collection.documentSecurity,
enabled
);
invalidate(Dependencies.COLLECTION);
addNotification({
message: `${$collection.name} has been updated`,
type: 'success'
});
trackEvent('submit_collection_update_enabled');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateName() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
collectionName,
$collection.$permissions,
$collection.documentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
showError = false;
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent('submit_collection_update_name');
} catch (error) {
addError('name', error.message, 'error');
}
}
async function updatePermissions() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
collectionPermissions,
$collection.documentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
arePermsDisabled = true;
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent('submit_collection_update_permissions');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateSecurity() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
collectionDocumentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
arePermsDisabled = true;
addNotification({
message: 'Security has been updated',
type: 'success'
});
trackEvent('submit_collection_update_security');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
import DangerZone from './dangerZone.svelte';
import UpdateName from './updateName.svelte';
import UpdatePermissions from './updatePermissions.svelte';
import UpdateSecurity from './updateSecurity.svelte';
import UpdateStatus from './updateStatus.svelte';
</script>
<Container>
{#if $collection}
<CardGrid>
<Heading tag="h2" size="7">{$collection.name}</Heading>
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="toggle"
label={enabled ? 'Enabled' : 'Disabled'}
bind:value={enabled} />
</ul>
<div>
<p>Created: {toLocaleDateTime($collection.$createdAt)}</p>
<p>Last Updated: {toLocaleDateTime($collection.$updatedAt)}</p>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={enabled === $collection.enabled} on:click={toggleCollection}>
Update
</Button>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Update Name</Heading>
<svelte:fragment slot="aside">
<ul>
<InputText
id="name"
label="Name"
placeholder="Enter name"
autocomplete={false}
bind:value={collectionName} />
{#if showError === 'name'}
<Helper type={errorType}>{errorMessage}</Helper>
{/if}
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={collectionName === $collection.name || !collectionName}
on:click={updateName}>Update</Button>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Update Permissions</Heading>
<p class="text">
Choose who can access your collection and documents. For more information, check out
the <a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Permissions Guide</a> in our documentation.
</p>
<svelte:fragment slot="aside">
{#if collectionPermissions}
<Permissions bind:permissions={collectionPermissions} withCreate />
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} on:click={updatePermissions}>Update</Button>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Update Document Security</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
bind:value={collectionDocumentSecurity}
id="security"
label="Document Security" />
</FormList>
<p class="text">
When document security is enabled, users will be able to access documents for
which they have been granted <b>either Document or Collection permissions</b>.
</p>
<p class="text">
If document security is disabled, users can access documents <b
>only if they have Collection permissions</b
>. Document permissions will be ignored.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={collectionDocumentSecurity === $collection.documentSecurity}
on:click={updateSecurity}>Update</Button>
</svelte:fragment>
</CardGrid>
<CardGrid danger>
<Heading tag="h6" size="7">Delete Collection</Heading>
<p>
The collection will be permanently deleted, including all the documents within it.
This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$collection.name}</h6>
</svelte:fragment>
<p>Last Updated: {toLocaleDateTime($collection.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
<UpdateStatus />
<UpdateName />
<UpdatePermissions />
<UpdateSecurity />
<DangerZone />
{/if}
</Container>
<Delete bind:showDelete />
@@ -0,0 +1,31 @@
<script lang="ts">
import { Box, CardGrid, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { collection } from '../store';
import Delete from './deleteCollection.svelte';
let showDelete = false;
</script>
<CardGrid danger>
<Heading tag="h6" size="7">Delete Collection</Heading>
<p>
The collection will be permanently deleted, including all the documents within it. This
action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$collection.name}</h6>
</svelte:fragment>
<p>Last Updated: {toLocaleDateTime($collection.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
<Delete bind:showDelete />
@@ -0,0 +1,66 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = $page.params.database;
let collectionName: string = null;
onMount(() => {
collectionName ??= $collection.name;
});
async function updateName() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
collectionName,
$collection.$permissions,
$collection.documentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent('submit_collection_update_name');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Form on:submit={updateName}>
<CardGrid>
<Heading tag="h6" size="7">Update Name</Heading>
<svelte:fragment slot="aside">
<ul>
<InputText
id="name"
label="Name"
placeholder="Enter name"
autocomplete={false}
bind:value={collectionName} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={collectionName === $collection.name || !collectionName} submit
>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,75 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Permissions } from '$lib/components/permissions';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = $page.params.database;
let arePermsDisabled = true;
let collectionPermissions: string[] = null;
onMount(() => {
collectionPermissions ??= $collection.$permissions;
});
async function updatePermissions() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
collectionPermissions,
$collection.documentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
arePermsDisabled = true;
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent('submit_collection_update_permissions');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
$: if (
collectionPermissions &&
symmetricDifference(collectionPermissions, $collection.$permissions).length
) {
arePermsDisabled = false;
} else arePermsDisabled = true;
</script>
<CardGrid>
<Heading tag="h6" size="7">Update Permissions</Heading>
<p class="text">
Choose who can access your collection and documents. For more information, check out the <a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Permissions Guide</a> in our documentation.
</p>
<svelte:fragment slot="aside">
{#if collectionPermissions}
<Permissions bind:permissions={collectionPermissions} withCreate />
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} on:click={updatePermissions}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,70 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, FormList, InputSwitch } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = $page.params.database;
let collectionDocumentSecurity: boolean = null;
onMount(() => {
collectionDocumentSecurity ??= $collection.documentSecurity;
});
async function updateSecurity() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
collectionDocumentSecurity,
$collection.enabled
);
invalidate(Dependencies.COLLECTION);
addNotification({
message: 'Security has been updated',
type: 'success'
});
trackEvent('submit_collection_update_security');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<CardGrid>
<Heading tag="h6" size="7">Update Document Security</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
bind:value={collectionDocumentSecurity}
id="security"
label="Document Security" />
</FormList>
<p class="text">
When document security is enabled, users will be able to access documents for which they
have been granted <b>either Document or Collection permissions</b>.
</p>
<p class="text">
If document security is disabled, users can access documents <b
>only if they have Collection permissions</b
>. Document permissions will be ignored.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={collectionDocumentSecurity === $collection.documentSecurity}
on:click={updateSecurity}>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,68 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, InputSwitch } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { collection } from '../store';
const databaseId = $page.params.database;
let enabled: boolean = null;
onMount(() => {
enabled ??= $collection.enabled;
});
async function toggleCollection() {
try {
await sdkForProject.databases.updateCollection(
databaseId,
$collection.$id,
$collection.name,
$collection.$permissions,
$collection.documentSecurity,
enabled
);
invalidate(Dependencies.COLLECTION);
addNotification({
message: `${$collection.name} has been updated`,
type: 'success'
});
trackEvent('submit_collection_update_enabled');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<CardGrid>
<Heading tag="h2" size="7">{$collection.name}</Heading>
<svelte:fragment slot="aside">
<ul>
<InputSwitch
id="toggle"
label={enabled ? 'Enabled' : 'Disabled'}
bind:value={enabled} />
</ul>
<div>
<p>Created: {toLocaleDateTime($collection.$createdAt)}</p>
<p>Last Updated: {toLocaleDateTime($collection.$updatedAt)}</p>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={enabled === $collection.enabled} on:click={toggleCollection}>
Update
</Button>
</svelte:fragment>
</CardGrid>
@@ -97,7 +97,7 @@
files = entrypoint = active = null;
showCreate = false;
dispatch('created');
trackEvent('submit_function_create');
trackEvent('submit_deployment_create');
} catch (error) {
addNotification({
type: 'error',
@@ -35,9 +35,9 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>last accessed</TableCellHead>
<TableCellHead>expiration date</TableCellHead>
<TableCellHead>scopes</TableCellHead>
<TableCellHead onlyDesktop>last accessed</TableCellHead>
<TableCellHead onlyDesktop>expiration date</TableCellHead>
<TableCellHead onlyDesktop>scopes</TableCellHead>
</TableHeader>
<TableBody>
{#each data.keys.keys as key}
@@ -45,13 +45,13 @@
<TableCellText title="Name">
{key.name}
</TableCellText>
<TableCellText title="Last Accessed">
<TableCellText onlyDesktop title="Last Accessed">
{key.accessedAt ? toLocaleDateTime(key.accessedAt) : 'never'}
</TableCellText>
<TableCellText title="Expiration Date">
<TableCellText onlyDesktop title="Expiration Date">
{key.expire ? toLocaleDateTime(key.expire) : 'never'}
</TableCellText>
<TableCellText title="Expiration Date">
<TableCellText onlyDesktop title="Expiration Date">
{key.scopes.length} Scopes
</TableCellText>
</TableRowLink>
@@ -5,7 +5,7 @@ import { Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, depends }) => {
depends(Dependencies.PROJECT);
depends(Dependencies.PLATFORM);
return {
header: Header,
@@ -20,11 +20,8 @@
import { project } from '../../../store';
import { platform } from './store';
import { Dependencies } from '$lib/constants';
import type { PageData } from './$types';
import { invalidate } from '$app/navigation';
export let data: PageData;
const types: Record<string, typeof SvelteComponent> = {
web: Web,
android: Android,
@@ -41,20 +38,22 @@
let showDelete = false;
let name: string = null;
let updating = false;
onMount(() => {
name ??= data.platform.name;
name ??= $platform.name;
});
const updateName = async () => {
async function updateName() {
updating = true;
try {
await sdkForConsole.projects.updatePlatform(
$project.$id,
data.platform.$id,
$platform.$id,
name,
data.platform.key,
data.platform.store,
data.platform.hostname
$platform.key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
addNotification({
@@ -67,7 +66,13 @@
message: error.message
});
}
};
}
$: {
// When platform name is updated, finalize the updating flow
$platform.name;
updating = false;
}
</script>
<Container>
@@ -87,7 +92,7 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $platform.name} submit>Update</Button>
<Button disabled={name === $platform.name || updating} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -10,10 +10,10 @@
import { project } from '../../../store';
import { platform } from './store';
let hostname: string = null;
let key: string = null;
onMount(() => {
hostname ??= $platform.hostname;
key ??= $platform.key;
});
const updateHostname = async () => {
@@ -22,9 +22,9 @@
$project.$id,
$platform.$id,
$platform.name,
$platform.key,
key,
$platform.store,
hostname
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
@@ -52,16 +52,16 @@
<svelte:fragment slot="aside">
<FormList>
<InputText
id="hostname"
id="key"
label="Package Name"
bind:value={hostname}
bind:value={key}
required
placeholder="com.company.appname" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={hostname === $platform.hostname} submit>Update</Button>
<Button disabled={key === $platform.hostname} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -31,9 +31,9 @@
projectId,
'android',
$createPlatform.name,
$createPlatform.key,
undefined,
undefined,
$createPlatform.hostname
undefined
);
$createPlatform.$id = platform.$id;
@@ -50,11 +50,11 @@
required
bind:value={$createPlatform.name} />
<InputText
id="hostname"
id="key"
label="Package Name"
placeholder="com.company.appname"
tooltip="Your package name is generally the applicationId in your app-level build.gradle file."
required
bind:value={$createPlatform.hostname} />
bind:value={$createPlatform.key} />
</FormList>
</WizardStep>
@@ -41,9 +41,9 @@
projectId,
platform,
$createPlatform.name,
$createPlatform.key,
undefined,
undefined,
$createPlatform.hostname
undefined
);
$createPlatform.$id = response.$id;
@@ -95,6 +95,6 @@
placeholder="com.company.appname"
tooltip="You can find your Bundle Identifier in the General tab for your app's primary target in Xcode."
required
bind:value={$createPlatform.hostname} />
bind:value={$createPlatform.key} />
</FormList>
</WizardStep>
@@ -25,7 +25,7 @@
let platform: Platform = Platform.Android;
const projectId = $page.params.project;
const suggestions = ['*.vercel.app', '*.netlify.app', '*.gitpod.app'];
const suggestions = ['*.vercel.app', '*.netlify.app', '*.gitpod.io'];
const placeholder: Record<
Platform,
{
@@ -97,9 +97,9 @@
projectId,
platform,
$createPlatform.name,
platform !== Platform.Web ? $createPlatform.key : undefined,
undefined,
undefined,
$createPlatform.hostname
platform === Platform.Web ? $createPlatform.hostname : undefined
);
$createPlatform.$id = response.$id;
@@ -160,15 +160,16 @@
placeholder={placeholder[platform].name}
required
bind:value={$createPlatform.name} />
<div>
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
tooltip={placeholder[platform].tooltip}
required
bind:value={$createPlatform.hostname} />
{#if platform === Platform.Web}
{#if platform === Platform.Web}
<div>
<InputText
id="hostname"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
tooltip={placeholder[platform].tooltip}
required
bind:value={$createPlatform.hostname} />
<div class="u-flex u-gap-16 u-margin-block-start-8">
{#each suggestions as suggestion}
<Pill
@@ -179,7 +180,15 @@
</Pill>
{/each}
</div>
{/if}
</div>
</div>
{:else}
<InputText
id="key"
label={hostname[platform]}
placeholder={placeholder[platform].hostname}
tooltip={placeholder[platform].tooltip}
required
bind:value={$createPlatform.key} />
{/if}
</FormList>
</WizardStep>
@@ -12,7 +12,7 @@
import Dark from './dark.svg';
const projectId = $page.params.project;
const suggestions = ['*.vercel.app', '*.netlify.app', '*.gitpod.app'];
const suggestions = ['*.vercel.app', '*.netlify.app', '*.gitpod.io'];
$wizard.media = $app.themeInUse === 'dark' ? Dark : Light;
async function beforeSubmit() {
@@ -17,6 +17,7 @@
let name: string = null;
let showDelete = false;
let updating = false;
const endpoint = sdkForConsole.client.config.endpoint;
const projectId = $page.params.project;
@@ -24,7 +25,8 @@
name ??= $project.name;
});
const updateName = async () => {
async function updateName() {
updating = true;
try {
await sdkForConsole.projects.update($project.$id, name);
invalidate(Dependencies.PROJECT);
@@ -39,9 +41,15 @@
message: error.message
});
}
};
}
const serviceUpdate = async (service: Service) => {
$: {
// When project name is updated, finalize the updating flow
$project.name;
updating = false;
}
async function serviceUpdate(service: Service) {
try {
await sdkForConsole.projects.updateServiceStatus(
$project.$id,
@@ -65,7 +73,7 @@
message: error.message
});
}
};
}
$: services.load($project);
</script>
@@ -105,7 +113,7 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $project.name} submit>Update</Button>
<Button disabled={name === $project.name || updating} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -4,13 +4,13 @@
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
TableRow,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
@@ -68,14 +68,14 @@
</Button>
</div>
{#if data.domains.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead>Domain Name</TableCellHead>
<TableCellHead width={150}>Domain Name</TableCellHead>
<TableCellHead width={100} />
<TableCellHead width={60}>Type</TableCellHead>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
<TableCellHead />
<TableCellHead width={90}>Name</TableCellHead>
<TableCellHead width={90}>Value</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each data.domains.domains as domain}
@@ -131,7 +131,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
{:else}
<Empty
single
@@ -1,353 +1,18 @@
<script lang="ts">
import { onMount } from 'svelte';
import { CardGrid, Box, Secret, Empty, EventModal, Heading } from '$lib/components';
import {
Button,
Form,
FormList,
InputText,
InputChoice,
InputPassword
} from '$lib/elements/forms';
import { TableList, TableCellText, TableCell } from '$lib/elements/table';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { webhook } from './store';
import { page } from '$app/stores';
import { toLocaleDateTime } from '$lib/helpers/date';
import Delete from './delete.svelte';
import Regenerate from './regenerate.svelte';
import { symmetricDifference } from '$lib/helpers/array';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { writable, type Writable } from 'svelte/store';
import { trackEvent } from '$lib/actions/analytics';
const projectId = $page.params.project;
let name: string = null;
let url: string = null;
const eventSet: Writable<Set<string>> = writable(new Set());
let httpUser: string = null;
let httpPass: string = null;
let security = false;
let showDelete = false;
let showCreateEvent = false;
let areEventsDisabled = true;
let showRegenerate = false;
onMount(async () => {
name ??= $webhook.name;
url ??= $webhook.url;
$eventSet = new Set($webhook.events);
httpUser ??= $webhook.httpUser;
httpPass ??= $webhook.httpPass;
security = $webhook.security;
});
async function updateName() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
name,
$webhook.events,
$webhook.url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook name has been updated'
});
trackEvent('submit_webhook_update_name');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function updateUrl() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
$webhook.events,
url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook url has been updated'
});
trackEvent('submit_webhook_update_url');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function updateEvents() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
Array.from($eventSet),
$webhook.url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
areEventsDisabled = true;
addNotification({
type: 'success',
message: 'Webhook events have been updated'
});
trackEvent('submit_webhook_update_events');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function updateSecurity() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
$webhook.events,
$webhook.url,
security,
httpUser,
httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook security has been updated'
});
trackEvent('submit_webhook_update_security');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
function handleEvent(event: CustomEvent) {
eventSet.set($eventSet.add(event.detail));
}
$: if ($eventSet) {
if (symmetricDifference(Array.from($eventSet), $webhook.events).length) {
areEventsDisabled = false;
} else areEventsDisabled = true;
}
import DangerZone from './dangerZone.svelte';
import UpdateEvents from './updateEvents.svelte';
import UpdateName from './updateName.svelte';
import UpdateSecurity from './updateSecurity.svelte';
import UpdateSignature from './updateSignature.svelte';
import UpdateUrl from './updateURL.svelte';
</script>
<Container>
<CardGrid>
<Heading tag="h2" size="7">Signature Key</Heading>
<p>
Add the Signature Key to the X-Appwrite-Webhook-Signature header to validate your
webhooks. <a
href="https://appwrite.io/docs/webhooks#verification"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about webhook validation.</a>
</p>
<svelte:fragment slot="aside">
<div>
<p>Key</p>
<Secret bind:value={$webhook.signatureKey} />
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button on:click={() => (showRegenerate = true)} secondary submit>
Regenerate Key
</Button>
</svelte:fragment>
</CardGrid>
<Form on:submit={updateName}>
<CardGrid>
<Heading tag="h2" size="7">Update Name</Heading>
<p>Choose any name that will help you distinguish between Webhooks.</p>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="name"
label="Name"
bind:value={name}
required
placeholder="Enter name" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $webhook.name || !name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateUrl}>
<CardGrid>
<Heading tag="h2" size="7">Update Url</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="url"
label="POST URL"
bind:value={url}
required
placeholder="https://example.com/callback" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={url === $webhook.url || !url} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateEvents}>
<CardGrid>
<Heading tag="h6" size="7">Update Events</Heading>
<p class="text">
Set the events that will trigger your webhook. Maximum 100 events allowed.
</p>
<svelte:fragment slot="aside">
{#if $eventSet.size}
<TableList>
{#each Array.from($eventSet) as event}
<li class="table-row">
<TableCellText title="id">
{event}
</TableCellText>
<TableCell showOverflow title="options" width={40}>
<button
class="button is-text is-only-icon"
aria-label="delete id"
on:click|preventDefault={() => {
$eventSet.delete(event);
eventSet.set($eventSet);
}}>
<span class="icon-x" aria-hidden="true" />
</button>
</TableCell>
</li>
{/each}
</TableList>
<div class="u-flex u-margin-block-start-16">
<Button text noMargin on:click={() => (showCreateEvent = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="u-text">Add event</span>
</Button>
</div>
{:else}
<Empty on:click={() => (showCreateEvent = true)}>
Add an event to get started
</Empty>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={areEventsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateSecurity}>
<CardGrid>
<Heading tag="h2" size="7">Security</Heading>
<p class="text">
Set an optional basic HTTP authentication username and password to protect your
endpoint from unauthorized access.
</p>
<svelte:fragment slot="aside">
<FormList>
<div>
<Heading tag="h3" size="7">HTTP Authentication</Heading>
<p class="text">Use to secure your endpoint from untrusted sources.</p>
</div>
<InputText
label="User"
id="user"
placeholder="Enter username"
bind:value={httpUser} />
<InputPassword
label="Password"
id="password"
minlength={0}
showPasswordButton
placeholder="Enter password"
bind:value={httpPass} />
<InputChoice
id="Security"
label="Certificate verification (SSL/TLS)"
bind:value={security}>
<span class="u-error">Warning:</span> Untrusted or self-signed certificates
may not be secure.
<a
href="https://appwrite.io/docs/custom-domains#enjoySSLCert"
target="_blank"
rel="noopener noreferrer"
class="link">
Learn more</a>
</InputChoice>
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={httpUser === $webhook.httpUser &&
httpPass === $webhook.httpPass &&
security === $webhook.security}
submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid danger>
<div>
<Heading tag="h2" size="7">Delete Webhook</Heading>
</div>
<p>The webhook will be permanently deleted. This action is irreversible.</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$webhook.name}</h6>
</svelte:fragment>
<p>Last updated: {toLocaleDateTime($webhook.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
<UpdateSignature />
<UpdateName />
<UpdateUrl />
<UpdateEvents />
<UpdateSecurity />
<DangerZone />
</Container>
<Delete bind:showDelete />
<Regenerate bind:show={showRegenerate} />
<EventModal bind:show={showCreateEvent} on:created={handleEvent} />
@@ -0,0 +1,30 @@
<script lang="ts">
import { Box, CardGrid, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { webhook } from './store';
import Delete from './delete.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
let showDelete = false;
</script>
<CardGrid danger>
<div>
<Heading tag="h2" size="7">Delete Webhook</Heading>
</div>
<p>The webhook will be permanently deleted. This action is irreversible.</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$webhook.name}</h6>
</svelte:fragment>
<p>Last updated: {toLocaleDateTime($webhook.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
<Delete bind:showDelete />
@@ -0,0 +1,109 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Empty, EventModal, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { TableCell, TableCellText, TableList } from '$lib/elements/table';
import { symmetricDifference } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import { webhook } from './store';
const projectId = $page.params.project;
const eventSet: Writable<Set<string>> = writable(new Set());
let showCreateEvent = false;
let areEventsDisabled = true;
onMount(async () => {
$eventSet = new Set($webhook.events);
});
async function updateEvents() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
Array.from($eventSet),
$webhook.url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
areEventsDisabled = true;
addNotification({
type: 'success',
message: 'Webhook events have been updated'
});
trackEvent('submit_webhook_update_events');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
function handleEvent(event: CustomEvent) {
eventSet.set($eventSet.add(event.detail));
}
$: if ($eventSet) {
if (symmetricDifference(Array.from($eventSet), $webhook.events).length) {
areEventsDisabled = false;
} else areEventsDisabled = true;
}
</script>
<Form on:submit={updateEvents}>
<CardGrid>
<Heading tag="h6" size="7">Update Events</Heading>
<p class="text">
Set the events that will trigger your webhook. Maximum 100 events allowed.
</p>
<svelte:fragment slot="aside">
{#if $eventSet.size}
<TableList>
{#each Array.from($eventSet) as event}
<li class="table-row">
<TableCellText title="id">
{event}
</TableCellText>
<TableCell showOverflow title="options" width={40}>
<button
class="button is-text is-only-icon"
aria-label="delete id"
on:click|preventDefault={() => {
$eventSet.delete(event);
eventSet.set($eventSet);
}}>
<span class="icon-x" aria-hidden="true" />
</button>
</TableCell>
</li>
{/each}
</TableList>
<div class="u-flex u-margin-block-start-16">
<Button text noMargin on:click={() => (showCreateEvent = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="u-text">Add event</span>
</Button>
</div>
{:else}
<Empty on:click={() => (showCreateEvent = true)}>Add an event to get started</Empty>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={areEventsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<EventModal bind:show={showCreateEvent} on:created={handleEvent} />
@@ -0,0 +1,66 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { webhook } from './store';
const projectId = $page.params.project;
let name: string = null;
onMount(async () => {
name ??= $webhook.name;
});
async function updateName() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
name,
$webhook.events,
$webhook.url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook name has been updated'
});
trackEvent('submit_webhook_update_name');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<Form on:submit={updateName}>
<CardGrid>
<Heading tag="h2" size="7">Update Name</Heading>
<p>Choose any name that will help you distinguish between Webhooks.</p>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="name"
label="Name"
bind:value={name}
required
placeholder="Enter name" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $webhook.name || !name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,110 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import {
Button,
Form,
FormList,
InputChoice,
InputPassword,
InputText
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { webhook } from './store';
const projectId = $page.params.project;
let httpUser: string = null;
let httpPass: string = null;
let security = false;
onMount(async () => {
httpUser ??= $webhook.httpUser;
httpPass ??= $webhook.httpPass;
security = $webhook.security;
});
async function updateSecurity() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
$webhook.events,
$webhook.url,
security,
httpUser,
httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook security has been updated'
});
trackEvent('submit_webhook_update_security');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<Form on:submit={updateSecurity}>
<CardGrid>
<Heading tag="h2" size="7">Security</Heading>
<p class="text">
Set an optional basic HTTP authentication username and password to protect your endpoint
from unauthorized access.
</p>
<svelte:fragment slot="aside">
<FormList>
<div>
<Heading tag="h3" size="7">HTTP Authentication</Heading>
<p class="text">Use to secure your endpoint from untrusted sources.</p>
</div>
<InputText
label="User"
id="user"
placeholder="Enter username"
bind:value={httpUser} />
<InputPassword
label="Password"
id="password"
minlength={0}
showPasswordButton
placeholder="Enter password"
bind:value={httpPass} />
<InputChoice
id="Security"
label="Certificate verification (SSL/TLS)"
bind:value={security}>
<span class="u-error">Warning:</span> Untrusted or self-signed certificates may
not be secure.
<a
href="https://appwrite.io/docs/custom-domains#enjoySSLCert"
target="_blank"
rel="noopener noreferrer"
class="link">
Learn more</a>
</InputChoice>
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
disabled={httpUser === $webhook.httpUser &&
httpPass === $webhook.httpPass &&
security === $webhook.security}
submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,30 @@
<script lang="ts">
import { CardGrid, Heading, Secret } from '$lib/components';
import { Button } from '$lib/elements/forms';
import Regenerate from './regenerate.svelte';
import { webhook } from './store';
let showRegenerate = false;
</script>
<CardGrid>
<Heading tag="h2" size="7">Signature Key</Heading>
<p>
Add the Signature Key to the X-Appwrite-Webhook-Signature header to validate your webhooks. <a
href="https://appwrite.io/docs/webhooks#verification"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about webhook validation.</a>
</p>
<svelte:fragment slot="aside">
<div>
<p>Key</p>
<Secret bind:value={$webhook.signatureKey} />
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button on:click={() => (showRegenerate = true)} secondary submit>Regenerate Key</Button>
</svelte:fragment>
</CardGrid>
<Regenerate bind:show={showRegenerate} />
@@ -0,0 +1,66 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { webhook } from './store';
const projectId = $page.params.project;
let url: string = null;
onMount(async () => {
url ??= $webhook.url;
});
async function updateUrl() {
try {
await sdkForConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
$webhook.events,
url,
$webhook.security,
$webhook.httpUser,
$webhook.httpPass
);
invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook url has been updated'
});
trackEvent('submit_webhook_update_url');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<Form on:submit={updateUrl}>
<CardGrid>
<Heading tag="h2" size="7">Update Url</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="url"
label="POST URL"
bind:value={url}
required
placeholder="https://example.com/callback" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={url === $webhook.url || !url} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -86,9 +86,9 @@
<Table>
<TableHeader>
<TableCellHead>Filename</TableCellHead>
<TableCellHead width={140}>Type</TableCellHead>
<TableCellHead width={100}>Size</TableCellHead>
<TableCellHead width={120}>Created</TableCellHead>
<TableCellHead onlyDesktop width={140}>Type</TableCellHead>
<TableCellHead onlyDesktop width={100}>Size</TableCellHead>
<TableCellHead onlyDesktop width={120}>Created</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -99,17 +99,17 @@
<div class="u-flex u-gap-12 u-main-space-between">
<span class="avatar is-size-small is-color-empty" />
<span class="text u-trim"> {file.name}</span>
<span class="text u-trim">{file.name}</span>
<div>
<Pill warning>Pending</Pill>
</div>
</div>
</TableCell>
<TableCellText title="Type">{file.mimeType}</TableCellText>
<TableCellText title="Size">
<TableCellText onlyDesktop title="Type">{file.mimeType}</TableCellText>
<TableCellText onlyDesktop title="Size">
{calculateSize(file.sizeOriginal)}
</TableCellText>
<TableCellText title="Date Created">
<TableCellText onlyDesktop title="Date Created">
{toLocaleDate(file.$createdAt)}
</TableCellText>
<TableCell>
@@ -137,14 +137,14 @@
<TableCell title="Name">
<div class="u-flex u-gap-12">
<Avatar size={32} src={getPreview(file.$id)} name={file.name} />
<span class="text u-trim"> {file.name}</span>
<span class="text u-trim">{file.name}</span>
</div>
</TableCell>
<TableCellText title="Type">{file.mimeType}</TableCellText>
<TableCellText title="Size">
<TableCellText onlyDesktop title="Type">{file.mimeType}</TableCellText>
<TableCellText onlyDesktop title="Size">
{calculateSize(file.sizeOriginal)}
</TableCellText>
<TableCellText title="Date Created">
<TableCellText onlyDesktop title="Date Created">
{toLocaleDate(file.$createdAt)}
</TableCellText>
<TableCell showOverflow>
@@ -36,8 +36,9 @@
arePermsDisabled = true,
encryption: boolean = null,
antivirus: boolean = null,
maxSize: number;
let byteUnit = writable('KB');
maxSize: number,
compression: string = null;
let byteUnit = writable('MB');
let sizeInBytes: number = null;
const options = [
{ label: 'Bytes', value: 'Bytes' },
@@ -45,6 +46,11 @@
{ label: 'Megabytes', value: 'MB' },
{ label: 'Gigabytes', value: 'GB' }
];
const compressionOptions = [
{ label: 'None', value: 'none' },
{ label: 'Gzip', value: 'gzip' },
{ label: 'Zstd', value: 'zstd' }
];
let suggestedExtensions = ['jpg', 'png', 'svg', 'gif', 'html', 'pdf', 'mp4'];
let extensions = $bucket.allowedFileExtensions;
let isExtensionsDisabled = true;
@@ -57,8 +63,10 @@
bucketPermissions ??= $bucket.$permissions;
encryption ??= $bucket.encryption;
antivirus ??= $bucket.antivirus;
maxSize = $bucket.maximumFileSize / 1024;
maxSize = $bucket.maximumFileSize / 1024 / 1024;
compression ??= $bucket.compression;
});
$: if (bucketPermissions) {
if (symmetricDifference(bucketPermissions, $bucket.$permissions).length) {
arePermsDisabled = false;
@@ -192,6 +200,17 @@
);
}
function updateCompression() {
updateBucket(
{
compression
},
{
trackEventName: 'submit_bucket_update_size'
}
);
}
function updateAllowedExtensions() {
updateBucket(
{
@@ -234,8 +253,8 @@
id="toggle"
bind:value={enabled} />
</FormList>
<p>Created: {toLocaleDateTime($bucket.$createdAt)}</p>
<p>Last Updated: {toLocaleDateTime($bucket.$updatedAt)}</p>
<p class="text">Created: {toLocaleDateTime($bucket.$createdAt)}</p>
<p class="text">Last Updated: {toLocaleDateTime($bucket.$updatedAt)}</p>
</div>
</svelte:fragment>
@@ -320,7 +339,7 @@
<Form on:submit={updateSecurity}>
<CardGrid>
<Heading tag="h2" size="7">Update Security Settings</Heading>
<p>
<p class="text">
Enable or disable security services for the bucket including <b>Ecryption</b>
and <b>Antivirus scanning.</b>
</p>
@@ -381,17 +400,40 @@
<Button
disabled={encryption === $bucket.encryption &&
antivirus === $bucket.antivirus}
submit
>Update
submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateCompression}>
<CardGrid>
<Heading tag="h2" size="6">Update Compression Algorithm</Heading>
<p class="text">
Choose an algorithm for compression. For files larger than 20MB, compression
will be skipped even if it's enabled.
</p>
<svelte:fragment slot="aside">
<FormList>
<InputSelect
id="compression"
label="Compression algorithm"
options={compressionOptions}
bind:value={compression} />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={compression === $bucket.compression} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateMaxSize}>
<CardGrid>
<Heading tag="h2" size="6">Update Maximum File Size</Heading>
<p>Set the maximum file size allowed in the bucket.</p>
<p class="text">Set the maximum file size allowed in the bucket.</p>
<svelte:fragment slot="aside">
<ul class="u-flex u-gap-12">
<InputNumber
@@ -414,7 +456,7 @@
<Form on:submit={updateAllowedExtensions}>
<CardGrid>
<Heading tag="h6" size="7">Update Allowed File Extensions</Heading>
<p>
<p class="text">
A maximum of 100 file extensions can be added. Leave blank to allow all file
types.
</p>
@@ -453,7 +495,7 @@
<CardGrid danger>
<Heading tag="h6" size="7">Delete Bucket</Heading>
<p>
<p class="text">
The bucket will be permanently deleted, including all the files within it. This
action is irreversible.
</p>
@@ -462,7 +504,7 @@
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$bucket.name}</h6>
</svelte:fragment>
<p>Last Updated: {toLocaleDateTime($bucket.$updatedAt)}</p>
<p class="text">Last Updated: {toLocaleDateTime($bucket.$updatedAt)}</p>
</Box>
</svelte:fragment>
+6 -1
View File
@@ -16,6 +16,11 @@
import FormList from '$lib/elements/forms/formList.svelte';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
import LoginLight from '$lib/images/login/login-light-mode.svg';
import LoginDark from '$lib/images/login/login-dark-mode.svg';
let imgLight = LoginLight;
let imgDark = LoginDark;
let name: string, mail: string, pass: string, disabled: boolean;
let terms = false;
@@ -42,7 +47,7 @@
<title>Sign up - Appwrite</title>
</svelte:head>
<Unauthenticated>
<Unauthenticated {imgLight} {imgDark}>
<svelte:fragment slot="title">Sign up</svelte:fragment>
<svelte:fragment>
<Form on:submit={register}>
@@ -0,0 +1,147 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import {
Button,
Form,
FormItem,
InputChoice,
InputEmail,
InputPassword,
InputText
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { endpoint, sdkForConsole } from '$lib/stores/sdk';
import { Unauthenticated } from '$lib/layout';
import FormList from '$lib/elements/forms/formList.svelte';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { isCloud } from '$lib/stores/app';
import LoginLight from '$lib/images/login/login-light-mode.svg';
import LoginDark from '$lib/images/login/login-dark-mode.svg';
let slug = $page.params.slug;
let imgLight = LoginLight;
let imgDark = LoginDark;
onMount(async () => {
if (isCloud) {
switch (slug) {
case 'mlh':
imgDark = (await import('./mlh-dark.svg')).default;
imgLight = (await import('./mlh-light.svg')).default;
title = 'Welcome MLH Hackers!';
break;
case 'appwrite':
imgDark = (await import('$lib/images/appwrite.svg')).default;
imgLight = (await import('$lib/images/appwrite.svg')).default;
title = 'Welcome Appwriters!';
break;
}
}
});
let name: string, mail: string, pass: string, code: string, disabled: boolean;
let title = 'Sign up';
let terms = false;
async function invite() {
try {
disabled = true;
const res = await fetch(`${endpoint}/account/invite`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userId: 'unique()',
email: mail,
password: pass,
code: code,
name: name ?? ''
})
});
if (!res.ok) {
throw new Error((await res.json()).message);
} else {
await sdkForConsole.account.createEmailSession(mail, pass);
await sdkForConsole.account.updatePrefs({ code });
await invalidate(Dependencies.ACCOUNT);
await goto(`${base}/console`);
trackEvent('submit_account_create', { code: code });
}
} catch (error) {
disabled = false;
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<svelte:head>
<title>Sign up - Appwrite</title>
</svelte:head>
<Unauthenticated {imgLight} {imgDark}>
<svelte:fragment slot="title">{title}</svelte:fragment>
<svelte:fragment>
<Form on:submit={invite}>
<FormList>
<InputText
id="name"
label="Name"
placeholder="Your name"
autofocus={true}
bind:value={name} />
<InputEmail
id="email"
label="Email"
placeholder="Your email"
required={true}
bind:value={mail} />
<InputPassword
id="password"
label="Password"
placeholder="Your password"
required={true}
showPasswordButton={true}
bind:value={pass} />
<InputText
id="Code"
label="Code"
placeholder="Your code"
required={true}
bind:value={code} />
<InputChoice required value={terms} id="terms" label="terms" showLabel={false}>
By registering, you agree that you have read, understand, and acknowledge our <a
class="link"
href="https://appwrite.io/policy/privacy"
target="_blank"
rel="noopener noreferrer">
Privacy Policy</a>
and accept our
<a
class="link"
href="https://appwrite.io/policy/terms"
target="_blank"
rel="noopener noreferrer">General Terms of Use</a
>.</InputChoice>
<FormItem>
<Button fullWidth submit {disabled}>Sign up</Button>
</FormItem>
</FormList>
</Form>
</svelte:fragment>
<svelte:fragment slot="links">
<li class="inline-links-item">
<span class="text">
Already got an account? <a class="link" href={`${base}/login`}>Sign in</a>
</span>
</li>
</svelte:fragment>
</Unauthenticated>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB