mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of https://github.com/appwrite/appwrite-console-poc into refactor-layout-state
This commit is contained in:
Generated
+14
-14
@@ -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
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
'flutter',
|
||||
'apple',
|
||||
'android',
|
||||
'node_js',
|
||||
'node',
|
||||
'php',
|
||||
'python',
|
||||
'ruby',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { writable, readable, get } from 'svelte/store';
|
||||
import { writable, readable } from 'svelte/store';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
|
||||
export type Tab = {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Appwrite - User</title>
|
||||
<title>User - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-7
@@ -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" />
|
||||
|
||||
+2
-6
@@ -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}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Appwrite - {$collection?.name ?? 'Collection'}</title>
|
||||
<title>{$collection?.name ?? 'Collection'} - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $collection && loaded}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Appwrite - Database Document</title>
|
||||
<title>Database Document - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $doc}
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+2
-2
@@ -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} />
|
||||
|
||||
+1
-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.
|
||||
|
||||
+3
-4
@@ -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 we’ll 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>
|
||||
+92
@@ -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>
|
||||
+21
@@ -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>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<script>
|
||||
import { Card } from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<h1>Monitors</h1>
|
||||
|
||||
<Card />
|
||||
</Container>
|
||||
+575
-4
@@ -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} />
|
||||
|
||||
+43
@@ -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>
|
||||
+69
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
+64
-1
@@ -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">Let’s 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}
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Appwrite - API Key</title>
|
||||
<title>API Key - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user