Merge branch 1.4.x into design-reviews-1.4

This commit is contained in:
tglide
2023-08-22 16:49:54 +01:00
269 changed files with 14435 additions and 2129 deletions
+3
View File
@@ -24,5 +24,8 @@ module.exports = {
browser: true,
es2017: true,
node: true
},
globals: {
globalThis: false // false means it is not writeable
}
};
+6552 -25
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -19,14 +19,16 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/console": "npm:matej-appwrite-console@7.1.126",
"@appwrite.io/pink": "^0.1.0-next.3",
"@analytics/google-tag-manager": "^0.5.3",
"@appwrite.io/console": "npm:matej-appwrite-console@7.1.126",
"@appwrite.io/pink": "0.1.0-next.4",
"@appwrite.io/pink-icons": "^0.1.0-next.4",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"ai": "^2.1.15",
"analytics": "^0.8.1",
"dayjs": "^1.11.9",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
+1 -1
View File
@@ -47,7 +47,7 @@
%sveltekit.head%
</head>
<body>
<body data-sveltekit-preload-data="hover">
<div id="svelte">%sveltekit.body%</div>
</body>
</html>
+8
View File
@@ -183,8 +183,13 @@ export enum Submit {
FunctionUpdateName = 'submit_function_update_name',
FunctionUpdatePermissions = 'submit_function_update_permissions',
FunctionUpdateSchedule = 'submit_function_update_schedule',
FunctionUpdateConfiguration = 'submit_function_update_configuration',
FunctionUpdateLogging = 'submit_function_update_logging',
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionConnectRepo = 'submit_function_disconnect_repo',
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
FunctionRedeploy = 'submit_function_redeploy',
DeploymentCreate = 'submit_deployment_create',
DeploymentDelete = 'submit_deployment_delete',
DeploymentUpdate = 'submit_deployment_update',
@@ -192,6 +197,7 @@ export enum Submit {
VariableCreate = 'submit_variable_create',
VariableDelete = 'submit_variable_delete',
VariableUpdate = 'submit_variable_update',
VariableEditor = 'submit_variable_editor',
KeyCreate = 'submit_key_create',
KeyDelete = 'submit_key_delete',
KeyUpdateName = 'submit_key_update_name',
@@ -223,6 +229,8 @@ export enum Submit {
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions',
InstallationCreate = 'submit_installation_create',
InstallationDelete = 'submit_installation_delete',
EmailChangeLocale = 'submit_email_change_locale',
EmailResetTemplate = 'submit_email_reset_template',
EmailUpdateInviteTemplate = 'submit_email_update_invite_template',
+6 -1
View File
@@ -9,8 +9,13 @@
import { isLanguage, type Language } from '$lib/components/code.svelte';
import { VARS } from '$lib/system';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
const { input, handleSubmit, completion, isLoading, complete } = useCompletion({
api: VARS.ASSISTANT_ENDPOINT
api: endpoint + '/console/assistant',
headers: {
'content-type': 'application/json'
}
});
const examples = [
+2 -2
View File
@@ -5,7 +5,7 @@ import { get } from 'svelte/store';
import type { Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { showCreateDeployment } from '$routes/console/project-[project]/functions/function-[function]/+page.svelte';
import { showCreateDeployment } from '$routes/console/project-[project]/functions/function-[function]/store';
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
return {
@@ -37,7 +37,7 @@ export const functionsSearcher = (async (query: string) => {
if (!$page.url.pathname.endsWith(func.$id)) {
await goto(`/console/project-${projectId}/functions/function-${func.$id}`);
}
showCreateDeployment();
showCreateDeployment.set(true);
},
group: 'functions',
icon: 'plus'
+20 -2
View File
@@ -2,11 +2,22 @@
import AvatarInitials from './avatarInitials.svelte';
export let avatars: string[] = [];
export let icons: string[] = [];
export let total = avatars.length;
export let size = 40;
export let avatarSize: keyof typeof Sizes = 'medium';
export let bordered = false;
enum Sizes {
xsmall = 'is-size-x-small',
small = 'is-size-small',
medium = '',
large = 'is-size-large',
xlarge = 'is-size-x-large'
}
</script>
<ul class="avatars-group">
<ul class="avatars-group" class:is-with-border={bordered}>
{#each avatars as name, index}
{#if index < 2}
<li class="avatars-group-item">
@@ -14,9 +25,16 @@
</li>
{/if}
{/each}
{#each icons as icon}
<li class="avatars-group-item">
<span class="avatar {Sizes[avatarSize]}"><span class={`icon-${icon}`} /></span>
</li>
{/each}
{#if total > 2}
<li class="avatars-group-item">
<div class="avatar">+{total - 2}</div>
<div class="avatar {Sizes[avatarSize]}">+{total - 2}</div>
</li>
{/if}
</ul>
+19 -8
View File
@@ -1,9 +1,20 @@
<div class="box">
<div class="u-flex u-gap-16">
<slot name="image" />
<div class="u-cross-child-center u-line-height-1-5">
<slot name="title" />
<slot />
</div>
</div>
<script lang="ts">
export let radius: keyof typeof radiuses = 'small';
export let padding = 24;
let classes = '';
export { classes as class };
enum radiuses {
xsmall = '--border-radius-extra-large',
small = '--border-radius-small',
medium = '--border-radius-medium',
large = '--border-radius-large'
}
</script>
<div
class="box {classes}"
style:--box-border-radius={`var(${radiuses[radius]})`}
style:--box-padding={`${padding / 16}rem`}>
<slot />
</div>
+9
View File
@@ -0,0 +1,9 @@
<div class="box">
<div class="u-flex u-gap-16">
<slot name="image" />
<div class="u-cross-child-center u-line-height-1-5">
<slot name="title" />
<slot />
</div>
</div>
</div>
+1 -1
View File
@@ -116,7 +116,7 @@
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: hsl(var(--p-box-background-color));
padding: 0;
padding-block-start: 4%;
margin: 0;
}
.prism-token {
+3 -1
View File
@@ -11,7 +11,9 @@
<slot name="beforetitle" />
<div>
<span class="text"><slot name="title" /></span>
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
{#if $$slots.subtitle}
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
{/if}
</div>
<div class="icon">
<span class="icon-cheveron-down" aria-hidden="true" />
+2 -1
View File
@@ -7,6 +7,7 @@
export let name: string;
export let id: string;
export let autofocus = true;
export let fullWidth = false;
$: if (!show) {
id = null;
@@ -21,7 +22,7 @@
}
</script>
<InnerModal bind:show>
<InnerModal bind:show {fullWidth}>
<svelte:fragment slot="title">{name} ID</svelte:fragment>
<svelte:fragment slot="subtitle">
Enter a custom {name} ID. Leave blank for a randomly generated one.
+7 -1
View File
@@ -4,10 +4,16 @@
export let href: string;
export let icon: string = null;
export let disabled = false;
export let external = false;
</script>
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
<a {href} class="drop-button" class:is-disabled={disabled}>
<a
{href}
class="drop-button"
class:is-disabled={disabled}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+7 -4
View File
@@ -4,6 +4,7 @@
import Dark from '$lib/images/search-dark.svg';
import PaginationInline from './paginationInline.svelte';
export let hidePagination = false;
export let hidePages = false;
</script>
@@ -18,7 +19,9 @@
</div>
</article>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
{#if !hidePagination}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
{/if}
+1 -1
View File
@@ -246,7 +246,7 @@
</script>
<Modal bind:show onSubmit={create} size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<svelte:fragment slot="header">Create event</svelte:fragment>
<slot />
<div>
<p class="u-text">Choose a service</p>
+3
View File
@@ -29,6 +29,7 @@ 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';
export { default as BoxAvatar } from './boxAvatar.svelte';
export { default as Search } from './search.svelte';
export { default as SearchQuery } from './searchQuery.svelte';
export { default as GridItem1 } from './gridItem1.svelte';
@@ -52,6 +53,8 @@ export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
export { default as ClickableList } from './clickableList.svelte';
export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
export { default as NumericList } from './numericList.svelte';
export { default as NumericListItem } from './numericListItem.svelte';
export { default as EyebrowHeading } from './eyebrowHeading.svelte';
export { default as SvgIcon } from './svgIcon.svelte';
export { default as MigrationBox } from './migrationBox.svelte';
+2 -1
View File
@@ -3,11 +3,12 @@
export let show = false;
export let closable = true;
export let fullWidth = false;
</script>
{#if show}
<FormItem>
<section class="modal is-inner-modal">
<section class="modal is-inner-modal" class:u-width-full-line={fullWidth}>
<div class="modal-form">
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
+8 -4
View File
@@ -31,10 +31,14 @@
bind:group
on:click />
<div class="u-flex u-flex-vertical u-gap-4">
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
<p class="u-color-text-gray u-small">
<slot />
</p>
{#if $$slots.title}
<h4 class="body-text-2 u-bold"><slot name="title" /></h4>
{/if}
{#if $$slots.default}
<p class="u-color-text-gray u-small">
<slot />
</p>
{/if}
</div>
{#if icon}
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
+3
View File
@@ -0,0 +1,3 @@
<ol class="numeric-list">
<slot />
</ol>
@@ -0,0 +1,7 @@
<script lang="ts">
export let fullWidth = false;
</script>
<li class="numeric-list-item">
<div class="u-margin-block-start-8" class:u-width-full-line={fullWidth}><slot /></div>
</li>
@@ -19,8 +19,8 @@
TableRow
} from '$lib/elements/table';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import Actions from './actions.svelte';
import Row from './row.svelte';
import Table from '$lib/elements/table/table.svelte';
@@ -32,13 +32,12 @@
let showTeam = false;
let showCustom = false;
let showDropdown = false;
let unsubscribe: Unsubscriber;
const groups = writable<Map<string, Permission>>(new Map());
onMount(() => {
permissions.forEach(fromPermissionString);
unsubscribe = groups.subscribe(() => {
return groups.subscribe(() => {
const current = exportRoles();
if (symmetricDifference(current, permissions).length) {
permissions = current;
@@ -46,12 +45,6 @@
});
});
onDestroy(() => {
if (unsubscribe) {
unsubscribe();
}
});
function create(event: CustomEvent<string[]>) {
for (const role of event.detail) {
addRole(role);
+3 -10
View File
@@ -10,8 +10,8 @@
TableRow
} from '$lib/elements/table';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import Actions from './actions.svelte';
import type { Permission } from './permissions.svelte';
import Row from './row.svelte';
@@ -22,13 +22,12 @@
let showTeam = false;
let showCustom = false;
let showDropdown = false;
let unsubscribe: Unsubscriber;
const groups = writable<Map<string, Permission>>(new Map());
onMount(() => {
roles.forEach(addRole);
unsubscribe = groups.subscribe((n) => {
return groups.subscribe((n) => {
const current = Array.from(n.keys());
if (symmetricDifference(current, roles).length) {
roles = current;
@@ -36,12 +35,6 @@
});
});
onDestroy(() => {
if (unsubscribe) {
unsubscribe();
}
});
function create(event: CustomEvent<string[]>) {
for (const role of event.detail) {
addRole(role);
+7 -1
View File
@@ -1,3 +1,9 @@
<ul class="secondary-tabs">
<script lang="ts">
export let large = false;
let classes: string = undefined;
export { classes as class };
</script>
<ul class="secondary-tabs {classes}" class:is-large={large}>
<slot />
</ul>
+21 -5
View File
@@ -1,21 +1,37 @@
<script lang="ts">
export let href: string;
export let href: string = null;
export let disabled = false;
export let stretch = false;
export let fullWidth = false;
export let center = false;
</script>
<li class="secondary-tabs-item">
<li class="secondary-tabs-item" class:u-stretch={stretch}>
{#if href}
{#if disabled}
<button class="secondary-tabs-button" disabled>
<button
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
disabled>
<span class="text"><slot /></span>
</button>
{:else}
<a class="secondary-tabs-button" {href}>
<a
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
{href}>
<span class="text"><slot /></span>
</a>
{/if}
{:else}
<button class="secondary-tabs-button" {disabled} on:click>
<button
class="secondary-tabs-button"
class:u-width-full-line={fullWidth}
class:u-text-center={center}
{disabled}
on:click>
<span class="text"><slot /></span>
</button>
{/if}
+16 -1
View File
@@ -3,8 +3,23 @@
export let name: string;
export let type: 'color' | 'grayscale' = 'color';
export let size = 40;
export let iconSize: keyof typeof iconSizes = 'medium';
let className = '';
export { className as class };
enum iconSizes {
small = '--icon-size-small',
medium = '--icon-size-medium',
large = '--icon-size-large',
xlarge = '--icon-size-extra-large'
}
</script>
<img class={className} src={$iconPath(name, type)} alt={name} />
<img
class={className}
width={size}
height={size}
style:inline-size={`var(${iconSizes[iconSize]})`}
src={$iconPath(name, type)}
alt={name} />
-1
View File
@@ -22,7 +22,6 @@
await goto(href);
await waitUntil(() => {
console.log('tickUntil', el);
return el.classList.contains('is-selected');
}, 1000);
el.focus();
+4 -2
View File
@@ -3,6 +3,7 @@
import { throttle } from '$lib/helpers/functions';
import { onMount } from 'svelte';
export let alternativeTrim = false;
let showTooltip = false;
let container: HTMLSpanElement | null;
@@ -15,11 +16,12 @@
<svelte:window on:resize={throttle(onResize, 250)} />
<span class="text u-trim" bind:this={container}>
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
{#if showTooltip}
<span
use:tooltip={{
content: container.innerText
content: container.innerText,
maxWidth: '30rem'
}}>
<slot />
</span>
+5
View File
@@ -5,6 +5,8 @@ export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
PROJECT_VARIABLES = 'dependency:project_variables',
PROJECT_INSTALLATIONS = 'dependency:project_installations',
PROJECTS = 'dependency:projects',
ACCOUNT = 'dependency:account',
ACCOUNT_SESSIONS = 'dependency:account_sessions',
@@ -23,8 +25,11 @@ export enum Dependencies {
FILE = 'dependency:file',
FILES = 'dependency:files',
FUNCTION = 'dependency:function',
FUNCTION_DOMAINS = 'dependency:function_domains',
FUNCTION_INSTALLATIONS = 'dependency:function_installations',
FUNCTIONS = 'dependency:functions',
VARIABLES = 'dependency:variables',
DEPLOYMENT = 'dependency:deployment',
DEPLOYMENTS = 'dependency:deployments',
EXECUTIONS = 'dependency:executions',
PLATFORM = 'dependency:platform',
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts">
export let fullWidth = false;
export let isMultiple = false;
</script>
<li class="form-item" class:u-width-full-line={fullWidth}>
<li class="form-item" class:is-multiple={isMultiple} class:u-width-full-line={fullWidth}>
<slot />
</li>
@@ -0,0 +1,8 @@
<script lang="ts">
export let fullWidth = false;
export let alignEnd = false;
</script>
<div class="form-item-part" class:u-cross-child-end={alignEnd} class:u-stretch={fullWidth}>
<slot />
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
export let isCommonSection = false;
export let gap = 24;
let classes: string = undefined;
let classes = '';
export { classes as class };
</script>
+1
View File
@@ -1,5 +1,6 @@
export { default as Form } from './form.svelte';
export { default as FormItem } from './formItem.svelte';
export { default as FormItemPart } from './formItemPart.svelte';
export { default as FormList } from './formList.svelte';
export { default as Button } from './button.svelte';
export { default as InputDomain } from './inputDomain.svelte';
+4 -2
View File
@@ -3,6 +3,7 @@
export let label: string | undefined = undefined;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
export let showLabel = true;
export let id: string;
export let value = false;
@@ -29,7 +30,7 @@
<FormItem>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
@@ -44,7 +45,8 @@
bind:this={element}
bind:checked={value}
on:invalid={handleInvalid}
on:click />
on:click
on:change />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+18 -1
View File
@@ -8,6 +8,7 @@
export let value = false;
export let required = false;
export let disabled = false;
export let tooltip: string = null;
let element: HTMLInputElement;
let error: string;
@@ -39,7 +40,23 @@
on:invalid={handleInvalid} />
<div class="choice-item-content">
<div class:u-hide={!showLabel} class="choice-item-title">{label}</div>
<h6 class:u-hide={!showLabel} class="choice-item-title">
{label}
</h6>
{#if tooltip}
<button class="tooltip" aria-label="variables info">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
<span class="tooltip-popup" role="tooltip">
<p class="text">
{tooltip}
</p>
</span>
</button>
{/if}
{#if $$slots.default}
<p class="choice-item-paragraph"><slot /></p>
{/if}
+10 -3
View File
@@ -2,7 +2,7 @@
import { Trim } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { onMount } from 'svelte';
import { Helper } from '.';
import { Helper, Label } from '.';
export let label: string = null;
export let files: FileList;
@@ -10,6 +10,8 @@
export let allowedFileExtensions: string[] = [];
export let maxSize: number = null;
export let required = false;
export let optionalText: string = null;
export let tooltip: string = null;
export let error: string = null;
let input: HTMLInputElement;
@@ -20,7 +22,9 @@
const hasInvalidExt = Array.from(value).some((file) => {
const fileExtension = file.name.split('.').pop();
return !allowedFileExtensions.includes(fileExtension);
return allowedFileExtensions?.length
? !allowedFileExtensions.includes(fileExtension)
: false;
});
if (hasInvalidExt) {
error = 'Invalid file extension';
@@ -89,11 +93,14 @@
<div>
{#if label}
<p class="text">{label}</p>
<Label {required} {optionalText} {tooltip} hide={!label}>
{label}
</Label>
{/if}
<div
class="box is-no-shadow u-padding-24"
style="--box-border-radius:var(--border-radius-xsmall); z-index: 1"
class:u-margin-block-start-8={!!label}
class:is-border-dashed={!hovering}
class:is-hover-with-file={hovering}
on:drop|preventDefault={dropHandler}
+2 -2
View File
@@ -47,12 +47,12 @@
</div>
</FormItem>
<div
class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center"
class="u-flex u-gap-4 u-margin-block-start-8 u-small"
class:u-color-text-warning={icon === 'exclamation'}>
<span
class:icon-info={icon === 'info'}
class:icon-exclamation={icon === 'exclamation'}
class="u-cross-center u-line-height-1 u-icon-small u-color-text-gray"
class="u-cross-center u-line-height-1 u-color-text-gray"
aria-hidden="true" />
<span class="text u-line-height-1-5">
Allowed characters: alphanumeric, non-leading hyphen, underscore, period
+1 -1
View File
@@ -51,7 +51,7 @@
{label}
</Label>
<div class="input-text-wrapper">
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
{#if showInPlainText}
<input
{id}
+2 -1
View File
@@ -8,6 +8,7 @@
export let value: string | number | boolean;
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let options: {
value: string | boolean | number;
@@ -46,7 +47,7 @@
<FormItem>
{#if label}
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
@@ -16,9 +16,11 @@
export let label: string;
export let name = 'elements';
export let optionalText: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let showLabel = true;
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let fullWidth = false;
export let autofocus = false;
@@ -84,11 +86,12 @@
value = option.value;
search = option.label;
// It's not working without this line.
if ($$slots.output) {
search = '';
} else {
if (!$$slots.output) {
element.value = search;
} else {
search = '';
}
hasFocus = false;
dispatch('select', option);
}
@@ -117,7 +120,7 @@
position="static"
fullWidth={true}
fixed>
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
{label}
</Label>
@@ -43,6 +43,7 @@
</div>
<div class="choice-item-content">
<div class="choice-item-title">{label}</div>
<slot name="description" />
</div>
</label>
{#if error}
+8 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
@@ -12,6 +12,7 @@
export let value = '';
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let nullable = false;
export let disabled = false;
export let readonly = false;
@@ -20,6 +21,7 @@
export let fullWidth = false;
export let maxlength: number = null;
export let tooltip: string = null;
export let isMultiple = false;
let element: HTMLInputElement;
let error: string;
@@ -65,11 +67,13 @@
type $$Events = {
input: Event & { target: HTMLInputElement };
};
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<FormItem {fullWidth}>
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
{/if}
@@ -110,4 +114,4 @@
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
</svelte:component>
+9 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
interface $$Props extends Partial<HTMLLabelElement> {
required?: boolean;
hideRequired?: boolean;
optionalText?: string | undefined;
hide?: boolean;
tooltip?: string;
@@ -8,12 +9,17 @@
}
export let required: $$Props['required'] = false;
export let hideRequired: $$Props['hideRequired'] = false;
export let optionalText: $$Props['optionalText'] = undefined;
export let hide: $$Props['hide'] = false;
export let tooltip: $$Props['tooltip'] = null;
</script>
<label class:is-required={required} class:u-hide={hide} class="label" {...$$restProps}>
<label
class:is-required={required && !hideRequired}
class:u-hide={hide}
class="label"
{...$$restProps}>
<slot />
</label>
@@ -22,8 +28,8 @@
{/if}
{#if tooltip}
<button class="tooltip" aria-label="variables info">
<span class="icon-info" aria-hidden="true" />
<button type="button" on:click|preventDefault class="tooltip" aria-label="input tooltip">
<span class="icon-info" aria-hidden="true" style="font-size: var(--icon-size-small)" />
<span class="tooltip-popup" role="tooltip">
<p class="text">
{tooltip}
+10 -1
View File
@@ -1,8 +1,17 @@
<script lang="ts">
export let href: string;
export let title: string;
export let external = false;
export let noStyle = false;
</script>
<div class="table-col" data-title={title} role="cell" data-private>
<a role="button" tabindex="0" class="link" {href}><slot /></a>
<a
role="button"
tabindex="0"
class:link={!noStyle}
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}><slot /></a>
</div>
+2
View File
@@ -1,12 +1,14 @@
<script lang="ts">
export let noMargin = false;
export let noStyles = false;
export let style = '';
</script>
<div
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
{style}
role="table"
data-private>
<slot />
+12
View File
@@ -1,3 +1,11 @@
import { browser } from '$app/environment';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
if (browser) {
dayjs.extend(relativeTime);
}
export const toLocaleDate = (datetime: string) => {
const date = new Date(datetime);
@@ -49,3 +57,7 @@ export const diffDays = (date1: Date, date2: Date) => {
const diffTime = Math.abs(date2.getTime() - date1.getTime());
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
};
export function timeFromNow(datetime: string): string {
return dayjs().to(dayjs(datetime));
}
+11
View File
@@ -37,6 +37,17 @@ export function deepEqual<T>(obj1: T, obj2: T): boolean {
return true;
}
/**
* Creates a deep clone of the given object. This function uses the JSON methods for cloning,
* so it may not be suitable for objects with functions, symbols, or other non-JSON-safe data.
*
* @param obj the object to be cloned
* @returns a deep clone of the provided object
*/
export function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
export type DeepObj<T> = {
[K in keyof T]: T[K] extends object ? DeepObj<T[K]> : T[K];
};
+320 -179
View File
@@ -1,40 +1,21 @@
<script lang="ts">
import { toLocaleDateTime } from '$lib/helpers/date';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { log } from '$lib/stores/logs';
import { Output, Status, Tab, Tabs } from '../components';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import type { Models } from '@appwrite.io/console';
import {
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { beforeNavigate } from '$app/navigation';
import { Pill } from '$lib/elements';
let selectedTab: string;
let rawData: string;
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
if ('buildId' in data) {
selectedTab = 'logs';
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployments/${$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 = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/executions/${$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' });
}
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
@@ -42,14 +23,47 @@
$log.show = false;
}
}
function parseUrl(url: string) {
if (!url) return;
const queryString = url.includes('?') ? url.split('?')[1] : '';
const queries: Record<string, string>[] = [];
for (const param of queryString.split('&')) {
let [key, ...valueArr] = param.split('=');
const value = valueArr.join('=');
if (key) {
queries.push({ key, value: value ?? '' });
}
}
return queries;
}
beforeNavigate((n) => {
if (!$log.show) return;
if (n.type === 'popstate') {
n.cancel();
}
$log.show = false;
});
$: parameters = parseUrl(execution?.requestPath);
$: execution = $log.data;
$: func = $log.func;
$: if (execution?.errors) {
selectedResponse = 'errors';
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if $log.data}
{#if execution}
<section class="cover-frame" data-private>
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
<h1 class="body-text-1 u-bold">Function ID: {$log.func.$id}</h1>
<div class="u-flex u-gap-8 u-cross-center">
<h1 class="body-text-1 u-bold">Function ID:</h1>
<Id value={func.$id}>{func.$id}</Id>
</div>
<button
on:click={() => ($log.show = false)}
class="button is-text is-only-icon"
@@ -58,158 +72,285 @@
<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>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2 u-bold">Deployment ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
</div>
<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">
<div class="status u-margin-inline-start-auto">
<Status status={$log.data.status}>{$log.data.status}</Status>
<time>{calculateTime($log.data.buildTime)}</time>
</div>
</div>
<div class="cover-frame-content u-flex u-flex-vertical">
<div class="u-flex u-gap-16">
<div class="avatar is-size-large">
<SvgIcon
size={56}
type="color"
name={func.runtime.split('-')[0]}
iconSize="large" />
</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>
<h2 class="body-text-2 u-bold">Execution ID:</h2>
<Id value={execution.$id}>{execution.$id}</Id>
</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 ? $log.data.buildStdout : 'No logs recorded'}
</code>
{:else}
<code class="code-panel-content">
{$log.data.buildStderr
? $log.data.buildStderr
: 'No errors recorded'}
</code>
{/if}
</section>
<ul>
<li class="text">
<b>Duration: </b>
<time>
{calculateTime(execution.duration)}
</time>
</li>
<li class="text">
<b>Created at:</b>
<time>
{toLocaleDateTime(execution.$createdAt)}
</time>
</li>
{#if execution?.requestHeaders?.host}
<li class="text">
<b>Host</b>
<span>
{execution.requestHeaders.host}
</span>
</li>
{/if}
</ul>
<div class="status u-margin-inline-start-auto">
<Pill
warning={execution.status === 'waiting'}
danger={execution.status === 'failed'}
success={execution.status === 'completed' || execution.status === 'ready'}
info={execution.status === 'processing' || execution.status === 'building'}>
{execution.status}
</Pill>
</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>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2 u-bold">Execution ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
</div>
<time class="u-block">
Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
</div>
<div>
<p>Triggered by: <b>{$log.data.trigger}</b></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 === '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 class="theme-dark u-stretch u-margin-block-start-32 u-overflow-hidden">
<section class="code-panel">
<header class="code-panel-header u-flex u-main-space-between u-width-full-line">
<div class="u-flex u-gap-24">
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Method:</h4>
<span class="u-text-color-gray">{execution.requestMethod}</span>
</div>
</header>
{#if selectedTab === 'logs'}
<code class="code-panel-content">
{$log.data.stdout ? $log.data.stdout : 'No logs recorded'}
</code>
{:else if selectedTab === 'errors'}
<code class="code-panel-content">
{$log.data.stderr ? $log.data.stderr : 'No errors recorded'}
</code>
{:else}
<code class="code-panel-content">
{$log.data.response ? $log.data.response : 'No response recorded'}
</code>
{/if}
</section>
</div>
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Path:</h4>
<span class="u-text-color-gray">{execution.requestPath}</span>
</div>
</div>
<div class="u-flex u-gap-24">
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Triggered by:</h4>
<span class="u-text-color-gray">{execution.trigger}</span>
</div>
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Status Code:</h4>
<span class="u-text-color-gray"
>{execution.responseStatusCode}</span>
</div>
</div>
</header>
<div class="code-panel-content grid-1-2" style="u-grid">
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<Heading tag="h3" size="6">Request</Heading>
<div class="u-sep-block-end">
<Tabs>
<Tab
selected={selectedRequest === 'parameters'}
on:click={() => (selectedRequest = 'parameters')}>
Parameters
</Tab>
<Tab
selected={selectedRequest === 'headers'}
on:click={() => (selectedRequest = 'headers')}>
Headers
</Tab>
<Tab
selected={selectedRequest === 'body'}
on:click={() => (selectedRequest = 'body')}>
Body
</Tab>
</Tabs>
</div>
{#if selectedRequest === 'parameters'}
{#if parameters?.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each parameters as param}
<TableRow>
<TableCellText title="Key">
{param.key}
</TableCellText>
<TableCellText title="Value">
{param.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
{/if}
<p class="text u-text-center u-padding-24">
{parameters?.length
? 'Not all parameters data is'
: 'Parameters data is not'} captured by Appwrite for your user's
security and privacy. To display parameters data in the Logs tab,
use
<b>context.log()</b>.
<a
href="https://appwrite.io/docs/functions-develop#logging"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
{:else if selectedRequest === 'headers'}
{#if execution.requestHeaders.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.requestHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value">
{header.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
{/if}
<p class="text u-text-center u-padding-24">
{execution.requestHeaders?.length
? 'Not all header data is'
: 'Header data is not'}
captured by Appwrite for your user's security and privacy. To display
header data in the Logs tab, use
<b>context.log()</b>.
<a
href="https://appwrite.io/docs/functions-develop#logging"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
{:else if selectedRequest === 'body'}
<p class="text u-text-center u-padding-24">
Body data is not captured by Appwrite for your user's security
and privacy. To display body data in the Logs tab, use
<b>context.log()</b>.
<a
href="https://appwrite.io/docs/functions-develop#logging"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
{/if}
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16">
<Heading tag="h3" size="6">Response</Heading>
<div class="u-sep-block-end">
<Tabs>
<Tab
selected={selectedResponse === 'logs'}
on:click={() => (selectedResponse = 'logs')}>
Logs
</Tab>
<Tab
selected={selectedResponse === 'errors'}
on:click={() => (selectedResponse = 'errors')}>
Errors
</Tab>
<Tab
selected={selectedResponse === 'headers'}
on:click={() => (selectedResponse = 'headers')}>
Headers
</Tab>
<Tab
selected={selectedResponse === 'body'}
on:click={() => (selectedResponse = 'body')}>
Body
</Tab>
</Tabs>
</div>
{#if selectedResponse === 'logs'}
{#if execution?.logs}
<Code withCopy noMargin code={execution.logs} language="sh" />
{:else}
<Card isDashed isTile>
<p class="text u-text-center">No response was recorded.</p>
</Card>
{/if}
{:else if selectedResponse === 'errors'}
{#if execution?.errors}
<Code withCopy noMargin code={execution.errors} language="sh" />
{:else}
<Card isDashed isTile>
<p class="text u-text-center">No response was recorded.</p>
</Card>
{/if}
{:else if selectedResponse === 'headers'}
{#if execution.responseHeaders.length}
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.responseHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value"
>{header.value}</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
{/if}
<p class="text u-text-center u-padding-24">
{execution.responseHeaders?.length
? 'Not all header data is'
: 'Header data is not'}
captured by Appwrite for your user's security and privacy. To display
header data in the Logs tab, use
<b>context.log()</b>.
<a
href="https://appwrite.io/docs/functions-develop#logging"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
{:else if selectedResponse === 'body'}
<p class="text u-text-center u-padding-24">
Body data is not captured by Appwrite for your user's security
and privacy. To display body data in the Logs tab, use
<b>context.log()</b>.
<a
href="https://appwrite.io/docs/functions-develop#logging"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
{/if}
</div>
</div>
</section>
</div>
{/if}
</div>
</section>
{/if}
+9 -6
View File
@@ -26,14 +26,17 @@
}
};
/**
* Cancel navigation when wizard is open and triggered by popstate
*/
beforeNavigate((n) => {
/**
* Hide wizard when navigation is triggered by popstate.
*/
if (n.type === 'popstate' && $wizard.show) {
wizard.hide();
if (!$wizard.show || !$wizard.cover) return;
if (n.type === 'popstate') {
n.cancel();
}
if (n.type !== 'leave') {
wizard.hide();
}
});
</script>
@@ -42,7 +45,7 @@
<main
class:grid-with-side={showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show}>
class:u-hide={$wizard.show || $log.show || $wizard.cover}>
<header class="main-header u-padding-inline-end-0">
<button
class:u-hide={!showSideNavigation}
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import { wizard } from '$lib/stores/wizard';
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
wizard.hide();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
<section class="cover-frame is-color-header">
<header class="cover-frame-header">
<div
class="container u-flex u-gap-16 u-main-space-between u-cross-center"
style:--p-container-padding-block="0">
<div class="u-flex u-gap-8 u-cross-center">
<div class="body-text-1 u-bold"><slot name="title" /></div>
</div>
<button
on:click={wizard.hide}
class="button is-text is-only-icon"
style:--button-size="1.5rem"
aria-label="close popup">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</header>
<div class="cover-frame-content u-flex u-flex-vertical u-overflow-y-auto">
<slot />
</div>
</section>
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import { invalidate } from '$app/navigation';
import { wizard } from '$lib/stores/wizard';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { dependencyStore, domain } from './wizard/store';
import Step1 from './wizard/step1.svelte';
import Step2 from './wizard/step2.svelte';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
onMount(() => {
domain.set({ $id: '', domain: '' });
return sdk.forConsole.client.subscribe<Models.ProxyRule>('console', (data) =>
domain.set(data.payload)
);
});
async function onFinish() {
await invalidate($dependencyStore);
wizard.hide();
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Add domain',
component: Step1
});
stepsComponents.set(2, {
label: 'Configuration',
component: Step2
});
</script>
<Wizard
title="Create domain"
steps={stepsComponents}
finalAction="Go to console"
on:exit={onFinish}
on:finish={onFinish} />
@@ -1,21 +1,21 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { project } from '../../store';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import type { Dependencies } from '$lib/constants';
export let showDelete = false;
export let selectedDomain: Models.Domain;
export let selectedDomain: Models.ProxyRule;
export let dependency: Dependencies;
async function deleteDomain() {
try {
await sdk.forConsole.projects.deleteDomain($project.$id, selectedDomain.$id);
await invalidate(Dependencies.DOMAINS);
await sdk.forProject.proxy.deleteRule(selectedDomain.$id);
await invalidate(dependency);
showDelete = false;
addNotification({
type: 'success',
@@ -38,10 +38,11 @@
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Domain</svelte:fragment>
<svelte:fragment slot="header">Delete domain</svelte:fragment>
{#if selectedDomain}
<p data-private>
Are you sure you want to delete <b>{selectedDomain.domain}</b> from '{$project.name}'?
Are you sure you want to delete <b>{selectedDomain.domain}</b>? You will no longer be
able to execute your function by visiting this domain.
</p>
{/if}
<svelte:fragment slot="footer">
+197
View File
@@ -0,0 +1,197 @@
<script context="module" lang="ts">
export enum ProxyTypes {
API = 'api',
FUNCTION = 'function'
}
</script>
<script lang="ts">
import { DropList, DropListItem, Empty, Heading, Modal, Trim } from '$lib/components';
import {
TableBody,
TableCell,
TableCellHead,
TableCellLink,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { dependencyStore, domain, typeStore } from './wizard/store';
import { toLocaleDate } from '$lib/helpers/date';
import { wizard } from '$lib/stores/wizard';
import type { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import Create from './create.svelte';
import Delete from './delete.svelte';
import Retry from './wizard/retry.svelte';
import { Pill } from '$lib/elements';
export let rules: Models.ProxyRuleList;
export let type: ProxyTypes;
export let dependency: Dependencies;
let showDomainsDropdown = [];
let showDelete = false;
let showRetry = false;
let selectedDomain: Models.ProxyRule;
let retryError = null;
onMount(() => {
typeStore.set(type);
dependencyStore.set(dependency);
});
function openWizard() {
wizard.start(Create);
}
function openRetry(rule: Models.ProxyRule, index?: number) {
retryError = null;
if (index !== undefined) {
showDomainsDropdown[index] = false;
}
domain.set(rule);
showRetry = true;
}
</script>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">
<slot name="heading" />
</Heading>
<Button on:click={openWizard}>
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create domain</span>
</Button>
</div>
{#if rules.total}
<TableScroll>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead width={180}>Verification Status</TableCellHead>
<TableCellHead>Certificate Status</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each rules.rules as domain, i}
<TableRow>
<TableCellLink title="Domain" href={`http://${domain.domain}`} external noStyle>
<span class="u-flex u-gap-4 u-cross-center">
<Trim>
<span class="link">{domain.domain}</span>
</Trim>
<span class="icon-external-link" aria-hidden="true" />
</span>
</TableCellLink>
<TableCell title="Verification">
{#if domain.status === 'created'}
<div class="u-flex u-gap-8 u-cross-center">
<Pill danger>
<span
class="icon-exclamation-circle u-color-text-danger"
aria-hidden="true" />
<span class="u-text">Failed</span>
</Pill>
<button type="button" on:click={() => openRetry(domain)}>
<span class="link">Retry</span>
</button>
</div>
{:else}
<Pill success>
<span
class="icon-check-circle u-color-text-success"
aria-hidden="true" />
<p class="text">Verified</p>
</Pill>
{/if}
</TableCell>
<TableCell title="Cartificate">
{#if domain.status === 'unverified'}
<div class="u-flex u-gap-8 u-cross-center">
<Pill danger>
<span
class="icon-exclamation-circle u-color-text-danger"
aria-hidden="true" />
<span class="u-text">Failed</span>
</Pill>
<button type="button" on:click={() => openRetry(domain)}>
<span class="link">Retry</span>
</button>
</div>
{:else if domain.status === 'verified'}
<div class="u-flex u-gap-8 u-cross-center">
<Pill success>
<span
class="icon-check-circle u-color-text-success"
aria-hidden="true" />
<span>Generated</span>
</Pill>
{#if domain.renewAt}
<span class="u-text-color-gray">
Auto-renewal: {toLocaleDate(domain.renewAt)}
</span>
{/if}
</div>
{:else}
<div class="u-flex u-gap-8 u-cross-center">
<Pill>
<span class="icon-clock u-text-color-gray" aria-hidden="true" />
<p class="text">Waiting to run</p>
</Pill>
</div>
{/if}
</TableCell>
<TableCell>
<DropList
bind:show={showDomainsDropdown[i]}
placement="bottom-start"
noArrow>
<Button
text
round
ariaLabel="more options"
on:click={() => (showDomainsDropdown[i] = !showDomainsDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
<DropListItem icon="refresh" on:click={() => openRetry(domain, i)}>
{domain.status === 'unverfied'
? 'Retry generation'
: 'Retry verification'}
</DropListItem>
<DropListItem
icon="trash"
on:click={() => {
selectedDomain = domain;
showDelete = true;
showDomainsDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</TableScroll>
{:else}
<Empty
single
href="https://appwrite.io/docs/custom-domains"
target="domain"
on:click={openWizard} />
{/if}
<Delete bind:showDelete bind:selectedDomain {dependency} />
<Modal bind:show={showRetry} headerDivider={false} bind:error={retryError} size="big">
<svelte:fragment slot="header">
Retry {$domain.status === 'unverfied' ? 'certificate generation' : 'verification'}
</svelte:fragment>
<Retry on:error={(e) => (retryError = e.detail)} />
<svelte:fragment slot="footer">
<Button text on:click={() => (showRetry = false)}>Close</Button>
</svelte:fragment>
</Modal>
+2
View File
@@ -0,0 +1,2 @@
export { default as ProxyRulesPage, ProxyTypes } from './index.svelte';
export { default as Retry } from './wizard/retry.svelte';
@@ -18,7 +18,7 @@
$: cnameValue = $domain.domain.replace('.' + registerable, '');
</script>
<Table noStyles noMargin>
<Table noMargin noStyles style="--p-table-bg-color: var(--transparent);">
<TableHeader>
<TableCellHead>Type</TableCellHead>
<TableCellHead>Name</TableCellHead>
+79
View File
@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { domain } from './store';
import CnameTable from './cnameTable.svelte';
import { createEventDispatcher } from 'svelte';
import { Box, Code, Trim } from '$lib/components';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
let retrying = false;
const dispatch = createEventDispatcher();
async function retry() {
try {
retrying = true;
$domain = await sdk.forProject.proxy.updateRuleVerification($domain.$id);
invalidate(Dependencies.FUNCTION_DOMAINS);
addNotification({
message:
$domain.status === 'unverfied'
? 'Domain certificate has been generated successfully'
: 'Domain has been verified successfully',
type: 'success'
});
trackEvent(Submit.DomainUpdateVerification);
} catch (error) {
dispatch('error', error.message);
trackError(error, Submit.DomainUpdateVerification);
} finally {
retrying = false;
}
}
</script>
{#if $domain.status === 'created'}
<Box radius="small">
<div class="u-flex u-gap-8 u-cross-center">
<span class="icon-exclamation-circle u-color-text-danger" aria-hidden="true" />
<p class="u-stretch">Verification failed</p>
<Button secondary on:click={retry} disabled={retrying}>
{#if retrying}
<div class="loader u-text-color-gray" />
{:else}
Retry
{/if}
</Button>
</div>
<p class="text u-margin-block-start-24">
In order to continue, set the following record on your DNS provider. Find a list of
domain providers and their DNS settings in our documentation. Changes may take time to
be effective.
</p>
<div class="u-margin-block-start-24">
<CnameTable />
</div>
</Box>
{:else if $domain.status === 'unverified'}
<Trim alternativeTrim><b>{$domain.domain}</b></Trim>
<Box radius="small">
<div class="u-flex u-gap-8 u-cross-center">
<span class="icon-exclamation-circle u-color-text-danger" aria-hidden="true" />
<p class="u-stretch">Generation failed</p>
<Button secondary on:click={retry} disabled={retrying}>
{#if retrying}
<div class="loader u-text-color-gray" />
{:else}
Retry
{/if}
</Button>
</div>
{#if $domain?.logs}
<Code language="sh" withCopy code={$domain.logs} />
{/if}
</Box>
{/if}
+59
View File
@@ -0,0 +1,59 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { FormList, InputDomain } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { func } from '$routes/console/project-[project]/functions/function-[function]/store';
import { ProxyTypes } from '../index.svelte';
import { domain, typeStore } from './store';
let error = null;
async function createDomain() {
try {
if ($domain.$id) {
await sdk.forProject.proxy.deleteRule($domain.$id);
}
$domain = await sdk.forProject.proxy.createRule(
$domain.domain,
$typeStore,
$typeStore === ProxyTypes.FUNCTION ? $func.$id : undefined
);
trackEvent(Submit.DomainCreate);
} catch (e) {
error = true;
trackError(e.message, Submit.DomainCreate);
}
}
</script>
<WizardStep beforeSubmit={createDomain}>
<svelte:fragment slot="title">Add function domain</svelte:fragment>
<svelte:fragment slot="subtitle">
Use your self-owned domain as the endpoint of your Appwrite API.
</svelte:fragment>
<FormList>
<InputDomain
id="domain"
label="Domain"
placeholder="appwrite.example.com"
autocomplete={false}
required
bind:value={$domain.domain} />
</FormList>
{#if error}
<div class="common-section">
<p>
You can find a list of domain providers and their DNS setting documentation <a
class="link"
href="https://appwrite.io/docs/custom-domains#addCNAME"
target="_blank"
rel="noreferrer">here</a
>. If your domain provider isn't listed, please contact us, and we'll include their
settings as well.
</p>
</div>
{/if}
</WizardStep>
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { domain } from './store';
import Retry from './retry.svelte';
import { addNotification } from '$lib/stores/notifications';
function onRetryError(event: CustomEvent<string>) {
addNotification({
title: 'Error',
message: event.detail,
type: 'error'
});
}
</script>
<WizardStep>
<svelte:fragment slot="title">{$domain.domain}</svelte:fragment>
<div class="boxes-wrapper u-margin-block-start-24">
{#if $domain.status === 'created'}
<Retry on:error={onRetryError} />
{:else}
<div class="u-flex u-gap-8 u-cross-center">
<span class="icon-check u-color-text-success" aria-hidden="true" />
<p class="u-stretch">Domain verified</p>
</div>
{/if}
<div class="box">
<div class="u-flex u-gap-8 u-cross-center">
{#if $domain.status === 'verifying'}
<div
class="loader"
style="color: hsl(var(--color-neutral-50)); inline-size: 1.25rem; block-size: 1.25rem" />
<p class="u-stretch">Generating certificate</p>
{:else if $domain.status === 'verified'}
<span class="icon-check u-color-text-success" aria-hidden="true" />
<p class="u-stretch">Certificate generated</p>
{:else}
<span class="icon-clock u-text-color-gray" aria-hidden="true" />
<p class="u-stretch">
Certificate generation will begin after domain verification
</p>
{/if}
</div>
</div>
</div>
</WizardStep>
+8
View File
@@ -0,0 +1,8 @@
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { ProxyTypes } from '../index.svelte';
import type { Dependencies } from '$lib/constants';
export const domain = writable<Partial<Models.ProxyRule>>({ $id: '', domain: '' });
export const typeStore = writable<ProxyTypes>();
export const dependencyStore = writable<Dependencies>();
+1 -1
View File
@@ -4,7 +4,7 @@ import { writable } from 'svelte/store';
export const log = writable<{
show: boolean;
func: Models.Function;
data: Models.Execution | Models.Deployment;
data: Models.Execution;
}>({
show: false,
func: null,
+701
View File
@@ -0,0 +1,701 @@
export const marketplace = [
{
icon: 'icon-lightning-bolt',
id: 'starter',
name: 'Starter function',
tagline:
'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions. 🚀',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/starter'
},
{
name: 'php-8.0',
commands: 'composer install',
entrypoint: 'src/index.php',
providerRootDirectory: 'php/starter'
},
{
name: 'ruby-3.0',
commands: 'bundle install',
entrypoint: 'lib/main.rb',
providerRootDirectory: 'ruby/starter'
},
{
name: 'python-3.9',
commands: 'pip install -r requirements.txt',
entrypoint: 'src/main.py',
providerRootDirectory: 'python/starter'
},
{
name: 'dart-2.17',
commands: 'dart pub get',
entrypoint: 'lib/main.dart',
providerRootDirectory: 'dart/starter'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to talk to Appwrite backend APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: false
}
]
},
{
icon: 'icon-open-ai',
id: 'prompt-chatgpt',
name: 'Prompt ChatGPT',
tagline: 'Ask questions and let OpenAI GPT-3.5-turbo answer.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/prompt-chatgpt'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/prompt-chatgpt">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'OPENAI_API_KEY',
description: `A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. <a class="u-bold" target="_blank" href="https://platform.openai.com/docs/quickstart/add-your-api-key">Learn more</a>.`,
value: '',
placeholder: 'sk-wzG...vcy',
required: true
},
{
name: 'OPENAI_MAX_TOKENS',
description: `The maximum number of tokens that the OpenAI response should contain. Be aware that OpenAI models read and write a maximum number of tokens per API call, which varies depending on the model. For GPT-3.5-turbo, the limit is 4096 tokens. <a class="u-bold" target="_blank" href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">Learn more</a>.`,
value: '512',
placeholder: '512',
required: false
}
]
},
{
icon: 'icon-perspective-api',
id: 'analyze-with-perspectiveapi',
name: 'Analyze with PerspectiveAPI',
tagline: 'Automate moderation by getting toxicity of messages.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/analyze-with-perspectiveapi'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/analyze-with-perspectiveapi">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'PERSPECTIVE_API_KEY',
description: `Google Perspective API key. It authenticates your function, allowing it to interact with the API. <a class="u-bold" target="_blank" href="https://developers.google.com/codelabs/setup-perspective-api">Learn more</a>.`,
value: '',
placeholder: 'AIzaS...fk-fuM',
required: true
}
]
},
{
icon: 'icon-pangea',
id: 'censor-with-redact',
name: 'Censor with Redact',
tagline:
'Censor sensitive information from a provided text string using Redact API by Pangea.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/censor-with-redact'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/censor-with-redact">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'PANGEA_REDACT_TOKEN',
description: `Access token for the Pangea Redact API. <a class="u-bold" target="_blank" href="https://pangea.cloud/docs/redact/getting-started/configuration">Learn more</a>.`,
value: '',
placeholder: 'pts_7p4...5wl4',
required: true
}
]
},
{
icon: 'icon-document',
id: 'generate-pdf',
name: 'Generate PDF',
tagline: 'Document containing sample invoice in PDF format.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/generate-pdf'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-pdf">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: []
},
{
icon: 'icon-discord',
id: 'discord-command-bot',
name: 'Discord command bot',
tagline: 'Simple command using Discord Interactions.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install && npm run setup',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/discord-command-bot'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/discord-command-bot">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'DISCORD_PUBLIC_KEY',
description: `Discord Public Key to verify request signature. <a class="u-bold" target="_blank" href="https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true
}
]
},
{
icon: 'icon-github',
id: 'github-issue-bot',
name: 'GitHub issue bot',
tagline:
'Automate the process of responding to newly opened issues in a GitHub repository.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/github-issue-bot'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/github-issue-bot">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'GITHUB_TOKEN',
description: `A personal access token from GitHub with the necessary permissions to post comments on issues. <a class="u-bold" target="_blank" href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token">Learn more</a>.`,
value: '',
placeholder: 'ghp_1...',
required: true
},
{
name: 'GITHUB_WEBHOOK_SECRET',
description: `The secret used to verify that the webhook request comes from GitHub. <a class="u-bold" target="_blank" href="https://docs.github.com/en/developers/webhooks-and-events/securing-your-webhooks">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true
}
]
},
{
icon: 'icon-bookmark',
id: 'url-shortener',
name: 'URL shortener',
tagline: 'Generate URL with short ID and redirect to the original URL when visited.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/url-shortener'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/url-shortener">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to talk to Appwrite backend APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database to store the short URLs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/databases">Learn more</a>.`,
value: 'urlShortener',
placeholder: 'urlShortener',
required: false
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection to store the short URLs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/collections">Learn more</a>.`,
value: 'urls',
placeholder: 'urls',
required: false
}
]
},
{
icon: 'icon-algolia',
id: 'sync-with-algolia',
name: 'Sync with Algolia',
tagline: 'Intuitive search bar for any data in Appwrite Databases.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/sync-with-algolia'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-algolia">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to talk to Appwrite backend APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the Appwrite database that contains the collection to sync. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/databases">Learn more</a>.`,
placeholder: '64a55...7b912',
required: true
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection in the Appwrite database to sync. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/collections">Learn more</a>.`,
placeholder: '7c3e8...2a9f1',
required: true
},
{
name: 'ALGOLIA_ADMIN_API_KEY',
description: `The admin API Key for your Algolia service. <a class="u-bold" target="_blank" href="https://www.algolia.com/doc/guides/security/api-keys/">Learn more</a>.`,
placeholder: 'fd0aa...136a8',
required: true
},
{
name: 'ALGOLIA_INDEX_ID',
description: `The ID of the index in Algolia where the documents are to be synced. <a class="u-bold" target="_blank" href="https://www.algolia.com/doc/api-client/methods/indexing/">Learn more</a>.`,
placeholder: 'appwrite_index',
required: true
},
{
name: 'ALGOLIA_SEARCH_API_KEY',
description: `The search API Key for your Algolia service. This key is used for searching the synced index. <a class="u-bold" target="_blank" href="https://www.algolia.com/doc/guides/security/api-keys/">Learn more</a>.`,
placeholder: 'bf2f5...df733',
required: true
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false
}
]
},
{
icon: 'icon-meilisearch',
id: 'sync-with-meilisearch',
name: 'Sync with Meilisearch',
tagline: 'Intuitive search bar for any data in Appwrite Databases.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/sync-with-meilisearch'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-meilisearch">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to talk to Appwrite backend APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the Appwrite database that contains the collection to sync. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/databases">Learn more</a>.`,
placeholder: '64a55...7b912',
required: true
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection in the Appwrite database to sync. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/collections">Learn more</a>.`,
placeholder: '7c3e8...2a9f1',
required: true
},
{
name: 'MEILISEARCH_ENDPOINT',
description: `The host URL of the Meilisearch server. <a class="u-bold" target="_blank" href="https://www.meilisearch.com/docs/learn/getting_started/quick_start/">Learn more</a>.`,
placeholder: 'http://127.0.0.1:7700',
required: true
},
{
name: 'MEILISEARCH_ADMIN_API_KEY',
description: `The admin API key for Meilisearch. <a class="u-bold" target="_blank" href="https://docs.meilisearch.com/reference/api/keys/">Learn more</a>.`,
placeholder: 'masterKey1234',
required: true
},
{
name: 'MEILISEARCH_SEARCH_API_KEY',
description: `API Key for Meilisearch search operations. <a class="u-bold" target="_blank" href="https://www.algolia.com/doc/guides/security/api-keys/">Learn more</a>.`,
placeholder: 'searchKey1234',
required: true
},
{
name: 'MEILISEARCH_INDEX_NAME',
description: `Name of the Meilisearch index to which the documents will be synchronized. <a class="u-bold" target="_blank" href="https://www.meilisearch.com/docs/learn/core_concepts/indexes/">Learn more</a>.`,
placeholder: 'appwrite_index',
required: true
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/getting-started-for-server">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false
}
]
},
{
icon: 'icon-vonage',
id: 'whatsapp-with-vonage',
name: 'WhatsApp with Vonage',
tagline: 'Simple bot to answer WhatsApp messages.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/whatsapp-with-vonage'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/whatsapp-with-vonage">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'VONAGE_API_KEY',
description: `API Key to use the Vonage API. <a class="u-bold" target="_blank" href="https://api.support.vonage.com/hc/en-us/articles/204014493-How-do-I-find-my-Voice-API-key-and-API-secret-">Learn more</a>.`,
value: '',
placeholder: '62...97',
required: true
},
{
name: 'VONAGE_API_SECRET',
description: `Secret to use the Vonage API. <a class="u-bold" target="_blank" href="https://api.support.vonage.com/hc/en-us/articles/204014493-How-do-I-find-my-Voice-API-key-and-API-secret-">Learn more</a>.`,
placeholder: 'Zjc...5PH',
required: true
},
{
name: 'VONAGE_API_SIGNATURE_SECRET',
description: `Secret to verify the JWT token sent by Vonage. <a class="u-bold" target="_blank" href="https://developer.vonage.com/en/getting-started/concepts/signing-messages">Learn more</a>.`,
placeholder: 'NXOi3...IBHDa',
required: true
},
{
name: 'VONAGE_WHATSAPP_NUMBER',
description: `Vonage WhatsApp number to send messages from. <a class="u-bold" target="_blank" href="https://api.support.vonage.com/hc/en-us/articles/4431993282580-Where-do-I-find-my-WhatsApp-Number-Certificate-">Learn more</a>.`,
placeholder: '+14000000102',
required: true
}
]
},
{
icon: 'icon-bell',
id: 'push-notification-with-fcm',
name: 'Push notification with FCM',
tagline: 'Send push notifications to your users using Firebase Cloud Messaging (FCM).',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/push-notification-with-fcm'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/push-notification-with-fcm">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'FCM_PROJECT_ID',
description: `A unique identifier for your FCM project. <a class="u-bold" target="_blank" href="https://firebase.google.com/docs/projects/learn-more#project-id">Learn more</a>.`,
value: '',
placeholder: 'mywebapp-f6e57',
required: true
},
{
name: 'FCM_CLIENT_EMAIL',
description: `Your FCM service account email. <a class="u-bold" target="_blank" href="https://github.com/appwrite/templates/tree/main/node/push-notification-with-fcm#:~:text=Documentation-,FCM%3A%20SDK%20Setup,-FCM_PRIVATE_KEY">Learn more</a>.`,
placeholder: 'fcm-adminsdk-2f0de@test-f7q57.iam.gserviceaccount.com',
required: true
},
{
name: 'FCM_PRIVATE_KEY',
description: `A unique private key used to authenticate with FCM. <a class="u-bold" target="_blank" href="https://github.com/appwrite/templates/tree/main/node/push-notification-with-fcm#:~:text=Documentation-,FCM%3A%20SDK%20Setup,-FCM_DATABASE_URL">Learn more</a>.`,
placeholder: '0b683...75675',
required: true
},
{
name: 'FCM_DATABASE_URL',
description: `URL of your FCM database. <a class="u-bold" target="_blank" href="https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments">Learn more</a>.`,
placeholder: 'https://my-app-f298e.firebaseio.com',
required: true
}
]
},
{
icon: 'icon-mail',
id: 'email-contact-form',
name: 'Email contact form',
tagline: 'Sends an email with the contents of a HTML form.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['placeholder'],
runtimes: [
{
name: 'node-18.0',
commands: 'npm install',
entrypoint: 'src/main.js',
providerRootDirectory: 'node/email-contact-form'
}
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/email-contact-form">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'SMTP_HOST',
description: `The address of your SMTP server. Many STMP providers will provide this information in their documentation. Some popular providers include: Mailgun, SendGrid, and Gmail.`,
value: '',
placeholder: 'smtp.mailgun.org',
required: true
},
{
name: 'SMTP_PORT',
description: `The port of your STMP server. Commnly used ports include 25, 465, and 587.`,
placeholder: '25',
required: true
},
{
name: 'SMTP_USERNAME',
description: `The username for your SMTP server. This is commonly your email address.`,
placeholder: 'no-reply@mywebapp.org',
required: true
},
{
name: 'SMTP_PASSWORD',
description: `The password for your SMTP server.`,
placeholder: '5up3r5tr0ngP4ssw0rd',
required: true
},
{
name: 'SUBMIT_EMAIL',
description: `The email address to send form submissions to.`,
placeholder: 'me@mywebapp.org',
required: true
},
{
name: 'ALLOWED_ORIGINS',
description: `An optional comma-separated list of allowed origins for CORS (defaults to *). This is an important security measure to prevent malicious users from abusing your function.`,
value: '',
placeholder: 'https://mywebapp.org,https://mywebapp.com',
required: false
}
]
}
];
export type Runtime = {
name: string;
commands: string;
entrypoint: string;
providerRootDirectory: string;
};
export type Variable = {
name: string;
description: string;
value?: string;
placeholder: string;
required: boolean;
};
export type MarketplaceTemplate = {
icon: string;
id: string;
name: string;
tagline: string;
permissions: string[];
events: string[];
cron: string;
timeout: number;
usecases: string[];
runtimes: Runtime[];
instructions: string;
vcsProvider: string;
providerRepositoryId: string;
providerOwner: string;
providerBranch: string;
variables: Variable[];
};
/*
Template:
{
"icon": "algolia",
"id": "sync-with-algolia",
"name": "Sync with Algolia",
"tagline": "Search your Appwrite Database with Algolia.",
"permissions": ["any"],
"events": [ "users.*.create" ],
"cron": "0 * * * *",
"timeout": 15,
"runtimes": [
{
"name": "node-18",
"entrypoint": "src/main.js",
"commands": "npm install"
}
],
"instructions": "# Some markdown stuff",
"vcsProvider": "github",
"vcsRepositoryName": "templates-for-node",
"vcsOwnerName": "loks0n",
"vcsRootDirectory": "sync-with-algolia",
"variables": [
{
"name": "ALGOLIA_API_KEY",
"description": "Algolia read-write API key.",
"value": "",
"placeholder": "sk_1df...h9jef",
"required": true
}
]
}
*/
+10 -5
View File
@@ -59,6 +59,16 @@ function createServices() {
label: 'GraphQL',
method: 'graphql',
value: null
},
{
label: 'VCS',
method: 'vcs',
value: null
},
{
label: 'Proxy',
method: 'proxy',
value: null
}
]
});
@@ -112,11 +122,6 @@ function createServices() {
label: 'Users',
method: 'users',
value: project.serviceStatusForUsers
},
{
label: 'GraphQL',
method: 'graphql',
value: project.serviceStatusForGraphql
}
];
set({ list });
+9 -3
View File
@@ -2,20 +2,23 @@ import { getProjectId } from '$lib/helpers/project';
import { VARS } from '$lib/system';
import {
Account,
Assistant,
Avatars,
Client,
Console,
Databases,
Functions,
Health,
Locale,
Migrations,
Projects,
Project,
Project as ProjectApi,
Projects,
Proxy,
Storage,
Teams,
Users,
Console,
Assistant
Vcs
} from '@appwrite.io/console';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
@@ -34,9 +37,12 @@ const sdkForProject = {
health: new Health(clientProject),
locale: new Locale(clientProject),
project: new Project(clientProject),
projectApi: new ProjectApi(clientProject),
storage: new Storage(clientProject),
teams: new Teams(clientProject),
users: new Users(clientProject),
vcs: new Vcs(clientProject),
proxy: new Proxy(clientProject),
migrations: new Migrations(clientProject)
};
+12 -4
View File
@@ -1,11 +1,12 @@
import { trackEvent } from '$lib/actions/analytics';
import type { ComponentType } from 'svelte';
import type { SvelteComponent } from 'svelte';
import { writable } from 'svelte/store';
export type WizardStore = {
show: boolean;
media?: string;
component?: ComponentType;
component?: typeof SvelteComponent;
cover?: typeof SvelteComponent;
interceptor?: () => Promise<void>;
nextDisabled: boolean;
step: number;
@@ -15,6 +16,7 @@ function createWizardStore() {
const { subscribe, update, set } = writable<WizardStore>({
show: false,
component: null,
cover: null,
interceptor: null,
media: null,
nextDisabled: false,
@@ -24,13 +26,14 @@ function createWizardStore() {
return {
subscribe,
set,
start: (component: ComponentType, media: string = null) =>
start: (component: typeof SvelteComponent, media: string = null) =>
update((n) => {
n.show = true;
n.component = component;
n.interceptor = null;
n.media = media;
n.step = 1;
n.cover = null;
trackEvent('wizard_start');
return n;
}),
@@ -53,7 +56,12 @@ function createWizardStore() {
n.interceptor = null;
n.media = null;
n.step = 1;
n.cover = null;
return n;
}),
showCover: (component: typeof SvelteComponent) =>
update((n) => {
n.cover = component;
return n;
}),
updateStep: (cb: (prevStep: number) => number) => {
-1
View File
@@ -11,7 +11,6 @@ export const VARS = {
CONSOLE_MODE: import.meta.env?.VITE_CONSOLE_MODE?.toString() as string | undefined,
VERCEL_ENV: import.meta.env?.VITE_VERCEL_ENV?.toString() as string | undefined,
GOOGLE_ANALYTICS: import.meta.env?.VITE_GA_PROJECT?.toString() as string | undefined,
ASSISTANT_ENDPOINT: import.meta.env?.VITE_ASSISTANT_ENDPOINT?.toString() as string | undefined,
GOOGLE_TAG: import.meta.env?.VITE_GTM_PROJECT?.toString() as string | undefined
};
@@ -0,0 +1,38 @@
<script lang="ts">
import { Box } from '$lib/components';
import { FormList, Helper, InputChoice, InputText } from '$lib/elements/forms';
import type { Variable } from '$lib/stores/marketplace';
import { templateConfig } from '../store';
export let appwriteVariable: Variable;
</script>
<Box radius="small" padding={16}>
<FormList>
<div>
<InputText
id={appwriteVariable.name}
label={appwriteVariable.name}
placeholder={appwriteVariable.placeholder ?? 'Enter value'}
required={appwriteVariable.required}
bind:value={$templateConfig.appwriteApiKey}
disabled={!!$templateConfig.generateKey} />
<Helper type="neutral">
This API key will allow you to interact with the Appwrite server APIs. <a
href="https://appwrite.io/docs/keys"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</Helper>
</div>
<InputChoice
bind:value={$templateConfig.generateKey}
id="generate"
label="Generate API key on completion"
disabled={!!$templateConfig.appwriteApiKey}>
The <code class="inline-code">APPWRITE_API_KEY</code> will be automatically generated for
your project and applied to this function once it's created.
</InputChoice>
</FormList>
</Box>
@@ -0,0 +1,232 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { EmptySearch, PaginationInline } from '$lib/components';
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { app } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { repositories } from '$routes/console/project-[project]/functions/function-[function]/store';
import { installation, installations, repository } from '../store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let selectedRepository: string = null;
export let hasInstallations = false;
export let action: 'button' | 'select' = 'select';
let offset = 0;
const limit = 5;
$: {
hasInstallations = $installations?.total > 0;
}
let selectedInstallation = null;
async function loadInstallations() {
const { installations } = await sdk.forProject.vcs.listInstallations();
if (installations.length) {
selectedInstallation = installations[0].$id;
installation.set(installations.find((entry) => entry.$id === selectedInstallation));
}
return installations;
}
let search = '';
async function loadRepositories(installationId: string, search: string) {
if (
!$repositories ||
$repositories.installationId !== installationId ||
$repositories.search !== search
) {
$repositories.repositories = (
await sdk.forProject.vcs.listRepositories(installationId, search || undefined)
).providerRepositories;
}
$repositories.search = search;
$repositories.installationId = installationId;
if ($repositories.repositories.length) {
selectedRepository = $repositories.repositories[0].id;
$repository = $repositories.repositories[0];
}
return $repositories.repositories;
}
function connectGitHub() {
const redirect = new URL($page.url);
redirect.searchParams.append('github-installed', 'true');
const target = new URL(`${sdk.forProject.client.config.endpoint}/vcs/github/authorize`);
target.searchParams.set('projectId', $page.params.project);
target.searchParams.set('success', redirect.toString());
target.searchParams.set('failure', redirect.toString());
return target;
}
</script>
{#if hasInstallations}
{#await loadInstallations()}
<div class="u-flex u-gap-8 u-cross-center u-main-center">
<div class="loader u-margin-32" />
</div>
{:then installations}
<div class="u-flex u-gap-16">
<div class="u-width-full-line">
<InputSelect
id="installation"
label="Select installation"
showLabel={false}
options={installations.map((entry) => {
return {
label: entry.organization,
value: entry.$id
};
})}
on:change={() => {
search = '';
installation.set(
installations.find((entry) => entry.$id === selectedInstallation)
);
}}
bind:value={selectedInstallation} />
</div>
<div class="u-width-full-line">
<InputSearch placeholder="Search repositories" bind:value={search} />
</div>
</div>
{/await}
{#if selectedInstallation}
<p class="text common-section">
Manage organization configuration in your <a
class="link"
href={`${base}/console/project-${$page.params.project}/settings`}
>project settings</a
>.
</p>
{#await loadRepositories(selectedInstallation, search)}
<div class="card-git card is-border-dashed is-no-shadow common-section">
<div class="u-flex u-gap-8 u-cross-center u-main-center">
<div class="loader u-margin-16" />
Loading repositories...
</div>
</div>
{:then response}
{#if response?.length}
<ul class="table is-remove-outer-styles common-section">
{#each response as repo, i}
{#if i < offset + limit && i > offset - limit}
<li class="table-row">
<div class="table-col">
<div
class="u-flex u-cross-center u-gap-8"
style="margin-block: .75rem;">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
on:change={() => repository.set(repo)}
value={repo.id} />
{/if}
<div
class="avatar is-size-x-small"
style:--p-text-size="1.25rem"
class:is-color-empty={!repo.runtime}>
{#if repo.runtime}
<img
src={`${base}/icons/${$app.themeInUse}/color/${
repo.runtime.split('-')[0]
}.svg`}
alt={repo.name} />
{/if}
</div>
<div class="u-flex u-gap-8">
<span class="text u-trim-1">{repo.name}</span>
{#if repo.private}
<span
class="icon-lock-closed"
style="font-size: var(--icon-size-small)"
aria-hidden="true" />
{/if}
<time
class="u-color-text-gray u-trim-1"
datetime={repo.pushedAt}>
{timeFromNow(repo.pushedAt)}
</time>
</div>
{#if action === 'button'}
<div class="u-margin-inline-start-auto">
<Button
secondary
on:click={() => dispatch('connect', repo)}>
Connect
</Button>
</div>
{/if}
</div>
</div>
</li>
{/if}
{/each}
</ul>
<!-- <div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {response?.length}</p>
<PaginationInline {limit} bind:offset sum={response?.length} />
</div> -->
{:else if search}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
<p>There are no repositories that match your search.</p>
</div>
<div class="u-flex u-gap-16 common-section u-main-center">
<Button external href="https://appwrite.io/docs/client/teams" text
>Documentation</Button>
<Button secondary on:click={() => (search = '')}>Clear search</Button>
</div>
</div>
</EmptySearch>
{:else}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<p class="text u-line-height-1-5">You have no repositories.</p>
<p class="text u-line-height-1-5">
Need a hand? Learn more in our <a
href="https://appwrite.io/docs/client/teams"
target="_blank"
rel="noopener noreferrer">
documentation</a
>.
</p>
</div>
</div>
</EmptySearch>
{/if}
{/await}
{/if}
{:else}
<div class="u-flex u-cross-center u-flex-vertical u-gap-16">
<Button href={connectGitHub().toString()} fullWidth secondary>
<span class="icon-github" aria-hidden="true" />
<span class="text">GitHub</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-gitlab" aria-hidden="true" />
<span class="text">GitLab (coming soon)</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-bitBucket" aria-hidden="true" />
<span class="text">BitBucket (coming soon)</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-azure" aria-hidden="true" />
<span class="text">Azure (coming soon)</span>
</Button>
</div>
{/if}
@@ -0,0 +1,75 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import SelectRepository from './steps/selectRepository.svelte';
import GitConfiguration from './steps/gitConfiguration.svelte';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
import { func } from '$routes/console/project-[project]/functions/function-[function]/store';
import { choices, installation, repository } from './store';
import { wizard } from '$lib/stores/wizard';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
async function createGitHubInstallation() {
try {
await sdk.forProject.functions.update(
$func.$id,
$func.name,
$func.runtime,
$func.entrypoint || undefined,
$func.execute || undefined,
$func.events || undefined,
$func.schedule || undefined,
$func.timeout || undefined,
$func.enabled || undefined,
$func.logging || undefined,
$func.commands || undefined,
$installation.$id,
$repository.id,
$choices.branch,
$choices.silentMode || undefined,
$choices.rootDir
);
trackEvent(Submit.FunctionConnectRepo, {
customId: !!$func.$id
});
await invalidate(Dependencies.FUNCTION);
resetState();
wizard.hide();
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionConnectRepo);
}
}
function resetState() {
choices.set({
branch: null,
silentMode: false,
rootDir: null
});
installation.set(null);
repository.set(null);
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Repository',
component: SelectRepository
});
stepsComponents.set(2, {
label: 'Git',
component: GitConfiguration
});
</script>
<Wizard
title="Connect Git"
steps={stepsComponents}
on:finish={createGitHubInstallation}
on:exit={resetState} />
+188
View File
@@ -0,0 +1,188 @@
<script context="module" lang="ts">
import CreateTemplate from './createTemplate.svelte';
export function connectTemplate(template: MarketplaceTemplate, runtime: string | null = null) {
const variables: any = {};
template.variables.forEach((variable) => {
variables[variable.name] = variable.value ?? '';
});
if (!runtime) {
runtime = template.runtimes[0].name;
}
templateStore.set(template);
templateConfig.set({
$id: null,
runtime,
name: template.name,
variables,
repositoryBehaviour: 'new',
repositoryName: template.id,
repositoryPrivate: false,
repositoryId: null
});
wizard.start(CreateTemplate);
}
</script>
<script lang="ts">
import { base } from '$app/paths';
import { AvatarGroup, Heading } from '$lib/components';
import WizardCover from '$lib/layout/wizardCover.svelte';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import { repository, templateConfig, template as templateStore } from './store';
import { marketplace, type MarketplaceTemplate } from '$lib/stores/marketplace';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import Repositories from './components/repositories.svelte';
import CreateManual from './createManual.svelte';
import CreateGit from './createGit.svelte';
import { Button } from '$lib/elements/forms';
import { page } from '$app/stores';
let hasInstallations: boolean;
let selectedRepository: string;
const quickStart = marketplace.find((template) => template.id === 'starter');
const templates = marketplace.filter((template) => template.id !== 'starter').slice(0, 3);
function connect(event: CustomEvent<Models.Repository>) {
repository.set(event.detail);
wizard.start(CreateGit);
}
async function loadRuntimes() {
let runtimes = await sdk.forProject.functions.listRuntimes();
return runtimes.runtimes;
}
</script>
<WizardCover>
<svelte:fragment slot="title">Create function</svelte:fragment>
<div class="wizard-container container">
<div class="grid-1-1 u-gap-24">
<div>
<div class="card u-cross-child-start">
<Heading size="6" tag="h2">Connect Git repository</Heading>
<p class="u-margin-block-start-8">
Create and deploy a function with a connected git repository.
</p>
<div class="u-margin-block-start-24">
<Repositories
bind:hasInstallations
bind:selectedRepository
action="button"
on:connect={connect} />
</div>
</div>
<p class="u-margin-block-start-16">
You can also create a function <button
class="link"
on:click={() => wizard.start(CreateManual)}>manually</button>
or using the CLI.
<a
href="https://appwrite.io/docs/functions-deploy"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
</div>
<div class="card">
<section class="common-section">
<h2 class="heading-level-6">Quick start</h2>
<p class="u-margin-block-start-8">
Use a starter templates to begin with the basics.
</p>
{#await loadRuntimes()}
<div class="avatar is-size-x-small">
<div class="loader u-margin-16" />
</div>
{:then runtimes}
<ul
class="grid-box u-margin-block-start-16"
style:--grid-item-size="8rem"
style:--grid-item-size-small-screens="9rem"
style:--grid-gap="1rem">
{#each quickStart.runtimes.filter((_template, index) => index < 6) as runtime}
{@const runtimeDetail = runtimes.find(
(r) => r.$id === runtime.name
)}
<li>
<button
on:click={() => connectTemplate(quickStart, runtime.name)}
class="box u-width-full-line u-flex u-cross-center u-gap-8"
style:--box-padding="1rem"
style:--box-border-radius="var(--border-radius-small)">
<div class="avatar is-size-small">
<img
style:--p-text-size="1.25rem"
src={`${base}/icons/${$app.themeInUse}/color/${
runtime.name.split('-')[0]
}.svg`}
alt={runtime.name} />
</div>
<div class="body-text-2">{runtimeDetail.name}</div>
</button>
</li>
{/each}
{#if quickStart.runtimes.length < 6}
<li>
<div
class="box u-width-full-line u-flex u-cross-center u-gap-8"
style:--box-padding="1rem"
style:--box-border-radius="var(--border-radius-small)">
<AvatarGroup
icons={['dotnet', 'deno']}
total={4}
avatarSize="small"
bordered />
</div>
</li>
{/if}
</ul>
{/await}
</section>
<div class="u-sep-block-start common-section" />
<section class="common-section">
<h2 class="heading-level-6">Templates</h2>
<p class="text u-margin-block-start-8">
Find the right template for your use case.
</p>
<ul class="clickable-list u-margin-block-start-16">
{#each templates as template}
<li class="clickable-list-item">
<button
type="button"
on:click={() => connectTemplate(template)}
class="clickable-list-button u-width-full-line u-flex u-gap-12">
<div class="avatar is-size-small" style:--p-text-size="1.25rem">
<span class={template.icon} />
</div>
<div>
<div class="body-text-2 u-bold u-trim">{template.name}</div>
<div class="u-trim-1 u-color-text-gray">
{template.tagline}
</div>
</div>
</button>
</li>
{/each}
</ul>
</section>
<Button
text
class="u-margin-inline-start-auto u-margin-block-start-16"
href={`${base}/console/project-${$page.params.project}/functions/templates`}>
<span> All templates </span>
<span class="icon-cheveron-right" aria-hidden="true" />
</Button>
</div>
</div>
</div>
</WizardCover>
@@ -0,0 +1,84 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { goto } from '$app/navigation';
import { choices, createFunction, installation, repository } from './store';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { page } from '$app/stores';
import ExecuteAccess from './steps/executeAccess.svelte';
import GitConfiguration from './steps/gitConfiguration.svelte';
import FunctionConfiguration from './steps/functionConfiguration.svelte';
async function create() {
try {
const response = await sdk.forProject.functions.create(
$createFunction.$id || ID.unique(),
$createFunction.name,
$createFunction.runtime,
$createFunction.entrypoint,
$createFunction.execute || undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
$createFunction.commands || undefined,
$installation.$id,
$repository.id,
$choices.branch,
$choices.silentMode || undefined,
$choices.rootDir || undefined
);
goto(
`${base}/console/project-${$page.params.project}/functions/function-${response.$id}`
);
addNotification({
message: `${$createFunction.name} has been created`,
type: 'success'
});
trackEvent(Submit.FunctionCreate, {
customId: !!$createFunction.$id
});
resetState();
wizard.hide();
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionCreate);
}
}
function resetState() {
createFunction.set({
$id: null,
name: null,
entrypoint: null,
execute: [],
runtime: null
});
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Git',
component: GitConfiguration
});
stepsComponents.set(2, {
label: 'Configuration',
component: FunctionConfiguration
});
stepsComponents.set(3, {
label: 'Execute access',
component: ExecuteAccess,
optional: true
});
</script>
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} on:exit={resetState} />
@@ -0,0 +1,92 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { goto } from '$app/navigation';
import {
choices,
createFunction,
createFunctionDeployment,
installation,
repository
} from './store';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { page } from '$app/stores';
import Details from './steps/details.svelte';
import Configuration from './steps/configuration.svelte';
import ExecuteAccess from './steps/executeAccess.svelte';
async function create() {
try {
const response = await sdk.forProject.functions.create(
$createFunction.$id || ID.unique(),
$createFunction.name,
$createFunction.runtime,
$createFunction.entrypoint,
$createFunction.execute || undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
$createFunction.commands || undefined
);
await sdk.forProject.functions.createDeployment(
response.$id,
$createFunctionDeployment[0],
true
);
goto(
`${base}/console/project-${$page.params.project}/functions/function-${response.$id}`
);
addNotification({
message: `${$createFunction.name} has been created`,
type: 'success'
});
trackEvent(Submit.FunctionCreate, {
customId: !!$createFunction.$id
});
resetState();
wizard.hide();
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionCreate);
}
}
function resetState() {
createFunction.reset();
choices.set({
branch: null,
silentMode: false,
rootDir: null
});
installation.set(null);
repository.set(null);
createFunctionDeployment.set(null);
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Details',
component: Details
});
stepsComponents.set(2, {
label: 'Configuration',
component: Configuration
});
stepsComponents.set(3, {
label: 'Execute access',
component: ExecuteAccess,
optional: true
});
</script>
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} on:exit={resetState} />
@@ -0,0 +1,105 @@
<script lang="ts">
import { ID } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { goto } from '$app/navigation';
import { choices, installation, repository, template, templateConfig } from './store';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { page } from '$app/stores';
import GitConfiguration from './steps/gitConfiguration.svelte';
import TemplateConfiguration from './steps/templateConfiguration.svelte';
import RepositoryBehaviour from './steps/repositoryBehaviour.svelte';
import CreateRepository from './steps/createRepository.svelte';
import TemplateVariables from './steps/templateVariables.svelte';
import { scopes } from '$lib/constants';
async function create() {
try {
const runtimeDetail = $template.runtimes.find(
(r) => r.name === $templateConfig.runtime
);
if ($templateConfig.appwriteApiKey) {
$templateConfig.variables['APPWRITE_API_KEY'] = $templateConfig.appwriteApiKey;
} else if ($templateConfig?.generateKey) {
const key = await sdk.forConsole.projects.createKey(
$page.params.project,
'Generated for Template',
scopes.map((scope) => scope.scope)
);
$templateConfig.variables['APPWRITE_API_KEY'] = key.secret;
}
const response = await sdk.forProject.functions.create(
$templateConfig.$id || ID.unique(),
$templateConfig.name,
$templateConfig.runtime,
runtimeDetail.entrypoint,
$template.permissions || undefined,
$template.events || undefined,
$template.cron || undefined,
$template.timeout || undefined,
undefined,
undefined,
runtimeDetail.commands || undefined,
$installation.$id,
$repository.id,
$choices.branch,
$choices.silentMode || undefined,
$choices.rootDir || undefined,
$template.providerRepositoryId,
$template.providerOwner,
runtimeDetail.providerRootDirectory,
$template.providerBranch
);
goto(
`${base}/console/project-${$page.params.project}/functions/function-${response.$id}`
);
addNotification({
message: `${response.name} has been created`,
type: 'success'
});
trackEvent(Submit.FunctionCreate, {
customId: !!response.$id
});
resetState();
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.FunctionCreate);
}
}
function resetState() {
wizard.hide();
templateConfig.set(null);
template.set(null);
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Configuration',
component: TemplateConfiguration
});
stepsComponents.set(2, {
label: 'Variables',
component: TemplateVariables
});
stepsComponents.set(3, {
label: 'Connect',
component: RepositoryBehaviour
});
stepsComponents.set(4, {
label: 'Repository',
component: CreateRepository
});
stepsComponents.set(5, {
label: 'Branch',
component: GitConfiguration
});
</script>
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} on:exit={resetState} />
@@ -0,0 +1,42 @@
<script lang="ts">
import { Collapsible, CollapsibleItem } from '$lib/components';
import { FormList, InputFile, InputText } from '$lib/elements/forms';
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
import { WizardStep } from '$lib/layout';
import { createFunction, createFunctionDeployment } from '../store';
</script>
<WizardStep>
<svelte:fragment slot="title">Configuration</svelte:fragment>
<svelte:fragment slot="subtitle">
Set your deployment configuration and any build commands here.
</svelte:fragment>
<FormList>
<InputFile
label="Upload a zip file (tar.gz) containing your function source code"
allowedFileExtensions={['gz']}
bind:files={$createFunctionDeployment}
required />
<InputText
label="Entrypoint"
id="entrypoint"
placeholder="Entrypoint"
bind:value={$createFunction.entrypoint}
required />
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">Build commands</svelte:fragment>
<svelte:fragment slot="subtitle">(optional)</svelte:fragment>
<FormList>
<InputTextarea
label="Commands"
placeholder="Enter a build commad (e.g. 'npm install')"
id="build"
tooltip="Enter a single command or chain multiple commands with the && operator"
bind:value={$createFunction.commands} />
</FormList>
</CollapsibleItem>
</Collapsible>
</FormList>
</WizardStep>
@@ -0,0 +1,158 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/elements/forms/button.svelte';
import FormList from '$lib/elements/forms/formList.svelte';
import InputChoice from '$lib/elements/forms/inputChoice.svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import InputText from '$lib/elements/forms/inputText.svelte';
import { WizardStep } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import Repositories from '../components/repositories.svelte';
import { installation, repository, templateConfig } from '../store';
let selectedInstallationId: string;
let hasInstallations: boolean;
let selectedRepository: string;
async function beforeSubmit() {
if (!hasInstallations || !$installation) {
throw new Error('Please connect a Git provider');
}
if ($templateConfig.repositoryBehaviour === 'new') {
const repo = await sdk.forProject.vcs.createRepository(
$installation.$id,
$templateConfig.repositoryName,
$templateConfig.repositoryPrivate
);
$repository = repo;
addNotification({
type: 'success',
message: 'Repository successfully created.'
});
}
if (!$repository) {
throw new Error('Please select a repository');
}
}
function connectGitHub() {
const redirect = new URL($page.url);
redirect.searchParams.append('github-installed', 'true');
const target = new URL(`${sdk.forProject.client.config.endpoint}/vcs/github/authorize`);
target.searchParams.set('projectId', $page.params.project);
target.searchParams.set('success', redirect.toString());
target.searchParams.set('failure', redirect.toString());
return target;
}
async function loadInstallations() {
const { installations } = await sdk.forProject.vcs.listInstallations();
if (installations.length) {
$installation = installations[0];
hasInstallations = true;
}
return installations;
}
function getProviderIcon(provider: string) {
switch (provider) {
case 'github':
return 'icon-github';
default:
return '';
}
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Repository</svelte:fragment>
<svelte:fragment slot="subtitle">
Select a Git repository that will trigger your function deployments when updated.
</svelte:fragment>
{#if $templateConfig.repositoryBehaviour === 'existing'}
<Repositories bind:hasInstallations bind:selectedRepository />
{:else}
{#await loadInstallations()}
<div class="u-flex u-gap-8 u-cross-center u-main-center">
<div class="loader u-margin-32" />
</div>
{:then installations}
{#if hasInstallations}
<div class="u-flex u-gap-16">
<div class="u-width-full-line">
<InputSelect
id="installation"
label="Git organization"
options={installations.map((entry) => {
return {
label: entry.organization,
value: entry.$id
};
})}
on:change={() => {
$installation = installations.find(
(entry) => entry.$id === selectedInstallationId
);
}}
bind:value={selectedInstallationId} />
</div>
</div>
{:else}
<div class="u-flex u-cross-center u-flex-vertical u-gap-16">
<Button href={connectGitHub().toString()} fullWidth secondary>
<span class="icon-github" aria-hidden="true" />
<span class="text">Continue with GitHub</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-gitlab" aria-hidden="true" />
<span class="text">GitLab (coming soon)</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-bitBucket" aria-hidden="true" />
<span class="text">BitBucket (coming soon)</span>
</Button>
<Button disabled fullWidth secondary>
<span class="icon-azure" aria-hidden="true" />
<span class="text">Azure (coming soon)</span>
</Button>
</div>
{/if}
{/await}
{#if $installation}
<div
class="box u-margin-block-start-20"
style:--box-border-radius="var(--border-radius-small)">
<div class="u-flex u-gap-16">
<div class="avatar is-size-x-small">
<span class={getProviderIcon($installation.provider)} />
</div>
<div class="u-cross-child-center u-line-height-1-5">
<h6 class="u-bold u-trim-1">
{$installation.organization}/{$templateConfig.repositoryName}
</h6>
</div>
</div>
<div class="u-margin-block-start-24">
<FormList>
<InputText
id="repositoryName"
label="Repository name"
placeholder="my-repository"
bind:value={$templateConfig.repositoryName} />
<InputChoice
id="repositoryPrivate"
label="Keep repository private"
bind:value={$templateConfig.repositoryPrivate} />
</FormList>
</div>
</div>
{/if}
{/if}
</WizardStep>
@@ -0,0 +1,57 @@
<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 { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { createFunction } from '../store';
let showCustomId = false;
let options = [];
onMount(async () => {
let runtimes = await sdk.forProject.functions.listRuntimes();
options = runtimes.runtimes.map((runtime) => ({
label: `${runtime.name} - ${runtime.version}`,
value: runtime.$id
}));
});
</script>
<WizardStep>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">Create and deploy your function manually.</svelte:fragment>
<FormList>
<InputText
label="Name"
id="name"
placeholder="Function name"
bind:value={$createFunction.name}
required />
<InputSelect
label="Runtime"
id="runtime"
placeholder="Select runtime"
bind:value={$createFunction.runtime}
{options}
required />
{#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}
fullWidth />
{/if}
</FormList>
</WizardStep>
@@ -0,0 +1,21 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { Roles } from '$lib/components/permissions';
import { createFunction } from '../store';
</script>
<WizardStep>
<svelte:fragment slot="title">Execution permissions</svelte:fragment>
<svelte:fragment slot="subtitle">
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>.
</svelte:fragment>
<Roles bind:roles={$createFunction.execute} />
</WizardStep>
@@ -0,0 +1,121 @@
<script lang="ts">
import { Collapsible, CollapsibleItem, CustomId } from '$lib/components';
import { Pill } from '$lib/elements';
import { FormList, InputSelect, InputText } from '$lib/elements/forms';
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import Label from '$lib/elements/forms/label.svelte';
import { choices, createFunction, installation, repository } from '../store';
let showCustomId = false;
let detectingRuntime = true;
async function loadDetection(
installationId: string,
repositoryId: string,
rootDirectory: string
) {
const detection = await sdk.forProject.vcs.createRepositoryDetection(
installationId,
repositoryId,
rootDirectory
);
return detection;
}
let options = [];
onMount(async () => {
let runtimes = await sdk.forProject.functions.listRuntimes();
options = runtimes.runtimes.map((runtime) => ({
label: `${runtime.name} - ${runtime.version}`,
value: runtime.$id
}));
try {
const detection = await loadDetection(
$installation.$id,
$repository.id,
$choices.rootDir
);
$createFunction.runtime = detection.runtime;
} catch (err) {
console.error(err);
} finally {
detectingRuntime = false;
}
});
</script>
<WizardStep>
<svelte:fragment slot="title">Configuration</svelte:fragment>
<svelte:fragment slot="subtitle">
Set your deployment configuration and any build commands here.
</svelte:fragment>
<FormList>
<InputText
label="Name"
id="name"
placeholder="Function name"
bind:value={$createFunction.name}
required />
{#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}
fullWidth />
{/if}
{#if detectingRuntime}
<div>
<Label required={true}>Runtime</Label>
<div class="card-git card is-border-dashed is-no-shadow">
<div class="u-flex u-gap-8 u-cross-center u-main-center">
<div class="loader u-margin-16" />
Detecting runtime...
</div>
</div>
</div>
{:else}
<InputSelect
label="Runtime"
id="runtime"
placeholder="Select runtime"
bind:value={$createFunction.runtime}
{options}
required />
{/if}
<InputText
label="Entrypoint"
id="entrypoint"
placeholder="Entrypoint"
bind:value={$createFunction.entrypoint}
required />
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">Build commands</svelte:fragment>
<svelte:fragment slot="subtitle">(optional)</svelte:fragment>
<FormList>
<InputTextarea
label="Commands"
placeholder="Enter a build commad (e.g. 'npm install')"
id="build"
tooltip="Enter a single command or chain multiple commands with the && operator"
bind:value={$createFunction.commands} />
</FormList>
</CollapsibleItem>
</Collapsible>
</FormList>
</WizardStep>
@@ -0,0 +1,105 @@
<script lang="ts">
import { FormList, InputChoice, InputText } from '$lib/elements/forms';
import InputSelectSearch from '$lib/elements/forms/inputSelectSearch.svelte';
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { sortBranches } from '$routes/console/project-[project]/functions/function-[function]/settings/updateConfiguration.svelte';
import { choices, installation, repository } from '../store';
$choices.rootDir ??= '';
function getProviderIcon(provider: string) {
switch (provider) {
case 'github':
return 'icon-github';
default:
return '';
}
}
async function loadBranches() {
const { branches } = await sdk.forProject.vcs.listRepositoryBranches(
$installation.$id,
$repository.id
);
const sorted = sortBranches(branches);
$choices.branch = sorted[0]?.name ?? null;
if (!$choices.branch) {
$choices.branch = 'main';
}
return sorted;
}
</script>
<WizardStep>
<svelte:fragment slot="title">Git</svelte:fragment>
<svelte:fragment slot="subtitle">
Configure the Git repository that will trigger your function deployments when updated.
</svelte:fragment>
<div class="box" style:--box-border-radius="var(--border-radius-small)">
<div class="u-flex u-gap-16">
<div class="avatar is-size-x-small">
<span class={getProviderIcon($repository.provider)} />
</div>
<div class="u-cross-child-center u-line-height-1-5">
<h6 class="u-bold u-trim-1">{$installation.organization}/{$repository.name}</h6>
</div>
</div>
{#await loadBranches()}
<div class="u-flex u-gap-8 u-cross-center u-main-center">
<div class="loader u-margin-32" />
</div>
{:then branches}
{@const options =
branches
?.map((branch) => {
return {
value: branch.name,
label: branch.name
};
})
?.sort((a, b) => {
return a.label > b.label ? 1 : -1;
}) ?? []}
<div class="u-margin-block-start-24">
<FormList>
<InputSelectSearch
required={true}
id="branch"
label="Production branch"
placeholder="Select branch"
tooltip="Every commit pushed to this branch will activate the deployment after a successful build"
hideRequired
bind:value={$choices.branch}
bind:search={$choices.branch}
on:select={(event) => {
$choices.branch = event.detail.value;
}}
interactiveOutput
name="branch"
{options} />
<InputText
id="root"
label="Root directory"
placeholder="functions/my-function"
bind:value={$choices.rootDir} />
<InputChoice
id="silent"
label="Silent mode"
tooltip="When enabled, comments will not be made on pull requests in this repository"
bind:value={$choices.silentMode} />
</FormList>
</div>
{/await}
</div>
<p class="text u-margin-block-start-8">
View your configuration in <a
href={$repository.html_url}
target="_blank"
rel="noopener noreferrer"
class="link">GitHub</a
>.
</p>
</WizardStep>
@@ -0,0 +1,33 @@
<script lang="ts">
import { LabelCard } from '$lib/components';
import { WizardStep } from '$lib/layout';
import { templateConfig } from '../store';
async function beforeSubmit() {
if (!$templateConfig.repositoryBehaviour) {
throw new Error('Please select repository behaviour.');
}
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Connect</svelte:fragment>
<svelte:fragment slot="subtitle">
Connect function to a new repository or to an existing one within a selected Git
organization.
</svelte:fragment>
<ul class="u-flex u-flex-vertical u-gap-24">
<LabelCard name="behaviour" value="new" bind:group={$templateConfig.repositoryBehaviour}>
<svelte:fragment slot="title">Create a new repository</svelte:fragment>
Clone the template and create a new repository in your selected organization.
</LabelCard>
<LabelCard
name="behaviour"
value="existing"
bind:group={$templateConfig.repositoryBehaviour}>
<svelte:fragment slot="title">Add to existing repository</svelte:fragment>
Clone the template to an existing repository in your selected organization.
</LabelCard>
</ul>
</WizardStep>
@@ -0,0 +1,23 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import Repositories from '../components/repositories.svelte';
let hasInstallations: boolean;
let selectedRepository: string;
async function beforeSubmit() {
if (!hasInstallations) {
throw new Error('Please connect a Git provider');
}
if (!selectedRepository) {
throw new Error('Please select a repository');
}
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Repository</svelte:fragment>
<svelte:fragment slot="subtitle">
Select a Git repository that will trigger your function deployments when updated.
</svelte:fragment>
<Repositories bind:hasInstallations bind:selectedRepository />
</WizardStep>
@@ -0,0 +1,76 @@
<script lang="ts">
import { CustomId } from '$lib/components';
import { Pill } from '$lib/elements';
import { FormList, InputSelect, InputText } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { template, templateConfig } from '../store';
let showCustomId = false;
async function beforeSubmit() {
if (!$templateConfig.runtime) {
throw new Error('Please select a runtime.');
}
}
async function loadRuntimes() {
let runtimes = await sdk.forProject.functions.listRuntimes();
const options = runtimes.runtimes
.map((runtime) => ({
label: `${runtime.name} - ${runtime.version}`,
value: runtime.$id
}))
.filter((runtime) => {
const allowedRuntimes = $template.runtimes.map((r) => r.name);
return allowedRuntimes.includes(runtime.value);
});
return options;
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">{$template.name}</svelte:fragment>
<svelte:fragment slot="subtitle">
{$template.tagline}
</svelte:fragment>
<FormList>
<InputText
label="Name"
id="name"
placeholder="Function name"
bind:value={$templateConfig.name}
required />
{#await loadRuntimes()}
<div class="avatar is-size-x-small">
<div class="loader u-margin-16" />
</div>
{:then options}
<InputSelect
label="Runtime"
id="runtime"
placeholder="Select runtime"
bind:value={$templateConfig.runtime}
{options}
required />
{/await}
</FormList>
<div class="u-margin-block-start-24">
{#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={$templateConfig.$id}
fullWidth />
{/if}
</div>
</WizardStep>
@@ -0,0 +1,102 @@
<script lang="ts">
import InputText from '$lib/elements/forms/inputText.svelte';
import { WizardStep } from '$lib/layout';
import { template, templateConfig } from '../store';
import { FormList, Helper } from '$lib/elements/forms';
import { Card, Collapsible, CollapsibleItem } from '$lib/components';
import AppwriteVariable from '../components/appwriteVariable.svelte';
async function beforeSubmit() {
for (const variable of $template.variables) {
if (!variable.required) {
continue;
}
if (!$templateConfig.variables[variable.name]) {
throw new Error(`Please set ${variable.name} variable.`);
}
}
}
$: requiredVariables = $template?.variables?.filter((v) => v.required);
$: optionalVariables = $template?.variables?.filter((v) => !v.required);
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Variables</svelte:fragment>
<svelte:fragment slot="subtitle">
Edit the values of the environment variables that will be passed to your function at
runtime.
</svelte:fragment>
{#if $template?.variables?.length}
{#if requiredVariables?.length}
<Collapsible>
<CollapsibleItem open={true}>
<svelte:fragment slot="title">Required variables</svelte:fragment>
<svelte:fragment slot="subtitle">
<span class="inline-tag">{requiredVariables.length}</span>
</svelte:fragment>
<FormList>
{#each requiredVariables as variable}
{#if variable.name === 'APPWRITE_API_KEY'}
<AppwriteVariable appwriteVariable={variable} />
{:else}
<div>
<InputText
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
bind:value={$templateConfig.variables[variable.name]} />
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/if}
{/each}
</FormList>
</CollapsibleItem>
</Collapsible>
{/if}
{#if optionalVariables?.length}
<Collapsible>
<CollapsibleItem open={!requiredVariables?.length}>
<svelte:fragment slot="title">Optional variables</svelte:fragment>
<svelte:fragment slot="subtitle">
<span class="inline-tag">{optionalVariables.length}</span>
</svelte:fragment>
<FormList>
{#each optionalVariables as variable}
{#if variable.name === 'APPWRITE_API_KEY'}
<AppwriteVariable appwriteVariable={variable} />
{:else}
<div>
<InputText
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
bind:value={$templateConfig.variables[variable.name]} />
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/if}
{/each}
</FormList>
</CollapsibleItem>
</Collapsible>
{/if}
{:else}
<Card isDashed>
<p class="text u-text-center">There are no environment variables to configure.</p>
</Card>
{/if}
</WizardStep>
+61
View File
@@ -0,0 +1,61 @@
import { page } from '$app/stores';
import type { MarketplaceTemplate } from '$lib/stores/marketplace';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
export const template = writable<MarketplaceTemplate>();
export const templateConfig = writable<{
$id: string;
name: string;
runtime: string;
variables: { [key: string]: string };
repositoryBehaviour: 'new' | 'existing';
repositoryName: string;
repositoryPrivate: boolean;
repositoryId: string;
appwriteApiKey?: string;
generateKey?: boolean;
}>();
export const repository = writable<Models.ProviderRepository>();
export const installation = writable<Models.Installation>();
export const choices = writable<{
branch: string;
rootDir: string;
silentMode: boolean;
}>({
branch: null,
rootDir: null,
silentMode: null
});
export const installations = derived(
page,
($page) => $page.data.installations as Models.InstallationList
);
const initialCreateFunction: Partial<Models.Function> = {
$id: null,
name: null,
entrypoint: null,
execute: [],
runtime: null,
commands: null
};
function createFunctionStore() {
const store = writable<Partial<Models.Function>>({
...initialCreateFunction
});
const reset = () => {
store.set({ ...initialCreateFunction });
};
return {
...store,
reset
};
}
export const createFunction = createFunctionStore();
export const createFunctionDeployment = writable<FileList>();
+7 -2
View File
@@ -15,13 +15,18 @@
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
import { loading } from './store';
import { loading, requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
}
onMount(async () => {
if ($page.url.searchParams.has('migrate')) {
const migrateData = $page.url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData));
}
/**
* Reporting Web Vitals.
*/
@@ -59,7 +64,7 @@
/**
* Handle initial load.
*/
if (!$page.url.pathname.startsWith('/auth')) {
if (!$page.url.pathname.startsWith('/auth') && !$page.url.pathname.startsWith('/git')) {
const acceptedRoutes = [
'/login',
'/register',
-7
View File
@@ -6,19 +6,12 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
if (url.searchParams.has('migrate')) {
const migrateData = url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData));
}
try {
const account = await sdk.forConsole.account.get();
+3 -1
View File
@@ -4,13 +4,15 @@
import { Heading } from '$lib/components';
import { Account, Client } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { VARS } from '$lib/system';
const client = new Client();
const account = new Account(client);
onMount(async () => {
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
const projectId = $page.url.searchParams.get('project');
client.setEndpoint(`${$page.url.origin}/v1`).setProject(projectId);
client.setEndpoint(endpoint).setProject(projectId);
const userId = $page.url.searchParams.get('userId');
const secret = $page.url.searchParams.get('secret');
@@ -112,6 +112,12 @@
case 'firebase': {
if ($provider.projectId) {
// OAuth
const res = await sdk.forProject.migrations.getFirebaseReportOAuth(
providerResources.firebase,
$provider.projectId
);
report = res;
} else if ($provider.serviceAccount) {
// Manual auth
const res = await projectSdk.migrations.getFirebaseReport(
@@ -37,11 +37,11 @@
const steps: WizardStepsType = new Map();
steps.set(1, {
label: 'Select project',
label: 'Project',
component: Step1
});
steps.set(2, {
label: 'Select data',
label: 'Resources',
component: Step2
});
</script>
+2 -5
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { INTERVAL } from '$lib/constants';
import { Logs } from '$lib/layout';
@@ -230,10 +229,6 @@
}
}
beforeNavigate(() => {
$log.show = false;
});
$: if (!$log.show) {
$log.data = null;
$log.func = null;
@@ -263,6 +258,8 @@
{#if $wizard.show && $wizard.component}
<svelte:component this={$wizard.component} />
{:else if $wizard.cover}
<svelte:component this={$wizard.cover} />
{/if}
<Create bind:show={$newOrgModal} />
+1 -1
View File
@@ -2,7 +2,7 @@ import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
export const load: LayoutLoad = async ({ fetch }) => {
const { endpoint, project } = sdk.forConsole.client.config;
const response = await fetch(`${endpoint}/health/version`, {
headers: {
+4 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button, Form, FormList, InputText, InputPassword } from '$lib/elements/forms';
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
import { CardGrid, BoxAvatar, Heading, AvatarInitials } from '$lib/components';
import { Container } from '$lib/layout';
import { onMount } from 'svelte';
import { user } from '$lib/stores/user';
@@ -155,21 +155,21 @@
</Form>
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Delete Account</Heading>
<Heading tag="h6" size="7">Delete account</Heading>
</div>
<p>
Your account will be permanently deleted and access will be lost to any of your teams
and data. This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<BoxAvatar>
<svelte:fragment slot="image">
<AvatarInitials size={48} name={$user.name} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold u-trim-1">{$user.name}</h6>
</svelte:fragment>
</Box>
</BoxAvatar>
</svelte:fragment>
<svelte:fragment slot="actions">
+1 -1
View File
@@ -35,7 +35,7 @@
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<svelte:fragment slot="header">Delete account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
+1 -1
View File
@@ -43,7 +43,7 @@
</script>
<Modal {error} onSubmit={create} size="big" bind:show>
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
<svelte:fragment slot="header">Create new organization</svelte:fragment>
<FormList>
<InputText
id="organization-name"
@@ -45,7 +45,7 @@
</script>
<Modal {error} onSubmit={create} size="big" bind:show>
<svelte:fragment slot="header">Create Project</svelte:fragment>
<svelte:fragment slot="header">Create project</svelte:fragment>
<FormList>
<InputText id="name" label="Name" bind:value={name} required autofocus={true} />
{#if !showCustomId}
@@ -50,7 +50,7 @@
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">
{isUser ? 'Leave Organization' : 'Delete Member'}
{isUser ? 'Leave organization' : 'Delete member'}
</svelte:fragment>
<p data-private>
{isUser
@@ -42,7 +42,7 @@
icon="exclamation"
state="warning"
headerDivider={false}>
<svelte:fragment slot="header">Delete Organization</svelte:fragment>
<svelte:fragment slot="header">Delete organization</svelte:fragment>
<p>
Are you sure you want to delete <b>{$organization.name}</b>? All projects ({$organization.total})
and data associated with this organization will be deleted. This action is irreversible.
@@ -1,5 +1,5 @@
<script lang="ts">
import { CardGrid, Box, AvatarGroup, Heading } from '$lib/components';
import { CardGrid, BoxAvatar, AvatarGroup, Heading } from '$lib/components';
import { InputText, Form, Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
@@ -64,14 +64,14 @@
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Delete Organization</Heading>
<Heading tag="h6" size="7">Delete organization</Heading>
</div>
<p>
The organization will be permanently deleted, including all projects and data
associated with this organization. This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<BoxAvatar>
<svelte:fragment slot="image">
<AvatarGroup {avatars} total={$members.total} />
</svelte:fragment>
@@ -79,7 +79,7 @@
<h6 class="u-bold u-trim-1">{$organization.name}</h6>
</svelte:fragment>
<p>{$organization.total} members</p>
</Box>
</BoxAvatar>
</svelte:fragment>
<svelte:fragment slot="actions">
@@ -16,7 +16,7 @@
} from '$lib/commandCenter/searchers';
import { MigrationBox } from '$lib/components';
onMount(async () => {
onMount(() => {
return sdk.forConsole.client.subscribe(['project', 'console'], (response) => {
if (response.events.includes('stats.connections')) {
for (const [projectId, value] of Object.entries(response.payload)) {
@@ -42,7 +42,7 @@
</script>
<Modal {error} size="big" bind:show={showCreate} onSubmit={create}>
<svelte:fragment slot="header">Create Team</svelte:fragment>
<svelte:fragment slot="header">Create team</svelte:fragment>
<FormList>
<InputText
id="name"
@@ -59,7 +59,7 @@
</script>
<Modal {error} size="big" bind:show={showCreate} onSubmit={create}>
<svelte:fragment slot="header">Create User</svelte:fragment>
<svelte:fragment slot="header">Create user</svelte:fragment>
<FormList>
<InputText
id="name"

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