Merge branch 'main' of https://github.com/appwrite/appwrite-console-poc into refactor-layout-state

This commit is contained in:
Torsten Dittmann
2022-10-24 10:49:12 +02:00
139 changed files with 4409 additions and 797 deletions
+14 -14
View File
@@ -9,8 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
"@aw-labs/icons": "0.0.0-63",
"@aw-labs/ui": "0.0.0-63",
"@aw-labs/icons": "0.0.0-64",
"@aw-labs/ui": "0.0.0-64",
"echarts": "^5.4.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
@@ -79,14 +79,14 @@
}
},
"node_modules/@aw-labs/icons": {
"version": "0.0.0-63",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-63.tgz",
"integrity": "sha512-aSVriQp0x2cfyhld3LodKrDBZwkS/rOErjvQtR5HWmrYTjDNm21dmehfg6rVhV5Al8Bvpd82HihAWdTVYP5cwA=="
"version": "0.0.0-64",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-64.tgz",
"integrity": "sha512-oSZptwIflTmoaUwotXS5sOQsrYE+aDHVw7/iI+lvC6bGDg1sljKc2/LrevXX2Z3GTG8Wu27aYHVVgVFB5khSPw=="
},
"node_modules/@aw-labs/ui": {
"version": "0.0.0-63",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-63.tgz",
"integrity": "sha512-+lNFpbMBT/AU2sVefN6gmBJE06Z1L93iqpr6B4Xl8T5YxDwrAeoxjTB5I50vVhughN/JkYxTJd+rMOCSOfh/DQ==",
"version": "0.0.0-64",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-64.tgz",
"integrity": "sha512-s6ntXWKssiUjaberQEZYBFQw7+Ipfn8BOcxJ4DqCMSnvllZvnx9HbPynvKWcSpYgRRUr6wUHrEKBTfKQah1SGA==",
"dependencies": {
"@aw-labs/icons": "*",
"normalize.css": "^8.0.1",
@@ -7789,14 +7789,14 @@
}
},
"@aw-labs/icons": {
"version": "0.0.0-63",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-63.tgz",
"integrity": "sha512-aSVriQp0x2cfyhld3LodKrDBZwkS/rOErjvQtR5HWmrYTjDNm21dmehfg6rVhV5Al8Bvpd82HihAWdTVYP5cwA=="
"version": "0.0.0-64",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-64.tgz",
"integrity": "sha512-oSZptwIflTmoaUwotXS5sOQsrYE+aDHVw7/iI+lvC6bGDg1sljKc2/LrevXX2Z3GTG8Wu27aYHVVgVFB5khSPw=="
},
"@aw-labs/ui": {
"version": "0.0.0-63",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-63.tgz",
"integrity": "sha512-+lNFpbMBT/AU2sVefN6gmBJE06Z1L93iqpr6B4Xl8T5YxDwrAeoxjTB5I50vVhughN/JkYxTJd+rMOCSOfh/DQ==",
"version": "0.0.0-64",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-64.tgz",
"integrity": "sha512-s6ntXWKssiUjaberQEZYBFQw7+Ipfn8BOcxJ4DqCMSnvllZvnx9HbPynvKWcSpYgRRUr6wUHrEKBTfKQah1SGA==",
"requires": {
"@aw-labs/icons": "*",
"normalize.css": "^8.0.1",
+2 -2
View File
@@ -19,8 +19,8 @@
},
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
"@aw-labs/icons": "0.0.0-63",
"@aw-labs/ui": "0.0.0-63",
"@aw-labs/icons": "0.0.0-64",
"@aw-labs/ui": "0.0.0-64",
"echarts": "^5.4.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
+5 -4
View File
@@ -1,15 +1,16 @@
<script lang="ts">
import Avatar from './avatar.svelte';
export let avatars = [];
import AvatarInitials from './avatarInitials.svelte';
export let avatars: string[] = [];
export let total = avatars.length;
export let size = 40;
</script>
<ul class="avatars-group">
{#each avatars as av, index}
{#each avatars as name, index}
{#if index < 2}
<li class="avatars-group-item">
<Avatar {size} src={av.img} name={av.name} />
<AvatarInitials {size} {name} />
</li>
{/if}
{/each}
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import { sdkForConsole } from '$lib/stores/sdk';
import Avatar from './avatar.svelte';
export let name: string;
export let size: number;
$: src = sdkForConsole.avatars.getInitials(name, size * 2, size * 2).toString();
</script>
<Avatar {name} {size} {src} />
+5 -3
View File
@@ -20,6 +20,7 @@
export let language: 'js' | 'html' | 'dart' | 'kotlin' | 'json' | 'sh' | 'yml' | 'swift';
export let withLineNumbers = false;
export let withCopy = false;
export let noMargin = false;
Prism.plugins.customClass.prefix('prism-');
@@ -28,7 +29,7 @@
});
</script>
<section class="box u-overflow-hidden common-section">
<section class="box u-overflow-hidden " class:common-section={!noMargin}>
<div
class="controls u-position-absolute u-inset-inline-end-8 u-inset-block-start-8 u-flex u-gap-8">
{#if label}
@@ -48,8 +49,9 @@
{/if}
</div>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}><code
>{code}</code></pre>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}>
<code>{code}</code>
</pre>
</section>
<style lang="scss" global>
+2 -1
View File
@@ -10,7 +10,8 @@
{#if isButton}
<button
on:click
on:click|preventDefault
type="button"
class="card u-grid u-cross-center u-width-full-line dashed"
class:common-section={single}>
{#if single}
+308
View File
@@ -0,0 +1,308 @@
<script lang="ts">
import { Button, Form } from '$lib/elements/forms';
import { Modal, Copy } from '$lib/components';
import { Pill } from '$lib/elements';
import { createEventDispatcher } from 'svelte';
export let show = false;
let selectedService: typeof services[0] = null;
let selectedRequest: typeof selectedService['requests'][0] = null;
let selectedEvent: string = null;
let selectedAttribute: string = null;
let helper: string = null;
let inputData: string = null;
let showInput = false;
const dispatch = createEventDispatcher();
function create() {
show = false;
dispatch('created', copyValue);
}
const events = ['create', 'update', 'delete'];
const services = [
{
name: 'buckets',
requests: [{ name: 'files', events }],
events
},
{
name: 'databases',
requests: [
{ name: 'collections', events },
{ name: 'documents', events }
],
events
},
{
name: 'functions',
requests: [
{ name: 'deployments', events },
{ name: 'executions', events }
],
events
},
{
name: 'teams',
requests: [{ name: 'memberships', events }],
events
},
{
name: 'users',
requests: [
{ name: 'recovery', events: ['create', 'delete'] },
{ name: 'sessions', events: ['create', 'delete'] },
{ name: 'verifications', events: ['create', 'delete'] }
],
events,
attributes: ['email', 'name', 'password', 'status', 'prefs']
}
];
function createEventString(
service: typeof selectedService,
request: typeof selectedRequest,
event: string,
attribute: string
) {
const data = new Map();
//SERVICE
if (service) {
data.set('service', { value: service.name, description: 'service' });
data.set('serviceId', {
value: '*',
description: `ID of ${service?.name.slice(0, -1)}`
});
}
//REQUEST
if (request) {
if (request.name === 'documents') {
data.set('request', {
value: 'collections',
description: 'request'
});
data.set('requestId', {
value: '*',
description: `ID of collection`
});
data.set('secondaryRequest', {
value: 'documents',
description: 'secondary request'
});
data.set('secondaryRequestId', {
value: '*',
description: `ID of document`
});
} else {
data.set('request', {
value: request.name,
description: `request`
});
data.set('requestId', {
value: '*',
description: `ID of ${request?.name.slice(0, -1)}`
});
}
}
//EVENT
if ((event && request?.events?.includes(event)) ?? service?.events?.includes(event)) {
data.set('event', { value: event, description: 'event' });
}
//ATTRIBUTE
if (attribute && !request) {
data.set('attribute', { value: attribute, description: 'attribute' });
} else if (event && service.name === 'users' && !request) {
data.set('attribute', { value: '*', description: `attribute` });
}
return data;
}
$: eventString = createEventString(
selectedService,
selectedRequest,
selectedEvent,
selectedAttribute
);
$: copyValue =
inputData ??
Array.from(eventString.values())
.map((d) => d.value)
.join('.');
$: if (selectedService) {
selectedRequest = null;
selectedEvent = null;
selectedAttribute = null;
helper = null;
}
$: if (!showInput) {
helper = null;
}
$: if (!show) {
selectedService = null;
selectedRequest = null;
selectedEvent = null;
selectedAttribute = null;
helper = null;
inputData = null;
showInput = false;
}
//TODO: remove inline style
</script>
<Form noStyle noMargin on:submit={create}>
<Modal bind:show size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<div>
<p class="u-text">Choose a service</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each services as service}
<Pill
disabled={showInput}
selected={service.name === selectedService?.name}
button
on:click={() => {
selectedService = service;
inputData = null;
}}>{service.name}</Pill>
{/each}
</div>
</div>
{#if selectedService}
<div>
<p class="u-text">Choose a request (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedService.requests as request}
<Pill
disabled={showInput}
selected={request.name === selectedRequest?.name}
button
on:click={() => {
selectedRequest === request
? (selectedRequest = null)
: (selectedRequest = request);
inputData = null;
}}>{request.name}</Pill>
{/each}
</div>
</div>
<div>
<p class="u-text">Choose an event (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedRequest?.events ?? selectedService?.events as event}
<Pill
disabled={showInput}
selected={selectedEvent === event}
button
on:click={() => {
selectedEvent === event
? (selectedEvent = null)
: (selectedEvent = event);
inputData = null;
}}>{event}</Pill>
{/each}
</div>
</div>
{#if selectedService?.name === 'users' && !selectedRequest?.name}
<div>
<p class="u-text">Choose an attribute (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedService?.attributes as attribute}
<Pill
disabled={showInput}
selected={selectedAttribute === attribute}
button
on:click={() => {
selectedAttribute === attribute
? (selectedAttribute = null)
: (selectedAttribute = attribute);
inputData = null;
}}>{attribute}</Pill>
{/each}
</div>
</div>
{/if}
{/if}
{#if showInput}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
<div class="options-list">
<button
on:click|preventDefault={() => {
showInput = false;
}}
class="options-list-button"
aria-label="confirm">
<span class="icon-check" aria-hidden="true" />
</button>
<button
on:click|preventDefault={() => {
inputData = null;
showInput = false;
}}
class="options-list-button"
aria-label="cancel">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</div>
{:else}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<div type="text" readonly style="min-height: 2.5rem;">
{#if inputData}
<span>{inputData}</span>
{:else}
{#each Array.from(eventString.values()) as route, i}
<button
class:u-opacity-0-5={helper !== route.description}
on:mouseenter={() => {
helper = route.description;
}}
on:mouseleave={() => {
helper = null;
}}>
{route.value}
</button>
<span class="u-opacity-0-5">
{i + 1 < eventString?.size ? '.' : ''}
</span>
{/each}
{/if}
</div>
<div class="options-list">
<button
on:click|preventDefault={() => {
inputData = copyValue;
showInput = true;
}}
class="options-list-button"
aria-label="edit event">
<span class="icon-pencil" aria-hidden="true" />
</button>
<button
disabled={!copyValue}
class="options-list-button"
aria-label="copy text">
<Copy value={copyValue}>
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
</button>
</div>
<p style="height: 2rem;">{helper ?? ''}</p>
</div>
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button disabled={showInput || !copyValue} submit>Create</Button>
</svelte:fragment>
</Modal>
</Form>
+3
View File
@@ -22,6 +22,7 @@ export { default as CollapsibleItem } from './collapsibleItem.svelte';
export { default as DropTabs } from './dropTabs.svelte';
export { default as DropTabsItem } from './dropTabsItem.svelte';
export { default as Avatar } from './avatar.svelte';
export { default as AvatarInitials } from './avatarInitials.svelte';
export { default as AvatarGroup } from './avatarGroup.svelte';
export { default as Alert } from './alert.svelte';
export { default as Box } from './box.svelte';
@@ -36,4 +37,6 @@ export { default as Secret } from './secret.svelte';
export { default as Trim } from './trim.svelte';
export { default as Tabs } from './tabs.svelte';
export { default as Tab } from './tab.svelte';
export { default as EventModal } from './eventModal.svelte';
export { default as Status } from './status.svelte';
export { default as Heading } from './heading.svelte';
+21 -19
View File
@@ -25,24 +25,26 @@
$: disabled = !value || $groups.has(value);
</script>
<Form on:submit={create} noMargin>
<Modal bind:show on:close={reset}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
{#if show}
<Form noMargin noStyle on:submit={create}>
<Modal bind:show on:close={reset}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<FormList>
<InputText
showLabel={false}
id="custom-permission"
label="Custom permission"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
</FormList>
<FormList>
<InputText
showLabel={false}
id="custom-permission"
label="Custom permission"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
</FormList>
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
{/if}
+2
View File
@@ -1 +1,3 @@
export { default as Permissions } from './permissions.svelte';
export { default as Team } from './team.svelte';
export { default as User } from './user.svelte';
@@ -22,11 +22,14 @@
import { difference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { Empty } from '../';
import Actions from './actions.svelte';
import Row from './row.svelte';
export let withCreate = false;
export let withCrud = true;
export let permissions: string[] = [];
export let executeAccess: string[] = [];
let showUser = false;
let showTeam = false;
@@ -142,74 +145,99 @@
return a.localeCompare(b);
}
$: if (executeAccess.length && !permissions.length) {
executeAccess.forEach((permission) => {
addRole(permission);
});
}
$: if ([...$groups]?.length && !permissions.length) {
executeAccess = [...$groups].sort(sortRoles).map(([role]) => role);
}
//TODO: click on Empty components opens the dropdown
</script>
<Table noMargin noStyles noMobile>
<TableHeader>
<TableCellHead>Role</TableCellHead>
{#if withCreate}
<TableCellHead width={70}>Create</TableCellHead>
{/if}
<TableCellHead width={70}>Read</TableCellHead>
<TableCellHead width={70}>Update</TableCellHead>
<TableCellHead width={70}>Delete</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each [...$groups].sort(sortRoles) as [role, permission]}
<TableRow>
<TableCellText title="Role">
<Row {role} />
</TableCellText>
{#if [...$groups]?.length}
<Table noMargin noStyles noMobile>
<TableHeader>
<TableCellHead>Role</TableCellHead>
{#if withCrud}
{#if withCreate}
<TableCell title="Create">
<input
type="checkbox"
class="icon-check"
aria-label="Create"
checked={permission.create}
on:change={() => togglePermission(role, 'create')} />
</TableCell>
<TableCellHead width={70}>Create</TableCellHead>
{/if}
<TableCell title="Read">
<input
type="checkbox"
class="icon-check"
aria-label="Read"
checked={permission.create}
on:change={() => togglePermission(role, 'read')} />
</TableCell>
<TableCell title="Update">
<input
type="checkbox"
class="icon-check"
aria-label="Update"
checked={permission.create}
on:change={() => togglePermission(role, 'update')} />
</TableCell>
<TableCell title="Delete">
<input
type="checkbox"
class="icon-check"
aria-label="Delete"
checked={permission.create}
on:change={() => togglePermission(role, 'delete')} />
</TableCell>
<TableCellText title="Remove">
<div class="u-flex">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => deleteRole(role)}>
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<TableCellHead width={70}>Read</TableCellHead>
<TableCellHead width={70}>Update</TableCellHead>
<TableCellHead width={70}>Delete</TableCellHead>
{/if}
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each [...$groups].sort(sortRoles) as [role, permission]}
<TableRow>
<TableCellText title="Role">
<Row {role} />
</TableCellText>
{#if withCrud}
{#if withCreate}
<TableCell title="Create">
<input
type="checkbox"
class="icon-check"
aria-label="Create"
checked={permission.create}
on:change={() => togglePermission(role, 'create')} />
</TableCell>
{/if}
<TableCell title="Read">
<input
type="checkbox"
class="icon-check"
aria-label="Read"
checked={permission.read}
on:change={() => togglePermission(role, 'read')} />
</TableCell>
<TableCell title="Update">
<input
type="checkbox"
class="icon-check"
aria-label="Update"
checked={permission.update}
on:change={() => togglePermission(role, 'update')} />
</TableCell>
<TableCell title="Delete">
<input
type="checkbox"
class="icon-check"
aria-label="Delete"
checked={permission.delete}
on:change={() => togglePermission(role, 'delete')} />
</TableCell>
{/if}
<TableCell title="Remove" width={40}>
<div class="u-flex">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => deleteRole(role)}>
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<span class="eyebrow-heading-3 u-sep-block-end">ROLE</span>
<Empty isButton on:click={() => (showDropdown = !showDropdown)}>
Add a role to get started
</Empty>
{/if}
<Actions
bind:showCustom
+51 -51
View File
@@ -3,7 +3,7 @@
import { createEventDispatcher } from 'svelte';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import { Avatar, Modal, Pagination } from '..';
import { AvatarInitials, Modal, Pagination } from '..';
import type { Writable } from 'svelte/store';
import type { Permission } from './permissions.svelte';
@@ -58,54 +58,54 @@
}
</script>
<Form on:submit={create} noMargin>
<Modal bind:show on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<InputSearch bind:value={search} />
{#if results?.teams}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.teams as team (team.$id)}
{@const role = `team:${team.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
<input
id={team.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="Team">
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
<Avatar
src={sdkForProject.avatars
.getInitials(team.name, 64, 64)
.toString()}
size={32}
name={team.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{team.name}</div>
<div class="u-x-small">{team.$id}</div>
</div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
{#if show}
<Form noStyle noMargin on:submit={create}>
<Modal bind:show on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<InputSearch bind:value={search} />
{#if results?.teams}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.teams as team (team.$id)}
{@const role = `team:${team.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td
class="table-col"
data-title="Enabled"
style="--p-col-width:40">
<input
id={team.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="Team">
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
<AvatarInitials size={32} name={team.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{team.name}</div>
<div class="u-x-small">{team.$id}</div>
</div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {results?.total}</p>
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
</div>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {results?.total}</p>
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
</div>
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
{/if}
+3 -8
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button, Form, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { Avatar, Modal, Pagination } from '..';
import { AvatarInitials, Modal, Pagination } from '..';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import type { Writable } from 'svelte/store';
@@ -58,7 +58,7 @@
}
</script>
<Form on:submit={create} noMargin>
<Form noStyle noMargin on:submit={create}>
<Modal bind:show on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<InputSearch bind:value={search} />
@@ -82,12 +82,7 @@
</td>
<td class="table-col" data-title="User">
<label class="u-flex u-cross-center u-gap-8" for={user.$id}>
<Avatar
src={sdkForProject.avatars
.getInitials(user.name, 64, 64)
.toString()}
size={32}
name={user.name} />
<AvatarInitials size={32} name={user.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{user.name}</div>
<div class="u-x-small">{user.$id}</div>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
export let status:
| 'waiting'
| 'pending'
| 'failed'
| 'complete'
| 'processing'
| 'ready'
| 'building'
| string = null;
//TODO: Remove type string once SDK is updated
</script>
<div
class="status"
class:is-pending={status === 'pending'}
class:is-failed={status === 'failed'}
class:is-complete={status === 'completed' || status === 'ready'}
class:is-processing={status === 'processing' || status === 'building'}>
{#if status}
<span class="status-icon" />
{/if}
<span class="text u-margin-inline-end-16"><slot /></span>
</div>
-2
View File
@@ -6,8 +6,6 @@
export let currentSub = 0;
export let isSub = false;
export let step: { text: string; substeps?: { text: string }[] };
//TODO: remove inline styling
</script>
<li
+5 -1
View File
@@ -9,7 +9,11 @@
<span class="text"><slot /></span>
</a>
{:else}
<button class="tabs-button" on:click|preventDefault class:is-selected={selected}>
<button
type="button"
class="tabs-button"
on:click|preventDefault
class:is-selected={selected}>
<span class="text"><slot /></span>
</button>
{/if}
-1
View File
@@ -10,7 +10,6 @@
export let fullWidth = false;
export let ariaLabel: string = null;
export let noMargin = false;
//TODO: add option to add aria-label to buttons that are only icons
</script>
{#if href}
+2 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
export let noMargin = false;
export let noStyle = false;
</script>
<!-- svelte-ignore a11y-no-redundant-roles -->
<form role="form" class="form" class:common-section={!noMargin} on:submit|preventDefault>
<form role="form" class:form={!noStyle} class:common-section={!noMargin} on:submit|preventDefault>
<slot />
</form>
+2 -1
View File
@@ -11,7 +11,6 @@ export { default as InputNumber } from './inputNumber.svelte';
export { default as InputSwitch } from './inputSwitch.svelte';
export { default as InputTags } from './inputTags.svelte';
export { default as InputFile } from './inputFile.svelte';
export { default as InputCustomId } from './inputCustomId.svelte';
export { default as InputDateTime } from './inputDateTime.svelte';
export { default as InputSearch } from './inputSearch.svelte';
export { default as InputRadio } from './inputRadio.svelte';
@@ -19,4 +18,6 @@ export { default as InputSelect } from './inputSelect.svelte';
export { default as InputCheckbox } from './inputCheckbox.svelte';
export { default as InputChoice } from './inputChoice.svelte';
export { default as InputPhone } from './inputPhone.svelte';
export { default as InputCron } from './inputCron.svelte';
export { default as InputURL } from './inputURL.svelte';
export { default as Helper } from './helper.svelte';
+69
View File
@@ -0,0 +1,69 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper } from '.';
export let label: string;
export let showLabel = true;
export let id: string;
export let value: string = null;
export let placeholder = '* * * * *';
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let maxlength: number = null;
export let minlength: number = null;
export let step: number | 'any' = 1;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
if (element.validity.patternMismatch) {
error = 'Please enter a valid CRON expression';
return;
}
error = element.validationMessage;
};
$: if (value) {
error = null;
}
</script>
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<div class="input-text-wrapper">
<input
{id}
{placeholder}
{disabled}
{required}
{minlength}
{maxlength}
{readonly}
{step}
type="text"
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid} />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
@@ -1,49 +0,0 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { FormItem } from '.';
export let id: string;
export let label: string;
export let value = '';
export let placeholder = '';
export let required = false;
export let autofocus = false;
let element: HTMLInputElement;
let unique = true;
let bench = '';
const toggle = async () => {
unique = !unique;
if (unique) {
bench = value;
value = 'unique()';
} else {
value = bench;
await tick();
element.focus();
}
};
onMount(() => {
value = 'unique()';
if (element && autofocus) {
element.focus();
}
});
</script>
<FormItem>
<label class="label" for={id}>{label}</label>
<div class="input-text-wrapper is-with-end-button">
<input
{placeholder}
{required}
type="text"
class="input-text"
disabled={unique}
bind:value
bind:this={element} />
<button class="input-button" aria-label="Switch" on:click={toggle} type="button">
<span class:icon-edit={unique} class:icon-shuffle={!unique} aria-hidden="true" />
</button>
</div>
</FormItem>
+66 -11
View File
@@ -1,17 +1,72 @@
<script lang="ts">
import { FormItem } from '.';
import { Button } from '.';
export let id: string;
export let label: string;
export let showLabel = true;
export let label: string = null;
export let files: FileList;
export let required = false;
export let disabled = false;
export let list = new DataTransfer();
let input: HTMLInputElement;
let hovering = false;
function dropHandler(ev: DragEvent) {
hovering = false;
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') {
list.items.clear();
list.items.add(ev.dataTransfer.items[i].getAsFile());
files = list.files;
}
}
}
}
function dragOverHandler() {
hovering = true;
}
</script>
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<div class="input-text-wrapper">
<input {id} {disabled} {required} type="file" bind:files />
<input bind:files bind:this={input} type="file" style="display: none" />
<div>
{#if label}
<p class="text">{label}</p>
{/if}
<div
class="card is-border-dashed is-no-shadow"
class:is-hover-with-file={hovering}
on:drop|preventDefault={dropHandler}
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">
<span class="icon-upload" aria-hidden="true" />
</div>
<div class="u-grid u-gap-16">
<p>Drag and drop files here to upload</p>
<div class="u-flex u-gap-8 ">
<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}
<button
on:click={() => (files = null)}
type="button"
class="x-button"
aria-label="remove file"
title="Remove file">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
{/if}
</div>
</div>
</div>
</div>
</FormItem>
</div>
+14 -4
View File
@@ -11,8 +11,8 @@
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let maxlength: number = null;
export let minlength: number = null;
export let min: number = null;
export let max: number = null;
export let step: number | 'any' = 1;
let element: HTMLInputElement;
@@ -32,6 +32,16 @@
return;
}
if (element.validity.rangeOverflow) {
error = `The value must be less than or equal to ${max}`;
return;
}
if (element.validity.rangeUnderflow) {
error = `The value must be greater than or equal to ${min}`;
return;
}
error = element.validationMessage;
};
@@ -48,8 +58,8 @@
{placeholder}
{disabled}
{required}
{minlength}
{maxlength}
{min}
{max}
{readonly}
{step}
type="number"
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper } from '.';
export let label: string;
export let showLabel = true;
export let id: string;
export let value = '';
export let placeholder = '';
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let maxlength: number = null;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.patternMismatch) {
error = 'Must be a valid URL';
return;
}
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
};
$: if (value) {
error = null;
}
</script>
<FormItem>
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
<div class="input-text-wrapper">
<input
{id}
{placeholder}
{disabled}
{required}
{maxlength}
{readonly}
type="url"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
on:invalid={handleInvalid} />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
+2
View File
@@ -1,10 +1,12 @@
export { default as Table } from './table.svelte';
export { default as TableScroll } from './tableScroll.svelte';
export { default as TableList } from './tableList.svelte';
export { default as TableBody } from './body.svelte';
export { default as TableHeader } from './header.svelte';
export { default as TableFooter } from './footer.svelte';
export { default as TableRow } from './row.svelte';
export { default as TableRowLink } from './rowLink.svelte';
export { default as TableRowButton } from './rowButton.svelte';
export { default as TableCell } from './cell.svelte';
export { default as TableCellHead } from './cellHead.svelte';
export { default as TableCellLink } from './cellLink.svelte';
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
</script>
<div class="table-row" role="row" on:keyup={clickOnEnter} on:click|preventDefault>
<slot />
</div>
+10
View File
@@ -0,0 +1,10 @@
<!-- TODO: is 100% height needed? -->
<div class="table-with-scroll">
<div class="table-wrapper">
<div class="table is-remove-outer-styles">
<ul class="table-thead">
<slot />
</ul>
</div>
</div>
</div>
+1 -1
View File
@@ -9,7 +9,7 @@ export const toLocaleDate = (datetime: string) => {
return date.toLocaleDateString('en', options);
};
export const toLocaleDateTime = (datetime: string) => {
export const toLocaleDateTime = (datetime: string | number) => {
const date = new Date(datetime);
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
+1
View File
@@ -24,6 +24,7 @@ export function humanFileSize(value: number): {
value: string;
unit: string;
} {
if (!value) return { value: 'No', unit: 'Bytes' };
const length = value.toString().length;
const unit =
length < 7
+25
View File
@@ -0,0 +1,25 @@
export function calculateTime(time: number) {
const milliseconds = Math.floor(time * 1000);
const seconds = Math.floor(time);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);
if (milliseconds < 1000) {
return milliseconds + 'ms';
} else if (seconds < 60) {
return `${seconds} s`;
} else if (minutes < 60) {
return `${minutes} m`;
} else if (hours < 24) {
return `${hours} h`;
} else if (days < 30) {
return `${days} d`;
} else if (months < 12) {
return `${months} M`;
} else {
return `${years} y`;
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

+2 -6
View File
@@ -1,9 +1,8 @@
<script lang="ts">
import { base } from '$app/paths';
import { onMount } from 'svelte';
import { Avatar, DropList, DropListItem, DropListLink } from '$lib/components';
import { AvatarInitials, DropList, DropListItem, DropListLink } from '$lib/components';
import { app } from '$lib/stores/app';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { breadcrumb } from '$lib/stores/layout';
@@ -52,10 +51,7 @@
arrow={false}
scrollable={true}>
<button class="user-profile-button" on:click={() => (showDropdown = !showDropdown)}>
<Avatar
size={40}
name={$user.name}
src={sdkForConsole.avatars.getInitials($user.name, 40, 40).toString()} />
<AvatarInitials size={40} name={$user.name} />
<span class="user-profile-info is-only-desktop">
<span class="name">{$user.name}</span>
{#if $organization}
+1
View File
@@ -6,6 +6,7 @@ export { default as Navigation } from './navigation.svelte';
export { default as Notification } from './notification.svelte';
export { default as Notifications } from './notifications.svelte';
export { default as Shell } from './shell.svelte';
export { default as Logs } from './logs.svelte';
export { default as Wizard } from './wizard.svelte';
export { default as WizardStep } from './wizardStep.svelte';
export { default as Breadcrumbs } from './breadcrumbs.svelte';
+189
View File
@@ -0,0 +1,189 @@
<script lang="ts">
import { toLocaleDateTime } from '$lib/helpers/date';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { log } from '$lib/stores/logs';
import { Status, Tab, Tabs } from '../components';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { sdkForConsole } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { calculateTime } from '$lib/helpers/timeConversion';
import type { Models } from '@aw-labs/appwrite-console';
let selectedTab: string;
let rawData: string;
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
if ('buildId' in data) {
selectedTab = 'logs';
rawData = `${sdkForConsole.client.config.endpoint}/functions/${$log.func.$id}/deployment/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
return true;
}
}
function isExecution(data: Models.Deployment | Models.Execution): data is Models.Execution {
if ('trigger' in data) {
selectedTab = 'response';
rawData = `${sdkForConsole.client.config.endpoint}/functions/${$log.func.$id}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
return true;
}
}
function scrollToTop() {
document
.getElementsByClassName('code-panel-content')[0]
.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
</script>
{#if $log.data}
<section class="cover-frame">
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
<h1 class="body-text-1" name="title">Function ID: {$log.func.$id}</h1>
<button on:click={() => ($log.show = false)} class="x-button" aria-label="close popup">
<span class="icon-x" aria-hidden="true" />
</button>
</header>
{#if isDeployment($log.data)}
{@const size = humanFileSize($log.data.size)}
<div class="cover-frame-content u-flex u-flex-vertical">
<div class="u-flex u-gap-16">
<div class="avatar is-size-large">
<img
height="28"
width="28"
src={`${base}/icons/${$app.themeInUse}/color/${
$log.func.runtime.split('-')[0]
}.svg`}
alt="technology" />
</div>
<div>
<h2 class="body-text-2">Deployment ID: {$log.data.$id}</h2>
<time class="u-block"
>Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
<div>Size: {size.value} {size.unit}</div>
</div>
<div class="status u-margin-inline-start-auto">
<Status status={$log.data.status}>{$log.data.status}</Status>
<time>TBI</time>
</div>
</div>
<div class="tabs u-margin-block-start-48 u-sep-block-end">
<Tabs>
<Tab
selected={selectedTab === 'logs'}
on:click={() => (selectedTab = 'logs')}>
Logs
</Tab>
<Tab
selected={selectedTab === 'errors'}
on:click={() => (selectedTab = 'errors')}>
Errors
</Tab>
</Tabs>
</div>
<div class="theme-dark u-stretch u-margin-block-start-32 u-overflow-hidden">
<section class="code-panel">
<header class="code-panel-header">
<div class="u-flex u-gap-16 u-margin-inline-start-auto">
<Button text external href={rawData}>
<span class="icon-external-link" aria-hidden="true" />
<span class="text">Raw data</span>
</Button>
<Button secondary on:click={scrollToTop}>
<span class="text">Scroll to top</span>
</Button>
</div>
</header>
{#if selectedTab === 'logs'}
<code class="code-panel-content">
{$log.data.buildStdout ?? 'No logs recorded'}
</code>
{:else}
<code class="code-panel-content">
{$log.data.buildStderr ?? 'No errors recorded'}
</code>
{/if}
</section>
</div>
</div>
{:else if isExecution($log.data)}
<div class="cover-frame-content u-flex u-flex-vertical">
<div class="u-flex u-gap-16">
<div class="avatar is-size-large">
<img
height="28"
width="28"
src={`${base}/icons/${$app.themeInUse}/color/${
$log.func.runtime.split('-')[0]
}.svg`}
alt="technology" />
</div>
<div>
<h2 class="body-text-2">Execution ID: {$log.data.$id}</h2>
<time class="u-block">
Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
</div>
<div>
<p>Triggered by: {$log.data.trigger}</p>
<p>Type: TBI</p>
</div>
<div class="status u-margin-inline-start-auto">
<Status status={$log.data.status}>{$log.data.status}</Status>
<time>{calculateTime($log.data.duration)}</time>
</div>
</div>
<div class="tabs u-margin-block-start-48 u-sep-block-end">
<Tabs>
<Tab
selected={selectedTab === 'response'}
on:click={() => (selectedTab = 'response')}>
Response
</Tab>
<Tab
selected={selectedTab === 'errors'}
on:click={() => (selectedTab = 'errors')}>
Errors
</Tab>
<Tab
selected={selectedTab === 'logs'}
on:click={() => (selectedTab = 'logs')}>
Logs
</Tab>
</Tabs>
</div>
<div class="theme-dark u-stretch u-margin-block-start-32 u-overflow-hidden">
<section class="code-panel">
<header class="code-panel-header">
<div class="u-flex u-gap-16 u-margin-inline-start-auto">
<Button text external href={rawData}>
<span class="icon-external-link" aria-hidden="true" />
<span class="text">Raw data</span>
</Button>
<Button secondary on:click={scrollToTop}>
<span class="text">Scroll to top</span>
</Button>
</div>
</header>
{#if selectedTab === 'logs'}
<code class="code-panel-content">
{$log.data.stdout ?? 'No logs recorded'}
</code>
{:else if selectedTab === 'errors'}
<code class="code-panel-content">
{$log.data.stderr ?? 'No errors recorded'}
</code>
{:else}
<code class="code-panel-content">
{$log.data.response ?? 'No response recorded'}
</code>
{/if}
</section>
</div>
</div>
{/if}
</section>
{/if}
+2 -1
View File
@@ -3,6 +3,7 @@
import { header } from '$lib/stores/layout';
import { browser } from '$app/environment';
import { wizard } from '$lib/stores/wizard';
import { log } from '$lib/stores/logs';
export let isOpen = false;
export let showSideNavigation = false;
@@ -32,7 +33,7 @@
class:grid-with-side={showSideNavigation}
class:grid={!showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show}>
class:u-hide={$wizard.show || $log.show}>
<header class="main-header">
<button
class:u-hide={!showSideNavigation}
+1 -1
View File
@@ -10,7 +10,7 @@
'flutter',
'apple',
'android',
'node_js',
'node',
'php',
'python',
'ruby',
+8 -8
View File
@@ -1,5 +1,13 @@
<script context="module" lang="ts">
export type UsagePeriods = '24h' | '30d' | '90d';
export function last(set: Models.Metric[]): Models.Metric | null {
return set.slice(-1)[0] ?? null;
}
export function total(set: Models.Metric[]): number {
return set.reduce((prev, curr) => prev + curr.value, 0);
}
</script>
<script lang="ts">
@@ -27,14 +35,6 @@
export let deletedMetadata: MetricMetadata;
export let range: UsagePeriods;
function last(set: Models.Metric[]): Models.Metric | null {
return set.slice(-1)[0] ?? null;
}
function total(set: Models.Metric[]): number {
return set.reduce((prev, curr) => prev + curr.value, 0);
}
</script>
<Container>
+1 -1
View File
@@ -105,7 +105,7 @@
<slot name="media" />
</div>
<div class="wizard-main">
<Form on:submit={submit}>
<Form noStyle on:submit={submit}>
{#each sortedSteps as [step, { component }]}
{#if currentStep === step}
<svelte:component this={component} />
+1 -1
View File
@@ -1,4 +1,4 @@
import { writable, readable, get } from 'svelte/store';
import { writable, readable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
export type Tab = {
+12
View File
@@ -0,0 +1,12 @@
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
export const log = writable<{
show: boolean;
func: Models.Function;
data: Models.Execution | Models.Deployment;
}>({
show: false,
func: null,
data: null
});
+17 -2
View File
@@ -5,9 +5,11 @@
import { organizationList, newOrgModal, redirectTo } from '$lib/stores/organization';
import Create from './_createOrganization.svelte';
import { page } from '$app/stores';
import { afterNavigate } from '$app/navigation';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import { Logs } from '$lib/layout';
import { log } from '$lib/stores/logs';
onMount(async () => {
if ($page.url.pathname === '/console' && !$newOrgModal) {
@@ -20,10 +22,19 @@
return await redirectTo();
}
});
beforeNavigate(() => {
$log.show = false;
});
$: if (!$log.show) {
$log.data = null;
$log.func = null;
}
</script>
<svelte:head>
<title>Appwrite - Console</title>
<title>Console - Appwrite</title>
</svelte:head>
<Shell
@@ -47,3 +58,7 @@
{#if $newOrgModal}
<Create bind:show={$newOrgModal} closable={!!$organizationList?.total} />
{/if}
{#if $log.show}
<Logs />
{/if}
+1 -1
View File
@@ -17,7 +17,7 @@
</script>
<svelte:head>
<title>Appwrite - User</title>
<title>User - Appwrite</title>
</svelte:head>
<slot />
+2 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button, Form, FormList, InputText, InputPassword } from '$lib/elements/forms';
import { CardGrid, Box, Avatar, Heading } from '$lib/components';
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { Container } from '$lib/layout';
import { onMount } from 'svelte';
import { user } from '$lib/stores/user';
@@ -21,8 +21,6 @@
email ??= $user.email;
});
const getAvatar = (name: string) => sdkForConsole.avatars.getInitials(name, 96, 96).toString();
async function updateName() {
try {
await sdkForConsole.account.updateName(name);
@@ -165,7 +163,7 @@
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
<Avatar size={48} name={$user.name} src={getAvatar($user.name)} />
<AvatarInitials size={48} name={$user.name} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold">{$user.name}</h6>
@@ -21,18 +21,9 @@
await organizationList.load();
});
const getAvatar = (name: string) => {
return sdkForConsole.avatars.getInitials(name, 80, 80).toString();
};
const getMemberships = async (teamId: string) => {
const memberships = await sdkForConsole.teams.listMemberships(teamId);
return memberships.memberships.map((team) => {
return {
name: team.userName,
img: getAvatar(team.userName)
};
});
return memberships.memberships.map((team) => team.userName);
};
let addOrganization = false;
@@ -38,7 +38,7 @@
</script>
<svelte:head>
<title>Appwrite - Organizations</title>
<title>Organizations - Appwrite</title>
</svelte:head>
{#if $organization}
@@ -8,7 +8,7 @@
TableCellText,
TableRow
} from '$lib/elements/table';
import { Avatar, Heading, Pagination } from '$lib/components';
import { AvatarInitials, Heading, Pagination } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
@@ -27,8 +27,6 @@
const url = `${$page.url.origin}/console/`;
const offset = createPersistentPagination($pageLimit);
const getAvatar = (name: string) =>
sdkForConsole.avatars.getInitials(name, 120, 120).toString();
const deleted = () =>
memberList.load(
$organization.$id,
@@ -83,10 +81,7 @@
<TableRow>
<TableCell title="Name">
<div class="u-flex u-gap-12 u-cross-center">
<Avatar
size={40}
src={getAvatar(member.userName)}
name={member.userName} />
<AvatarInitials size={40} name={member.userName} />
<span class="text u-trim">
{member.userName ? member.userName : 'n/a'}
</span>
@@ -36,12 +36,7 @@
memberList.subscribe((value) => {
if (value?.total > 0) {
avatarsTotal = value.total;
avatars = value.memberships.map((team) => {
return {
name: team.userName,
img: sdkForConsole.avatars.getInitials(team.userName, 120, 120).toString()
};
});
avatars = value.memberships.map((team) => team.userName);
}
});
</script>
@@ -17,7 +17,7 @@
</script>
<svelte:head>
<title>Appwrite - Users</title>
<title>Users - Appwrite</title>
</svelte:head>
<slot />
@@ -1,7 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { sdkForProject } from '$lib/stores/sdk';
import { Empty, EmptySearch, Pagination, Avatar, Copy, Search } from '$lib/components';
import { Empty, EmptySearch, Pagination, Copy, Search, AvatarInitials } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
@@ -29,7 +28,6 @@
const offset = createPersistentPagination($pageLimit);
const project = $page.params.project;
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 32, 32).toString();
const userCreated = async (event: CustomEvent<Models.User<Record<string, unknown>>>) => {
await goto(`${base}/console/project-${project}/authentication/user/${event.detail.$id}`);
};
@@ -60,7 +58,7 @@
<Table>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Identifier</TableCellHead>
<TableCellHead>Identifiers</TableCellHead>
<TableCellHead width={130}>Status</TableCellHead>
<TableCellHead width={100}>ID</TableCellHead>
<TableCellHead>Joined</TableCellHead>
@@ -73,10 +71,7 @@
<div class="u-flex u-gap-12 u-cross-center">
{#if user.email || user.phone}
{#if user.name}
<Avatar
size={32}
src={getAvatar(user.name)}
name={user.name} />
<AvatarInitials size={32} name={user.name} />
<span class="text u-trim">{user.name}</span>
{:else}
<div class="avatar is-size-small ">
@@ -91,7 +86,7 @@
{/if}
</div>
</TableCell>
<TableCellText title="Identifier">
<TableCellText title="Identifiers">
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
@@ -74,8 +74,8 @@
<Heading tag="h2" size="6">Users Limit</Heading>
<p>
Limit new users from signing up for your project, regardless of authentication method.
You can still create users and team memberships from your Appwrite console. <b>
The maximum limit is 10,000 users.</b>
You can still create users and team memberships from your Appwrite console.
<b> The maximum limit is 10,000 users.</b>
</p>
<svelte:fragment slot="aside">
@@ -121,6 +121,7 @@
name="limit"
id="limit"
class="input-text"
max="10000"
disabled={isLimited === 'unlimited'}
bind:value={newLimit} />
</div>
@@ -1,6 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { sdkForProject } from '$lib/stores/sdk';
import {
Table,
TableHeader,
@@ -11,7 +10,7 @@
TableCell
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
import { Empty, EmptySearch, Pagination, Avatar, Search } from '$lib/components';
import { Empty, EmptySearch, Pagination, Search, AvatarInitials } from '$lib/components';
import Create from '../_createTeam.svelte';
import { goto } from '$app/navigation';
import { event } from '$lib/actions/analytics';
@@ -28,7 +27,6 @@
const offset = createPersistentPagination($pageLimit);
const project = $page.params.project;
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 32, 32).toString();
const teamCreated = async (event: CustomEvent<Models.Team>) => {
await goto(`${base}/console/project-${project}/authentication/teams/${event.detail.$id}`);
};
@@ -68,7 +66,7 @@
href={`${base}/console/project-${project}/authentication/teams/${team.$id}`}>
<TableCell title="ID">
<div class="u-flex u-gap-12">
<Avatar size={32} name={team.name} src={getAvatar(team.name)} />
<AvatarInitials size={32} name={team.name} />
<span class="text u-trim">{team.name}</span>
</div>
</TableCell>
@@ -27,7 +27,7 @@
</script>
<svelte:head>
<title>Appwrite - Team</title>
<title>Team - Appwrite</title>
</svelte:head>
{#if $team}
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { Avatar, CardGrid, Box, Heading } from '$lib/components';
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { Container } from '$lib/layout';
import { Button, InputText, Form } from '$lib/elements/forms';
import { sdkForProject } from '$lib/stores/sdk';
@@ -10,8 +10,6 @@
import { team } from './store';
import DeleteTeam from './_deleteTeam.svelte';
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 48, 48).toString();
let showDelete = false;
let teamName: string = null;
@@ -39,7 +37,7 @@
<Container>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<Avatar size={48} name={$team.name} src={getAvatar($team.name)} />
<AvatarInitials size={48} name={$team.name} />
<div>
<Heading tag="h6" size="7">{$team.name}</Heading>
</div>
@@ -89,7 +87,7 @@
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
<Avatar size={48} name={$team.name} src={getAvatar($team.name)} />
<AvatarInitials size={48} name={$team.name} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold">{$team.name}</h6>
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { Pagination, Empty, EmptySearch, Avatar, Search } from '$lib/components';
import { Pagination, Empty, EmptySearch, Search, AvatarInitials } from '$lib/components';
import {
Table,
TableHeader,
@@ -14,7 +14,6 @@
import { Query, type Models } from '@aw-labs/appwrite-console';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { sdkForProject } from '$lib/stores/sdk';
import { toLocaleDateTime } from '$lib/helpers/date';
import { memberships } from '../store';
import CreateMember from '../_createMembership.svelte';
@@ -29,7 +28,6 @@
const project = $page.params.project;
const offset = createPersistentPagination($pageLimit);
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 32, 32).toString();
$: if (search) $offset = 0;
$: memberships.load(
@@ -79,10 +77,7 @@
href={`${base}/console/project-${project}/authentication/user/${membership.userId}`}>
<TableCellText title="Name">
<div class="u-flex u-gap-12">
<Avatar
size={32}
src={getAvatar(membership.userName)}
name={membership.userName} />
<AvatarInitials size={32} name={membership.userName} />
<span>{membership.userName ? membership.userName : 'n/a'}</span>
</div>
</TableCellText>
@@ -27,7 +27,7 @@
</script>
<svelte:head>
<title>Appwrite - User</title>
<title>User - Appwrite</title>
</svelte:head>
{#if $user}
@@ -1,7 +1,14 @@
<script lang="ts">
import { page } from '$app/stores';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Avatar, CardGrid, Box, DropList, DropListItem, Heading } from '$lib/components';
import {
CardGrid,
Box,
DropList,
DropListItem,
Heading,
AvatarInitials
} from '$lib/components';
import { Pill } from '$lib/elements';
import {
Button,
@@ -48,8 +55,6 @@
userPhone ??= $user.phone;
});
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 48, 48).toString();
async function updateVerificationEmail() {
showVerifcationDropdown = false;
try {
@@ -190,7 +195,7 @@
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
{#if $user.email || $user.phone}
{#if $user.name}
<Avatar size={48} src={getAvatar($user.name)} name={$user.name} />
<AvatarInitials size={48} name={$user.name} />
<Heading tag="h6" size="7">{$user.name}</Heading>
{:else}
<div class="avatar">
@@ -452,7 +457,7 @@
<svelte:fragment slot="image">
{#if $user.email || $user.phone}
{#if $user.name}
<Avatar size={48} src={getAvatar($user.name)} name={$user.name} />
<AvatarInitials size={48} name={$user.name} />
{:else}
<div class="avatar">
<span class="icon-minus-sm" aria-hidden="true" />
@@ -1,7 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import { base } from '$app/paths';
import { Pagination, Empty, Avatar } from '$lib/components';
import { Pagination, Empty, AvatarInitials } from '$lib/components';
import {
Table,
TableHeader,
@@ -26,7 +26,6 @@
const project = $page.params.project;
const offset = createPersistentPagination($pageLimit);
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 32, 32).toString();
$: request = sdkForProject.users.listMemberships($page.params.user);
@@ -57,10 +56,7 @@
href={`${base}/console/project-${project}/authentication/teams/${membership.teamId}`}>
<TableCellText title="Name">
<div class="u-flex u-gap-12">
<Avatar
size={32}
src={getAvatar(membership.teamName)}
name={membership.teamName} />
<AvatarInitials size={32} name={membership.teamName} />
<span>{membership.teamName ? membership.teamName : 'n/a'}</span>
</div>
</TableCellText>
@@ -19,7 +19,7 @@
</script>
<svelte:head>
<title>Appwrite - Database</title>
<title>Database - Appwrite</title>
</svelte:head>
{#if loaded}
@@ -29,7 +29,7 @@
</script>
<svelte:head>
<title>Appwrite - Database</title>
<title>Database - Appwrite</title>
</svelte:head>
{#if loaded}
@@ -28,7 +28,7 @@
</script>
<svelte:head>
<title>Appwrite - {$collection?.name ?? 'Collection'}</title>
<title>{$collection?.name ?? 'Collection'} - Appwrite</title>
</svelte:head>
{#if $collection && loaded}
@@ -28,7 +28,7 @@
</script>
<svelte:head>
<title>Appwrite - Database Document</title>
<title>Database Document - Appwrite</title>
</svelte:head>
{#if $doc}
@@ -46,7 +46,7 @@
</script>
<svelte:head>
<title>Appwrite - Document</title>
<title>Document - Appwrite</title>
</svelte:head>
<Container>
@@ -84,7 +84,7 @@
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Delete document</Heading>
<Heading tag="h6" size="7">Delete Document</Heading>
<p>
The document will be permanently deleted, including all the data within it. This
action is irreversible.
@@ -12,7 +12,7 @@
{id}
{label}
required={attribute.required}
minlength={attribute.min}
maxlength={attribute.max}
min={attribute.min}
max={attribute.max}
bind:value
step={attribute.type === 'double' ? 'any' : 1} />
@@ -214,7 +214,7 @@
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Delete collection</Heading>
<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.
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { Card, Avatar, CardGrid, Box, Heading } from '$lib/components';
import { Card, CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { Container } from '$lib/layout';
import { Button, InputText, Helper } from '$lib/elements/forms';
import { sdkForProject } from '$lib/stores/sdk';
@@ -11,7 +11,6 @@
import { toLocaleDateTime } from '$lib/helpers/date';
const databaseId = $page.params.database;
const getAvatar = (name: string) => sdkForProject.avatars.getInitials(name, 48, 48).toString();
let showDelete = false;
let showError: false | 'name' | 'email' | 'password' = false;
@@ -50,7 +49,7 @@
<div class="common-section grid-1-2">
<div class="grid-1-2-col-1">
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<Avatar size={48} name={$database.name} src={getAvatar($database.name)} />
<AvatarInitials size={48} name={$database.name} />
<Heading tag="h6" size="7">{$database.name}</Heading>
</div>
</div>
@@ -99,7 +98,7 @@
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
<Avatar size={48} name={$database.name} src={getAvatar($database.name)} />
<AvatarInitials size={48} name={$database.name} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold">{$database.name}</h6>
@@ -1,26 +1,51 @@
<script>
// import { afterNavigate } from '$app/navigation';
// import { updateLayout } from '$lib/stores/layout';
// import { onMount } from 'svelte';
<script lang="ts">
import { browser } from '$app/environment';
import { afterNavigate } from '$app/navigation';
import { updateLayout } from '$lib/stores/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type { Models } from '@aw-labs/appwrite-console';
import { deploymentList } from './function/[function]/store';
import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
// onMount(handle);
// afterNavigate(handle);
let loaded = false;
// function handle(event = null) {
// updateLayout({
// navigate: event,
// title: 'Functions',
// level: 3,
// breadcrumbs: {
// href: 'functions',
// title: 'Functions'
// }
// });
// }
if (browser) {
sdkForConsole.client.subscribe<Models.Deployment>('console', (message) => {
if (message.events.includes('functions.*.deployments.*.create')) {
deploymentList.createDeployment(message.payload);
return;
}
if (message.events.includes('functions.*.deployments.*.update')) {
deploymentList.updateDeployment(message.payload);
return;
}
if (message.events.includes('functions.*.deployments.*.delete')) {
//TODO: add delete method
return;
}
});
}
onMount(handle);
afterNavigate(handle);
function handle() {
updateLayout({
breadcrumb: Breadcrumbs,
header: Header
});
loaded = true;
}
</script>
<svelte:head>
<title>Appwrite - Functions</title>
<title>Functions - Appwrite</title>
</svelte:head>
<slot />
{#if loaded}
<slot />
{/if}
@@ -1,63 +1,124 @@
<script lang="ts">
import { page } from '$app/stores';
import { sdkForProject } from '$lib/stores/sdk';
import { Button, InputSearch } from '$lib/elements/forms';
import { Card, Empty, Pagination, Tile, Tiles } from '$lib/components';
import Create from './_create.svelte';
import { Button } from '$lib/elements/forms';
import { Empty, CardContainer, Pagination, Copy, GridItem1, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import type { Models } from '@aw-labs/appwrite-console';
import Create from './createFunction.svelte';
import { Container } from '$lib/layout';
import { base } from '$app/paths';
import { tooltip } from '$lib/actions/tooltip';
import { functionList } from './store';
import { app } from '$lib/stores/app';
import { onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import { beforeNavigate } from '$app/navigation';
import { Query } from '@aw-labs/appwrite-console';
import { toLocaleDateTime } from '$lib/helpers/date';
import { cardLimit } from '$lib/stores/layout';
let search = '';
let showCreate = false;
let offset = 0;
const limit = 25;
const project = $page.params.project;
$: request = sdkForProject.functions.list([Query.limit(0), Query.offset(0)], search);
let deployments: Record<string, Models.Deployment> = null;
onMount(async () => {
deployments = await functionList.getDeployments($functionList.functions);
});
function openWizard() {
wizard.start(Create);
}
beforeNavigate(() => {
wizard.hide();
});
$: functionList.load(
[Query.limit($cardLimit), Query.offset(offset), Query.orderDesc('$createdAt')],
search
);
$: if (search) offset = 0;
</script>
<Container>
<h1>Functions</h1>
<Card>
<InputSearch bind:value={search} />
</Card>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Functions</Heading>
<Button on:click={openWizard}>
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create function</span>
</Button>
</div>
{#await request}
<div aria-busy="true" />
{:then response}
{#if response.total}
<p>{response.total} functions found</p>
<Tiles>
{#each response.functions as func}
<Tile
href={`${base}/console/project-${project}/functions/function/${func.$id}`}
title={func.name} />
{/each}
</Tiles>
{#if $functionList?.total}
<CardContainer total={$functionList.total} {offset} on:click={openWizard}>
{#each $functionList.functions as func}
<GridItem1
href={`${base}/console/project-${project}/functions/function/${func.$id}`}>
<svelte:fragment slot="title">
<div class="u-flex u-gap-32 u-cross-center">
<div class="avatar is-medium">
<img
src={`${base}/icons/${$app.themeInUse}/color/${
func.runtime.split('-')[0]
}.svg`}
alt="technology" />
</div>
<span class="text">{func.name}</span>
</div>
</svelte:fragment>
<svelte:fragment slot="status">
{#if deployments && deployments[func.$id]}
{deployments[func.$id].status ?? 'No deployment'}
{/if}
</svelte:fragment>
<svelte:fragment slot="icons">
{#if deployments && deployments[func.$id] && deployments[func.$id].status === 'failed'}
<li>
<span
class="icon-exclamation"
aria-hidden="true"
use:tooltip={{
content: 'Build error'
}} />
</li>
{/if}
{#if func.scheduleNext}
<li>
<span
class="icon-clock"
aria-hidden="true"
use:tooltip={{
content: `Next execution:
${toLocaleDateTime(func.scheduleNext)}`
}} />
</li>
{/if}
</svelte:fragment>
<Pagination {limit} bind:offset sum={response.total} />
{:else if search}
<Empty>
<div class="u-flex u-flex-vertical">
No results found for <b>{search}</b>
</div>
</Empty>
{:else}
<Empty>
<div class="u-flex u-flex-vertical">
<div class="common-section">No Functions Found</div>
<div class="common-section">
You haven't created any functions for your project yet.
</div>
</div>
</Empty>
{/if}
{/await}
<Button on:click={() => (showCreate = true)}>Create Function</Button>
<Copy value={func.$id}>
<Pill button><i class="icon-duplicate" />Function ID</Pill>
</Copy>
</GridItem1>
{/each}
<svelte:fragment slot="empty">
<p>Create a new function</p>
</svelte:fragment>
</CardContainer>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$functionList.total}</p>
<Pagination limit={$cardLimit} bind:offset sum={$functionList.total} />
</div>
{:else}
<Empty isButton single on:click={openWizard}>
<div class="u-text-center">
<p class="text">Create your first function to get started</p>
<p class="text">Need a hand? Check out our documentation.</p>
</div>
<div class="u-flex u-gap-12">
<Button external text>Documentation</Button>
<Button secondary on:click={openWizard}>Create function</Button>
</div>
</Empty>
{/if}
</Container>
<Create bind:showCreate />
@@ -1,39 +0,0 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, Form, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
export let showCreate = false;
const dispatch = createEventDispatcher();
let name = '';
const create = async () => {
try {
await sdkForProject.storage.createBucket('unique()', name);
name = null;
showCreate = false;
dispatch('created');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form on:submit={create}>
<Modal bind:show={showCreate}>
<svelte:fragment slot="header">Create Function</svelte:fragment>
<InputText id="name" label="Name" bind:value={name} required />
<svelte:fragment slot="footer">
<Button submit>Create</Button>
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,22 @@
<script lang="ts">
import { Breadcrumbs } from '$lib/layout';
import { organization } from '$lib/stores/organization';
import { project } from '../store';
$: breadcrumbs = [
{
href: `/console/organization-${$organization.$id}`,
title: $organization.name
},
{
href: `/console/project-${$project.$id}`,
title: $project.name
},
{
href: `/console/project-${$project.$id}/functions`,
title: 'Functions'
}
];
</script>
<Breadcrumbs {breadcrumbs} />
@@ -0,0 +1,86 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import { functionList } from './store';
import { sdkForProject } from '$lib/stores/sdk';
import { onDestroy } from 'svelte';
import { addNotification } from '$lib/stores/notifications';
import { wizard } from '$lib/stores/wizard';
import Step1 from './wizard/step1.svelte';
import Step2 from './wizard/step2.svelte';
import Step3 from './wizard/step3.svelte';
import Step4 from './wizard/step4.svelte';
import Step5 from './wizard/step5.svelte';
import { createFunction } from './wizard/store';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { Query } from '@aw-labs/appwrite-console';
const create = async () => {
try {
const response = await sdkForProject.functions.create(
$createFunction.id ?? 'unique()',
$createFunction.name,
$createFunction.execute,
$createFunction.runtime,
$createFunction.events,
$createFunction.schedule,
$createFunction.timeout
);
$createFunction.vars.forEach(
async (v) =>
await sdkForProject.functions.createVariable(response.$id, v.key, v.value)
);
addNotification({
message: 'Function has been created',
type: 'success'
});
functionList.load([Query.limit(6), Query.offset(0), Query.orderDesc('$createdAt')]);
wizard.hide();
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
};
onDestroy(() => {
$createFunction = {
id: null,
name: null,
execute: [],
runtime: null,
vars: [],
events: [],
schedule: null,
timeout: null
};
});
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Function Details',
component: Step1
});
stepsComponents.set(2, {
label: 'Execute access',
component: Step2,
optional: true
});
stepsComponents.set(3, {
label: 'Events',
component: Step3,
optional: true
});
stepsComponents.set(4, {
label: 'Scheduling',
component: Step4,
optional: true
});
stepsComponents.set(5, {
label: 'Variables',
component: Step5,
optional: true
});
</script>
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} />
@@ -0,0 +1,57 @@
<script lang="ts">
import { Button, Form } from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { InputText, InputPassword, FormList } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import type { Models } from '@aw-labs/appwrite-console';
export let showCreate = false;
export let selectedVar: Partial<Models.Variable> = null;
let pair = {
$id: selectedVar?.$id,
key: selectedVar?.key,
value: selectedVar?.value
};
const dispatch = createEventDispatcher();
const handleVariable = () => {
if (selectedVar) {
dispatch('updated', pair);
} else {
dispatch('created', pair);
}
showCreate = false;
};
</script>
<Form noStyle noMargin on:submit={handleVariable}>
<Modal bind:show={showCreate} size="big">
<svelte:fragment slot="header">
{selectedVar ? 'Update' : 'Create'} Variable
</svelte:fragment>
<FormList>
<InputText
id="key"
label="Key"
placeholder="Enter key"
bind:value={pair.key}
required
autofocus />
<InputPassword
id="value"
label="Value"
minlength={0}
showPasswordButton
placeholder="Enter value"
bind:value={pair.value}
required />
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit>{selectedVar ? 'Update' : 'Create'}</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -2,7 +2,11 @@
import { afterNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { func } from './store';
import { func, execute } from './store';
import Execute from './execute.svelte';
import { updateLayout } from '$lib/stores/layout';
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
const functionId = $page.params.function;
@@ -15,36 +19,17 @@
await promise;
}
// updateLayout({
// navigate: event,
// title: $func.name,
// level: 4,
// breadcrumbs: {
// title: $func.name,
// href: `function/${functionId}`
// },
// tabs: [
// {
// href: path,
// title: 'Overview'
// },
// {
// href: `${path}/monitors`,
// title: 'Monitors'
// },
// {
// href: `${path}/logs`,
// title: 'Logs'
// },
// {
// href: `${path}/settings`,
// title: 'Settings'
// }
// ]
// });
updateLayout({
breadcrumb: Breadcrumbs,
header: Header
});
}
</script>
{#if $func}
<slot />
{/if}
{#if $execute}
<Execute selectedFunction={$execute} />
{/if}
@@ -1,81 +1,256 @@
<script lang="ts">
import { page } from '$app/stores';
import { sdkForProject } from '$lib/stores/sdk';
import { Button, InputSearch } from '$lib/elements/forms';
import { Card, Pagination } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import {
CardGrid,
Pagination,
Copy,
DropList,
DropListItem,
Empty,
Status,
Heading
} from '$lib/components';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCellHead,
TableCellLink,
TableCell,
TableCellText
} from '$lib/elements/table';
import { func } from './store';
import { deploymentList, execute, func } from './store';
import { Container } from '$lib/layout';
import Create from './_create.svelte';
import Create from './create.svelte';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { pageLimit } from '$lib/stores/layout';
import type { Models } from '@aw-labs/appwrite-console';
import Delete from './delete.svelte';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import Activate from './activate.svelte';
import { Query } from '@aw-labs/appwrite-console';
import { toLocaleDateTime } from '$lib/helpers/date';
import { log } from '$lib/stores/logs';
let search = '';
let showCreate = false;
let offset = 0;
let showCreate = false;
let showDropdown = [];
let showDelete = false;
let showActivate = false;
let selectedDeployment: Models.Deployment = null;
const limit = 25;
const project = $page.params.project;
const functionId = $page.params.function;
$: request = sdkForProject.functions.listDeployments(
const handleActivate = () => {
func.load(functionId);
};
function loadDeployments() {
deploymentList.load(
functionId,
[Query.offset(offset), Query.limit($pageLimit), Query.orderDesc('$createdAt')],
search
);
}
$: deploymentList.load(
functionId,
[Query.offset(offset), Query.limit(limit)],
[Query.offset(offset), Query.limit($pageLimit), Query.orderDesc('$createdAt')],
search
);
$: if (search) offset = 0;
$: activeDeployment = $deploymentList?.deployments?.find((d) => d.$id === $func?.deployment);
</script>
<Container>
<h1>Overview</h1>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Deployments</Heading>
<Button on:click={() => (showCreate = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create deployment</span>
</Button>
</div>
{#if $deploymentList?.total}
<div class="common-section">
<Heading tag="h3" size="7">Active</Heading>
</div>
{#if activeDeployment}
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<div class="avatar is-medium">
<img
src={`${base}/icons/${$app.themeInUse}/color/${
$func.runtime.split('-')[0]
}.svg`}
alt="technology" />
</div>
<div>
<p><b>Function ID: {$func.$id} </b></p>
<p>Deployment ID: {$func.deployment}</p>
</div>
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div>
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
<p>Entrypoint: {activeDeployment?.entrypoint}</p>
</div>
<Card>
<p><b>Function ID:</b> {$func.$id}</p>
<p><b>Runtime:</b> {$func.runtime}</p>
<p><b>Last Updated:</b> {$func.$updatedAt}</p>
<p><b>Created:</b> {$func.$createdAt}</p>
</Card>
<Status status={activeDeployment.status}>
{activeDeployment.status}
</Status>
</div>
</svelte:fragment>
<h1>Deployments</h1>
<svelte:fragment slot="actions">
<Button
text
on:click={() => {
$log.show = true;
$log.func = $func;
$log.data = activeDeployment;
}}>Logs</Button>
<Button secondary on:click={() => execute.set($func)}>Execute now</Button>
</svelte:fragment>
</CardGrid>
{:else}
<Empty single isButton on:click={() => (showCreate = true)}>
<div class="u-text-center">
<p>
Create a new deployment, or activate an existing one to see your function in
action.
</p>
<p>Need a hand? Check out our documentation.</p>
</div>
<div class="u-flex u-gap-12">
<Button text external href="https://appwrite.io/docs/functions#createFunction">
Documentation
</Button>
<Button secondary on:click={() => (showCreate = true)}>
Create Deployment
</Button>
</div>
</Empty>
{/if}
<Card>
<InputSearch bind:value={search} />
</Card>
<div class="common-section">
<Heading tag="h3" size="7">Inactive</Heading>
</div>
{#await request}
<div aria-busy="true" />
{:then response}
<Table>
<TableHeader>
<TableCellHead>ID</TableCellHead>
<TableCellHead>Active</TableCellHead>
<TableCellHead>Deployment ID</TableCellHead>
<TableCellHead width={140}>Created</TableCellHead>
<TableCellHead width={100}>Status</TableCellHead>
<TableCellHead width={90}>Build time</TableCellHead>
<TableCellHead width={70}>Size</TableCellHead>
<TableCellHead width={60} />
<TableCellHead width={25} />
</TableHeader>
<TableBody>
{#each response.deployments as deployment}
<TableRow>
<TableCellLink
title="ID"
href={`${base}/console/project-${project}/storage/bucket/${functionId}/${deployment.$id}`}>
{deployment.$id}
</TableCellLink>
<TableCellText title="Active">{deployment.activate}</TableCellText>
</TableRow>
{#each $deploymentList.deployments as deployment, index}
{#if deployment.$id !== $func.deployment}
<TableRow>
<TableCell title="Deployment ID">
<Copy value={deployment.$id}>
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
<span class="text u-trim">{deployment.$id}</span>
</Pill>
</Copy>
</TableCell>
<TableCellText title="Created">
{toLocaleDateTime(deployment.$createdAt)}
</TableCellText>
<TableCell title="Status">
<Status status={deployment.status}>
{deployment.status}
</Status>
</TableCell>
<!-- TODO: replace with build time, when implemented -->
<TableCellText title="Build Time">TBI</TableCellText>
<TableCellText title="Size">
{calculateSize(deployment.size)}
</TableCellText>
<TableCell>
<Button
secondary
on:click={() => {
selectedDeployment = deployment;
showActivate = true;
}}>
<span class="text">Activate</span>
</Button>
</TableCell>
<TableCell showOverflow>
<DropList
bind:show={showDropdown[index]}
position="bottom"
horizontal="left"
arrow={false}>
<button
class="button is-only-icon is-text"
aria-label="More options"
on:click|preventDefault={() => {
showDropdown[index] = !showDropdown[index];
}}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem
icon="terminal"
on:click={() => {
selectedDeployment = deployment;
showDropdown = [];
}}>Output</DropListItem>
<DropListItem
icon="trash"
on:click={() => {
selectedDeployment = deployment;
showDropdown = [];
showDelete = true;
}}>Delete</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/if}
{/each}
</TableBody>
</Table>
<Pagination {limit} bind:offset sum={response.total} />
{/await}
<Button on:click={() => (showCreate = true)}>Upload</Button>
{:else}
<Empty isButton single on:click={() => (showCreate = true)}>
<div class="u-text-center">
<p>
Create a new deployment, or activate an existing one to see your function in
action.
</p>
<p>Need a hand? Check out our documentation.</p>
</div>
<div class="u-flex u-gap-12">
<Button text external href="https://appwrite.io/docs/functions#createFunction">
Documentation
</Button>
<Button secondary on:click={() => (showCreate = true)}>Create Deployment</Button>
</div>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$deploymentList?.total}</p>
<Pagination limit={$pageLimit} bind:offset sum={$deploymentList?.total} />
</div>
</Container>
<Create bind:showCreate />
<Create bind:showCreate on:created={loadDeployments} />
{#if selectedDeployment}
<Delete {selectedDeployment} bind:showDelete />
<Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} />
{/if}
@@ -1,72 +0,0 @@
<script lang="ts">
import { InputSwitch, Button, InputFile, InputText, Form } from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { sdkForProject } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/stores';
import { clickOnEnter } from '$lib/helpers/a11y';
export let showCreate = false;
let showCli = true;
let entrypoint: string;
let code: FileList;
let active: boolean;
const functionId = $page.params.function;
const dispatch = createEventDispatcher();
const create = async () => {
try {
await sdkForProject.functions.createDeployment(functionId, entrypoint, code[0], active);
code = entrypoint = active = null;
showCreate = false;
dispatch('created');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form on:submit={create}>
<Modal bind:show={showCreate}>
<svelte:fragment slot="header">Create Deployment</svelte:fragment>
<ul class="tabs">
<li class="tabs-item">
<span
class="tabs-button"
on:click={() => (showCli = true)}
on:keyup={clickOnEnter}
class:is-selected={showCli}>
<span class="text">Files</span>
</span>
</li>
<li class="tabs-item">
<span
class="tabs-button"
on:click={() => (showCli = false)}
on:keyup={clickOnEnter}
class:is-selected={!showCli}>
<span class="text">Usage</span>
</span>
</li>
</ul>
{#if showCli}
<p>Unix</p>
<p>Powershell</p>
<p>Learn more about creating deployments, installing and using the Appwrite CLI.</p>
{:else}
<InputText id="entrypoint" label="Entrypoint" bind:value={entrypoint} required />
<InputFile id="file" label="File" bind:files={code} required />
<InputSwitch id="active" label="Activate Deployment after build" bind:value={active} />
{/if}
<svelte:fragment slot="footer">
<Button submit>Create</Button>
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,44 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { createEventDispatcher } from 'svelte';
export let showActivate = false;
export let selectedDeployment: Models.Deployment = null;
const dispatch = createEventDispatcher();
const handleSubmit = async () => {
try {
await sdkForProject.functions.updateDeployment(
selectedDeployment.resourceId,
selectedDeployment.$id
);
showActivate = false;
addNotification({
type: 'success',
message: `Deployment has been activated`
});
dispatch('activated');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form noMargin on:submit={handleSubmit}>
<Modal bind:show={showActivate}>
<svelte:fragment slot="header">Activate Deployment</svelte:fragment>
<p>Are you sure you want to activate this deployment?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showActivate = false)}>Cancel</Button>
<Button secondary submit>Activate</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,27 @@
<script lang="ts">
import { Breadcrumbs } from '$lib/layout';
import { organization } from '$lib/stores/organization';
import { project } from '../../../store';
import { func } from './store';
$: breadcrumbs = [
{
href: `/console/organization-${$organization.$id}`,
title: $organization.name
},
{
href: `/console/project-${$project.$id}`,
title: $project.name
},
{
href: `/console/project-${$project.$id}/functions`,
title: 'Functions'
},
{
href: `/console/project-${$project.$id}/functions/function/${$func.$id}`,
title: $func.name
}
];
</script>
<Breadcrumbs {breadcrumbs} />
@@ -0,0 +1,147 @@
<script lang="ts">
import { InputChoice, Button, InputText, InputFile, Form } from '$lib/elements/forms';
import { Modal, Collapsible, CollapsibleItem, Tabs, Tab, Code } from '$lib/components';
import { sdkForProject } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/stores';
import FormList from '$lib/elements/forms/formList.svelte';
import Github from '$lib/images/github-illustration.svg';
export let showCreate = false;
enum Mode {
CLI,
Github,
Manual
}
let mode: Mode = Mode.CLI;
let entrypoint: string;
let active: boolean;
let files: FileList;
const functionId = $page.params.function;
const dispatch = createEventDispatcher();
const create = async () => {
try {
await sdkForProject.functions.createDeployment(
functionId,
entrypoint,
files[0],
active
);
files = entrypoint = active = null;
showCreate = false;
dispatch('created');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
const codeSnippets = {
Unix: {
code: `appwrite functions createDeployment \\
--functionId=${functionId} \\
--entrypoint='index.js' \\
--code="." \\
--activate=true`,
language: 'bash'
},
CMD: {
code: `appwrite functions createDeployment ^
--functionId=${functionId} ^
--entrypoint='index.js' ^
--code="." ^
--activate=true`,
language: 'CMD'
},
PowerShell: {
code: `appwrite functions createDeployment ,
--functionId=${functionId} ,
--entrypoint='index.js' ,
--code="." ,
--activate=true`,
language: 'PowerShell'
}
};
</script>
<Form noStyle noMargin on:submit={create}>
<Modal size="big" bind:show={showCreate}>
<svelte:fragment slot="header">Create Deployment</svelte:fragment>
<Tabs>
<Tab on:click={() => (mode = Mode.CLI)} selected={mode === Mode.CLI}>CLI</Tab>
<Tab on:click={() => (mode = Mode.Github)} selected={mode === Mode.Github}>
GitHub - Soon!
</Tab>
<Tab on:click={() => (mode = Mode.Manual)} selected={mode === Mode.Manual}>Manual</Tab>
</Tabs>
{#if mode === Mode.CLI}
<p class="text">
You can deploy your function from the Appwrite CLI using Unix, CMD, or PowerShell.
Check out our Documentation to learn more about <a
href="https://appwrite.io/docs/functions#deployFunction"
target="_blank"
rel="noopener noreferrer"
class="link">deploying your functions</a
>, or how to install and use the
<a href="#/" target="_blank" rel="noopener noreferrer" class="link">Appwrite CLI</a
>.
</p>
<Collapsible>
{#each ['Unix', 'CMD', 'PowerShell'] as category}
<CollapsibleItem>
<svelte:fragment slot="title">{category}</svelte:fragment>
<Code
withLineNumbers
withCopy
language="sh"
label={codeSnippets[category].language}
code={codeSnippets[category].code} />
</CollapsibleItem>
{/each}
</Collapsible>
{:else if mode === Mode.Github}
<div class="common-section grid-1-2">
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<img src={Github} alt="github" />
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-24">
<h3 class="body-text-1">Coming Soon!</h3>
<p>
Creating deployments will be easier than ever with our upcoming Git
Integration. Just set up a repository and well do the rest.
</p>
</div>
</div>
{:else if mode === Mode.Manual}
<FormList>
<InputText
label="Entrypoint"
placeholder="main.py"
id="entrypoint"
bind:value={entrypoint}
required />
<InputFile label="Gzipped code (tar.gz)" bind:files />
<InputChoice
label="Activate Deployment after build"
id="activate"
bind:value={active}>
This deployment will be activated after the build is completed.</InputChoice>
</FormList>
{/if}
<svelte:fragment slot="footer">
{#if mode === Mode.Manual}
<Button secondary on:click={() => (showCreate = false)}>Close</Button>
<Button submit>Create</Button>
{:else}
<Button secondary on:click={() => (showCreate = false)}>Close</Button>
{/if}
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,46 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
export let showDelete = false;
export let selectedDeployment: Models.Deployment = null;
const handleSubmit = async () => {
try {
await sdkForProject.functions.deleteDeployment(
selectedDeployment.resourceId,
selectedDeployment.$id
);
showDelete = false;
addNotification({
type: 'success',
message: `Deployment has been deleted`
});
await goto(
`${base}/console/project-${$page.params.project}/functions/${selectedDeployment.resourceId}`
);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form noStyle noMargin on:submit={handleSubmit}>
<Modal bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Deployment</svelte:fragment>
<p>Are you sure you want to delete this deployment?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,68 @@
<script lang="ts">
import { Code, Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { InputTextarea, FormList, InputChoice } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
export let selectedFunction: Models.Function = null;
let show = false;
let data: string = null;
let showJson = false;
const example = `
{
firstName: "hello",
lastName:"world",
age:"old"
}`;
$: if (selectedFunction && !show) {
show = true;
}
const handleSubmit = async () => {
try {
await sdkForProject.functions.createExecution(
selectedFunction.$id,
data?.length ? data : undefined
);
close();
addNotification({
type: 'success',
message: `Function has been executed`
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
function close() {
selectedFunction = null;
show = false;
}
</script>
<Form noStyle noMargin on:submit={handleSubmit}>
<Modal bind:show size="big" on:close={close}>
<svelte:fragment slot="header">Execute Function</svelte:fragment>
<FormList>
<InputTextarea bind:value={data} id="data" label="Custom data (optional)" />
<InputChoice type="switchbox" id="json" label="Show example JSON" bind:value={showJson}>
Here's an example of some custom data.</InputChoice>
{#if showJson}
<Code noMargin language="json" withLineNumbers code={example} />
{/if}
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={close}>Cancel</Button>
<Button submit>Execute Now</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,92 @@
<script lang="ts">
import { page } from '$app/stores';
import { pageLimit } from '$lib/stores/layout';
import { executionList } from './store';
import { Container } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Pagination, Copy, Empty, Status, Heading } from '$lib/components';
import {
Table,
TableHeader,
TableBody,
TableCellHead,
TableCell,
TableCellText,
TableRowButton
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import { calculateTime } from '$lib/helpers/timeConversion';
import { Query } from '@aw-labs/appwrite-console';
import { log } from '$lib/stores/logs';
import { func } from '../store';
let search = '';
let offset = 0;
const functionId = $page.params.function;
$: executionList.load(functionId, [Query.offset(offset), Query.limit($pageLimit)], search);
//TODO: add optional hover state to rows
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Logs</Heading>
</div>
{#if $executionList?.total}
<Table>
<TableHeader>
<TableCellHead>Deployment ID</TableCellHead>
<TableCellHead width={140}>Created</TableCellHead>
<TableCellHead width={70}>Status</TableCellHead>
<TableCellHead width={90}>Trigger</TableCellHead>
<TableCellHead width={70}>Type</TableCellHead>
<TableCellHead width={100}>Build Time</TableCellHead>
</TableHeader>
<TableBody>
{#each $executionList.executions as execution}
<TableRowButton
on:click={() => {
$log.show = true;
$log.func = $func;
$log.data = execution;
}}>
<TableCell title="Deployment ID">
<Copy value={execution.$id}>
<Pill button
><span class="icon-duplicate" aria-hidden="true" />
<span class="text u-trim">{execution.$id}</span></Pill>
</Copy>
</TableCell>
<TableCellText title="Created">
{toLocaleDateTime(execution.$createdAt)}
</TableCellText>
<TableCellText title="Status">
<Status status={execution.status}>
{execution.status}
</Status>
</TableCellText>
<TableCellText title="Trigger">
<Pill>
<span class="text u-trim">{execution.trigger}</span>
</Pill>
</TableCellText>
<TableCellText title="Type">TBI</TableCellText>
<TableCellText title="Duration">
{calculateTime(execution.duration)}</TableCellText>
</TableRowButton>
{/each}
</TableBody>
</Table>
{:else}
<Empty isButton single>
<p>Execute your function to view execution logs</p>
<Button external secondary href="#">Documentation</Button>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$executionList?.total}</p>
<Pagination limit={$pageLimit} bind:offset sum={$executionList?.total} />
</div>
</Container>
@@ -0,0 +1,21 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { cachedStore } from '$lib/helpers/cache';
export const executionList = cachedStore<
Models.ExecutionList,
{
load: (functionId: string, queries?: string[], search?: string) => Promise<void>;
}
>('executionList', function ({ set }) {
return {
load: async (functionId, queries, search) => {
const response = await sdkForProject.functions.listExecutions(
functionId,
queries,
search
);
set(response);
}
};
});
@@ -0,0 +1,57 @@
<script lang="ts">
import { page } from '$app/stores';
import { Copy, Tab, Tabs } from '$lib/components';
import { Pill } from '$lib/elements';
import { Cover } from '$lib/layout';
import { func } from './store';
const projectId = $page.params.project;
const functionId = $page.params.function;
const path = `/console/project-${projectId}/functions/function/${functionId}`;
const tabs = [
{
href: path,
title: 'Deployments'
},
{
href: `${path}/usage`,
title: 'Usage'
},
{
href: `${path}/executions`,
title: 'Executions'
},
{
href: `${path}/settings`,
title: 'Settings'
}
];
</script>
<Cover>
<svelte:fragment slot="header">
<a
class="back-button"
href={`/console/project-${projectId}/functions`}
aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
<h1 class="heading-level-4">
<span class="text">{$func.name}</span>
</h1>
<Copy value={$func.$id}>
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
ID Details
</Pill>
</Copy>
</svelte:fragment>
<Tabs>
{#each tabs as tab}
<Tab href={tab.href} selected={$page.url.pathname === tab.href}>
{tab.title}
</Tab>
{/each}
</Tabs>
</Cover>
@@ -1,10 +0,0 @@
<script>
import { Card } from '$lib/components';
import { Container } from '$lib/layout';
</script>
<Container>
<h1>Logs</h1>
<Card />
</Container>
@@ -1,10 +0,0 @@
<script>
import { Card } from '$lib/components';
import { Container } from '$lib/layout';
</script>
<Container>
<h1>Monitors</h1>
<Card />
</Container>
@@ -1,10 +1,581 @@
<script>
import { Card } from '$lib/components';
<script lang="ts">
import {
CardGrid,
Box,
DropList,
DropListItem,
Copy,
Empty,
EventModal
} from '$lib/components';
import { Container } from '$lib/layout';
import { Button, InputNumber, InputText, InputCron, Form, FormList } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { execute, func } from '../store';
import Delete from './delete.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import type { Models } from '@aw-labs/appwrite-console';
import Variable from '../../../createVariable.svelte';
import Upload from './uploadVariables.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import { variableList } from '../../../store';
import { Permissions } from '$lib/components/permissions';
import { difference } from '$lib/helpers/array';
import TableList from '$lib/elements/table/tableList.svelte';
import { TableCell, TableCellText } from '$lib/elements/table';
import Heading from '$lib/components/heading.svelte';
import { writable, type Writable } from 'svelte/store';
const functionId = $page.params.function;
let showDelete = false;
let selectedVar: Models.Variable = null;
let showVariablesUpload = false;
let showVariablesModal = false;
let showVariablesValue = [];
let showVariablesDropdown = [];
let timeout: number = null;
let functionName: string = null;
let functionSchedule: string = null;
let permissions: string[] = [];
let arePermsDisabled = true;
const eventSet: Writable<Set<string>> = writable(new Set());
let showEvents = false;
let areEventsDisabled = true;
onMount(async () => {
if ($func?.$id !== functionId) {
await func.load(functionId);
}
await variableList.load(functionId);
timeout ??= $func.timeout;
functionName ??= $func.name;
functionSchedule ??= $func.schedule;
permissions = $func.execute;
$eventSet = new Set($func.events);
});
async function updateName() {
try {
await sdkForProject.functions.update(functionId, functionName, $func.execute);
$func.name = functionName;
addNotification({
message: 'Name has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePermissions() {
try {
await sdkForProject.functions.update(functionId, $func.name, permissions);
$func.execute = permissions;
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateEvents() {
try {
await sdkForProject.functions.update(
functionId,
$func.name,
$func.execute,
Array.from($eventSet)
);
$func.events = Array.from($eventSet);
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateSchedule() {
try {
await sdkForProject.functions.update(
functionId,
$func.name,
$func.execute,
$func.events,
functionSchedule,
timeout
);
$func.schedule = functionSchedule;
addNotification({
type: 'success',
message: 'CRON Schedule has been updated'
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function updateTimeout() {
try {
await sdkForProject.functions.update(
functionId,
$func.name,
$func.execute,
$func.events,
$func.schedule,
timeout
);
addNotification({
type: 'success',
message: 'Timeout has been updated'
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function handleVariableCreated(dispatcedData: CustomEvent) {
const variable = dispatcedData.detail;
try {
await variableList.create(functionId, variable.key, variable.value);
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
variableList.load(functionId);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function handleVariableUpdated(dispatcedData: CustomEvent) {
const variable = dispatcedData.detail;
try {
await variableList.update(functionId, variable.$id, variable.key, variable.value);
selectedVar = null;
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
variableList.load(functionId);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
async function handleVariableDeleted(variableId: string) {
try {
await variableList.delete(functionId, variableId);
addNotification({
type: 'success',
message: `Variable has been deleted`
});
variableList.load(functionId);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
function downloadVariables() {
if ($variableList?.total) {
let data = $variableList.variables
.map((variable) => {
return `${variable.key}=${variable.value}`;
})
.join('\n');
const file = new File([data], '.env', {
type: 'application/x-envoy'
});
const link = document.createElement('a');
const url = URL.createObjectURL(file);
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
}
function handleEvent(event: CustomEvent) {
eventSet.set($eventSet.add(event.detail));
}
$: if (permissions) {
if (
difference(permissions, $func.execute).length ||
difference($func.execute, permissions).length
) {
arePermsDisabled = false;
} else arePermsDisabled = true;
}
$: if ($eventSet) {
if (
difference(Array.from($eventSet), $func.events).length ||
difference($func.events, Array.from($eventSet)).length
) {
areEventsDisabled = false;
} else areEventsDisabled = true;
}
</script>
<Container>
<h1>Settings</h1>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<div class="avatar is-medium">
<img
src={`${base}/icons/${$app.themeInUse}/color/${
$func.runtime.split('-')[0]
}.svg`}
alt="technology" />
</div>
<div>
<Heading tag="h6" size="7">{$func.name}</Heading>
<Card />
<p>{$func.runtime}</p>
</div>
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div>
<p>Function ID: {$func.$id}</p>
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
on:click={() => {
$execute = $func;
}}>Execute now</Button>
</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={functionName} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={functionName === $func.name || !functionName} submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePermissions}>
<CardGrid>
<Heading tag="h6" size="7">Execute Access</Heading>
<p>
Choose who can execute this function using the client API. For more information,
check out the Permissions Guide in our documentation.
</p>
<svelte:fragment slot="aside">
<Permissions withCrud={false} bind:executeAccess={permissions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateEvents}>
<CardGrid>
<Heading tag="h6" size="7">Update Events</Heading>
<p>Set the events that will trigger your function. 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>
{:else}
<Empty isButton on:click={() => (showEvents = true)}>
Add a event to get started
</Empty>
{/if}
<div class="u-flex u-margin-block-start-16">
<Button text noMargin on:click={() => (showEvents = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="u-text">Add event</span>
</Button>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={areEventsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateSchedule}>
<CardGrid>
<Heading tag="h6" size="7">Update CRON Schedule</Heading>
<p>
Set a CRON schedule to trigger your function. Leave blank for no schedule. <a
href="#/"
target="_blank"
rel="noopener noreferrer"
class="link">
More details on CRON syntax here.</a>
</p>
<svelte:fragment slot="aside">
<FormList>
<InputCron
bind:value={$func.schedule}
label="Schedule (CRON Syntax)"
id="schedule" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={$func.schedule === functionSchedule} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateTimeout}>
<CardGrid>
<Heading tag="h6" size="7">Update Timeout</Heading>
<p>
Limit the execution time of your function. Maximum value is 900 seconds (15
minutes).
</p>
<svelte:fragment slot="aside">
<FormList>
<InputNumber
min={1}
max={900}
id="time"
label="Time (in seconds)"
bind:value={timeout} />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={$func.timeout === timeout || timeout < 1} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid>
<Heading tag="h6" size="7">Update Function Variables</Heading>
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
<svelte:fragment slot="aside">
<div class="u-flex u-margin-inline-start-auto u-gap-16">
<Button secondary on:click={downloadVariables}>
<span class="icon-download" />
<span class="text">Download .env file</span>
</Button>
<Button secondary on:click={() => (showVariablesUpload = true)}>
<span class="icon-upload" />
<span class="text">Import .env file</span>
</Button>
</div>
<table class="table is-remove-outer-styles">
<thead class="table-thead">
<tr class="table-row">
<th class="table-thead-col">
<span class="eyebrow-heading-3">Key</span>
</th>
<th class="table-thead-col">
<span class="eyebrow-heading-3">Value</span>
</th>
<th class="table-thead-col" style="--p-col-width:40" />
</tr>
</thead>
<tbody class="table-tbody">
{#if $variableList?.total}
{#each $variableList.variables as variable, i}
<tr class="table-row">
<td class="table-col" data-title="Key">
<span class="text">{variable.key}</span>
</td>
<td class="table-col u-overflow-visible" data-title="value">
<div class="interactive-text-output">
{#if showVariablesValue[i]}
<span class="text">{variable.value}</span>
{:else}
<span class="text">••••••••</span>
{/if}
<div class="u-flex u-cross-child-start u-gap-8">
<button
on:click|preventDefault={() =>
(showVariablesValue[i] =
!showVariablesValue[i])}
class="interactive-text-output-button"
aria-label="show hidden text">
{#if showVariablesValue[i]}
<span class="icon-eye-off" aria-hidden="true" />
{:else}
<span class="icon-eye" aria-hidden="true" />
{/if}
</button>
<Copy bind:value={variable.value}>
<button
class="interactive-text-output-button"
aria-label="copy text">
<span
class="icon-duplicate"
aria-hidden="true" />
</button>
</Copy>
</div>
</div>
</td>
<td class="table-col u-overflow-visible" data-title="options">
<DropList
bind:show={showVariablesDropdown[i]}
position="bottom"
horizontal="left"
arrow={false}>
<button
class="button is-text is-only-icon"
aria-label="more options"
on:click|preventDefault={() =>
(showVariablesDropdown[i] =
!showVariablesDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
on:click={() => {
selectedVar = variable;
showVariablesDropdown[i] = false;
showVariablesModal = true;
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
on:click={async () => {
handleVariableDeleted(variable.$id);
showVariablesDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
<div class="u-flex u-margin-block-start-16">
<button
class="button is-text u-padding-inline-0"
type="button"
on:click={() => {
showVariablesModal = true;
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create variable</span>
</button>
</div>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading tag="h6" size="7">Delete Function</Heading>
<p>
The function will be permanently deleted, including all deployments associated with it.
This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold">{$func.name}</h6>
</svelte:fragment>
<p>Last Updated: {toLocaleDateTime($func.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
</Container>
<Delete bind:showDelete />
{#if showVariablesModal}
<Variable
bind:showCreate={showVariablesModal}
bind:selectedVar
on:created={handleVariableCreated}
on:updated={handleVariableUpdated} />
{/if}
<Upload bind:show={showVariablesUpload} on:uploaded={handleVariableCreated} />
<EventModal bind:show={showEvents} on:created={handleEvent} />
@@ -0,0 +1,43 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForProject } from '$lib/stores/sdk';
export let showDelete = false;
const functionId = $page.params.function;
const handleSubmit = async () => {
try {
await sdkForProject.functions.delete(functionId);
showDelete = false;
addNotification({
type: 'success',
message: `Function has been deleted`
});
await goto(`${base}/console/project-${$page.params.project}/functions`);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form noMargin on:submit={handleSubmit}>
<Modal bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Function</svelte:fragment>
<p>
Are you sure you want to delete this function and all associated deployments from your
project?
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -0,0 +1,69 @@
<script lang="ts">
import { page } from '$app/stores';
import { Modal } from '$lib/components';
import { Button, Form, InputFile } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { createEventDispatcher } from 'svelte';
import { variableList } from '../../../store';
export let show = false;
const dispatch = createEventDispatcher();
const functionId = $page.params.function;
let files: FileList;
const handleSubmit = async () => {
if (files?.length) {
const variables = await parseFile(files[0]);
for (const variable of variables) {
try {
await variableList.create(functionId, variable.key, variable.value);
addNotification({
type: 'success',
message: 'Variable uploaded'
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
dispatch('uploaded', variables);
} else {
addNotification({
type: 'error',
message: 'No file uploaded'
});
}
};
async function parseFile(file: File) {
if (file) {
let variables = [];
let text = await file.text();
text.split('\n').forEach((line) => {
const [key, value] = line.split('=');
variables.push({ key, value });
});
return variables;
}
}
</script>
<Form noStyle noMargin on:submit={handleSubmit}>
<Modal bind:show>
<svelte:fragment slot="header">Upload Variables</svelte:fragment>
<p>
Upload multiple variables via a .env file that will be passed to your function at
runtime.
</p>
<InputFile bind:files label="Attach a file" />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -1,17 +1,72 @@
import { cachedStore } from '$lib/helpers/cache';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
function createFunctionStore() {
const { subscribe, set } = writable<Models.Function>();
import { writable, type Writable } from 'svelte/store';
export const func = cachedStore<
Models.Function,
{
load: (functionId: string) => Promise<void>;
getDeployment: (functionId: string, deploymentId: string) => Promise<Models.Deployment>;
}
>('func', function ({ set }) {
return {
subscribe,
set,
load: async (functionId: string) => {
load: async (functionId) => {
set(await sdkForProject.functions.get(functionId));
},
getDeployment: async (functionId, deploymentId) => {
return await sdkForProject.functions.getDeployment(functionId, deploymentId);
}
};
}
});
export const func = createFunctionStore();
export const deploymentList = cachedStore<
Models.DeploymentList,
{
load: (functionId: string, queries?: string[], search?: string) => Promise<void>;
createDeployment: (deployment: Models.Deployment) => void;
updateDeployment: (deployment: Models.Deployment) => void;
}
>('deploymentList', function ({ set, update }) {
return {
load: async (functionId, queries, search) => {
const response = await sdkForProject.functions.listDeployments(
functionId,
queries,
search
);
set(response);
},
createDeployment: (deployment) =>
update((n) => {
n.deployments.unshift(deployment);
return n;
}),
updateDeployment: (deployment) =>
update((n) => {
const index = n.deployments.findIndex((a) => a.$id === deployment.$id);
if (index !== -1) {
n.deployments[index] = deployment;
}
return n;
})
};
});
export const execute: Writable<Models.Function> = writable();
export const functionUsage = cachedStore<
Models.UsageFunctions,
{
load: (functionId: string, range: string) => Promise<void>;
}
>('functionUsage', function ({ set }) {
return {
load: async (functionId, range) => {
const usages = await sdkForProject.functions.getFunctionUsage(functionId, range);
set(usages);
}
};
});
@@ -1 +1,64 @@
<h1>Usage</h1>
<script lang="ts">
import { page } from '$app/stores';
import { functionUsage } from '../store';
import { Container } from '$lib/layout';
import type { UsagePeriods } from '$lib/layout/usage.svelte';
import { Card, DropTabs, DropTabsItem, Heading } from '$lib/components';
import type { Models } from '@aw-labs/appwrite-console';
const functionId = $page.params.function;
import { last } from '$lib/layout/usage.svelte';
import { BarChart } from '$lib/charts';
let range: UsagePeriods = '30d';
$: functionUsage.load(functionId, range);
// TODO: metric type is wrong
$: count = $functionUsage?.executionsTotal as unknown as Models.Metric[];
$: errors = $functionUsage?.buildsFailure as unknown as Models.Metric[];
</script>
<Container>
<div class="u-flex u-main-space-between common-section">
<Heading tag="h2" size="5">Functions</Heading>
<DropTabs>
<DropTabsItem on:click={() => (range = '24h')} disabled={range === '24h'}>
24h
</DropTabsItem>
<DropTabsItem on:click={() => (range = '30d')} disabled={range === '30d'}>
30d
</DropTabsItem>
<DropTabsItem on:click={() => (range = '90d')} disabled={range === '90d'}>
90d
</DropTabsItem>
</DropTabs>
</div>
{#if count}
<Card>
<Heading tag="h6" size="6">{last(count).value}</Heading>
<p>Executions</p>
<div class="u-margin-block-start-16" />
<BarChart
series={[
{
name: 'Count of function executions over time',
data: [...count.map((e) => [e.date, e.value])]
}
]} />
</Card>
{/if}
{#if errors}
<Card>
<Heading tag="h6" size="6">{last(errors).value}</Heading>
<p>Errors</p>
<div class="u-margin-block-start-16" />
<BarChart
series={[
{
name: 'Count of function errors over time',
data: [...errors.map((e) => [e.date, e.value])]
}
]} />
</Card>
{/if}
</Container>
@@ -0,0 +1,11 @@
<script lang="ts">
import { Cover } from '$lib/layout';
</script>
<Cover>
<svelte:fragment slot="header">
<h1 class="heading-level-4">
<span class="text">Functions</span>
</h1>
</svelte:fragment>
</Cover>
@@ -0,0 +1,73 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { cachedStore } from '$lib/helpers/cache';
import { browser } from '$app/environment';
export const functionList = cachedStore<
Models.FunctionList,
{
load: (queries?: string[], search?: string) => Promise<void>;
getDeployments: (
functions: Models.Function[]
) => Promise<Record<string, Models.Deployment>>;
}
>('functionList', function ({ set }) {
return {
load: async (queries, search) => {
const response = await sdkForProject.functions.list(queries, search);
set(response);
},
getDeployments: async (functions) => {
let activeDeployments = {};
activeDeployments = browser
? JSON.parse(sessionStorage.getItem('activeDeployments')) ?? {}
: {};
for (let i = 0; i < functions.length; i++) {
const fn = functions[i];
if (fn.deployment) {
const response = await sdkForProject.functions.getDeployment(
fn.$id,
fn.deployment
);
activeDeployments[fn.$id] = response;
}
}
sessionStorage?.setItem('activeDeployments', JSON.stringify(activeDeployments));
return activeDeployments;
}
};
});
export const variableList = cachedStore<
Models.VariableList,
{
load: (functionId: string) => Promise<void>;
delete: (functionId: string, variableId: string) => Promise<void>;
create: (functionId: string, key: string, value: string) => Promise<void>;
update: (
functionId: string,
variableId: string,
key: string,
value: string
) => Promise<void>;
}
>('variableList', function ({ set }) {
return {
load: async (functionId) => {
const response = await sdkForProject.functions.listVariables(functionId);
set(response);
},
delete: async (functionId, variableId) => {
await sdkForProject.functions.deleteVariable(functionId, variableId);
},
create: async (functionId, key, value) => {
await sdkForProject.functions.createVariable(functionId, key, value);
},
update: async (functionId, variableId, key, value) => {
await sdkForProject.functions.updateVariable(functionId, variableId, key, value);
}
};
});
@@ -0,0 +1,56 @@
<script lang="ts">
import { CustomId } from '$lib/components';
import { Pill } from '$lib/elements';
import { InputText, InputSelect, FormList } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { sdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { createFunction } from './store';
let showCustomId = false;
let options = [];
onMount(async () => {
let runtimes = await sdkForProject.functions.listRuntimes();
options = runtimes.runtimes.map((runtime) => ({
label: runtime.name,
value: runtime.$id
}));
});
</script>
<WizardStep>
<svelte:fragment slot="title">Create your function</svelte:fragment>
<svelte:fragment slot="subtitle">Lets create a function.</svelte:fragment>
<FormList>
<InputText
label="Name"
id="name"
placeholder="Function name"
bind:value={$createFunction.name}
required />
<InputSelect
label="Runtimes"
id="runtimes"
placeholder="Select runtime"
bind:value={$createFunction.runtime}
{options}
required />
<ul class="form-list">
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Function ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Function" bind:id={$createFunction.id} />
{/if}
</ul>
</FormList>
</WizardStep>
@@ -0,0 +1,17 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { createFunction } from './store';
import { Permissions } from '$lib/components/permissions';
</script>
<WizardStep>
<svelte:fragment slot="title">Execute access (optional)</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose who can execute this function using the client API. Check out our documentation for
more on <a href="#/" target="_blank" rel="noopener noreferrer" class="link">
Permissions</a
>.
</svelte:fragment>
<Permissions withCrud={false} bind:executeAccess={$createFunction.execute} />
</WizardStep>
@@ -0,0 +1,64 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { Empty } from '$lib/components';
import { createFunction } from './store';
import { EventModal } from '$lib/components';
import { TableList, TableCellText, TableCell } from '$lib/elements/table';
let showCreate = false;
let eventSet = new Set<string>();
function handleCreated(event: CustomEvent) {
eventSet.add(event.detail);
$createFunction.events = Array.from(eventSet);
}
</script>
<WizardStep>
<svelte:fragment slot="title">Events (optional)</svelte:fragment>
<svelte:fragment slot="subtitle">
Set the events that will trigger your function. Maximum 100 events allowed.
</svelte:fragment>
<span class="u-sep-block-end">EVENT</span>
{#if $createFunction?.events?.length}
<TableList>
{#each $createFunction.events 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);
$createFunction.events = Array.from(eventSet);
}}>
<span class="icon-x" aria-hidden="true" />
</button>
</TableCell>
</li>
{/each}
</TableList>
{:else}
<Empty isButton on:click={() => (showCreate = !showCreate)}
>Add a event to get started
</Empty>
{/if}
<div class="u-flex u-margin-block-start-16">
<Button text noMargin on:click={() => (showCreate = !showCreate)}>
<span class="icon-plus" aria-hidden="true" />
<span class="u-text">Add event</span>
</Button>
</div>
</WizardStep>
{#if showCreate}
<EventModal bind:show={showCreate} on:created={handleCreated} />
{/if}
@@ -0,0 +1,23 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { FormList, InputCron } from '$lib/elements/forms';
import { createFunction } from './store';
</script>
<WizardStep>
<svelte:fragment slot="title">Scheduling (optional)</svelte:fragment>
<svelte:fragment slot="subtitle">
Set a CRON schedule to trigger your function. Leave blank for no schedule. <a
href="#/"
target="_blank"
rel="noopener noreferrer"
class="link">More details on CRON syntax here</a
>.
</svelte:fragment>
<FormList>
<InputCron
bind:value={$createFunction.schedule}
label="Schedule (CRON Syntax)"
id="schedule" />
</FormList>
</WizardStep>
@@ -0,0 +1,119 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { createFunction } from './store';
import Create from '../createVariable.svelte';
import { DropList, DropListItem, Secret, Empty } from '$lib/components';
import type { Models } from '@aw-labs/appwrite-console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
let showCreate = false;
let selectedVar: Partial<Models.Variable> = null;
let showDropdown = [];
function handleCreated(dispatchEvent: CustomEvent) {
createFunction.update((n) => {
n.vars.push(dispatchEvent.detail);
return n;
});
}
function handleUpdated(dispatchEvent: CustomEvent) {
createFunction.update((n) => {
n.vars = n.vars.map((v) => (v.key === selectedVar.key ? dispatchEvent.detail : v));
return n;
});
}
</script>
<WizardStep>
<svelte:fragment slot="title">Variables (optional)</svelte:fragment>
<svelte:fragment slot="subtitle">
Create a variable (or secret key) that will be passed to your function at runtime.
</svelte:fragment>
{#if $createFunction.vars.length}
<Table noStyles>
<TableHeader>
<TableCellHead>Key</TableCellHead>
<TableCellHead>Value</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
{#each $createFunction.vars as variable, i}
<TableRow>
<TableCellText title="key">
{variable.key}
</TableCellText>
<TableCell showOverflow title="value">
<Secret value={variable.value} />
</TableCell>
<TableCell showOverflow title="options">
<DropList
bind:show={showDropdown[i]}
position="bottom"
horizontal="left"
arrow={false}>
<button
class="button is-text is-only-icon"
aria-label="more options"
on:click|preventDefault={() =>
(showDropdown[i] = !showDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
on:click={() => {
selectedVar = $createFunction.vars[i];
showDropdown[i] = false;
showCreate = true;
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
on:click={() => {
$createFunction.vars.splice(i, 1);
$createFunction = $createFunction;
showDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<span class="u-sep-block-end">VARIABLES</span>
<Empty isButton on:click={() => (showCreate = !showCreate)}
>Create a variable to get started
</Empty>
{/if}
<div class="u-flex u-margin-block-start-16">
<Button text noMargin on:click={() => (showCreate = !showCreate)}>
<span class="icon-plus" aria-hidden="true" />
<span class="u-text">Create variable</span>
</Button>
</div>
</WizardStep>
{#if showCreate}
<Create
bind:selectedVar
bind:showCreate
on:created={handleCreated}
on:updated={handleUpdated} />
{/if}
@@ -0,0 +1,22 @@
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
export const createFunction = writable<{
id?: string;
name: string;
execute: string[];
runtime: string;
vars?: Partial<Models.Variable>[];
events?: string[];
schedule?: string;
timeout?: number;
}>({
id: null,
name: null,
execute: [],
runtime: null,
vars: [],
events: [],
schedule: null,
timeout: null
});
@@ -83,7 +83,7 @@
</script>
<svelte:head>
<title>Appwrite - Console</title>
<title>Console - Appwrite</title>
</svelte:head>
{#if $project}
@@ -102,7 +102,7 @@
</script>
<svelte:head>
<title>Appwrite - API Key</title>
<title>API Key - Appwrite</title>
</svelte:head>
<Container>
@@ -23,12 +23,13 @@
import AppleTvos from './appleTvOS.svelte';
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import Android from './android.svelte';
const projectId = $page.params.project;
const platformId = $page.params.platform;
const types: Record<string, typeof SvelteComponent> = {
web: Web,
android: Web,
android: Android,
'apple-ios': AppleiOs,
'apple-macos': AppleMacOs,
'apple-watchos': AppleWatchOs,
@@ -113,7 +114,6 @@
<svelte:fragment slot="aside">
<div class="box">
<div class="u-flex u-gap-16">
<!-- <img class="avatar" src="/icons/dark/color/android.svg" /> -->
<div class="u-cross-child-center u-line-height-1-5">
<h6 class="u-bold">{$platform.name}</h6>
<p>{$platform.hostname}</p>

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