mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 1.4.x into design-reviews-1.4
This commit is contained in:
@@ -24,5 +24,8 @@ module.exports = {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
globalThis: false // false means it is not writeable
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+6552
-25
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -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
@@ -47,7 +47,7 @@
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div id="svelte">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
await goto(href);
|
||||
await waitUntil(() => {
|
||||
console.log('tickUntil', el);
|
||||
return el.classList.contains('is-selected');
|
||||
}, 1000);
|
||||
el.focus();
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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,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,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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
+8
-7
@@ -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">
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ProxyRulesPage, ProxyTypes } from './index.svelte';
|
||||
export { default as Retry } from './wizard/retry.svelte';
|
||||
+1
-1
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>();
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
@@ -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 });
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -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>();
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user