Merge branch 'main' of github.com:appwrite/console into feat-4840-allow-retrying-failed-builds

This commit is contained in:
Torsten Dittmann
2023-06-01 17:42:18 +02:00
61 changed files with 918 additions and 737 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
VITE_APPWRITE_ENDPOINT=
VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_CONSOLE_MODE=self-hosted
+2 -2
View File
@@ -22,11 +22,11 @@ jobs:
# run: npm audit --audit-level low
- name: Install dependencies
run: npm ci
- name: Build Console
run: npm run build
- name: Svelte Diagnostics
run: npm run check
- name: Linter
run: npm run lint
- name: Unit Tests
run: npm test
- name: Build Console
run: npm run build
+34 -21
View File
@@ -10,25 +10,30 @@ If you are worried about or dont know where to start, check out the next sect
```
├── src
│ ├── lib // All non-route components, accessible over "import ... from '$lib'"
│ │ ├── components // Re-usable components
│ │ ├── elements // Re-usable elements
│ │ ├── layout // Global components for the layout (Nav/Content/Container)
│ │ ── stores // Global stores (state management)
└─── routes
├── console // Routes that need authentication
│ ├──[project]
├── database // Database Service
├── [collection] // Nested Route for the collection "/console/[PROJECT_ID]/database/[COLLECTION_ID]"
│ │ │ ├── _create.svelte // Component to Create collections
└── index.svelte // Entrypoint for "/console/[PROJECT_ID]/database"
│ │ │ ├── storage // Storage Service "/console/[PROJECT]/storage"
│ │ │ └── auth // Users Service "/console/[PROJECT]/auth"
│ │ └──...
├── login.svelte // Component for Login "/console/login"
└── register.svelte // Component for Register "/console/register"
├── build // Compiled application
└── static // Static assets
│ ├── lib // Reusable logic (accessible with '$lib')
│ │ ├── actions // Svelte actions
│ │ ├── charts // Chart components
│ │ ├── components // Re-usable components
│ │ ── elements // Re-usable elements
│ ├── helpers // Small functions used through out the console
├── images // Images used in the console
│ ├── layout // Global components for the layout (Nav/Content/Container)
├── mock // Mock components used for testing
└── stores // Global stores (state management)
└── routes
└── console // Routes that need authentication
│ │ └── project-[project]
│ │ │ └── database // Database Service
│ │ │ │ ├── +layout.svelte // Layout head and other logic like realtime events is set here
│ │ │ ├── +layout.ts // Layout data is set here (Header, Breadcrumbs, ...)
│ │ │ ├── +page.svelte // Page displayed on "/console/project-[PROJECT_ID]/database"
│ │ │ │ ├── +page.ts // Necessary data for the page is fetched here
│ │ │ │ └── create.svelte // Component to create databases
│ │ │ └── ... // Other services
│ │ └── ...
│ └── ... // Routes that don't need authentication
├── build // Compiled application
└── static // Static assets
```
## Development
@@ -41,11 +46,19 @@ git clone https://github.com/appwrite/console.git appwrite-console
### 2. Install dependencies with npm
Navigate to the Appwrite Console repository and install dependencies.
```bash
npm install
cd appwrite-console && npm install
```
### 3. Setup environment variables
### 3. Install and run Appwrite locally
When you run the Appwrite Console locally, it needs to point to a backend as well. The easiest way to do this is to run an Appwrite instance locally.
Follow the [install instructions](https://appwrite.io/docs/installation) in the Appwrite docs.
### 4. Setup environment variables
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
+1 -1
View File
@@ -42,7 +42,7 @@
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first {target} to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
+16 -16
View File
@@ -31,7 +31,7 @@
}
}
}
checkOverflow();
requestAnimationFrame(checkOverflow);
window.addEventListener('resize', checkOverflow);
return {
@@ -46,22 +46,22 @@
}
</script>
<div
class="interactive-text-output is-buttons-on-top"
style:min-inline-size="0"
style:display="inline-flex">
<span
style:white-space="nowrap"
class="text u-line-height-1-5"
style:overflow="hidden"
use:truncateText>
<slot />
</span>
<div class="interactive-text-output-buttons">
<Copy {value} {event}>
<Copy {value} {event}>
<div
class="interactive-text-output is-buttons-on-top"
style:min-inline-size="0"
style:display="inline-flex">
<span
style:white-space="nowrap"
class="text u-line-height-1-5"
style:overflow="hidden"
use:truncateText>
<slot />
</span>
<div class="interactive-text-output-buttons">
<button class="interactive-text-output-button is-hidden" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</Copy>
</div>
</div>
</div>
</Copy>
+1 -1
View File
@@ -134,7 +134,7 @@
You have no teams. Create a team to see them here.
</p>
<p class="text u-line-height-1-5">
Need a hand? Check out our <a
Need a hand? Learn more in our <a
href="https://appwrite.io/docs/client/teams"
target="_blank"
rel="noopener noreferrer">
+1 -1
View File
@@ -153,7 +153,7 @@
You have no users. Create a user to see them here.
</p>
<p class="text u-line-height-1-5">
Need a hand? Check out our <a
Need a hand? Learn more in our <a
href="https://appwrite.io/docs/server/users"
target="_blank"
rel="noopener noreferrer">
+1 -1
View File
@@ -84,7 +84,7 @@
bind:value
bind:this={element}
on:invalid={handleInvalid}
style:--amount-of-buttons={required ? 0 : 1.75} />
style:--amount-of-buttons={nullable && !required ? 1.75 : 0} />
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if nullable && !required}
+9 -7
View File
@@ -27,15 +27,17 @@
error = element.validationMessage;
};
$: if (element && required && !value) {
element.setCustomValidity('This field is required');
const isNotEmpty = (value: string | number | boolean) => {
return typeof value === 'boolean' ? true : !!value;
};
$: if (required && !isNotEmpty(value)) {
element?.setCustomValidity('This field is required');
} else {
element?.setCustomValidity('');
}
$: if (element && required && value) {
element.setCustomValidity('');
}
$: if (value) {
$: if (isNotEmpty(value)) {
error = null;
}
+18 -13
View File
@@ -53,6 +53,9 @@
value = prevValue;
}
}
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
</script>
<FormItem>
@@ -75,19 +78,21 @@
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if maxlength}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if showTextCounter}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if showNullCheckbox}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+19 -14
View File
@@ -50,6 +50,9 @@
value = prevValue;
}
}
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
</script>
<FormItem>
@@ -72,20 +75,22 @@
bind:this={element}
on:invalid={handleInvalid}
style:--amount-of-buttons={required ? undefined : 0.25} />
<ul
class="buttons-list u-gap-8 u-cross-center u-position-absolute d u-inset-block-end-1 u-inset-inline-end-1 u-padding-block-8 u-padding-inline-12"
style="border-end-end-radius:0.0625rem;">
{#if maxlength}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-gap-8 u-cross-center u-position-absolute d u-inset-block-end-1 u-inset-inline-end-1 u-padding-block-8 u-padding-inline-12"
style="border-end-end-radius:0.0625rem;">
{#if showTextCounter}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if showNullCheckbox}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{/if}
</div>
{#if error}
+27 -7
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
export let label: string;
export let optionalText: string | undefined = undefined;
@@ -9,6 +11,7 @@
export let value = '';
export let placeholder = '';
export let required = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -39,15 +42,20 @@
error = element.validationMessage;
};
const handleInput = () => {
if (value === '') {
value = null;
}
};
$: if (value) {
error = null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem>
@@ -66,9 +74,21 @@
type="url"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
on:input={handleInput}
bind:this={element}
on:invalid={handleInvalid} />
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if maxlength}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if nullable && !required}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+20 -5
View File
@@ -2,11 +2,28 @@
import type { Action } from 'svelte/action';
export let isSticky = false;
let isOverflowing = false;
const hasOverflow: Action<HTMLDivElement, (value: boolean) => void> = (node, callback) => {
const hasOverflow: Action<HTMLDivElement> = (node) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width < entry.target.scrollWidth);
let overflowing = false;
if (entry.contentRect.width < entry.target.scrollWidth) {
overflowing = true;
}
const cols = entry.target.querySelectorAll('.table-thead-col');
for (let i = 0; i < cols.length; i++) {
const col = cols[i];
const cs = getComputedStyle(col);
const innerWidth =
col.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
if (innerWidth < 32) {
overflowing = true;
}
}
isOverflowing = overflowing;
}
});
@@ -18,12 +35,10 @@
}
};
};
let isOverflowing = false;
</script>
<div class="table-with-scroll u-margin-block-start-32" data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<div class="table-wrapper" use:hasOverflow>
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
<slot />
</table>
+6
View File
@@ -11,6 +11,12 @@ async function securedCopy(value: string) {
function unsecuredCopy(value: string) {
const textArea = document.createElement('textarea');
textArea.value = value;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
@@ -81,7 +81,7 @@
disabled={(secret === provider.secret &&
enabled === provider.enabled &&
appId === provider.appId) ||
!(appId && clientSecret && endpoint)}
!(appId && clientSecret)}
submit>Update</Button>
</svelte:fragment>
</Modal>
@@ -1,17 +1,17 @@
<script lang="ts">
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { InputSwitch } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { app } from '$lib/stores/app';
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
import { addNotification } from '$lib/stores/notifications';
import type { Provider } from '$lib/stores/oauth-providers';
import { OAuthProviders } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
import { OAuthProviders } from '$lib/stores/oauth-providers';
import { app } from '$lib/stores/app';
import { page } from '$app/stores';
import type { Provider } from '$lib/stores/oauth-providers';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
const projectId = $page.params.project;
@@ -78,7 +78,7 @@
provider: provider.name.toLowerCase()
});
}}>
<div class="image-item">
<div class="avatar">
<img
height="20"
width="20"
@@ -15,6 +15,7 @@
try {
await sdk.forProject.users.deleteSession($page.params.user, selectedSessionId);
await invalidate(Dependencies.SESSIONS);
showDelete = false;
addNotification({
type: 'success',
message: 'Session has been deleted'
@@ -1,16 +1,16 @@
<script lang="ts">
import { EmptySearch } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableHeader,
TableRow,
TableCellHead,
TableCell,
TableCellText
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import DeleteAllSessions from '../deleteAllSessions.svelte';
@@ -47,7 +47,7 @@
<TableRow>
<TableCell title="Client">
<div class="u-flex u-gap-12 u-cross-center">
<div class="image-item">
<div class="avatar">
<img
height="20"
width="20"
@@ -19,7 +19,7 @@
await invalidate(Dependencies.USER);
addNotification({
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
$user.emailVerification ? 'unverified' : 'verified'
!$user.emailVerification ? 'unverified' : 'verified'
}`,
type: 'success'
});
@@ -75,7 +75,7 @@
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
@@ -180,7 +180,7 @@
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
@@ -34,8 +34,7 @@
</script>
<script lang="ts">
import { InputChoice } from '$lib/elements/forms';
import Boolean from '../document-[document]/attributes/boolean.svelte';
import { InputChoice, InputSelect } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeBoolean> = {
@@ -49,18 +48,17 @@
}
</script>
<Boolean
<InputSelect
id="default"
label="Default value"
bind:value={data.default}
attribute={{
key: data.key,
required: data.required,
status: 'enabled',
type: 'boolean',
array: data.array
}}
disabled={data.array || data.required} />
placeholder="Select a value"
disabled={data.required || data.array}
options={[
{ label: 'NULL', value: null },
{ label: 'True', value: true },
{ label: 'False', value: false }
]}
bind:value={data.default} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -48,7 +48,7 @@
id="default"
label="Default value"
bind:value={data.default}
disabled={data.required} />
disabled={data.required || data.array} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -71,31 +71,19 @@
</svelte:fragment>
<FormList>
{#if selectedAttribute?.type !== 'relationship'}
<div>
<InputText
id="key"
label="Attribute Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
autofocus
required
readonly />
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
<span
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
</span>
</div>
</div>
<InputText
id="key"
label="Attribute Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
autofocus
readonly />
{/if}
{#if option}
<svelte:component
this={option.component}
bind:data={selectedAttribute}
editing
bind:data={selectedAttribute}
on:close={() => (option = null)} />
{/if}
</FormList>
@@ -50,7 +50,8 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required} />
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -36,8 +36,7 @@
</script>
<script lang="ts">
import { InputChoice, InputTags } from '$lib/elements/forms';
import Enum from '../document-[document]/attributes/enum.svelte';
import { InputChoice, InputSelect, InputTags } from '$lib/elements/forms';
export let editing = false;
export let data: Partial<Models.AttributeEnum>;
@@ -45,6 +44,20 @@
$: if (data.required || data.array) {
data.default = null;
}
$: options = [
...(data?.elements ?? []).map((element) => {
return {
label: element,
value: element
};
}),
!data.required &&
!data.array && {
label: 'NULL',
value: null
}
].filter(Boolean);
</script>
<InputTags
@@ -53,19 +66,12 @@
bind:tags={data.elements}
placeholder="Add elements here"
required />
<Enum
<InputSelect
id="default"
label="Default value"
attribute={{
key: data.key,
type: 'string',
status: 'enabled',
format: 'enum',
elements: data.elements ?? [],
required: data.required,
default: data.default
}}
disabled={data.array || data.required}
placeholder="Select a value"
{options}
bind:value={data.default} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
@@ -54,9 +54,20 @@
}
</script>
<InputNumber id="min" label="Min" placeholder="Enter size" bind:value={data.min} />
<InputNumber id="max" label="Max" placeholder="Enter size" bind:value={data.max} />
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
step="any"
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
step="any"
required={editing} />
<InputNumber
id="default"
label="Default value"
@@ -65,6 +76,7 @@
max={data.max}
bind:value={data.default}
disabled={data.required || data.array}
nullable={!data.required && !data.array}
step="any" />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
@@ -54,9 +54,18 @@
}
</script>
<InputNumber id="min" label="Min" placeholder="Enter size" bind:value={data.min} />
<InputNumber id="max" label="Max" placeholder="Enter size" bind:value={data.max} />
<InputNumber
id="min"
label="Min"
placeholder="Enter size"
bind:value={data.min}
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
required={editing} />
<InputNumber
id="default"
label="Default value"
@@ -64,7 +73,8 @@
min={data.min}
max={data.max}
bind:value={data.default}
disabled={data.required || data.array} />
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -48,7 +48,8 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required || data.array} />
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -98,7 +98,6 @@
});
// Reactive statements
$: getCollections(search).then((res) => (collectionList = res));
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
@@ -219,7 +218,6 @@
placeholder="Select a relation"
options={relationshipType}
disabled={editing} />
<div class="u-flex u-flex-vertical u-gap-16">
<div class="box">
<div class="u-flex u-align u-cross-center u-main-center u-gap-32">
@@ -34,8 +34,7 @@
</script>
<script lang="ts">
import { InputChoice, InputNumber } from '$lib/elements/forms';
import String from '../document-[document]/attributes/string.svelte';
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
export let data: Partial<Models.AttributeString> = {
required: false,
@@ -55,22 +54,27 @@
label="Size"
placeholder="Enter size"
bind:value={data.size}
required
required={!editing}
readonly={editing} />
<String
id="default"
label="Default value"
attribute={{
key: 'default',
type: 'string',
required: data.required,
array: data.array,
size: data.size,
default: data.default,
status: 'enabled'
}}
disabled={data.required || data.array}
bind:value={data.default} />
{#if data.size >= 50}
<InputTextarea
id="default"
label="Default"
placeholder="Enter string"
disabled={data.required || data.array}
nullable={!data.required && !data.array}
maxlength={data.size}
bind:value={data.default} />
{:else}
<InputText
id="default"
label="Default"
placeholder="Enter string"
disabled={data.required || data.array}
nullable={!data.required && !data.array}
maxlength={data.size}
bind:value={data.default} />
{/if}
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -49,7 +49,8 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required || data.array} />
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
Indicate whether this is a required attribute
</InputChoice>
@@ -7,7 +7,6 @@
export let value: boolean;
export let attribute: Models.AttributeBoolean;
export let optionalText: string | undefined = undefined;
export let disabled = false;
</script>
<InputSelect
@@ -22,5 +21,4 @@
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)}
bind:value
{disabled} />
bind:value />
@@ -7,7 +7,6 @@
export let value: string;
export let attribute: Models.AttributeEnum;
export let optionalText: string | undefined = undefined;
export let disabled = false;
$: options = [
...attribute.elements.map((element) => {
@@ -31,5 +30,4 @@
{optionalText}
required={attribute.required}
placeholder="Select a value"
showLabel={!!label?.length}
{disabled} />
showLabel={!!label?.length} />
@@ -17,5 +17,5 @@
required={attribute.required}
min={attribute.min}
max={attribute.max}
bind:value
step={attribute.type === 'double' ? 'any' : 1} />
step={attribute.type === 'double' ? 'any' : 1}
bind:value />
@@ -3,12 +3,12 @@
import { PaginationInline } from '$lib/components';
import { SelectSearchItem } from '$lib/elements';
import { Button, InputSelectSearch, Label } from '$lib/elements/forms';
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { doc } from '../store';
import { isRelationshipToMany } from './store';
import { preferences } from '$lib/stores/preferences';
export let id: string;
export let label: string;
@@ -32,7 +32,7 @@
onMount(async () => {
if (value) {
if (isRelationshipToMany(attribute)) {
relatedList = value as string[];
relatedList = (value as string[]).slice();
} else {
singleRel = value as string;
}
@@ -7,30 +7,27 @@
export let value: string;
export let attribute: Models.AttributeString;
export let optionalText: string | undefined = undefined;
export let disabled = false;
</script>
{#if attribute.size >= 50}
<InputTextarea
{id}
{label}
nullable
nullable={!attribute.required}
placeholder="Enter string"
showLabel={!!label?.length}
required={attribute.required}
maxlength={attribute.size}
{disabled}
bind:value />
{:else}
<InputText
{id}
{label}
{optionalText}
nullable
nullable={!attribute.required}
placeholder="Enter string"
showLabel={!!label?.length}
required={attribute.required}
maxlength={attribute.size}
{disabled}
bind:value />
{/if}
@@ -15,5 +15,6 @@
{optionalText}
placeholder="Enter URL"
showLabel={!!label?.length}
nullable={!attribute.required}
required={attribute.required}
bind:value />
@@ -125,7 +125,7 @@
<div class="u-text-center">
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
@@ -3,13 +3,13 @@
import { page } from '$app/stores';
import { Id } from '$lib/components';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRowLink
TableRowLink,
TableScroll
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import type { PageData } from './$types';
@@ -20,7 +20,7 @@
const databaseId = $page.params.database;
</script>
<Table>
<TableScroll>
<TableHeader>
{#each $columns as column}
{#if column.show}
@@ -54,4 +54,4 @@
</TableRowLink>
{/each}
</TableBody>
</Table>
</TableScroll>
@@ -3,13 +3,13 @@
import { page } from '$app/stores';
import { Id } from '$lib/components';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRowLink
TableRowLink,
TableScroll
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import type { PageData } from './$types';
@@ -19,7 +19,7 @@
const projectId = $page.params.project;
</script>
<Table>
<TableScroll>
<TableHeader>
{#each $columns as column}
{#if column.show}
@@ -55,4 +55,4 @@
</TableRowLink>
{/each}
</TableBody>
</Table>
</TableScroll>
@@ -62,10 +62,10 @@
function setCodeSnippets(lang: string) {
return {
Unix: {
code: `appwrite functions createDeployment \\
--functionId=${functionId} \\
--entrypoint='index.${lang}' \\
--code="." \\
code: `appwrite functions createDeployment \\
--functionId=${functionId} \\
--entrypoint='index.${lang}' \\
--code="." \\
--activate=true`,
language: 'bash'
},
@@ -110,7 +110,7 @@
<p class="text u-line-height-1-5">
You have no execution logs. Create and activate a deployment to see it here.
</p>
<p class="text u-line-height-1-5">Need a hand? Check out our documentation</p>
<p class="text u-line-height-1-5">Need a hand? Learn more in our documentation</p>
</div>
<div class="u-flex u-gap-16">
<Button text external href="https://appwrite.io/docs/functions#execute">
@@ -40,7 +40,9 @@
<CoverTitle href={`/console/project-${projectId}/functions`}>
{$func?.name}
</CoverTitle>
<Id value={$func?.$id} event="function">Function ID</Id>
{#if $func?.$id}
<Id value={$func.$id} event="function">{$func.$id}</Id>
{/if}
</svelte:fragment>
<Tabs>
@@ -1,520 +1,25 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import {
Box,
CardGrid,
DropList,
DropListItem,
Empty,
Output,
PaginationInline,
Secret
} from '$lib/components';
import Heading from '$lib/components/heading.svelte';
import { Roles } from '$lib/components/permissions';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputCron, InputNumber, InputText } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { app } from '$lib/stores/app';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
import Variable from '../../createVariable.svelte';
import { execute, func } from '../store';
import UploadVariables from './uploadVariables.svelte';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow
} from '$lib/elements/table';
import type { PageData } from './$types';
import Delete from './delete.svelte';
import UpdateEvents from './updateEvents.svelte';
import ExecuteFunction from './executeFunction.svelte';
import UpdateName from './updateName.svelte';
import UpdatePermissions from './updatePermissions.svelte';
import UpdateSchedule from './updateSchedule.svelte';
import UpdateVariables from './updateVariables.svelte';
import UpdateTimeout from './updateTimeout.svelte';
import DangerZone from './dangerZone.svelte';
export let data: PageData;
const functionId = $page.params.function;
let showDelete = false;
let selectedVar: Models.Variable = null;
let showVariablesUpload = false;
let showVariablesModal = false;
let showVariablesDropdown = [];
let timeout: number = null;
let functionName: string = null;
let functionSchedule: string = null;
let permissions: string[] = [];
let arePermsDisabled = true;
let offset = 0;
onMount(async () => {
timeout ??= $func.timeout;
functionName ??= $func.name;
functionSchedule ??= $func.schedule;
permissions = $func.execute;
});
async function updateName() {
try {
await sdk.forProject.functions.update(
functionId,
functionName,
$func.execute || undefined,
$func.events || undefined,
$func.schedule || undefined,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent(Submit.FunctionUpdateName);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionUpdateName);
}
}
async function updatePermissions() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
permissions,
$func.events || undefined,
$func.schedule || undefined,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent(Submit.FunctionUpdatePermissions);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionUpdatePermissions);
}
}
async function updateSchedule() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
$func.execute || undefined,
$func.events || undefined,
functionSchedule,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
type: 'success',
message: 'Cron Schedule has been updated'
});
trackEvent(Submit.FunctionUpdateSchedule);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateSchedule);
}
}
async function updateTimeout() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
$func.execute || undefined,
$func.events || undefined,
$func.schedule || undefined,
timeout,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
type: 'success',
message: 'Timeout has been updated'
});
trackEvent(Submit.FunctionUpdateTimeout);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateTimeout);
}
}
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
const variable = event.detail;
try {
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
await invalidate(Dependencies.VARIABLES);
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
trackEvent(Submit.VariableCreate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableCreate);
}
}
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
const variable = event.detail;
try {
await sdk.forProject.functions.updateVariable(
functionId,
variable.$id,
variable.key,
variable.value
);
await invalidate(Dependencies.VARIABLES);
selectedVar = null;
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
trackEvent(Submit.VariableUpdate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableUpdate);
}
}
async function handleVariableDeleted(variable: Models.Variable) {
try {
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
await invalidate(Dependencies.VARIABLES);
addNotification({
type: 'success',
message: `Variable has been deleted`
});
trackEvent(Submit.VariableDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableDelete);
}
}
function downloadVariables() {
if (data.variables.total) {
let content = data.variables.variables
.map((variable) => `${variable.key}=${variable.value}`)
.join('\n');
const file = new File([content], '.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);
}
}
$: if (permissions) {
if (symmetricDifference(permissions, $func.execute).length) {
arePermsDisabled = false;
} else arePermsDisabled = true;
}
export let data;
</script>
<Container>
<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>
<p class="text u-capitalize">{$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 onSubmit={updateName}>
<CardGrid>
<Heading tag="h6" size="7">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 onSubmit={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 <a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">
Permissions Guide
</a>.
</p>
<svelte:fragment slot="aside">
<Roles bind:roles={permissions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<ExecuteFunction />
<UpdateName />
<UpdatePermissions />
<UpdateEvents />
<Form onSubmit={updateSchedule}>
<CardGrid>
<Heading tag="h6" size="7">Schedule</Heading>
<p>
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://en.wikipedia.org/wiki/Cron"
target="_blank"
rel="noopener noreferrer"
class="link">
More details on Cron syntax here.</a>
</p>
<svelte:fragment slot="aside">
<FormList>
<InputCron
bind:value={functionSchedule}
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>
<CardGrid>
<Heading tag="h6" size="7">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
event="download_env"
disabled={!data.variables.total}
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>
{@const limit = 10}
{@const sum = data.variables.total}
{#if sum}
<div class="u-flex u-flex-vertical u-gap-16">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>Key</TableCellHead>
<TableCellHead width={180}>Value</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
{#each data.variables.variables.slice(offset, offset + limit) as variable, i}
<TableRow>
<TableCell title="key">
<Output value={variable.key} hideCopyIcon>
{variable.key}
</Output>
</TableCell>
<TableCell showOverflow title="value">
<Secret copyEvent="variable" value={variable.value} />
</TableCell>
<TableCell showOverflow title="options">
<DropList
bind:show={showVariablesDropdown[i]}
placement="bottom-start"
noArrow>
<Button
text
round
ariaLabel="more options"
on:click={() =>
(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);
showVariablesDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Button text noMargin on:click={() => (showVariablesModal = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create variable</span>
</Button>
<div class="u-flex u-main-space-between">
<p class="text">Total variables: {sum}</p>
<PaginationInline {sum} {limit} bind:offset hidePages />
</div>
</div>
{:else}
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
Create a variable to get started
</Empty>
{/if}
</svelte:fragment>
</CardGrid>
<Form onSubmit={updateTimeout}>
<CardGrid>
<Heading tag="h6" size="7">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 danger>
<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 u-trim-1">{$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>
<UpdateSchedule />
<UpdateVariables variableList={data.variables} />
<UpdateTimeout />
<DangerZone />
</Container>
<Delete bind:showDelete />
{#if showVariablesModal}
<Variable
bind:selectedVar
bind:showCreate={showVariablesModal}
on:created={handleVariableCreated}
on:updated={handleVariableUpdated} />
{/if}
{#if showVariablesUpload}
<UploadVariables bind:show={showVariablesUpload} />
{/if}
@@ -0,0 +1,33 @@
<script lang="ts">
import { Box, CardGrid } from '$lib/components';
import Heading from '$lib/components/heading.svelte';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import Delete from './deleteModal.svelte';
import { func } from '../store';
let showDelete = false;
</script>
<CardGrid danger>
<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 u-trim-1">{$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>
<Delete bind:showDelete />
@@ -0,0 +1,36 @@
<script lang="ts">
import { base } from '$app/paths';
import { CardGrid, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { app } from '$lib/stores/app';
import { execute, func } from '../store';
</script>
<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>
<p class="text u-capitalize">{$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>
@@ -0,0 +1,66 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { func } from '../store';
const functionId = $page.params.function;
let functionName: string = null;
onMount(async () => {
functionName ??= $func.name;
});
async function updateName() {
try {
await sdk.forProject.functions.update(
functionId,
functionName,
$func.execute || undefined,
$func.events || undefined,
$func.schedule || undefined,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Name has been updated',
type: 'success'
});
trackEvent(Submit.FunctionUpdateName);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionUpdateName);
}
}
</script>
<Form onSubmit={updateName}>
<CardGrid>
<Heading tag="h6" size="7">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>
@@ -0,0 +1,78 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { func } from '../store';
import { Roles } from '$lib/components/permissions';
import { symmetricDifference } from '$lib/helpers/array';
const functionId = $page.params.function;
let arePermsDisabled = true;
let permissions: string[] = [];
onMount(async () => {
permissions = $func.execute;
});
async function updatePermissions() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
permissions,
$func.events || undefined,
$func.schedule || undefined,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent(Submit.FunctionUpdatePermissions);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionUpdatePermissions);
}
}
$: if (permissions) {
if (symmetricDifference(permissions, $func.execute).length) {
arePermsDisabled = false;
} else arePermsDisabled = true;
}
</script>
<Form onSubmit={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 <a
href="https://appwrite.io/docs/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">
Permissions Guide
</a>.
</p>
<svelte:fragment slot="aside">
<Roles bind:roles={permissions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,71 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputCron } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { func } from '../store';
const functionId = $page.params.function;
let functionSchedule: string = null;
onMount(async () => {
functionSchedule ??= $func.schedule;
});
async function updateSchedule() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
$func.execute || undefined,
$func.events || undefined,
functionSchedule,
$func.timeout || undefined,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
type: 'success',
message: 'Cron Schedule has been updated'
});
trackEvent(Submit.FunctionUpdateSchedule);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateSchedule);
}
}
</script>
<Form onSubmit={updateSchedule}>
<CardGrid>
<Heading tag="h6" size="7">Schedule</Heading>
<p>
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://en.wikipedia.org/wiki/Cron"
target="_blank"
rel="noopener noreferrer"
class="link">
More details on Cron syntax here.</a>
</p>
<svelte:fragment slot="aside">
<FormList>
<InputCron
bind:value={functionSchedule}
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>
@@ -0,0 +1,66 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputNumber } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { func } from '../store';
const functionId = $page.params.function;
let timeout: number = null;
onMount(async () => {
timeout ??= $func.timeout;
});
async function updateTimeout() {
try {
await sdk.forProject.functions.update(
functionId,
$func.name,
$func.execute || undefined,
$func.events || undefined,
$func.schedule || undefined,
timeout,
$func.enabled
);
await invalidate(Dependencies.FUNCTION);
addNotification({
type: 'success',
message: 'Timeout has been updated'
});
trackEvent(Submit.FunctionUpdateTimeout);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FunctionUpdateTimeout);
}
}
</script>
<Form onSubmit={updateTimeout}>
<CardGrid>
<Heading tag="h6" size="7">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>
@@ -0,0 +1,233 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
import {
CardGrid,
Heading,
DropList,
DropListItem,
Empty,
Output,
PaginationInline,
Secret
} from '$lib/components';
import Variable from '../../createVariable.svelte';
import UploadVariables from './uploadVariablesModal.svelte';
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { func } from '../store';
export let variableList: Models.VariableList;
const functionId = $page.params.function;
let showVariablesDropdown = [];
let selectedVar: Models.Variable = null;
let showVariablesUpload = false;
let showVariablesModal = false;
let offset = 0;
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
const variable = event.detail;
try {
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
await invalidate(Dependencies.VARIABLES);
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
trackEvent(Submit.VariableCreate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableCreate);
}
}
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
const variable = event.detail;
try {
await sdk.forProject.functions.updateVariable(
functionId,
variable.$id,
variable.key,
variable.value
);
await invalidate(Dependencies.VARIABLES);
selectedVar = null;
showVariablesModal = false;
addNotification({
type: 'success',
message: `${$func.name} variables have been updated`
});
trackEvent(Submit.VariableUpdate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableUpdate);
}
}
async function handleVariableDeleted(variable: Models.Variable) {
try {
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
await invalidate(Dependencies.VARIABLES);
addNotification({
type: 'success',
message: `Variable has been deleted`
});
trackEvent(Submit.VariableDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.VariableDelete);
}
}
function downloadVariables() {
if (variableList.total) {
let content = variableList.variables
.map((variable) => `${variable.key}=${variable.value}`)
.join('\n');
const file = new File([content], '.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);
}
}
</script>
<CardGrid>
<Heading tag="h6" size="7">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
event="download_env"
disabled={!variableList.total}
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>
{@const limit = 10}
{@const sum = variableList.total}
{#if sum}
<div class="u-flex u-flex-vertical u-gap-16">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>Key</TableCellHead>
<TableCellHead width={180}>Value</TableCellHead>
<TableCellHead width={30} />
</TableHeader>
<TableBody>
{#each variableList.variables.slice(offset, offset + limit) as variable, i}
<TableRow>
<TableCell title="key">
<Output value={variable.key} hideCopyIcon>
{variable.key}
</Output>
</TableCell>
<TableCell showOverflow title="value">
<Secret copyEvent="variable" value={variable.value} />
</TableCell>
<TableCell showOverflow title="options">
<DropList
bind:show={showVariablesDropdown[i]}
placement="bottom-start"
noArrow>
<Button
text
round
ariaLabel="more options"
on:click={() =>
(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);
showVariablesDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Button text noMargin on:click={() => (showVariablesModal = true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create variable</span>
</Button>
<div class="u-flex u-main-space-between">
<p class="text">Total variables: {sum}</p>
<PaginationInline {sum} {limit} bind:offset hidePages />
</div>
</div>
{:else}
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
Create a variable to get started
</Empty>
{/if}
</svelte:fragment>
</CardGrid>
{#if showVariablesModal}
<Variable
bind:selectedVar
bind:showCreate={showVariablesModal}
on:created={handleVariableCreated}
on:updated={handleVariableUpdated} />
{/if}
{#if showVariablesUpload}
<UploadVariables bind:show={showVariablesUpload} />
{/if}
@@ -10,6 +10,8 @@ export const load: PageLoad = async ({ params, depends }) => {
const key = await sdk.forConsole.projects.getKey(params.project, params.key);
if (key.expire) {
key.expire = new Date(key.expire).toISOString().slice(0, 23);
} else {
key.expire = undefined;
}
return {
@@ -142,7 +142,7 @@
<div class="u-text-center">
<Heading size="7" tag="h4">Create your first platform to get started.</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">
Need a hand? Check out our documentation.
Need a hand? Learn more in our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
@@ -5,7 +5,7 @@
const example1 = `dependencies:
appwrite: ^${$versions['client-flutter']}`;
const example2 = `pub get appwrite`;
const example2 = `flutter pub add appwrite`;
</script>
<WizardStep>
@@ -35,8 +35,12 @@
</svelte:fragment>
{#if method === Method.NPM}
<p>
Use <a href="https://npmjs.org" target="_blank" rel="noopener noreferrer" class="link"
>NPM (node package manager)</a> from your command line to add Appwrite SDK to your project.
Use <a
href="https://npmjs.com/package/appwrite"
target="_blank"
rel="noopener noreferrer"
class="link">NPM (node package manager)</a> from your command line to add Appwrite SDK
to your project.
</p>
<Code label="Bash" language="sh" code="npm install appwrite" withCopy />
<p class="common-section">
@@ -115,7 +115,10 @@
<CardGrid>
<Heading tag="h6" size="7">Services</Heading>
<p class="text">Choose services you wish to enable or disable.</p>
<p class="text">
Choose services you wish to enable or disable for the client API. When disabled, the
services are not accessible to client SDKs but remain accessible to server SDKs.
</p>
<svelte:fragment slot="aside">
<FormList>
<form class="form">
@@ -6,13 +6,13 @@
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import {
Table,
TableBody,
TableRowLink,
TableCellHead,
TableCell,
TableCellText,
TableHeader
TableHeader,
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { wizard } from '$lib/stores/wizard';
@@ -46,10 +46,10 @@
</div>
{#if data.webhooks.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>POST URL</TableCellHead>
<TableCellHead width={200}>Name</TableCellHead>
<TableCellHead width={180}>POST URL</TableCellHead>
<TableCellHead width={80}>Events</TableCellHead>
</TableHeader>
<TableBody>
@@ -57,7 +57,7 @@
<TableRowLink
href={`${base}/console/project-${projectId}/settings/webhooks/${webhook.$id}`}>
<TableCell title="Name">
<div class="u-flex u-main-space-between">
<div class="u-flex u-main-space-between u-cross-center">
{webhook.name}
{#if webhook.security === false}
<Pill>SLL/TLS disabled</Pill>
@@ -69,7 +69,7 @@
</TableRowLink>
{/each}
</TableBody>
</Table>
</TableScroll>
{:else}
<Empty
single
@@ -34,7 +34,9 @@
<CoverTitle href={`/console/project-${projectId}/storage`}>
{$bucket?.name}
</CoverTitle>
<Id value={$bucket?.$id} event="bucket">Bucket ID</Id>
{#if $bucket?.$id}
<Id value={$bucket.$id} event="bucket">{$bucket.$id}</Id>
{/if}
</svelte:fragment>
<Tabs>