Merge branch 'main' of github.com:appwrite/console into fix-table-update

This commit is contained in:
Arman
2023-01-10 15:47:08 +01:00
48 changed files with 1133 additions and 804 deletions
+1
View File
@@ -1,4 +1,5 @@
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_SENTRY_DSN=
VITE_CONSOLE_MODE=self-hosted
+7 -7
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"
@@ -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"
+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,
+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>
+28 -24
View File
@@ -3,11 +3,10 @@
import { Pill } from '$lib/elements';
import { sdkForProject } from '$lib/stores/sdk';
import { Avatar } from '$lib/components';
import { base } from '$app/paths';
import { page } from '$app/stores';
async function removeFile($id: string, bucketId: string) {
const file = await sdkForProject.storage.getFile(bucketId, $id);
uploader.removeFile(file);
}
let hovering = false;
const getPreview = (fileId: string, bucketId: string) =>
sdkForProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
@@ -28,7 +27,7 @@
<span class="icon-cheveron-up" aria-hidden="true" />
</button>
<button
on:click={() => uploader.close()}
on:click={() => uploader.reset()}
class="icon-button"
aria-label="close upload box">
<span class="icon-x" aria-hidden="true" />
@@ -37,8 +36,12 @@
<div class="upload-box-content" class:is-open={!$uploader.isCollapsed}>
<ul class="upload-box-list">
{#each $uploader.files as file}
{@const progress = Math.round(file.progress)}
{#if file.completed || file.progress === 100}
<li class="upload-box-item">
<a
class="upload-box-item"
href={`${base}/console/project-${$page.params.project}/storage/bucket-${$page.params.bucket}/file-${file.$id}`}>
<div class="u-margin-inline-end-16">
<Avatar
size={32}
@@ -46,50 +49,51 @@
name={file.name} />
</div>
<label for={file.name} class="file-name">{file.name}</label>
<span class="icon-check" />
</li>
<label for={file.name} class="file-name u-trim">{file.name}</label>
<button
class="icon-button"
aria-label="Failed"
on:mouseenter={() => (hovering = true)}
on:mouseleave={() => (hovering = false)}
on:click|preventDefault={() => uploader.removeFromQueue(file.$id)}>
<span class:icon-check={!hovering} class:icon-x={hovering} />
</button>
</a>
{:else if file.failed}
<li class="upload-box-item">
<div class="upload-image u-margin-inline-end-16">
<div
class="progress"
role="progressbar"
aria-valuenow={file.progress}
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100} />
<span class="icon">{file.progress}%</span>
<span class="icon">{progress}%</span>
</div>
<label for={file.name} class="file-name">{file.name}</label>
<label for={file.name} class="file-name u-trim">{file.name}</label>
<Pill danger>Failed</Pill>
<button
class="icon-button"
aria-label="Failed"
on:click={() => removeFile(file.$id, file.bucketId)}>
on:click|preventDefault={() => uploader.removeFromQueue(file.$id)}>
<span class="icon-x" />
</button>
</li>
{:else if file.cancelled}
cancelled?
{:else}
<li class="upload-box-item">
<div class="upload-image u-margin-inline-end-16">
<div
class="progress"
role="progressbar"
aria-valuenow={file.progress}
style={`--progress-value:${progress}`}
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100} />
<span class="icon">{file.progress}%</span>
<span class="icon">{progress}%</span>
</div>
<label for={file.name} class="file-name">{file.name}</label>
<label for={file.name} class="file-name u-trim">{file.name}</label>
<Pill warning>Pending</Pill>
<button
class="icon-button"
aria-label="Pending"
on:click={() => removeFile(file.$id, file.bucketId)}>
<span class="icon-x" />
</button>
</li>
{/if}
{/each}
+4 -2
View File
@@ -3,10 +3,12 @@ export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Mode {
CLOUD = 'mode:cloud',
SELF_HOSTED = 'mode:self-hosted'
CLOUD = 'cloud',
SELF_HOSTED = 'self-hosted'
}
export const growthEndpoint = import.meta.env.VITE_APPWRITE_GROWTH_ENDPOINT;
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
+9 -5
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { Trim } from '$lib/components';
import { Button } from '.';
export let label: string = null;
@@ -47,20 +48,23 @@
on:dragover|preventDefault={dragOverHandler}
on:dragleave|preventDefault={() => (hovering = false)}>
<div class="u-flex u-main-center u-cross-center u-gap-32">
<div class="avatar is-size-large">
<div class="avatar is-size-large u-min-width-0">
<span class="icon-folder" aria-hidden="true" />
</div>
<div class="u-grid u-gap-16">
<div class="u-grid u-gap-16 u-min-width-0">
<p>Drag and drop files here to upload</p>
<div class="u-flex u-gap-8 ">
<div class="u-flex u-gap-8 u-min-width-0">
<Button secondary on:click={() => input.click()}>
<span class="icon-upload" aria-hidden="true" />
<span class="text">Choose File</span>
</Button>
{#if files?.length}
<div class="u-flex ">
{files.item(0).name}
{@const fileName = files.item(0).name.split('.')}
<div class="u-flex u-cross-center u-min-width-0">
<Trim>{fileName[0]}</Trim>
<span class="u-min-width-0 u-flex-shrink-0 u-margin-inline-end-16"
>.{fileName[1]}</span>
<button
on:click={() => (files = null)}
type="button"
+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}
+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;
}
+2 -1
View File
@@ -4,6 +4,7 @@
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';
const technologies = [
'js',
@@ -25,7 +26,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>
+2 -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 = {
@@ -59,7 +60,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 });
+32 -21
View File
@@ -1,5 +1,6 @@
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { sdkForProject } from './sdk';
type UploaderFile = {
$id: string;
@@ -8,7 +9,6 @@ type UploaderFile = {
progress: number;
completed: boolean;
failed: boolean;
cancelled: boolean;
error?: string;
};
export type Uploader = {
@@ -42,7 +42,6 @@ const createUploader = () => {
close: () =>
update((n) => {
n.isOpen = !n.isOpen;
return n;
}),
toggle: () =>
@@ -57,22 +56,43 @@ const createUploader = () => {
isCollapsed: false,
files: []
}),
addFile: async (file: Models.File, isOpen = true, isCollapsed = false) => {
uploadFile: async (bucketId: string, id: string, file: File, permissions: string[]) => {
const newFile: UploaderFile = {
$id: file.$id,
bucketId: file.bucketId,
$id: id,
bucketId: bucketId,
name: file.name,
progress: calculateProgress(file),
completed: calculateProgress(file) === 100 ? true : false,
failed: false,
cancelled: false
progress: 0,
completed: false,
failed: false
};
return update((n) => {
n.isOpen = isOpen;
n.isCollapsed = isCollapsed;
update((n) => {
n.isOpen = true;
n.isCollapsed = false;
n.files.unshift(newFile);
return n;
});
const uploadedFile = await sdkForProject.storage.createFile(
bucketId,
id ?? 'unique()',
file,
permissions,
(p) => {
newFile.$id = p.$id;
newFile.progress = p.progress;
newFile.completed = p.progress === 100 ? true : false;
updateFile(p.$id, newFile);
}
);
newFile.$id = uploadedFile.$id;
newFile.progress = 100;
newFile.completed = true;
updateFile(newFile.$id, newFile);
},
removeFromQueue: (id: string) => {
update((n) => {
n.files = n.files.filter((f) => f.$id !== id);
return n;
});
},
removeFile: async (file: Models.File) => {
if (file.chunksTotal === file.chunksUploaded) {
@@ -81,18 +101,9 @@ const createUploader = () => {
return n;
});
} else {
if (confirm('Are you sure you want to cancel the upload?')) {
updateFile(file.$id, { cancelled: true });
}
}
}
};
};
export const uploader = createUploader();
function calculateProgress(file: Models.File) {
const progress = file.chunksUploaded / file.chunksTotal;
return Math.round(progress * 100);
}
+6 -1
View File
@@ -49,11 +49,16 @@
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)) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
+9 -4
View File
@@ -8,9 +8,6 @@ import type { LayoutLoad } from './$types';
export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
if (url.pathname.startsWith('/auth')) {
return;
}
depends(Dependencies.ACCOUNT);
try {
const account = await sdkForConsole.account.get();
@@ -20,7 +17,15 @@ export const load: LayoutLoad = async ({ depends, url }) => {
organizations: sdkForConsole.teams.list()
};
} catch (error) {
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
const acceptedRoutes = [
'/login',
'/register',
'/recover',
'/invite',
'/auth/magic-url',
'/auth/oauth2/success',
'/auth/oauth2/failure'
];
if (!acceptedRoutes.includes(url.pathname)) {
throw redirect(303, '/login');
+4 -2
View File
@@ -1,17 +1,19 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
export let showDelete = false;
const deleteAccount = async () => {
try {
await sdkForConsole.users.delete($user.$id);
await sdkForConsole.account.updateStatus();
showDelete = false;
invalidate(Dependencies.ACCOUNT);
addNotification({
type: 'success',
message: `Account was deleted `
@@ -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>
@@ -19,7 +19,7 @@
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.emailVerification ? 'verified' : 'unverified'
$user.emailVerification ? 'unverified' : 'verified'
}`,
type: 'success'
});
@@ -38,7 +38,7 @@
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.phoneVerification ? 'verified' : 'unverified'
$user.phoneVerification ? 'unverified' : 'verified'
}`,
type: 'success'
});
@@ -56,7 +56,7 @@
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.status ? 'unblocked' : 'blocked'
$user.status ? 'blocked' : 'unblocked'
}`,
type: 'success'
});
@@ -63,9 +63,11 @@
{/if}
</div>
</TableCell>
<TableCellText onlyDesktop title="Type">{attribute.type}</TableCellText>
<TableCellText onlyDesktop title="Type">
{`${attribute.type}${attribute.array ? '[]' : ''}`}
</TableCellText>
<TableCellText onlyDesktop title="Default Value">
{attribute.default ? attribute.default : '-'}
{attribute.default !== null ? attribute.default : '-'}
</TableCellText>
<TableCell showOverflow>
<DropList
@@ -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
);
}
@@ -1,262 +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">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>
@@ -12,7 +12,8 @@
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">Permissions Guide</a> in our documentation.
rel="noopener noreferrer"
class="link">Permissions Guide</a> in our documentation.
</svelte:fragment>
{#if $collection.documentSecurity}
<div class="common-section">
@@ -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',
@@ -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() {
@@ -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>
@@ -162,11 +162,15 @@
</button>
<svelte:fragment slot="list">
<DropListLink
on:click={() => {
showDropdown[index] = false;
}}
href={`${base}/console/project-${projectId}/storage/bucket-${bucketId}/file-${file.$id}`}
icon="pencil">Update</DropListLink>
<DropListItem
icon="trash"
on:click={() => {
showDropdown[index] = false;
selectedFile = file;
showDelete = true;
}}>Delete</DropListItem>
@@ -2,7 +2,6 @@
import { Button, FormList, InputFile } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Modal, CustomId, Heading, Alert } from '$lib/components';
import { sdkForProject } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { page } from '$app/stores';
import { uploader } from '$lib/stores/uploader';
@@ -25,20 +24,15 @@
async function create() {
try {
const file = await sdkForProject.storage.createFile(
bucketId,
id ?? 'unique()',
files[0],
permissions
);
files = null;
showCreate = false;
await uploader.uploadFile(bucketId, id, files[0], permissions);
files = null;
showCustomId = false;
uploader.addFile(file);
// uploader.addFile(file);
dispatch('created');
addNotification({
type: 'success',
message: `${file.name} has been created`
message: `File has been uploaded`
});
trackEvent('submit_file_create');
} catch ({ message }) {
@@ -74,8 +68,11 @@
<Heading tag="h6" size="7">Update Permissions</Heading>
<p class="text">
Choose who can access your buckets and files. For more information, check out the
<a href="https://appwrite.io/docs/permissions" target="_blank" rel="noopener noreferrer"
>Permissions Guide</a> in our documentation.
<a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Permissions Guide</a> in our documentation.
</p>
{#if $bucket.fileSecurity}
<div class="common-section">
@@ -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>
@@ -275,7 +294,8 @@
the <a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer">Permissions Guide</a> in our documentation.
rel="noopener noreferrer"
class="link">Permissions Guide</a> in our documentation.
</p>
<svelte:fragment slot="aside">
{#if bucketPermissions}
@@ -319,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>
@@ -380,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
@@ -413,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>
@@ -452,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>
@@ -461,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>