Merge branch 'main' of github.com:appwrite/console into feat-custom-id-pattern

This commit is contained in:
Arman
2023-01-10 15:52:07 +01:00
100 changed files with 2412 additions and 1616 deletions
+4 -2
View File
@@ -1,3 +1,5 @@
VITE_APPWRITE_ENDPOINT=
VITE_GOOGLE_ANALYTICS=
VITE_SENTRY_DSN=
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_SENTRY_DSN=
VITE_CONSOLE_MODE=self-hosted
-4
View File
@@ -4,10 +4,6 @@
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![appwrite.io](https://img.shields.io/badge/appwrite-.io-f02e65?style=flat-square)](https://appwrite.io)
<p align="center">
<a href="https://www.producthunt.com/posts/console-2-0?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-console&#0045;2&#0045;0" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=367206&theme=light" alt="Console&#0032;2&#0046;0 - Backend&#0032;as&#0032;a&#0032;Service&#0044;&#0032;reimagined&#0032;by&#0032;Appwrite | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
![Appwrite](github.png)
[Appwrite](https://appwrite.io) Console is the Graphical User Interface that developers interact with when accessing their Appwrite instance in the web browser.
+21 -21
View File
@@ -9,9 +9,9 @@
"version": "0.0.1",
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@aw-labs/appwrite-console": "^13.0.0",
"@aw-labs/icons": "0.0.0-78",
"@aw-labs/ui": "0.0.0-78",
"@aw-labs/appwrite-console": "^13.1.0",
"@aw-labs/icons": "0.0.0-82",
"@aw-labs/ui": "0.0.0-82",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.19.0",
"@sentry/tracing": "^7.19.0",
@@ -145,23 +145,23 @@
"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"
}
},
"node_modules/@aw-labs/icons": {
"version": "0.0.0-78",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-78.tgz",
"integrity": "sha512-M1yH0EUanjuHw1McVJfVKrkbHOoSd4PF8ZOYqMXOhSwuwk37MFR8jUapYb2NPu3WdfLTGI6H3N+HwaILePotCg=="
"version": "0.0.0-82",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-82.tgz",
"integrity": "sha512-TQ3abZGBjisd4WpeBVSUihY98drljdlcdw++z8icNpQVYohmUejRHU59mhmIfXCkhpA1s0tW+5xEMp0WmLNp9Q=="
},
"node_modules/@aw-labs/ui": {
"version": "0.0.0-78",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-78.tgz",
"integrity": "sha512-YjIqkwAqZLvjJ6MQBxTEbAuotXHL1E/LXwmHOH9/Tu6TvF5p8e83LlyHVG6d8rXwsmsGfUcXLJEvEcc8RMEOMQ==",
"version": "0.0.0-82",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-82.tgz",
"integrity": "sha512-zZDTP117uxMrwzAacOcpSXI77FMFcfv6+Da1R5sDWoqxhvZ0Wyx0Z6cn3dakV0M3zJfYZQ49LmSnU/5wqHPc7A==",
"dependencies": {
"@aw-labs/icons": "*",
"normalize.css": "^8.0.1",
@@ -7740,23 +7740,23 @@
"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"
}
},
"@aw-labs/icons": {
"version": "0.0.0-78",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-78.tgz",
"integrity": "sha512-M1yH0EUanjuHw1McVJfVKrkbHOoSd4PF8ZOYqMXOhSwuwk37MFR8jUapYb2NPu3WdfLTGI6H3N+HwaILePotCg=="
"version": "0.0.0-82",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-82.tgz",
"integrity": "sha512-TQ3abZGBjisd4WpeBVSUihY98drljdlcdw++z8icNpQVYohmUejRHU59mhmIfXCkhpA1s0tW+5xEMp0WmLNp9Q=="
},
"@aw-labs/ui": {
"version": "0.0.0-78",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-78.tgz",
"integrity": "sha512-YjIqkwAqZLvjJ6MQBxTEbAuotXHL1E/LXwmHOH9/Tu6TvF5p8e83LlyHVG6d8rXwsmsGfUcXLJEvEcc8RMEOMQ==",
"version": "0.0.0-82",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-82.tgz",
"integrity": "sha512-zZDTP117uxMrwzAacOcpSXI77FMFcfv6+Da1R5sDWoqxhvZ0Wyx0Z6cn3dakV0M3zJfYZQ49LmSnU/5wqHPc7A==",
"requires": {
"@aw-labs/icons": "*",
"normalize.css": "^8.0.1",
+3 -3
View File
@@ -21,9 +21,9 @@
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"analytics": "^0.8.1",
"@aw-labs/appwrite-console": "^13.0.0",
"@aw-labs/icons": "0.0.0-78",
"@aw-labs/ui": "0.0.0-78",
"@aw-labs/appwrite-console": "^13.1.0",
"@aw-labs/icons": "0.0.0-82",
"@aw-labs/ui": "0.0.0-82",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.19.0",
"@sentry/tracing": "^7.19.0",
+4 -2
View File
@@ -3,12 +3,13 @@ import googleAnalytics from '@analytics/google-analytics';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { growthEndpoint, Mode } from '$lib/constants';
const analytics = Analytics({
app: 'appwrite',
plugins: [
googleAnalytics({
measurementIds: ['G-R4YJ9JN8L4']
measurementIds: [import.meta.env.VITE_GA_PROJECT?.toString() || 'G-R4YJ9JN8L4']
})
]
});
@@ -39,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'
@@ -48,6 +49,7 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
action: event,
label: event,
url: window.location.origin + path,
account: import.meta.env.VITE_CONSOLE_MODE?.toString() || Mode.SELF_HOSTED,
data: {
email,
name,
+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}
+7
View File
@@ -2,6 +2,13 @@ export const PAGE_LIMIT = 12; // default page limit
export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Mode {
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}
+1 -1
View File
@@ -4,6 +4,6 @@
export let showOverflow = false;
</script>
<div class="table-col " class:u-overflow-visible={showOverflow} data-title={title} role="cell">
<div class="table-col" class:u-overflow-visible={showOverflow} data-title={title} role="cell">
<Trim><slot /></Trim>
</div>
+15
View File
@@ -3,6 +3,7 @@ export function intersection(arr1: unknown[], arr2: unknown[]) {
const intersection = new Set(arr1.filter((elem) => set.has(elem)));
return Array.from(intersection);
}
export function difference(arr1: unknown[], arr2: unknown[]) {
const set = new Set(arr2);
const intersection = new Set(arr1.filter((elem) => !set.has(elem)));
@@ -12,3 +13,17 @@ export function difference(arr1: unknown[], arr2: unknown[]) {
export function symmetricDifference(arr1: unknown[], arr2: unknown[]) {
return difference(arr1, arr2).concat(difference(arr2, arr1));
}
/**
* Removes the element at the specified index from the array, and returns a new array.
*
* @export
* @template T
* @param {T[]} arr
* @param {number} index
* @returns {T[]}
*/
export function remove<T>(arr: T[], index: number): T[] {
// Remove the element at the given index, return a new array
return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
+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'
+22
View File
@@ -5,6 +5,8 @@ import Apple from '../../routes/console/project-[project]/auth/appleOAuth.svelte
import Microsoft from '../../routes/console/project-[project]/auth/microsoftOAuth.svelte';
import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte';
import Auth0 from '../../routes/console/project-[project]/auth/auth0OAuth.svelte';
import Authentik from '../../routes/console/project-[project]/auth/authentikOAuth.svelte';
import GitLab from '../../routes/console/project-[project]/auth/gitlabOAuth.svelte';
import Main from '../../routes/console/project-[project]/auth/mainOAuth.svelte';
export type Provider = Models.Provider & {
@@ -36,6 +38,13 @@ const setProviders = (project: Models.Project): Provider[] => {
docs = 'https://auth0.com/developers';
component = Auth0;
break;
case 'authentik':
docs = 'https://goauthentik.io/integrations/sources/oauth/';
component = Authentik;
break;
case 'autodesk':
docs = 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/';
break;
case 'bitbucket':
docs = 'https://developer.atlassian.com/bitbucket';
break;
@@ -45,12 +54,21 @@ const setProviders = (project: Models.Project): Provider[] => {
case 'box':
docs = 'https://developer.box.com/reference/';
break;
case 'dailymotion':
docs = 'https://developers.dailymotion.com/api/';
break;
case 'discord':
docs = 'https://discordapp.com/developers/docs/topics/oauth2';
break;
case 'disqus':
docs = 'https://disqus.com/api/docs/auth/';
break;
case 'dropbox':
docs = 'https://www.dropbox.com/developers/documentation';
break;
case 'etsy':
docs = 'https://developers.etsy.com/';
break;
case 'facebook':
docs = 'https://developers.facebook.com/';
break;
@@ -59,6 +77,7 @@ const setProviders = (project: Models.Project): Provider[] => {
break;
case 'gitlab':
docs = 'https://docs.gitlab.com/ee/api/';
component = GitLab;
break;
case 'google':
docs = 'https://support.google.com/googleapi/answer/6158849';
@@ -84,6 +103,9 @@ const setProviders = (project: Models.Project): Provider[] => {
icon = 'paypal';
docs = 'https://developer.paypal.com/docs/api/overview/';
break;
case 'podio':
docs = 'https://developers.podio.com/doc/oauth-authorization';
break;
case 'salesforce':
docs = 'https://developer.salesforce.com/docs/';
break;
+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 `
@@ -55,7 +55,9 @@
</button>
<svelte:fragment slot="list">
{#each $organizationList.teams as org}
<DropListLink href={`${base}/console/organization-${org.$id}`}>
<DropListLink
href={`${base}/console/organization-${org.$id}`}
on:click={() => (showDropdown = false)}>
{org.name}
</DropListLink>
{/each}
@@ -84,7 +84,7 @@
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/session/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
@@ -93,7 +93,7 @@
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/session/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
@@ -0,0 +1,108 @@
<script lang="ts">
import { page } from '$app/stores';
import { Modal, CopyInput, Alert } from '$lib/components';
import { Button, InputPassword, InputText, InputSwitch, FormList } from '$lib/elements/forms';
import { sdkForConsole } from '$lib/stores/sdk';
import type { Provider } from '$lib/stores/oauth-providers';
import { addNotification } from '$lib/stores/notifications';
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
export let provider: Provider;
let appId: string = null;
let enabled: boolean = null;
let clientSecret: string = null;
let authentikDomain: string = null;
let error: string;
onMount(() => {
appId ??= provider.appId;
enabled ??= provider.enabled;
if (provider.secret) ({ clientSecret, authentikDomain } = JSON.parse(provider.secret));
});
const projectId = $page.params.project;
const update = async () => {
try {
await sdkForConsole.projects.updateOAuth2(
projectId,
provider.name.toLowerCase(),
appId,
secret,
enabled
);
addNotification({
type: 'success',
message: `${provider.name} authentication has been ${
provider.enabled ? 'enabled' : 'disabled'
}`
});
trackEvent('submit_provider_update', {
provider,
enabled
});
provider = null;
invalidate(Dependencies.PROJECT);
} catch ({ message }) {
error = message;
}
};
$: secret =
clientSecret && authentikDomain
? JSON.stringify({ clientSecret, authentikDomain })
: provider.secret;
</script>
<Modal {error} size="big" show on:submit={update} on:close>
<svelte:fragment slot="header">{provider.name} OAuth2 Settings</svelte:fragment>
<FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="clientID"
label="Client ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="Client Secret"
placeholder="Enter Client Secret"
minlength={0}
showPasswordButton
bind:value={clientSecret} />
<InputText
id="domain"
label="Authentik Base-Domain"
placeholder="Your Authentik domain"
bind:value={authentikDomain} />
<Alert type="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert>
<div>
<p>URI</p>
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
disabled={(secret === provider.secret &&
enabled === provider.enabled &&
appId === provider.appId) ||
!(appId && clientSecret && authentikDomain)}
submit>Update</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,106 @@
<script lang="ts">
import { page } from '$app/stores';
import { Modal, CopyInput, Alert } from '$lib/components';
import { Button, InputPassword, InputText, InputSwitch, FormList } from '$lib/elements/forms';
import { sdkForConsole } from '$lib/stores/sdk';
import type { Provider } from '$lib/stores/oauth-providers';
import { addNotification } from '$lib/stores/notifications';
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
export let provider: Provider;
let appId: string = null;
let enabled: boolean = null;
let clientSecret: string = null;
let endpoint: string = null;
let error: string;
onMount(() => {
appId ??= provider.appId;
enabled ??= provider.enabled;
if (provider.secret) ({ clientSecret, endpoint } = JSON.parse(provider.secret));
});
const projectId = $page.params.project;
const update = async () => {
try {
await sdkForConsole.projects.updateOAuth2(
projectId,
provider.name.toLowerCase(),
appId,
secret,
enabled
);
addNotification({
type: 'success',
message: `${provider.name} authentication has been ${
provider.enabled ? 'enabled' : 'disabled'
}`
});
trackEvent('submit_provider_update', {
provider,
enabled
});
provider = null;
invalidate(Dependencies.PROJECT);
} catch ({ message }) {
error = message;
}
};
$: secret =
clientSecret && endpoint ? JSON.stringify({ clientSecret, endpoint }) : provider.secret;
</script>
<Modal {error} size="big" show on:submit={update} on:close>
<svelte:fragment slot="header">{provider.name} OAuth2 Settings</svelte:fragment>
<FormList>
<p>
To use {provider.name} authentication in your application, first fill in this form. For more
info you can
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
>visit the docs.</a>
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="appID"
label="App ID"
autofocus={true}
placeholder="Enter ID"
bind:value={appId} />
<InputPassword
id="secret"
label="App Secret"
placeholder="Enter App Secret"
minlength={0}
showPasswordButton
bind:value={clientSecret} />
<InputText
id="endpoint"
label="Endpoint (optional)"
placeholder="Your endpoint"
bind:value={endpoint} />
<Alert type="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert>
<div>
<p>URI</p>
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button
disabled={(secret === provider.secret &&
enabled === provider.enabled &&
appId === provider.appId) ||
!(appId && clientSecret && endpoint)}
submit>Update</Button>
</svelte:fragment>
</Modal>
@@ -84,7 +84,7 @@
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/session/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
@@ -93,7 +93,7 @@
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/session/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
@@ -35,7 +35,8 @@
projectId,
provider.name.toLowerCase(),
appId,
secret
secret,
enabled
);
addNotification({
type: 'success',
@@ -102,7 +103,7 @@
<CopyInput
value={`${
sdkForConsole.client.config.endpoint
}/account/session/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
</div>
</FormList>
<svelte:fragment slot="footer">
@@ -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>
@@ -1,500 +1,20 @@
<script lang="ts">
import { toLocaleDateTime } from '$lib/helpers/date';
import {
CardGrid,
Box,
DropList,
DropListItem,
Heading,
AvatarInitials
} from '$lib/components';
import { Pill } from '$lib/elements';
import {
Button,
Form,
InputText,
InputEmail,
InputPassword,
InputPhone
} from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { sdkForProject } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import DeleteUser from './deleteUser.svelte';
import { user } from './store';
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
$: if (prefs) {
if (JSON.stringify(prefs) !== JSON.stringify(Object.entries($user.prefs))) {
if (!!prefs[prefs.length - 1][0] && !!prefs[prefs.length - 1][1]) {
arePrefsDisabled = false;
} else {
arePrefsDisabled = true;
}
} else {
arePrefsDisabled = true;
}
}
let showDelete = false;
let userName: string = null,
userEmail: string = null,
userPhone: string = null,
newPassword: string = null;
let prefs: [string, unknown][] = null;
let arePrefsDisabled = true;
let showVerifcationDropdown = false;
onMount(async () => {
prefs = Object.entries($user.prefs);
if (!prefs?.length) {
prefs.push(['', '']);
}
userName ??= $user.name;
userEmail ??= $user.email;
userPhone ??= $user.phone;
});
async function updateVerificationEmail() {
showVerifcationDropdown = false;
try {
await sdkForProject.users.updateEmailVerification($user.$id, !$user.emailVerification);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.emailVerification ? 'verified' : 'unverified'
}`,
type: 'success'
});
trackEvent('submit_user_update_verification_email');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateVerificationPhone() {
showVerifcationDropdown = false;
try {
await sdkForProject.users.updatePhoneVerification($user.$id, !$user.phoneVerification);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.phoneVerification ? 'verified' : 'unverified'
}`,
type: 'success'
});
trackEvent('submit_user_update_verification_phone');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateStatus() {
try {
await sdkForProject.users.updateStatus($user.$id, !$user.status);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.status ? 'unblocked' : 'blocked'
}`,
type: 'success'
});
trackEvent('submit_user_update_status');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateName() {
try {
await sdkForProject.users.updateName($user.$id, userName);
invalidate(Dependencies.USER);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent('submit_user_update_name');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateEmail() {
try {
await sdkForProject.users.updateEmail($user.$id, userEmail);
invalidate(Dependencies.USER);
addNotification({
message: 'Email has been updated',
type: 'success'
});
trackEvent('submit_user_update_email');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePhone() {
try {
await sdkForProject.users.updatePhone($user.$id, userPhone);
invalidate(Dependencies.USER);
addNotification({
message: 'Phone has been updated',
type: 'success'
});
trackEvent('submit_user_update_phone');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePassword() {
try {
await sdkForProject.users.updatePassword($user.$id, newPassword);
newPassword = null;
addNotification({
message: 'Password has been updated',
type: 'success'
});
trackEvent('submit_user_update_password');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePrefs() {
try {
let updatedPrefs = Object.fromEntries(prefs);
await sdkForProject.users.updatePrefs($user.$id, updatedPrefs);
invalidate(Dependencies.USER);
arePrefsDisabled = true;
addNotification({
message: 'Preferences have been updated',
type: 'success'
});
trackEvent('submit_user_update_preferences');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
import DangerZone from './dangerZone.svelte';
import UpdateEmail from './updateEmail.svelte';
import UpdateName from './updateName.svelte';
import UpdatePassword from './updatePassword.svelte';
import UpdatePhone from './updatePhone.svelte';
import UpdatePrefs from './updatePrefs.svelte';
import UpdateStatus from './updateStatus.svelte';
</script>
<Container>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
{#if $user.email || $user.phone}
{#if $user.name}
<AvatarInitials size={48} name={$user.name} />
<Heading tag="h6" size="7">{$user.name}</Heading>
{:else}
<div class="avatar">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
{:else}
<div class="avatar">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{/if}
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div>
{#if $user.email}
<p class="title">{$user.email}</p>
{/if}
{#if $user.phone}
<p class="title">{$user.phone}</p>
{/if}
<p>Joined: {toLocaleDateTime($user.registration)}</p>
</div>
{#if !$user.status}
<Pill danger>blocked</Pill>
{:else if $user.email && $user.phone}
<Pill success={$user.emailVerification || $user.phoneVerification}>
{$user.emailVerification && $user.phoneVerification
? 'verified'
: $user.emailVerification
? 'verified email'
: $user.phoneVerification
? 'verified phone'
: 'unverified'}
</Pill>
{:else}
<Pill success={$user.emailVerification || $user.phoneVerification}>
{$user.emailVerification
? 'verified '
: $user.phoneVerification
? 'verified '
: 'unverified'}
</Pill>
{/if}
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button text={$user.status} secondary={!$user.status} on:click={() => updateStatus()}>
{$user.status ? 'Block account' : 'Unblock account'}
</Button>
{#if $user.status}
{#if $user.phone && $user.email}
<DropList bind:show={showVerifcationDropdown} placement="top-start">
<Button
secondary
on:click={() => (showVerifcationDropdown = !showVerifcationDropdown)}>
{$user.emailVerification ? 'Unverify' : 'Verify'} account
</Button>
<svelte:fragment slot="list">
<DropListItem icon="mail" on:click={() => updateVerificationEmail()}>
{$user.emailVerification ? 'Unverify' : 'Verify'} email
</DropListItem>
<DropListItem icon="phone" on:click={() => updateVerificationPhone()}>
{$user.phoneVerification ? 'Unverify' : 'Verify'} phone
</DropListItem>
</svelte:fragment>
</DropList>
{:else if $user.email}
<Button secondary on:click={() => updateVerificationEmail()}>
{$user.emailVerification ? 'Unverify' : 'Verify'} account
</Button>
{:else if $user.phone}
<Button secondary on:click={() => updateVerificationPhone()}>
{$user.phoneVerification ? 'Unverify' : 'Verify'} account
</Button>
{/if}
{/if}
</svelte:fragment>
</CardGrid>
<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={userName} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userName === $user.name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateEmail}>
<CardGrid>
<Heading tag="h6" size="7">Update Email</Heading>
<svelte:fragment slot="aside">
<ul>
<InputEmail
id="email"
label="Email"
placeholder="Enter email"
autocomplete={false}
bind:value={userEmail} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userEmail === $user.email} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePhone}>
<CardGrid>
<Heading tag="h6" size="7">Update Phone</Heading>
<svelte:fragment slot="aside">
<ul>
<InputPhone
id="phone"
label="Phone"
placeholder="Enter phone number"
autocomplete={false}
bind:value={userPhone} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userPhone === $user.phone} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePassword}>
<CardGrid>
<div>
<Heading tag="h6" size="7">Update Password</Heading>
</div>
<p>
Enter a new password. A password must contain <b>at least 8 characters.</b>
</p>
<svelte:fragment slot="aside">
<ul>
<InputPassword
id="newPassword"
label="New Password"
placeholder="Enter new password"
autocomplete={false}
meter={false}
showPasswordButton={true}
bind:value={newPassword} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!newPassword} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePrefs}>
<CardGrid>
<Heading tag="h6" size="7">User Preferences</Heading>
<p>
You can update your user preferences by storing information on the user's objects so
they can easily be shared across devices and sessions.
</p>
<svelte:fragment slot="aside">
<form class="form u-grid u-gap-16">
<ul class="form-list">
{#if prefs}
{#each prefs as [key, value], index}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch">
<label class="label" for={`key-${index}`}>Key</label>
<div class="input-text-wrapper">
<input
id={`key-${key}`}
placeholder="Enter key"
type="text"
class="input-text"
bind:value={key} />
</div>
</div>
<div class="form-item-part u-stretch">
<label class="label" for={`value-${index}`}> Value </label>
<div class="input-text-wrapper">
<input
id={`value-${value}`}
placeholder="Enter value"
type="text"
class="input-text"
bind:value />
</div>
</div>
<div class="form-item-part u-cross-child-end">
<Button
text
disabled={(!key || !value) && index === 0}
on:click={() => {
if (index === 0) {
prefs = [['', '']];
} else {
prefs.splice(index, 1);
prefs = prefs;
}
}}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
{/if}
</ul>
<Button
noMargin
text
on:click={() => {
if (prefs[prefs.length - 1][0] && prefs[prefs.length - 1][1]) {
prefs.push(['', '']);
prefs = prefs;
}
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add Preference</span>
</Button>
</form>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePrefsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Danger Zone</Heading>
</div>
<p>
The user will be permanently deleted, including all data associated with this user. This
action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
{#if $user.email || $user.phone}
{#if $user.name}
<AvatarInitials size={48} name={$user.name} />
{:else}
<div class="avatar">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
{:else}
<div class="avatar">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">
{$user.name || $user.email || $user.phone || 'Anonymous'}
</h6>
</svelte:fragment>
<p class="u-trim-1">
{$user.email && $user.phone
? [$user.email, $user.phone].join(',')
: $user.email || $user.phone}
</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)} event="delete_user"
>Delete</Button>
</svelte:fragment>
</CardGrid>
<UpdateStatus />
<UpdateName />
<UpdateEmail />
<UpdatePhone />
<UpdatePassword />
<UpdatePrefs />
<DangerZone />
</Container>
<DeleteUser bind:showDelete />
@@ -0,0 +1,53 @@
<script lang="ts">
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { Button } from '$lib/elements/forms';
import DeleteUser from './deleteUser.svelte';
import { user } from './store';
let showDelete = false;
</script>
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Danger Zone</Heading>
</div>
<p>
The user will be permanently deleted, including all data associated with this user. This
action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
{#if $user.email || $user.phone}
{#if $user.name}
<AvatarInitials size={48} name={$user.name} />
{:else}
<div class="avatar">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
{:else}
<div class="avatar">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">
{$user.name || $user.email || $user.phone || 'Anonymous'}
</h6>
</svelte:fragment>
<p class="u-trim-1">
{$user.email && $user.phone
? [$user.email, $user.phone].join(',')
: $user.email || $user.phone}
</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)} event="delete_user">Delete</Button>
</svelte:fragment>
</CardGrid>
<DeleteUser bind:showDelete />
@@ -72,7 +72,7 @@
<Button
external
secondary
href="https://appwrite.io/docs/server/auth?sdk=nodejs-default#usersGetMemberships">
href="https://appwrite.io/docs/server/users?sdk=nodejs-default#usersListMemberships">
Documentation
</Button>
</Empty>
@@ -0,0 +1,53 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputEmail } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { user } from './store';
let userEmail: string = null;
onMount(async () => {
userEmail ??= $user.email;
});
async function updateEmail() {
try {
await sdkForProject.users.updateEmail($user.$id, userEmail);
invalidate(Dependencies.USER);
addNotification({
message: 'Email has been updated',
type: 'success'
});
trackEvent('submit_user_update_email');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Form on:submit={updateEmail}>
<CardGrid>
<Heading tag="h6" size="7">Update Email</Heading>
<svelte:fragment slot="aside">
<ul>
<InputEmail
id="email"
label="Email"
placeholder="Enter email"
autocomplete={false}
bind:value={userEmail} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userEmail === $user.email} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,54 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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 { user } from './store';
let userName: string = null;
onMount(async () => {
userName ??= $user.name;
});
async function updateName() {
try {
await sdkForProject.users.updateName($user.$id, userName);
invalidate(Dependencies.USER);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent('submit_user_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={userName} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userName === $user.name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,55 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Button, Form, InputPassword } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { user } from './store';
let newPassword: string = null;
async function updatePassword() {
try {
await sdkForProject.users.updatePassword($user.$id, newPassword);
newPassword = null;
addNotification({
message: 'Password has been updated',
type: 'success'
});
trackEvent('submit_user_update_password');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Form on:submit={updatePassword}>
<CardGrid>
<div>
<Heading tag="h6" size="7">Update Password</Heading>
</div>
<p>
Enter a new password. A password must contain <b>at least 8 characters.</b>
</p>
<svelte:fragment slot="aside">
<ul>
<InputPassword
id="newPassword"
label="New Password"
placeholder="Enter new password"
autocomplete={false}
meter={false}
showPasswordButton={true}
bind:value={newPassword} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!newPassword} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,53 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputPhone } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { user } from './store';
let userPhone: string = null;
onMount(async () => {
userPhone ??= $user.phone;
});
async function updatePhone() {
try {
await sdkForProject.users.updatePhone($user.$id, userPhone);
invalidate(Dependencies.USER);
addNotification({
message: 'Phone has been updated',
type: 'success'
});
trackEvent('submit_user_update_phone');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Form on:submit={updatePhone}>
<CardGrid>
<Heading tag="h6" size="7">Update Phone</Heading>
<svelte:fragment slot="aside">
<ul>
<InputPhone
id="phone"
label="Phone"
placeholder="Enter phone number"
autocomplete={false}
bind:value={userPhone} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={userPhone === $user.phone} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,129 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { user } from './store';
$: if (prefs) {
if (JSON.stringify(prefs) !== JSON.stringify(Object.entries($user.prefs))) {
if (!!prefs[prefs.length - 1][0] && !!prefs[prefs.length - 1][1]) {
arePrefsDisabled = false;
} else {
arePrefsDisabled = true;
}
} else {
arePrefsDisabled = true;
}
}
let prefs: [string, unknown][] = null;
let arePrefsDisabled = true;
onMount(async () => {
prefs = Object.entries($user.prefs);
if (!prefs?.length) {
prefs.push(['', '']);
}
});
async function updatePrefs() {
try {
let updatedPrefs = Object.fromEntries(prefs);
await sdkForProject.users.updatePrefs($user.$id, updatedPrefs);
invalidate(Dependencies.USER);
arePrefsDisabled = true;
addNotification({
message: 'Preferences have been updated',
type: 'success'
});
trackEvent('submit_user_update_preferences');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Form on:submit={updatePrefs}>
<CardGrid>
<Heading tag="h6" size="7">User Preferences</Heading>
<p>
You can update your user preferences by storing information on the user's objects so
they can easily be shared across devices and sessions.
</p>
<svelte:fragment slot="aside">
<form class="form u-grid u-gap-16">
<ul class="form-list">
{#if prefs}
{#each prefs as [key, value], index}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch">
<label class="label" for={`key-${index}`}>Key</label>
<div class="input-text-wrapper">
<input
id={`key-${key}`}
placeholder="Enter key"
type="text"
class="input-text"
bind:value={key} />
</div>
</div>
<div class="form-item-part u-stretch">
<label class="label" for={`value-${index}`}> Value </label>
<div class="input-text-wrapper">
<input
id={`value-${value}`}
placeholder="Enter value"
type="text"
class="input-text"
bind:value />
</div>
</div>
<div class="form-item-part u-cross-child-end">
<Button
text
disabled={(!key || !value) && index === 0}
on:click={() => {
if (index === 0) {
prefs = [['', '']];
} else {
prefs.splice(index, 1);
prefs = prefs;
}
}}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
{/if}
</ul>
<Button
noMargin
text
on:click={() => {
if (prefs[prefs.length - 1][0] && prefs[prefs.length - 1][1]) {
prefs.push(['', '']);
prefs = prefs;
}
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add Preference</span>
</Button>
</form>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePrefsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,157 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { AvatarInitials, CardGrid, DropList, DropListItem, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { user } from './store';
let showVerifcationDropdown = false;
async function updateVerificationEmail() {
showVerifcationDropdown = false;
try {
await sdkForProject.users.updateEmailVerification($user.$id, !$user.emailVerification);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.emailVerification ? 'unverified' : 'verified'
}`,
type: 'success'
});
trackEvent('submit_user_update_verification_email');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateVerificationPhone() {
showVerifcationDropdown = false;
try {
await sdkForProject.users.updatePhoneVerification($user.$id, !$user.phoneVerification);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.phoneVerification ? 'unverified' : 'verified'
}`,
type: 'success'
});
trackEvent('submit_user_update_verification_phone');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateStatus() {
try {
await sdkForProject.users.updateStatus($user.$id, !$user.status);
invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.status ? 'blocked' : 'unblocked'
}`,
type: 'success'
});
trackEvent('submit_user_update_status');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
{#if $user.email || $user.phone}
{#if $user.name}
<AvatarInitials size={48} name={$user.name} />
<Heading tag="h6" size="7">{$user.name}</Heading>
{:else}
<div class="avatar">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
{/if}
{:else}
<div class="avatar">
<span class="icon-anonymous" aria-hidden="true" />
</div>
{/if}
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div>
{#if $user.email}
<p class="title">{$user.email}</p>
{/if}
{#if $user.phone}
<p class="title">{$user.phone}</p>
{/if}
<p>Joined: {toLocaleDateTime($user.registration)}</p>
</div>
{#if !$user.status}
<Pill danger>blocked</Pill>
{:else if $user.email && $user.phone}
<Pill success={$user.emailVerification || $user.phoneVerification}>
{$user.emailVerification && $user.phoneVerification
? 'verified'
: $user.emailVerification
? 'verified email'
: $user.phoneVerification
? 'verified phone'
: 'unverified'}
</Pill>
{:else}
<Pill success={$user.emailVerification || $user.phoneVerification}>
{$user.emailVerification
? 'verified '
: $user.phoneVerification
? 'verified '
: 'unverified'}
</Pill>
{/if}
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button text={$user.status} secondary={!$user.status} on:click={() => updateStatus()}>
{$user.status ? 'Block account' : 'Unblock account'}
</Button>
{#if $user.status}
{#if $user.phone && $user.email}
<DropList bind:show={showVerifcationDropdown} placement="top-start">
<Button
secondary
on:click={() => (showVerifcationDropdown = !showVerifcationDropdown)}>
{$user.emailVerification ? 'Unverify' : 'Verify'} account
</Button>
<svelte:fragment slot="list">
<DropListItem icon="mail" on:click={() => updateVerificationEmail()}>
{$user.emailVerification ? 'Unverify' : 'Verify'} email
</DropListItem>
<DropListItem icon="phone" on:click={() => updateVerificationPhone()}>
{$user.phoneVerification ? 'Unverify' : 'Verify'} phone
</DropListItem>
</svelte:fragment>
</DropList>
{:else if $user.email}
<Button secondary on:click={() => updateVerificationEmail()}>
{$user.emailVerification ? 'Unverify' : 'Verify'} account
</Button>
{:else if $user.phone}
<Button secondary on:click={() => updateVerificationPhone()}>
{$user.phoneVerification ? 'Unverify' : 'Verify'} account
</Button>
{/if}
{/if}
</svelte:fragment>
</CardGrid>
@@ -63,9 +63,11 @@
{/if}
</div>
</TableCell>
<TableCellText title="Type">{attribute.type}</TableCellText>
<TableCellText title="Type">
{`${attribute.type}${attribute.array ? '[]' : ''}`}
</TableCellText>
<TableCellText title="Default Value">
{attribute.default ? attribute.default : '-'}
{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
);
}
@@ -12,6 +12,7 @@
import { createDocument } from './wizard/store';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
const databaseId = $page.params.database;
const collectionId = $page.params.collection;
@@ -42,6 +43,7 @@
message: 'Document has been created',
type: 'success'
});
trackEvent('submit_document_create');
invalidate(Dependencies.DOCUMENTS);
wizard.hide();
} catch (error) {
@@ -12,6 +12,7 @@
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import Attribute from './attribute.svelte';
import { trackEvent } from '$lib/actions/analytics';
let disableUpdate = true;
let currentDoc: string;
@@ -61,6 +62,9 @@
currentDoc = JSON.stringify($work);
invalidate(Dependencies.DOCUMENT);
trackEvent('submit_document_update', {
type: 'android'
});
disableUpdate = true;
addNotification({
message: 'Document was updated!',
@@ -1,16 +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;
@@ -24,115 +25,91 @@
{ value: 'unique', label: 'Unique' },
{ value: 'fulltext', label: 'FullText' }
];
let newAttr = false;
let selectedType = 'key';
$: attributeOptions = $collection.attributes.map((attribute: Attributes) => ({
let attributeOptions = $collection.attributes.map((attribute: Attributes) => ({
value: attribute.key,
label: attribute.key
}));
$: attributeList = [];
let attributeList = [{ value: '', order: '' }];
let creating = false;
let selectedAttribute = '';
let selectedOrder = '';
onMount(() => {
if (externalAttribute) {
attributeList = [{ value: externalAttribute.key, order: 'ASC' }];
}
});
$: if (showCreateIndex) {
attributeList = [];
selectedOrder = selectedAttribute = '';
function initialize() {
attributeList = externalAttribute
? [{ value: externalAttribute.key, order: 'ASC' }]
: [{ value: '', order: '' }];
selectedType = 'key';
key = null;
}
const created = async () => {
if (key && selectedAttribute && selectedOrder && selectedType) {
if (selectedAttribute && selectedOrder) {
attributeList.push({ value: selectedAttribute, order: selectedOrder });
selectedAttribute = selectedOrder = '';
}
try {
await sdkForProject.databases.createIndex(
databaseId,
$collection.$id,
key,
selectedType,
attributeList.map((a) => a.value),
attributeList.map((a) => a.order)
);
invalidate(Dependencies.COLLECTION);
addNotification({
message: 'Index has been created',
type: 'success'
});
trackEvent('submit_index_create');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
$: 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,
$collection.$id,
key,
selectedType,
attributeList.map((a) => a.value),
attributeList.map((a) => a.order)
);
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'
});
trackEvent('submit_index_create');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
} finally {
showCreateIndex = false;
} else error = 'All fields are required';
};
creating = false;
}
}
function addAttribute() {
if (addAttributeDisabled) return;
// We assign instead of pushing to trigger Svelte's reactivity
attributeList = [...attributeList, { value: '', order: '' }];
}
</script>
<Modal bind:error size="big" on:submit={created} bind:show={showCreateIndex}>
<Modal bind:error size="big" on:submit={create} bind:show={showCreateIndex}>
<svelte:fragment slot="header">Create Index</svelte:fragment>
<FormList>
<InputText id="key" label="Index Key" placeholder="Enter Key" bind:value={key} autofocus />
<InputSelect options={types} id="type" label="Index type" bind:value={selectedType} />
{#if attributeList?.length}
{#each attributeList as index, i}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch">
<Select id="attribute" label="Attribute" bind:value={index.value}>
<optgroup label="Internal">
<option value="$id">$id</option>
<option value="$createdAt">$createdAt</option>
<option value="$updatedAt">$updatedAt</option>
</optgroup>
<optgroup label="Attributes">
{#each attributeOptions as option}
<option
value={option.value}
selected={option.value === selectedAttribute}>
{option.label}
</option>
{/each}
</optgroup>
</Select>
</div>
<div class="form-item-part u-stretch">
<Select id="order" label="Order" bind:value={index.order}>
<option value="ASC"> ASC </option>
<option value="DESC"> DESC </option>
</Select>
</div>
<div class="form-item-part u-cross-child-end">
<Button
text
disabled={externalAttribute && i === 0}
on:click={() => {
if (i === 0) attributeList = [];
attributeList = attributeList.splice(i, 1);
}}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/each}
{/if}
{#if !attributeList?.length || newAttr}
{#each attributeList as attribute, i}
<li class="form-item is-multiple">
<div class="form-item-part u-stretch" style="align-items: flex-start;">
<Select id="attribute" label="Attribute" bind:value={selectedAttribute}>
<option value="" disabled selected hidden>Select Attribute</option>
<div class="form-item-part u-stretch">
<Select id={`attribute-${i}`} label="Attribute" bind:value={attribute.value}>
<option value="" disabled hidden>Select Attribute</option>
<optgroup label="Internal">
<option value="$id">$id</option>
@@ -141,9 +118,7 @@
</optgroup>
<optgroup label="Attributes">
{#each attributeOptions as option}
<option
value={option.value}
selected={option.value === selectedAttribute}>
<option value={option.value}>
{option.label}
</option>
{/each}
@@ -151,42 +126,34 @@
</Select>
</div>
<div class="form-item-part u-stretch">
<Select id="order" label="Order" bind:value={selectedOrder}>
<option value="" disabled selected hidden>Select Order</option>
<Select id={`order-${i}`} label="Order" bind:value={attribute.order}>
<option value="" disabled hidden>Select Order</option>
<option value="ASC"> ASC </option>
<option value="DESC"> DESC </option>
</Select>
</div>
<div class="form-item-part u-cross-child-end">
<Button
text
disabled={false}
disabled={attributeList.length <= 1}
on:click={() => {
newAttr = false;
selectedAttribute = selectedOrder = '';
attributeList = remove(attributeList, i);
}}>
<span class="icon-x" aria-hidden="true" />
</Button>
</div>
</li>
{/if}
<Button
text
noMargin
on:click={() => {
newAttr = true;
if (selectedAttribute && selectedOrder) {
attributeList.push({ value: selectedAttribute, order: selectedOrder });
selectedAttribute = selectedOrder = '';
}
}}>
{/each}
<Button text noMargin on:click={addAttribute} disabled={addAttributeDisabled}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add attribute</span>
</Button>
</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,257 +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
);
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
);
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
);
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">
@@ -16,14 +16,19 @@
const dispatch = createEventDispatcher();
const handleVariable = () => {
function close() {
showCreate = false;
selectedVar = null;
}
function handleVariable() {
if (selectedVar) {
dispatch('updated', pair);
} else {
dispatch('created', pair);
}
showCreate = false;
};
close();
}
</script>
<Modal bind:show={showCreate} on:submit={handleVariable} size="big">
@@ -48,7 +53,7 @@
required />
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button secondary on:click={close}>Cancel</Button>
<Button submit>{selectedVar ? 'Update' : 'Create'}</Button>
</svelte:fragment>
</Modal>
@@ -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',
@@ -62,7 +62,15 @@
async function updateName() {
try {
await sdkForProject.functions.update(functionId, functionName, $func.execute);
await sdkForProject.functions.update(
functionId,
functionName,
$func.execute,
$func.events,
$func.schedule,
$func.timeout,
$func.enabled
);
invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Name has been updated',
@@ -79,7 +87,15 @@
async function updatePermissions() {
try {
await sdkForProject.functions.update(functionId, $func.name, permissions);
await sdkForProject.functions.update(
functionId,
$func.name,
permissions,
$func.events,
$func.schedule,
$func.timeout,
$func.enabled
);
invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Permissions have been updated',
@@ -100,7 +116,10 @@
functionId,
$func.name,
$func.execute,
Array.from($eventSet)
Array.from($eventSet),
$func.schedule,
$func.timeout,
$func.enabled
);
invalidate(Dependencies.FUNCTION);
addNotification({
@@ -124,7 +143,8 @@
$func.execute,
$func.events,
functionSchedule,
timeout
$func.timeout,
$func.enabled
);
invalidate(Dependencies.FUNCTION);
@@ -149,9 +169,11 @@
$func.execute,
$func.events,
$func.schedule,
timeout
timeout,
$func.enabled
);
invalidate(Dependencies.FUNCTION);
addNotification({
type: 'success',
message: 'Timeout has been updated'
@@ -570,10 +592,12 @@
</Container>
<Delete bind:showDelete />
<Variable
bind:selectedVar
bind:showCreate={showVariablesModal}
on:created={handleVariableCreated}
on:updated={handleVariableUpdated} />
{#if showVariablesModal}
<Variable
bind:selectedVar
bind:showCreate={showVariablesModal}
on:created={handleVariableCreated}
on:updated={handleVariableUpdated} />
{/if}
<!-- <Upload bind:show={showVariablesUpload} on:uploaded={handleVariableCreated} /> -->
<EventModal bind:show={showEvents} on:created={handleEvent} />
@@ -2,5 +2,5 @@ import { page } from '$app/stores';
import { derived, writable, type Writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
export const func = derived(page, ($page) => $page.data.function);
export const func = derived(page, ($page) => $page.data.function as Models.Function);
export const execute: Writable<Models.Function> = writable();
@@ -13,6 +13,7 @@
import { key } from './store';
import Scopes from '../scopes.svelte';
import Delete from './delete.svelte';
import { trackEvent } from '$lib/actions/analytics';
let showDelete = false;
let name: string = null;
@@ -37,6 +38,7 @@
$key.expire
);
invalidate(Dependencies.KEY);
trackEvent('submit_key_update_name');
addNotification({
type: 'success',
message: 'API Key name has been updated'
@@ -59,6 +61,7 @@
expire
);
invalidate(Dependencies.KEY);
trackEvent('submit_key_update_expire');
addNotification({
type: 'success',
message: 'API Key expiration has been updated'
@@ -73,8 +76,15 @@
async function updateScopes() {
try {
await sdkForConsole.projects.updateKey($project.$id, $key.$id, $key.name, scopes);
await sdkForConsole.projects.updateKey(
$project.$id,
$key.$id,
$key.name,
scopes,
$key.expire
);
invalidate(Dependencies.KEY);
trackEvent('submit_key_update_scopes');
addNotification({
type: 'success',
message: 'API Key scopes has been updated'
@@ -12,6 +12,7 @@
import { onDestroy } from 'svelte';
import { onboarding } from '../../store';
import { Dependencies } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
async function onFinish() {
try {
@@ -24,6 +25,7 @@
if ($onboarding) {
invalidate(Dependencies.PROJECT);
}
trackEvent('submit_key_create');
goto(`/console/project-${$page.params.project}/overview/keys/${$id}`);
} catch (error) {
addNotification({
@@ -48,7 +48,14 @@
const updateName = async () => {
try {
await sdkForConsole.projects.updatePlatform($project.$id, data.platform.$id, name);
await sdkForConsole.projects.updatePlatform(
$project.$id,
data.platform.$id,
name,
data.platform.key,
data.platform.store,
data.platform.hostname
);
invalidate(Dependencies.PLATFORM);
addNotification({
type: 'success',
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -9,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 () => {
@@ -21,11 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
undefined,
undefined,
hostname
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'android'
});
addNotification({
type: 'success',
message: 'Platform Package Name has been updated'
@@ -48,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>
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'apple-ios'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'apple-macos'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'apple-tvos'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'apple-watchos'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'flutter-android'
});
addNotification({
type: 'success',
message: 'Platform Package Name has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'flutter-ios'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'flutter-linux'
});
addNotification({
type: 'success',
message: 'Platform Package Name has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'flutter-macos'
});
addNotification({
type: 'success',
message: 'Platform Bundle ID has been updated'
@@ -1,5 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
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';
@@ -21,9 +22,14 @@
$project.$id,
$platform.$id,
$platform.name,
key
key,
$platform.store,
$platform.hostname
);
invalidate(Dependencies.PLATFORM);
trackEvent('submit_platform_update', {
type: 'flutter-windows'
});
addNotification({
type: 'success',
message: 'Platform Package Name has been updated'
@@ -21,8 +21,8 @@
$project.$id,
$platform.$id,
$platform.name,
undefined,
undefined,
$platform.key,
$platform.store,
hostname
);
invalidate(Dependencies.PLATFORM);
@@ -19,8 +19,8 @@
projectId,
$createPlatform.$id,
$createPlatform.name,
undefined,
undefined,
$createPlatform.key,
$createPlatform.store,
$createPlatform.hostname
);
@@ -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>
@@ -29,8 +29,8 @@
projectId,
$createPlatform.$id,
$createPlatform.name,
undefined,
undefined,
$createPlatform.key,
$createPlatform.store,
$createPlatform.hostname
);
@@ -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,
{
@@ -85,8 +85,8 @@
projectId,
$createPlatform.$id,
$createPlatform.name,
undefined,
undefined,
$createPlatform.key,
$createPlatform.store,
$createPlatform.hostname
);
@@ -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>
@@ -8,6 +8,8 @@ function createPlatformStore() {
$id: null,
name: null,
hostname: null,
key: null,
store: null,
type: null
});
@@ -20,6 +22,8 @@ function createPlatformStore() {
$id: null,
name: null,
hostname: null,
key: null,
store: null,
type: null
});
}
@@ -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() {
@@ -21,8 +21,8 @@
projectId,
$createPlatform.$id,
$createPlatform.name,
undefined,
undefined,
$createPlatform.key,
$createPlatform.store,
$createPlatform.hostname
);
@@ -12,8 +12,12 @@
let method: Method = Method.NPM;
const example1 = `import { Appwrite } from 'appwrite';`;
const example2 = `<script src="https://cdn.jsdelivr.net/npm/appwrite@${$versions['client-web']}" />`;
const example1 = `import { Client } from 'appwrite';`;
const example2 =
`<script src="https://cdn.jsdelivr.net/npm/appwrite@${$versions['client-web']}" />
<script>
const { Client } = Appwrite;
</script` + `>`; // Prevent svelte from processing the closing script tag
</script>
<WizardStep>
@@ -4,9 +4,9 @@
import { sdkForProject } from '$lib/stores/sdk';
const { endpoint, project } = sdkForProject.client.config;
const code = `const appwrite = new Appwrite();
const code = `const client = new Client();
appwrite
client
.setEndpoint('${endpoint}')
.setProject('${project}');`;
</script>
@@ -1,346 +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
);
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
);
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
);
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"
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">
@@ -24,6 +24,8 @@
import { Dependencies } from '$lib/constants';
import Delete from '../deleteBucket.svelte';
import { trackEvent } from '$lib/actions/analytics';
import { writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
let showDelete = false;
@@ -34,14 +36,21 @@
arePermsDisabled = true,
encryption: boolean = null,
antivirus: boolean = null,
maxSize: number;
let byteUnit: 'Bytes' | 'KB' | 'MB' | 'GB' = 'MB',
options = [
{ label: 'Bytes', value: 'Bytes' },
{ label: 'Kilobytes', value: 'KB' },
{ label: 'Megabytes', value: 'MB' },
{ label: 'Gigabytes', value: 'GB' }
];
maxSize: number,
compression: string = null;
let byteUnit = writable('MB');
let sizeInBytes: number = null;
const options = [
{ label: 'Bytes', value: 'Bytes' },
{ label: 'Kilobytes', value: 'KB' },
{ 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;
@@ -54,7 +63,10 @@
bucketPermissions ??= $bucket.$permissions;
encryption ??= $bucket.encryption;
antivirus ??= $bucket.antivirus;
maxSize = $bucket.maximumFileSize / 1024 / 1024;
compression ??= $bucket.compression;
});
$: if (bucketPermissions) {
if (symmetricDifference(bucketPermissions, $bucket.$permissions).length) {
arePermsDisabled = false;
@@ -66,78 +78,44 @@
} else isExtensionsDisabled = true;
}
async function toggleBucket() {
type TUpdateBucketMisc = {
successMessage?: string;
trackEventName: string;
trackEventData?: { value: boolean };
arePermsDisabled?: boolean;
};
async function updateBucket(updates: Partial<Models.Bucket>, misc: TUpdateBucketMisc) {
const values = { ...$bucket, ...updates };
try {
await sdkForProject.storage.updateBucket(
$bucket.$id,
$bucket.name,
undefined,
undefined,
enabled
values.$id,
values.name,
values.$permissions,
values.fileSecurity,
values.enabled,
values.maximumFileSize,
values.allowedFileExtensions,
values.compression,
values.encryption,
values.antivirus
);
invalidate(Dependencies.BUCKET);
if (misc.arePermsDisabled !== undefined) {
arePermsDisabled = misc.arePermsDisabled;
}
addNotification({
message: `${$bucket.name} has been updated`,
message: misc.successMessage ?? `${$bucket.name} has been updated`,
type: 'success'
});
trackEvent('submit_bucket_enable', {
value: enabled
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateName() {
try {
await sdkForProject.storage.updateBucket($bucket.$id, bucketName);
invalidate(Dependencies.BUCKET);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent('submit_bucket_update_name');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePermissions() {
try {
await sdkForProject.storage.updateBucket($bucket.$id, $bucket.name, bucketPermissions);
invalidate(Dependencies.BUCKET);
arePermsDisabled = true;
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent('submit_bucket_update_permissions');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateFileSecurity() {
try {
await sdkForProject.storage.updateBucket(
$bucket.$id,
$bucket.name,
$bucket.$permissions,
bucketFileSecurity
);
invalidate(Dependencies.BUCKET);
arePermsDisabled = true;
addNotification({
message: 'Security has been updated',
type: 'success'
});
trackEvent('submit_bucket_update_file_security');
if (misc.trackEventName) {
trackEvent(misc.trackEventName, misc.trackEventData);
}
} catch (error) {
addNotification({
message: error.message,
@@ -146,81 +124,118 @@
}
}
async function updateSecurity() {
try {
await sdkForProject.storage.updateBucket(
$bucket.$id,
$bucket.name,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
function toggleBucket() {
updateBucket(
{
enabled
},
{
trackEventName: 'submit_bucket_enable',
trackEventData: {
value: enabled
}
}
);
}
function updateName() {
updateBucket(
{
name: bucketName
},
{
successMessage: 'Name has been updated',
trackEventName: 'submit_bucket_update_name'
}
);
}
function updatePermissions() {
updateBucket(
{
$permissions: bucketPermissions
},
{
successMessage: 'Permissions have been updated',
trackEventName: 'submit_bucket_update_permissions',
arePermsDisabled: true
}
);
}
function updateFileSecurity() {
updateBucket(
{
fileSecurity: bucketFileSecurity
},
{
successMessage: 'Security has been updated',
trackEventName: 'submit_bucket_update_file_security',
arePermsDisabled: true
}
);
}
function updateSecurity() {
updateBucket(
{
encryption,
antivirus
);
invalidate(Dependencies.BUCKET);
addNotification({
message: `${$bucket.name} has been updated`,
type: 'success'
});
trackEvent('submit_bucket_update_security');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateMaxSize() {
let size = sizeToBytes(maxSize, byteUnit);
try {
await sdkForProject.storage.updateBucket(
$bucket.$id,
$bucket.name,
undefined,
undefined,
undefined,
size
);
invalidate(Dependencies.BUCKET);
addNotification({
message: `${$bucket.name} has been updated`,
type: 'success'
});
trackEvent('submit_bucket_update_size');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
},
{
trackEventName: 'submit_bucket_update_security'
}
);
}
async function updateAllowedExtensions() {
try {
await sdkForProject.storage.updateBucket(
$bucket.$id,
$bucket.name,
undefined,
undefined,
undefined,
undefined,
extensions
);
invalidate(Dependencies.BUCKET);
addNotification({
message: `${$bucket.name} has been updated`,
type: 'success'
});
trackEvent('submit_bucket_update_extensions');
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
function updateMaxSize() {
const size = sizeToBytes(maxSize, $byteUnit);
updateBucket(
{
maximumFileSize: size
},
{
trackEventName: 'submit_bucket_update_size'
}
);
}
function updateCompression() {
updateBucket(
{
compression
},
{
trackEventName: 'submit_bucket_update_size'
}
);
}
function updateAllowedExtensions() {
updateBucket(
{
allowedFileExtensions: extensions
},
{
trackEventName: 'submit_bucket_update_extensions'
}
);
}
byteUnit.subscribe((b) => {
if (b === 'Bytes') {
maxSize = sizeInBytes;
} else if (b === 'KB') {
maxSize = sizeInBytes / 1024;
} else if (b === 'MB') {
maxSize = sizeInBytes / 1024 / 1024;
} else if (b === 'GB') {
maxSize = sizeInBytes / 1024 / 1024 / 1024;
}
});
$: if (maxSize) {
sizeInBytes = sizeToBytes(maxSize, $byteUnit);
}
</script>
@@ -238,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>
@@ -279,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}
@@ -323,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>
@@ -384,7 +400,32 @@
<Button
disabled={encryption === $bucket.encryption &&
antivirus === $bucket.antivirus}
submit>Update</Button>
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>
@@ -392,20 +433,22 @@
<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
id="size"
label="Size"
placeholder="256"
placeholder={$bucket.maximumFileSize.toString()}
bind:value={maxSize} />
<InputSelect id="bytes" label="Bytes" {options} bind:value={byteUnit} />
<InputSelect id="bytes" label="Bytes" {options} bind:value={$byteUnit} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!maxSize} submit>Update</Button>
<Button disabled={sizeInBytes === $bucket.maximumFileSize} submit
>Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5466 17C20.686 16.7584 20.7727 16.4863 20.7727 16.2419V7.75808C20.7727 7.51367 20.6862 7.24157 20.5466 7.00001L11.8864 12L20.5466 17Z" fill="#00599C"/>
<path d="M12.6559 21.8167L20.0031 17.5748C20.2147 17.4526 20.4071 17.2416 20.5465 17L11.8863 12L3.22607 17.0001C3.36553 17.2416 3.55795 17.4527 3.76954 17.5748L11.1167 21.8167C11.54 22.0611 12.2326 22.0611 12.6559 21.8167Z" fill="#004482"/>
<path d="M20.5466 6.99999C20.4071 6.75838 20.2147 6.54737 20.0031 6.42517L12.6559 2.1833C12.2327 1.9389 11.54 1.9389 11.1168 2.1833L3.76957 6.42517C3.34628 6.66951 3 7.26937 3 7.75812V16.2419C3 16.4863 3.0866 16.7585 3.22611 17L11.8863 12L20.5466 6.99999Z" fill="#659AD2"/>
<path d="M11.8864 17.9242C8.61976 17.9242 5.96216 15.2666 5.96216 12C5.96216 8.73336 8.61976 6.07576 11.8864 6.07576C13.9943 6.07576 15.9601 7.21008 17.0167 9.03602L14.4529 10.5196C13.924 9.60567 12.9406 9.03787 11.8864 9.03787C10.253 9.03787 8.92427 10.3666 8.92427 12C8.92427 13.6333 10.253 14.9621 11.8864 14.9621C12.9407 14.9621 13.9241 14.3943 14.453 13.4802L17.0169 14.9637C15.9603 16.7898 13.9944 17.9242 11.8864 17.9242Z" fill="white"/>
<path d="M17.8106 11.671H17.1523V11.0128H16.4941V11.671H15.8358V12.3292H16.4941V12.9875H17.1523V12.3292H17.8106V11.671Z" fill="white"/>
<path d="M20.2791 11.671H19.6208V11.0128H18.9626V11.671H18.3043V12.3292H18.9626V12.9875H19.6208V12.3292H20.2791V11.671Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="21" height="9" viewBox="0 0 21 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5367 6.77066C1.44246 6.76892 1.35244 6.789 1.27079 6.82299L1.27077 6.82297L1.26809 6.82415C1.18619 6.86013 1.11066 6.91165 1.04778 6.97763C0.984416 7.04106 0.933719 7.11661 0.900246 7.20081L0.900241 7.2008L0.899816 7.2019C0.867868 7.28413 0.850324 7.37148 0.850116 7.46019M1.5367 6.77066C1.62683 6.77086 1.71694 6.78819 1.80221 6.82566C1.88344 6.86135 1.95839 6.91232 2.02096 6.97752C2.08783 7.04102 2.13744 7.11653 2.17282 7.19799L2.17349 7.19953L2.17348 7.19954C2.20882 7.28349 2.22627 7.37286 2.22627 7.4633C2.22627 7.55374 2.20882 7.64311 2.17348 7.72707C2.13908 7.80877 2.08878 7.88483 2.02167 7.94834C1.95873 8.01224 1.88363 8.06296 1.80079 8.09756L1.74298 7.95915L1.80079 8.09756C1.7175 8.13234 1.62807 8.15013 1.5378 8.1499M1.5367 6.77066C1.53724 6.77067 1.53778 6.77068 1.53832 6.7707L1.53515 6.92066V6.77066C1.53567 6.77066 1.53619 6.77066 1.5367 6.77066ZM1.5378 8.1499C1.44662 8.15149 1.35607 8.13418 1.27193 8.09904C1.1879 8.06394 1.11207 8.01181 1.0493 7.94594C0.985862 7.88244 0.935939 7.80764 0.901383 7.72555C0.866254 7.6421 0.848375 7.55224 0.850116 7.46019M1.5378 8.1499C1.53715 8.1499 1.5365 8.1499 1.53585 8.14989L1.53667 7.99989L1.53973 8.14986C1.53908 8.14988 1.53844 8.14989 1.5378 8.1499ZM0.850116 7.46019C0.850126 7.45965 0.850137 7.45912 0.850148 7.45858L1.00011 7.46179H0.850114C0.850114 7.46126 0.850115 7.46072 0.850116 7.46019ZM4.46673 7.90618V2.91575C4.46994 2.80356 4.47033 2.69143 4.46804 2.57937L7.96337 7.9876L8.0077 8.05618H8.08935H9.08495H9.23495V7.90618V1V0.85H9.08495H8.27631H8.12631V1V5.85487C8.12306 6.00128 8.12426 6.14793 8.12979 6.29447L4.68624 0.919087L4.64198 0.85H4.55993H3.5081H3.3581V1V7.90618V8.05618H3.5081H4.31673H4.46673V7.90618ZM14.7176 7.14829L14.7185 6.99739H14.5676H11.86V4.90464H14.2104H14.3604V4.75464V4.02608V3.87608H14.2104H11.86V1.88158H14.3928H14.5428V1.73158V1V0.85H14.3928H10.8816H10.7316V1V7.90618V8.05618H10.8816H14.563H14.7121L14.713 7.90708L14.7176 7.14829ZM15.1862 0.85H15.0362V1V1.73158V1.88158H15.1862H17.0274V7.90618V8.05618H17.1774H17.986H18.136V7.90618V1.88158H20H20.15V1.73158V1V0.85H20H15.1862Z" fill="white" stroke="white" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5466 6.99999C20.4071 6.75838 20.2147 6.54737 20.0031 6.42517L12.6559 2.1833C12.2327 1.9389 11.54 1.9389 11.1168 2.1833L3.76957 6.42517C3.34628 6.66951 3 7.26937 3 7.75812V16.2419C3 16.4863 3.0866 16.7585 3.22611 17L3.22607 17C3.36553 17.2416 3.55795 17.4527 3.76954 17.5748L11.1167 21.8167C11.54 22.0611 12.2326 22.0611 12.6559 21.8167L20.0031 17.5748C20.2147 17.4526 20.4071 17.2416 20.5465 17L15.76 14.2365L17.0169 14.9638C15.9603 16.7899 13.9944 17.9243 11.8864 17.9243C8.61976 17.9243 5.96216 15.2667 5.96216 12C5.96216 8.7334 8.61976 6.07581 11.8864 6.07581C13.9943 6.07581 15.9601 7.21012 17.0167 9.03606L14.4529 10.5197C13.924 9.60572 12.9406 9.03792 11.8864 9.03792C10.253 9.03792 8.92427 10.3667 8.92427 12C8.92427 13.6333 10.253 14.9622 11.8864 14.9622C12.9402 14.9622 13.9232 14.3949 14.4523 13.4815L11.8863 12L11.8864 12L14.4523 13.4815L14.453 13.4802L15.7254 14.2165L20.5466 17C20.686 16.7584 20.7727 16.4863 20.7727 16.2419V7.75807C20.7727 7.51367 20.6862 7.24156 20.5466 7L18.6993 8.06657L20.5466 6.99999ZM17.8106 11.6709H17.1523V11.0127H16.4941V11.6709H15.8358V12.3292H16.4941V12.9874H17.1523V12.3292H17.8106V11.6709ZM19.6208 11.0127V11.6709H20.2791V12.3292H19.6208V12.9874H18.9626V12.3292H18.3043V11.6709H18.9626V11.0127H19.6208Z" fill="#C4C6D7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.53673 13.7707C2.44248 13.7689 2.35245 13.789 2.27079 13.823L2.27078 13.823L2.26809 13.8241C2.18619 13.8601 2.11067 13.9117 2.04778 13.9776C1.98442 14.0411 1.93372 14.1166 1.90025 14.2008L1.90024 14.2008L1.89982 14.2019C1.86787 14.2841 1.85032 14.3715 1.85012 14.4602M2.53673 13.7707C2.62685 13.7709 2.71695 13.7882 2.80221 13.8257C2.88344 13.8613 2.9584 13.9123 3.02097 13.9775C3.08783 14.041 3.13744 14.1165 3.17282 14.198L3.17349 14.1995L3.17348 14.1995C3.20882 14.2835 3.22627 14.3729 3.22627 14.4633C3.22627 14.5537 3.20882 14.6431 3.17348 14.7271C3.13908 14.8088 3.08879 14.8848 3.02168 14.9483C2.95873 15.0122 2.88363 15.063 2.80079 15.0976C2.71749 15.1323 2.62805 15.1501 2.53777 15.1499C2.4466 15.1515 2.35607 15.1342 2.27193 15.099C2.1879 15.0639 2.11208 15.0118 2.0493 14.9459C1.98587 14.8824 1.93594 14.8076 1.90138 14.7256C1.86625 14.6421 1.84837 14.5522 1.85012 14.4602M2.53673 13.7707C2.53726 13.7707 2.53779 13.7707 2.53832 13.7707L2.53515 13.9207V13.7707C2.53568 13.7707 2.5362 13.7707 2.53673 13.7707ZM1.85012 14.4602C1.85013 14.4597 1.85014 14.4591 1.85015 14.4586L2.00011 14.4618H1.85011C1.85011 14.4613 1.85011 14.4607 1.85012 14.4602ZM5.46673 14.9062V9.91575C5.46994 9.80356 5.47033 9.69143 5.46804 9.57937L8.96337 14.9876L9.0077 15.0562H9.08935H10.0849H10.2349V14.9062V8V7.85H10.0849H9.27631H9.12631V8V12.8549C9.12306 13.0013 9.12426 13.1479 9.12979 13.2945L5.68624 7.91909L5.64198 7.85H5.55993H4.5081H4.3581V8V14.9062V15.0562H4.5081H5.31673H5.46673V14.9062ZM15.7176 14.1483L15.7185 13.9974H15.5676H12.86V11.9046H15.2104H15.3604V11.7546V11.0261V10.8761H15.2104H12.86V8.88158H15.3928H15.5428V8.73158V8V7.85H15.3928H11.8816H11.7316V8V14.9062V15.0562H11.8816H15.563H15.7121L15.713 14.9071L15.7176 14.1483ZM16.1862 7.85H16.0362V8V8.73158V8.88158H16.1862H18.0274V14.9062V15.0562H18.1774H18.986H19.136V14.9062V8.88158H21H21.15V8.73158V8V7.85H21H16.1862Z" fill="#C4C6D7" stroke="#C4C6D7" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5466 17C20.686 16.7584 20.7727 16.4863 20.7727 16.2419V7.75807C20.7727 7.51367 20.6862 7.24156 20.5466 7L11.8864 12L20.5466 17Z" fill="#00599C"/>
<path d="M12.6559 21.8167L20.0031 17.5748C20.2147 17.4526 20.4071 17.2416 20.5465 17L11.8863 12L3.22607 17C3.36553 17.2416 3.55795 17.4527 3.76954 17.5748L11.1167 21.8167C11.54 22.0611 12.2326 22.0611 12.6559 21.8167Z" fill="#004482"/>
<path d="M20.5466 6.99999C20.4071 6.75838 20.2147 6.54737 20.0031 6.42517L12.6559 2.1833C12.2327 1.9389 11.54 1.9389 11.1168 2.1833L3.76957 6.42517C3.34628 6.66951 3 7.26937 3 7.75812V16.2419C3 16.4863 3.0866 16.7585 3.22611 17L11.8863 12L20.5466 6.99999Z" fill="#659AD2"/>
<path d="M11.8864 17.9242C8.61976 17.9242 5.96216 15.2666 5.96216 12C5.96216 8.73334 8.61976 6.07574 11.8864 6.07574C13.9943 6.07574 15.9601 7.21006 17.0167 9.036L14.4529 10.5196C13.924 9.60566 12.9406 9.03786 11.8864 9.03786C10.253 9.03786 8.92427 10.3666 8.92427 12C8.92427 13.6333 10.253 14.9621 11.8864 14.9621C12.9407 14.9621 13.9241 14.3943 14.453 13.4802L17.0169 14.9637C15.9603 16.7898 13.9944 17.9242 11.8864 17.9242Z" fill="white"/>
<path d="M17.8106 11.671H17.1523V11.0128H16.4941V11.671H15.8358V12.3292H16.4941V12.9875H17.1523V12.3292H17.8106V11.671Z" fill="white"/>
<path d="M20.2791 11.671H19.6208V11.0128H18.9626V11.671H18.3043V12.3292H18.9626V12.9875H19.6208V12.3292H20.2791V11.671Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="21" height="9" viewBox="0 0 21 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5367 6.77066C1.44246 6.76892 1.35244 6.789 1.27079 6.82299L1.27077 6.82297L1.26809 6.82415C1.18619 6.86013 1.11066 6.91165 1.04778 6.97763C0.984416 7.04106 0.933719 7.11661 0.900246 7.20081L0.900241 7.2008L0.899816 7.2019C0.867868 7.28413 0.850324 7.37148 0.850116 7.46019M1.5367 6.77066C1.62683 6.77086 1.71694 6.78819 1.80221 6.82566C1.88344 6.86135 1.95839 6.91232 2.02096 6.97752C2.08783 7.04102 2.13744 7.11653 2.17282 7.19799L2.17349 7.19953L2.17348 7.19954C2.20882 7.28349 2.22627 7.37286 2.22627 7.4633C2.22627 7.55374 2.20882 7.64311 2.17348 7.72707C2.13908 7.80877 2.08878 7.88483 2.02167 7.94834C1.95873 8.01224 1.88363 8.06296 1.80079 8.09756L1.74298 7.95915L1.80079 8.09756C1.7175 8.13234 1.62807 8.15013 1.5378 8.1499M1.5367 6.77066C1.53724 6.77067 1.53778 6.77068 1.53832 6.7707L1.53515 6.92066V6.77066C1.53567 6.77066 1.53619 6.77066 1.5367 6.77066ZM1.5378 8.1499C1.44662 8.15149 1.35607 8.13418 1.27193 8.09904C1.1879 8.06394 1.11207 8.01181 1.0493 7.94594C0.985862 7.88244 0.935939 7.80764 0.901383 7.72555C0.866254 7.6421 0.848375 7.55224 0.850116 7.46019M1.5378 8.1499C1.53715 8.1499 1.5365 8.1499 1.53585 8.14989L1.53667 7.99989L1.53973 8.14986C1.53908 8.14988 1.53844 8.14989 1.5378 8.1499ZM0.850116 7.46019C0.850126 7.45965 0.850137 7.45912 0.850148 7.45858L1.00011 7.46179H0.850114C0.850114 7.46126 0.850115 7.46072 0.850116 7.46019ZM4.46673 7.90618V2.91575C4.46994 2.80356 4.47033 2.69143 4.46804 2.57937L7.96337 7.9876L8.0077 8.05618H8.08935H9.08495H9.23495V7.90618V1V0.85H9.08495H8.27631H8.12631V1V5.85487C8.12306 6.00128 8.12426 6.14793 8.12979 6.29447L4.68624 0.919087L4.64198 0.85H4.55993H3.5081H3.3581V1V7.90618V8.05618H3.5081H4.31673H4.46673V7.90618ZM14.7176 7.14829L14.7185 6.99739H14.5676H11.86V4.90464H14.2104H14.3604V4.75464V4.02608V3.87608H14.2104H11.86V1.88158H14.3928H14.5428V1.73158V1V0.85H14.3928H10.8816H10.7316V1V7.90618V8.05618H10.8816H14.563H14.7121L14.713 7.90708L14.7176 7.14829ZM15.1862 0.85H15.0362V1V1.73158V1.88158H15.1862H17.0274V7.90618V8.05618H17.1774H17.986H18.136V7.90618V1.88158H20H20.15V1.73158V1V0.85H20H15.1862Z" fill="#512BD4" stroke="#512BD4" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5466 6.99999C20.4071 6.75838 20.2147 6.54737 20.0031 6.42517L12.6559 2.1833C12.2327 1.9389 11.54 1.9389 11.1168 2.1833L3.76957 6.42517C3.34628 6.66951 3 7.26937 3 7.75812V16.2419C3 16.4863 3.0866 16.7585 3.22611 17L3.22607 17C3.36553 17.2416 3.55795 17.4527 3.76954 17.5748L11.1167 21.8167C11.54 22.0611 12.2326 22.0611 12.6559 21.8167L20.0031 17.5748C20.2147 17.4526 20.4071 17.2416 20.5465 17L15.76 14.2365L17.0169 14.9638C15.9603 16.7899 13.9944 17.9243 11.8864 17.9243C8.61976 17.9243 5.96216 15.2667 5.96216 12C5.96216 8.7334 8.61976 6.07581 11.8864 6.07581C13.9943 6.07581 15.9601 7.21012 17.0167 9.03606L14.4529 10.5197C13.924 9.60572 12.9406 9.03792 11.8864 9.03792C10.253 9.03792 8.92427 10.3667 8.92427 12C8.92427 13.6333 10.253 14.9622 11.8864 14.9622C12.9402 14.9622 13.9232 14.3949 14.4523 13.4815L11.8863 12L11.8864 12L14.4523 13.4815L14.453 13.4802L15.7254 14.2165L20.5466 17C20.686 16.7584 20.7727 16.4863 20.7727 16.2419V7.75807C20.7727 7.51367 20.6862 7.24156 20.5466 7L18.6993 8.06657L20.5466 6.99999ZM17.8106 11.6709H17.1523V11.0127H16.4941V11.6709H15.8358V12.3292H16.4941V12.9874H17.1523V12.3292H17.8106V11.6709ZM19.6208 11.0127V11.6709H20.2791V12.3292H19.6208V12.9874H18.9626V12.3292H18.3043V11.6709H18.9626V11.0127H19.6208Z" fill="#373B4D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.53673 13.7707C2.44248 13.7689 2.35245 13.789 2.27079 13.823L2.27078 13.823L2.26809 13.8241C2.18619 13.8601 2.11067 13.9117 2.04778 13.9776C1.98442 14.0411 1.93372 14.1166 1.90025 14.2008L1.90024 14.2008L1.89982 14.2019C1.86787 14.2841 1.85032 14.3715 1.85012 14.4602M2.53673 13.7707C2.62685 13.7709 2.71695 13.7882 2.80221 13.8257C2.88344 13.8613 2.9584 13.9123 3.02097 13.9775C3.08783 14.041 3.13744 14.1165 3.17282 14.198L3.17349 14.1995L3.17348 14.1995C3.20882 14.2835 3.22627 14.3729 3.22627 14.4633C3.22627 14.5537 3.20882 14.6431 3.17348 14.7271C3.13908 14.8088 3.08879 14.8848 3.02168 14.9483C2.95873 15.0122 2.88363 15.063 2.80079 15.0976C2.71749 15.1323 2.62805 15.1501 2.53777 15.1499C2.4466 15.1515 2.35607 15.1342 2.27193 15.099C2.1879 15.0639 2.11208 15.0118 2.0493 14.9459C1.98587 14.8824 1.93594 14.8076 1.90138 14.7256C1.86625 14.6421 1.84837 14.5522 1.85012 14.4602M2.53673 13.7707C2.53726 13.7707 2.53779 13.7707 2.53832 13.7707L2.53515 13.9207V13.7707C2.53568 13.7707 2.5362 13.7707 2.53673 13.7707ZM1.85012 14.4602C1.85013 14.4597 1.85014 14.4591 1.85015 14.4586L2.00011 14.4618H1.85011C1.85011 14.4613 1.85011 14.4607 1.85012 14.4602ZM5.46673 14.9062V9.91575C5.46994 9.80356 5.47033 9.69143 5.46804 9.57937L8.96337 14.9876L9.0077 15.0562H9.08935H10.0849H10.2349V14.9062V8V7.85H10.0849H9.27631H9.12631V8V12.8549C9.12306 13.0013 9.12426 13.1479 9.12979 13.2945L5.68624 7.91909L5.64198 7.85H5.55993H4.5081H4.3581V8V14.9062V15.0562H4.5081H5.31673H5.46673V14.9062ZM15.7176 14.1483L15.7185 13.9974H15.5676H12.86V11.9046H15.2104H15.3604V11.7546V11.0261V10.8761H15.2104H12.86V8.88158H15.3928H15.5428V8.73158V8V7.85H15.3928H11.8816H11.7316V8V14.9062V15.0562H11.8816H15.563H15.7121L15.713 14.9071L15.7176 14.1483ZM16.1862 7.85H16.0362V8V8.73158V8.88158H16.1862H18.0274V14.9062V15.0562H18.1774H18.986H19.136V14.9062V8.88158H21H21.15V8.73158V8V7.85H21H16.1862Z" fill="#373B4D" stroke="#373B4D" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB