Merge branch '1.6.x' into feat-implement-webauthn
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.7",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -150,9 +150,10 @@
|
||||
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
|
||||
},
|
||||
"node_modules/@appwrite.io/console": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.2.tgz",
|
||||
"integrity": "sha512-3qYknuFwhvTN2GnPB8G1w5DgxfVorGtfj4uuEaXbTmMUmjA8fHaW5VkJriuUxCi6/TpxDxqV/hhEkdoXL+5H4w=="
|
||||
"name": "matej-appwrite-console-16x",
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmjs.org/matej-appwrite-console-16x/-/matej-appwrite-console-16x-0.6.7.tgz",
|
||||
"integrity": "sha512-orhonLVjV0vzJE/OrI49DRJ75EAkEa+8QzWy30h1N04jNyAcEGFfx4OKmhWg4uG5XRrmWrrDj9nobHKCiBaDCg=="
|
||||
},
|
||||
"node_modules/@appwrite.io/pink": {
|
||||
"version": "0.23.0",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.7",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -194,6 +194,7 @@ export enum Submit {
|
||||
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
|
||||
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
|
||||
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
|
||||
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
SessionDelete = 'submit_session_delete',
|
||||
@@ -226,13 +227,16 @@ export enum Submit {
|
||||
FunctionUpdateLogging = 'submit_function_update_logging',
|
||||
FunctionUpdateTimeout = 'submit_function_update_timeout',
|
||||
FunctionUpdateEvents = 'submit_function_update_events',
|
||||
FunctionUpdateScopes = 'submit_function_key_update_scopes',
|
||||
FunctionConnectRepo = 'submit_function_connect_repo',
|
||||
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
|
||||
FunctionRedeploy = 'submit_function_redeploy',
|
||||
DeploymentCreate = 'submit_deployment_create',
|
||||
DeploymentDelete = 'submit_deployment_delete',
|
||||
DeploymentUpdate = 'submit_deployment_update',
|
||||
DeploymentCancel = 'submit_deployment_cancel',
|
||||
ExecutionCreate = 'submit_execution_create',
|
||||
ExecutionDelete = 'submit_execution_delete',
|
||||
VariableCreate = 'submit_variable_create',
|
||||
VariableDelete = 'submit_variable_delete',
|
||||
VariableUpdate = 'submit_variable_update',
|
||||
|
||||
@@ -42,9 +42,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if couponData.credits >= 100}
|
||||
<p class="inline-tag" use:tooltip={{ content: formatCurrency(couponData.credits) }}>
|
||||
Credits applied
|
||||
</p>
|
||||
<p class="inline-tag">Credits applied</p>
|
||||
{:else}
|
||||
<span class="u-color-text-success">-{formatCurrency(couponData.credits)}</span>
|
||||
{/if}
|
||||
|
||||
@@ -32,9 +32,10 @@
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="card u-margin-block-start-32 u-flex u-flex-vertical u-gap-8"
|
||||
class="card u-flex u-flex-vertical u-gap-8"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
<slot />
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">{currentPlan.name} plan</p>
|
||||
<p class="text">{formatCurrency(currentPlan.price)}</p>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
}`}>
|
||||
{#if $$slots.list}
|
||||
<section
|
||||
class:u-max-width-none={noMaxWidthList}
|
||||
class:u-overflow-y-auto={scrollable}
|
||||
class:u-max-height-200={scrollable}
|
||||
class="drop-section">
|
||||
class="drop-section"
|
||||
style={noMaxWidthList ? 'max-inline-size: 100%' : ''}>
|
||||
<ul class="drop-list">
|
||||
<slot name="list" />
|
||||
</ul>
|
||||
|
||||
@@ -6,96 +6,25 @@
|
||||
InputText,
|
||||
InputTags,
|
||||
FormList,
|
||||
InputSelectCheckbox
|
||||
InputSelectCheckbox,
|
||||
InputDateTime
|
||||
} from '$lib/elements/forms';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { tags, type Operator, queries, type TagValue } from './store';
|
||||
import { tags, operators, addFilter, queries } from './store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { TagList } from '.';
|
||||
|
||||
// We cast to any to not cause type errors in the input components
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
export let value: any = null;
|
||||
export let columns: Writable<Column[]>;
|
||||
export let columnId: string | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
clear: void;
|
||||
apply: { applied: number };
|
||||
}>();
|
||||
export let arrayValues: string[] = [];
|
||||
export let operatorKey: string | null = null;
|
||||
export let singleCondition = false;
|
||||
|
||||
$: column = $columns.find((c) => c.id === columnId) as Column;
|
||||
let arrayValues: string[] = [];
|
||||
|
||||
dispatch('apply', { applied: $tags.length });
|
||||
|
||||
const operators: Record<string, Operator> = {
|
||||
'starts with': {
|
||||
toQuery: Query.startsWith,
|
||||
toTag: (attribute, input) => `**${attribute}** starts with **${input}**`,
|
||||
types: ['string']
|
||||
},
|
||||
'ends with': {
|
||||
toQuery: Query.endsWith,
|
||||
toTag: (attribute, input) => `**${attribute}** ends with **${input}**`,
|
||||
types: ['string']
|
||||
},
|
||||
'greater than': {
|
||||
toQuery: (attr, input) => Query.greaterThan(attr, Number(input)),
|
||||
toTag: (attribute, input) => `**${attribute}** greater than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'greater than or equal': {
|
||||
toQuery: (attr, input) => Query.greaterThanEqual(attr, Number(input)),
|
||||
toTag: (attribute, input) => `**${attribute}** greater than or equal to **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'less than': {
|
||||
toQuery: Query.lessThan,
|
||||
toTag: (attribute, input) => `**${attribute}** less than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'less than or equal': {
|
||||
toQuery: Query.lessThanEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** less than or equal to **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
equal: {
|
||||
toQuery: Query.equal,
|
||||
toTag: (attribute, input) => `**${attribute}** equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'not equal': {
|
||||
toQuery: Query.notEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** not equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'is not null': {
|
||||
toQuery: Query.isNotNull,
|
||||
toTag: (attribute) => `**${attribute}** is not null`,
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
|
||||
hideInput: true
|
||||
},
|
||||
'is null': {
|
||||
toQuery: Query.isNull,
|
||||
toTag: (attribute) => `**${attribute}** is null`,
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
|
||||
hideInput: true
|
||||
},
|
||||
contains: {
|
||||
toQuery: Query.contains,
|
||||
toTag: (attribute, input) => {
|
||||
if (Array.isArray(input) && input.length > 2) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** contains **${formatArray(input)}** `
|
||||
};
|
||||
} else {
|
||||
return `**${attribute}** contains **${input}**`;
|
||||
}
|
||||
},
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'enum']
|
||||
}
|
||||
};
|
||||
|
||||
$: operatorsForColumn = Object.entries(operators)
|
||||
.filter(([, v]) => v.types.includes(column?.type))
|
||||
@@ -104,65 +33,43 @@
|
||||
value: k
|
||||
}));
|
||||
|
||||
let operatorKey: string | null = null;
|
||||
$: operator = operatorKey ? operators[operatorKey] : null;
|
||||
$: {
|
||||
columnId;
|
||||
operatorKey = null;
|
||||
}
|
||||
|
||||
// We cast to any to not cause type errors in the input components
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
let value: any = null;
|
||||
$: isDisabled = !operator;
|
||||
|
||||
onMount(() => {
|
||||
value = column?.array ? [] : null;
|
||||
if (column?.type === 'datetime') {
|
||||
const today = new Date();
|
||||
value = today.toISOString();
|
||||
}
|
||||
});
|
||||
|
||||
// This Map is keyed by tags, and has a query as the value
|
||||
function addFilter() {
|
||||
if (!column || !operator) return;
|
||||
if (column.array) {
|
||||
queries.addFilter({ column, operator, value: arrayValues });
|
||||
columnId = null;
|
||||
arrayValues = [];
|
||||
} else {
|
||||
queries.addFilter({ column, operator, value: value ?? '' });
|
||||
columnId = null;
|
||||
value = null;
|
||||
function addFilterAndReset() {
|
||||
addFilter($columns, columnId, operatorKey, value, arrayValues);
|
||||
columnId = null;
|
||||
operatorKey = null;
|
||||
value = null;
|
||||
arrayValues = [];
|
||||
if (singleCondition) {
|
||||
queries.apply();
|
||||
}
|
||||
}
|
||||
|
||||
function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
function formatArray(array: string[]) {
|
||||
if (!array?.length) return;
|
||||
if (array.length > 2) {
|
||||
return `${array[0]} or ${array.length - 1} others`;
|
||||
} else {
|
||||
return array.join(' or ');
|
||||
}
|
||||
}
|
||||
|
||||
function isTypeTagValue(obj: any): obj is TagValue {
|
||||
if (typeof obj === 'string') return false;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.tag === 'string' &&
|
||||
(typeof obj.value === 'string' ||
|
||||
typeof obj.value === 'number' ||
|
||||
Array.isArray(obj.value))
|
||||
);
|
||||
}
|
||||
|
||||
$: isDisabled = !operator;
|
||||
const dispatch = createEventDispatcher<{
|
||||
clear: void;
|
||||
apply: { applied: number };
|
||||
}>();
|
||||
dispatch('apply', { applied: $tags.length });
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<form on:submit|preventDefault={addFilter}>
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<form on:submit|preventDefault={addFilterAndReset}>
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16 u-flex-vertical-mobile">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns
|
||||
@@ -188,10 +95,10 @@
|
||||
name="value"
|
||||
bind:tags={arrayValues}
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({
|
||||
label: value,
|
||||
value,
|
||||
checked: arrayValues.includes(value)
|
||||
options={column?.elements?.map((e) => ({
|
||||
label: e?.label ?? e,
|
||||
value: e?.value ?? e,
|
||||
checked: arrayValues.includes(e?.value ?? e)
|
||||
}))}>
|
||||
</InputSelectCheckbox>
|
||||
{:else}
|
||||
@@ -210,7 +117,10 @@
|
||||
id="value"
|
||||
bind:value
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({ label: value, value }))}
|
||||
options={column?.elements?.map((e) => ({
|
||||
label: e?.label ?? e,
|
||||
value: e?.value ?? e
|
||||
}))}
|
||||
label="Value"
|
||||
showLabel={false} />
|
||||
{:else if column.type === 'integer' || column.type === 'double'}
|
||||
@@ -225,48 +135,29 @@
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value />
|
||||
{:else if column.type === 'datetime'}
|
||||
{#key value}
|
||||
<InputDateTime id="value" bind:value label="value" showLabel={false} />
|
||||
{/key}
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
|
||||
<i class="icon-plus" />
|
||||
Add filter
|
||||
</Button>
|
||||
{#if !singleCondition}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" noMargin submit>
|
||||
<i class="icon-plus" />
|
||||
Add condition
|
||||
</Button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
|
||||
{#each $tags as tag (tag)}
|
||||
{#if isTypeTagValue(tag)}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString()
|
||||
}}
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{#if !singleCondition}
|
||||
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
|
||||
<TagList />
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -5,17 +5,30 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import Content from './content.svelte';
|
||||
import { queries, queriesAreDirty, queryParamToMap, tags } from './store';
|
||||
import {
|
||||
addFilter,
|
||||
queries,
|
||||
queriesAreDirty,
|
||||
queryParamToMap,
|
||||
tags,
|
||||
ValidOperators
|
||||
} from './store';
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let disabled = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let singleCondition = false;
|
||||
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
let value: any = null;
|
||||
let selectedColumn: string | null = null;
|
||||
let operatorKey: string | null = null;
|
||||
let arrayValues: string[] = [];
|
||||
|
||||
// We need to separate them so we don't trigger Drop's handlers
|
||||
let showFiltersDesktop = false;
|
||||
let showFiltersMobile = false;
|
||||
@@ -33,14 +46,105 @@
|
||||
queries.clearAll();
|
||||
}
|
||||
|
||||
function apply() {
|
||||
if (
|
||||
selectedColumn &&
|
||||
operatorKey &&
|
||||
(operatorKey === ValidOperators.IsNotNull ||
|
||||
operatorKey === ValidOperators.IsNull ||
|
||||
value ||
|
||||
arrayValues.length)
|
||||
) {
|
||||
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
|
||||
selectedColumn = null;
|
||||
value = null;
|
||||
operatorKey = null;
|
||||
arrayValues = [];
|
||||
}
|
||||
queries.apply();
|
||||
}
|
||||
|
||||
function afterApply(
|
||||
e: CustomEvent<{
|
||||
applied: number;
|
||||
}>
|
||||
) {
|
||||
applied = e.detail.applied;
|
||||
if (singleCondition) {
|
||||
showFiltersDesktop = false;
|
||||
showFiltersMobile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!showFiltersDesktop && !showFiltersMobile) {
|
||||
selectedColumn = null;
|
||||
value = null;
|
||||
operatorKey = null;
|
||||
arrayValues = [];
|
||||
}
|
||||
|
||||
$: isButtonDisabled = $queriesAreDirty
|
||||
? false
|
||||
: !selectedColumn ||
|
||||
!operatorKey ||
|
||||
(!value &&
|
||||
!arrayValues.length &&
|
||||
operatorKey !== ValidOperators.IsNotNull &&
|
||||
operatorKey !== ValidOperators.IsNull);
|
||||
|
||||
function toggleDropdown() {
|
||||
showFiltersDesktop = !showFiltersDesktop;
|
||||
}
|
||||
function toggleMobileModal() {
|
||||
showFiltersMobile = !showFiltersMobile;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="is-not-mobile">
|
||||
<Drop bind:show={showFiltersDesktop} noArrow>
|
||||
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)} {disabled}>
|
||||
<slot {disabled} toggle={toggleDropdown}>
|
||||
<Button secondary on:click={toggleDropdown} {disabled}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card">
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
<hr />
|
||||
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<slot name="mobile" {disabled} toggle={toggleMobileModal}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showFiltersMobile = !showFiltersMobile)}
|
||||
{fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
@@ -49,34 +153,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card">
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
{columns}
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
on:clear={() => (applied = 0)} />
|
||||
<hr />
|
||||
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</slot>
|
||||
|
||||
<Modal
|
||||
title="Filters"
|
||||
@@ -85,12 +162,21 @@
|
||||
size="big">
|
||||
<Content
|
||||
{columns}
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button
|
||||
></svelte:fragment>
|
||||
{#if singleCondition}
|
||||
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Filters } from './filters.svelte';
|
||||
export { default as TagList } from './tagList.svelte';
|
||||
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
|
||||
|
||||
@@ -4,6 +4,8 @@ import { derived, get, writable } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import deepEqual from 'deep-equal';
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
|
||||
export type TagValue = {
|
||||
tag: string;
|
||||
@@ -11,7 +13,11 @@ export type TagValue = {
|
||||
};
|
||||
|
||||
export type Operator = {
|
||||
toTag: (attribute: string, input?: string | number | string[]) => string | TagValue;
|
||||
toTag: (
|
||||
attribute: string,
|
||||
input?: string | number | string[],
|
||||
type?: string
|
||||
) => string | TagValue;
|
||||
toQuery: (attribute: string, input?: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
@@ -38,7 +44,10 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
|
||||
function addFilter({ column, operator, value }: AddFilterArgs) {
|
||||
queries.update((map) => {
|
||||
map.set(operator.toTag(column.id, value), operator.toQuery(column.id, value));
|
||||
map.set(
|
||||
operator.toTag(column.title, value, column?.type),
|
||||
operator.toQuery(column.id, value)
|
||||
);
|
||||
return map;
|
||||
});
|
||||
}
|
||||
@@ -57,7 +66,7 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
function apply() {
|
||||
const queryParam = mapToQueryParams(get(queries));
|
||||
const currentLocation = window.location.pathname;
|
||||
goto(`${currentLocation}?query=${queryParam}`);
|
||||
goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -84,3 +93,186 @@ export const hasPageQueries = derived(page, ($page) => {
|
||||
});
|
||||
|
||||
export const tags = derived(queries, ($queries) => Array.from($queries.keys()));
|
||||
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
export function addFilter(
|
||||
columns: Column[],
|
||||
columnId: string,
|
||||
operatorKey: string,
|
||||
value: any, // We cast to any to not cause type errors in the input components
|
||||
arrayValues: string[] = []
|
||||
) {
|
||||
const operator = operatorKey ? operators[operatorKey] : null;
|
||||
const column = columns.find((c) => c.id === columnId) as Column;
|
||||
if (!column || !operator) return;
|
||||
if (column.array) {
|
||||
queries.addFilter({ column, operator, value: arrayValues });
|
||||
} else {
|
||||
queries.addFilter({ column, operator, value: value ?? '' });
|
||||
}
|
||||
}
|
||||
|
||||
export enum ValidOperators {
|
||||
StartsWith = 'starts with',
|
||||
EndsWith = 'ends with',
|
||||
GreaterThan = 'greater than',
|
||||
GreaterThanOrEqual = 'greater than or equal',
|
||||
LessThan = 'less than',
|
||||
LessThanOrEqual = 'less than or equal',
|
||||
Equal = 'equal',
|
||||
NotEqual = 'not equal',
|
||||
IsNotNull = 'is not null',
|
||||
IsNull = 'is null',
|
||||
Contains = 'contains'
|
||||
}
|
||||
|
||||
export enum ValidTypes {
|
||||
String = 'string',
|
||||
Integer = 'integer',
|
||||
Double = 'double',
|
||||
Boolean = 'boolean',
|
||||
Datetime = 'datetime',
|
||||
Relationship = 'relationship',
|
||||
Enum = 'enum'
|
||||
}
|
||||
|
||||
const operatorsDefault = new Map<
|
||||
ValidOperators,
|
||||
{
|
||||
query: (attr: string, input: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
}
|
||||
>([
|
||||
[ValidOperators.StartsWith, { query: Query.startsWith, types: [ValidTypes.String] }],
|
||||
[ValidOperators.EndsWith, { query: Query.endsWith, types: [ValidTypes.String] }],
|
||||
[
|
||||
ValidOperators.GreaterThan,
|
||||
{
|
||||
query: Query.greaterThan,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.GreaterThanOrEqual,
|
||||
{
|
||||
query: Query.greaterThanEqual,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.LessThan,
|
||||
{
|
||||
query: Query.lessThan,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.LessThanOrEqual,
|
||||
{
|
||||
query: Query.lessThanEqual,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.Equal,
|
||||
{
|
||||
query: Query.equal,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Enum
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.NotEqual,
|
||||
{
|
||||
query: Query.notEqual,
|
||||
types: [ValidTypes.String, ValidTypes.Integer, ValidTypes.Double, ValidTypes.Boolean]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.IsNotNull,
|
||||
{
|
||||
query: Query.isNotNull,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Relationship
|
||||
],
|
||||
hideInput: true
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.IsNull,
|
||||
{
|
||||
query: Query.isNull,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Relationship
|
||||
],
|
||||
hideInput: true
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.Contains,
|
||||
{
|
||||
query: Query.contains,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Enum
|
||||
]
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
function formatArray(array: string[]) {
|
||||
if (!array?.length) return;
|
||||
if (array.length > 2) {
|
||||
return `${array[0]} or ${array.length - 1} others`;
|
||||
} else {
|
||||
return array.join(' or ');
|
||||
}
|
||||
}
|
||||
|
||||
function generateDefaultOperators() {
|
||||
const operators: Record<string, Operator> = {};
|
||||
operatorsDefault.forEach((operator, operatorName) => {
|
||||
operators[operatorName] = {
|
||||
toQuery: operator.query,
|
||||
toTag: (attribute, input = null, type = null) => {
|
||||
if (input === null) {
|
||||
return `**${attribute}** ${operatorName}`;
|
||||
} else if (Array.isArray(input) && input.length > 2) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
|
||||
};
|
||||
} else if (type === ValidTypes.Datetime) {
|
||||
return `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`;
|
||||
} else {
|
||||
return `**${attribute}** ${operatorName} **${input}**`;
|
||||
}
|
||||
},
|
||||
types: operator.types,
|
||||
hideInput: operator.hideInput
|
||||
};
|
||||
});
|
||||
return operators;
|
||||
}
|
||||
|
||||
export const operators = generateDefaultOperators();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
import { queries, tags, type TagValue } from './store';
|
||||
|
||||
function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
// We cast to any to not cause type errors in the input components
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
function isTypeTagValue(obj: any): obj is TagValue {
|
||||
if (typeof obj === 'string') return false;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.tag === 'string' &&
|
||||
(typeof obj.value === 'string' ||
|
||||
typeof obj.value === 'number' ||
|
||||
Array.isArray(obj.value))
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each $tags as tag (tag)}
|
||||
{#if isTypeTagValue(tag)}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString()
|
||||
}}
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -17,6 +17,7 @@
|
||||
export let allowNoColumns = false;
|
||||
export let showColsTextMobile = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let hideText = false;
|
||||
|
||||
let showSelectColumns = false;
|
||||
|
||||
@@ -84,7 +85,10 @@
|
||||
class="icon-view-boards u-opacity-50"
|
||||
aria-hidden="true"
|
||||
aria-label="columns" />
|
||||
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}">Columns</span>
|
||||
{#if !hideText}
|
||||
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}"
|
||||
>Columns</span>
|
||||
{/if}
|
||||
<span class="inline-tag">{selectedColumnsNumber}</span>
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class={resolvedClasses}
|
||||
style="pointer-events: {internalDisabled ? 'none' : 'auto'};"
|
||||
aria-label={ariaLabel}
|
||||
use:multiAction={actions}>
|
||||
<slot />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let tag: FormItemTag = 'li';
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
export let stackOnMobile = false;
|
||||
export let style: string = '';
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
@@ -16,6 +17,7 @@
|
||||
class="form-item {classes}"
|
||||
{style}
|
||||
class:is-multiple={isMultiple}
|
||||
class:u-width-full-line={fullWidth}>
|
||||
class:u-width-full-line={fullWidth}
|
||||
class:u-flex-vertical-mobile={stackOnMobile}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -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';
|
||||
|
||||
export let label: string;
|
||||
@@ -16,6 +16,8 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -53,9 +55,10 @@
|
||||
}
|
||||
|
||||
$: isNullable = nullable && !required;
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
@@ -93,4 +96,4 @@
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
</svelte:component>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let minlength: number = null;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
@@ -83,6 +84,7 @@
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
{minlength}
|
||||
{maxlength}
|
||||
{pattern}
|
||||
{readonly}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<div
|
||||
class:input-text-wrapper={!$$slots.default}
|
||||
class:u-flex={$$slots.default}
|
||||
class:u-gap-16={$$slots.default}
|
||||
class:u-gap-8={$$slots.default}
|
||||
class:u-cross-center={$$slots.default}>
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -14,6 +14,9 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
export let step: number | 'any' = 60;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -38,9 +41,10 @@
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
@@ -53,7 +57,7 @@
|
||||
{required}
|
||||
{min}
|
||||
{max}
|
||||
step="60"
|
||||
{step}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="time"
|
||||
class="input-text"
|
||||
@@ -68,4 +72,4 @@
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
</svelte:component>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll {classes}" class:u-margin-block-start-32={!noMargin} data-private>
|
||||
<div class="table-with-scroll {classes}" class:u-margin-block-start-16={!noMargin} data-private>
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<table
|
||||
class="table"
|
||||
|
||||
@@ -38,7 +38,7 @@ export type Column = {
|
||||
filter?: boolean;
|
||||
array?: boolean;
|
||||
format?: string;
|
||||
elements?: string[];
|
||||
elements?: string[] | { value: string; label: string }[];
|
||||
};
|
||||
|
||||
export function isValueOfStringEnum<T extends Record<string, string>>(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let disabled: boolean;
|
||||
export let buttonText: string;
|
||||
export let buttonMethod: () => void | Promise<void>;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let icon = 'plus';
|
||||
export let showIcon = true;
|
||||
@@ -31,7 +32,8 @@
|
||||
secondary={buttonType === 'secondary'}
|
||||
on:click={buttonMethod}
|
||||
event={buttonEvent}
|
||||
{disabled}>
|
||||
{disabled}
|
||||
href={buttonHref}>
|
||||
{#if showIcon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
export let buttonText: string = null;
|
||||
export let buttonMethod: () => void = null;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let buttonDisabled = false;
|
||||
|
||||
@@ -177,7 +178,8 @@
|
||||
disabled={isButtonDisabled}
|
||||
{buttonText}
|
||||
{buttonEvent}
|
||||
{buttonMethod} />
|
||||
{buttonMethod}
|
||||
{buttonHref} />
|
||||
{/if}
|
||||
</slot>
|
||||
</header>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { getServiceLimit, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
@@ -163,9 +162,7 @@
|
||||
</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"
|
||||
class:theme-dark={$app.themeInUse === 'light'}>
|
||||
<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>
|
||||
@@ -276,9 +273,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0"
|
||||
class:theme-dark={$app.themeInUse === 'light'}>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<Heading tag="h3" size="6">Response</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Heading } from '$lib/components';
|
||||
import { Avatar, AvatarInitials, Card, Heading } from '$lib/components';
|
||||
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
|
||||
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
|
||||
import LoginDark from '$lib/images/login/login-dark-mode.png';
|
||||
import LoginLight from '$lib/images/login/login-light-mode.png';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { campaigns } from '$lib/stores/campaigns';
|
||||
import { campaigns, type CampaignData } from '$lib/stores/campaigns';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
export const imgLight = LoginLight;
|
||||
@@ -15,8 +15,13 @@
|
||||
export let campaign: string = null;
|
||||
export let coupon: Coupon = null;
|
||||
|
||||
$: variation = coupon?.campaign ?? campaign ? 'card' : 'default';
|
||||
$: selectedCampaign = campaigns.get(coupon?.campaign ?? campaign);
|
||||
$: variation = (coupon?.campaign ?? campaign ? selectedCampaign?.template : 'default') as
|
||||
| 'default'
|
||||
| CampaignData['template'];
|
||||
|
||||
let currentReviewNumber = 0;
|
||||
$: currentReview = selectedCampaign?.data?.reviews?.[currentReviewNumber];
|
||||
|
||||
function generateTitle() {
|
||||
if (coupon?.credits) {
|
||||
@@ -37,17 +42,17 @@
|
||||
|
||||
<main class="grid-1-1 is-full-page" id="main">
|
||||
<section
|
||||
class={`u-flex u-flex-vertical ${variation === 'card' ? 'u-cross-center u-main-center u-height-100-percent u-width-full-line side-bg' : 'side-default'}`}
|
||||
style:--url={variation === 'card'
|
||||
class={`u-flex u-flex-vertical ${variation !== 'default' ? 'u-cross-center u-main-center u-height-100-percent u-width-full-line side-bg' : 'side-default'}`}
|
||||
style:--url={variation !== 'default'
|
||||
? ''
|
||||
: `url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
|
||||
{#if variation === 'card'}
|
||||
{#if variation !== 'default'}
|
||||
<div class="side-bg-container" />
|
||||
{/if}
|
||||
<div
|
||||
class="logo u-flex u-gap-16"
|
||||
class:is-not-mobile={variation === 'default'}
|
||||
class:logo-variation={variation === 'card'}>
|
||||
class:logo-variation={variation !== 'default'}>
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
@@ -72,15 +77,15 @@
|
||||
<p>Build like a team of hundreds<span class="underscore">_</span></p>
|
||||
</div>
|
||||
{:else if variation === 'card'}
|
||||
<div
|
||||
<section
|
||||
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line">
|
||||
<img
|
||||
src={`/images/campaigns/${coupon?.campaign ?? campaign}/${$app.themeInUse}.png`}
|
||||
class="u-block u-image-object-fit-cover side-bg-img"
|
||||
alt="promo" />
|
||||
|
||||
<div class="u-text-center">
|
||||
<div class="is-only-mobile u-width-full-line auth-container">
|
||||
<div class="u-text-center auth-container">
|
||||
<div class="is-only-mobile u-width-full-line">
|
||||
<Heading
|
||||
size="5"
|
||||
tag="h3"
|
||||
@@ -90,7 +95,7 @@
|
||||
{generateTitle()}
|
||||
</Heading>
|
||||
</div>
|
||||
<div class="is-not-mobile u-width-full-line auth-container">
|
||||
<div class="is-not-mobile u-width-full-line">
|
||||
<Heading
|
||||
size="4"
|
||||
tag="h3"
|
||||
@@ -100,11 +105,70 @@
|
||||
{generateTitle()}
|
||||
</Heading>
|
||||
</div>
|
||||
<p class="u-margin-block-start-16 auth-container">
|
||||
<p class="u-margin-block-start-16">
|
||||
{generateDesc()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{:else if variation === 'review'}
|
||||
<section
|
||||
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line review-container">
|
||||
<div class="u-text-center">
|
||||
<div class="is-only-mobile u-width-full-line">
|
||||
<Heading
|
||||
size="5"
|
||||
tag="h3"
|
||||
class="u-margin-block-start-48"
|
||||
trimmed={false}
|
||||
style="font-weight:normal">
|
||||
{generateTitle()}
|
||||
</Heading>
|
||||
</div>
|
||||
<div class="is-not-mobile u-width-full-line">
|
||||
<Heading
|
||||
size="3"
|
||||
tag="h3"
|
||||
class="u-margin-block-start-32"
|
||||
trimmed={false}
|
||||
style="font-weight:normal">
|
||||
{generateTitle()}
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="body-text-1 u-margin-block-start-16 u-text-center">{generateDesc()}</p>
|
||||
<div class="review-container u-margin-block-start-64">
|
||||
<Card style="--p-card-padding: 1.25rem">
|
||||
<p class="body-text-1">
|
||||
{currentReview.review}
|
||||
</p>
|
||||
<div class="u-margin-block-start-16 u-flex u-gap-16">
|
||||
{#if currentReview?.img}
|
||||
<Avatar
|
||||
src={`/images/campaigns/${coupon?.campaign ?? campaign}/reviewers/${currentReview.img}`}
|
||||
name={currentReview.name}
|
||||
size={40} />
|
||||
{:else}
|
||||
<AvatarInitials size={40} name={currentReview.name} />
|
||||
{/if}
|
||||
<div>
|
||||
<p class="body-text-2 u-bold">{currentReview.name}</p>
|
||||
<p class="body-text-2">{currentReview.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{#if selectedCampaign?.footer}
|
||||
<div
|
||||
class="u-flex u-gap-16 u-cross-center u-main-center review-footer-container">
|
||||
<p class="u-bold" style:text-transform="uppercase">provided to you by</p>
|
||||
<img
|
||||
style:max-block-size="2.5rem"
|
||||
src={`/images/campaigns/${coupon?.campaign ?? campaign}/footer/${$app.themeInUse}.png`}
|
||||
alt={coupon?.campaign ?? campaign} />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
<section class="grid-1-1-col-2 u-flex u-flex-vertical u-cross-center u-main-center">
|
||||
@@ -116,7 +180,7 @@
|
||||
</h1>
|
||||
<h1
|
||||
class="heading-level-5 u-margin-block-start-auto is-only-mobile"
|
||||
class:u-padding-block-start-24={variation === 'card'}>
|
||||
class:u-padding-block-start-24={variation !== 'default'}>
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<div class="u-margin-block-start-24">
|
||||
@@ -303,6 +367,9 @@
|
||||
.auth-container {
|
||||
max-inline-size: 27.5rem;
|
||||
}
|
||||
.review-container {
|
||||
max-inline-size: 30rem;
|
||||
}
|
||||
.logo-variation {
|
||||
padding-block-start: 2rem;
|
||||
|
||||
@@ -313,4 +380,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-footer-container {
|
||||
padding-block-start: 10rem;
|
||||
@media #{$break1} {
|
||||
padding-block-start: 5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
</div>
|
||||
<div class="wizard-secondary-content-sep"></div>
|
||||
<div class="wizard-secondary-content-2">
|
||||
<div class="wizard-secondary-content-sticky">
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,18 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let confirmExit = false;
|
||||
export let href = '';
|
||||
type $$Props =
|
||||
| {
|
||||
confirmExit: boolean;
|
||||
href?: string;
|
||||
}
|
||||
| {
|
||||
confirmExit?: boolean;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export let confirmExit: $$Props['confirmExit'] = false;
|
||||
export let href: $$Props['href'] = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import { AppwriteException, Runtime, type Client, type Payload } from '@appwrite.io/console';
|
||||
|
||||
export type Specs = {
|
||||
cpus: number[];
|
||||
memory: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Variable
|
||||
*/
|
||||
type Variable = {
|
||||
/**
|
||||
* Variable ID.
|
||||
*/
|
||||
$id: string;
|
||||
/**
|
||||
* Variable creation date in ISO 8601 format.
|
||||
*/
|
||||
$createdAt: string;
|
||||
/**
|
||||
* Variable creation date in ISO 8601 format.
|
||||
*/
|
||||
$updatedAt: string;
|
||||
/**
|
||||
* Variable key.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Variable value.
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* Service to which the variable belongs. Possible values are "project", "function"
|
||||
*/
|
||||
resourceType: string;
|
||||
/**
|
||||
* ID of resource to which the variable belongs. If resourceType is "project", it is empty. If resourceType is "function", it is ID of the function.
|
||||
*/
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export type Func = {
|
||||
/**
|
||||
* Function ID.
|
||||
*/
|
||||
$id: string;
|
||||
/**
|
||||
* Function creation date in ISO 8601 format.
|
||||
*/
|
||||
$createdAt: string;
|
||||
/**
|
||||
* Function update date in ISO 8601 format.
|
||||
*/
|
||||
$updatedAt: string;
|
||||
/**
|
||||
* Execution permissions.
|
||||
*/
|
||||
execute: string[];
|
||||
/**
|
||||
* Function name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Function enabled.
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Is the function deployed with the latest configuration? This is set to false if you've changed an environment variables, entrypoint, commands, or other settings that needs redeploy to be applied. When the value is false, redeploy the function to update it with the latest configuration.
|
||||
*/
|
||||
live: boolean;
|
||||
/**
|
||||
* Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.
|
||||
*/
|
||||
logging: boolean;
|
||||
/**
|
||||
* Function execution runtime.
|
||||
*/
|
||||
runtime: string;
|
||||
/**
|
||||
* Function's active deployment ID.
|
||||
*/
|
||||
deployment: string;
|
||||
/**
|
||||
* Function variables.
|
||||
*/
|
||||
vars: Variable[];
|
||||
/**
|
||||
* Function trigger events.
|
||||
*/
|
||||
events: string[];
|
||||
/**
|
||||
* Function execution schedult in CRON format.
|
||||
*/
|
||||
schedule: string;
|
||||
/**
|
||||
* Function execution timeout in seconds.
|
||||
*/
|
||||
timeout: number;
|
||||
/**
|
||||
* The entrypoint file used to execute the deployment.
|
||||
*/
|
||||
entrypoint: string;
|
||||
/**
|
||||
* The build command used to build the deployment.
|
||||
*/
|
||||
commands: string;
|
||||
/**
|
||||
* Version of Open Runtimes used for the function.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Function VCS (Version Control System) installation id.
|
||||
*/
|
||||
installationId: string;
|
||||
/**
|
||||
* VCS (Version Control System) Repository ID
|
||||
*/
|
||||
providerRepositoryId: string;
|
||||
/**
|
||||
* VCS (Version Control System) branch name
|
||||
*/
|
||||
providerBranch: string;
|
||||
/**
|
||||
* Path to function in VCS (Version Control System) repository
|
||||
*/
|
||||
providerRootDirectory: string;
|
||||
/**
|
||||
* Is VCS (Version Control System) connection is in silent mode? When in silence mode, no comments will be posted on the repository pull or merge requests
|
||||
*/
|
||||
providerSilentMode: boolean;
|
||||
/**
|
||||
* CPU limit
|
||||
*/
|
||||
cpus: number;
|
||||
/**
|
||||
* Memory limit
|
||||
*/
|
||||
memory: number;
|
||||
};
|
||||
|
||||
export class SpecsFunctions {
|
||||
client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async getSpecs(): Promise<Specs> {
|
||||
const path = '/functions/specs';
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call('GET', uri, {
|
||||
'content-type': 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update function
|
||||
*
|
||||
* Update function by its unique ID.
|
||||
*
|
||||
* @param {string} functionId
|
||||
* @param {string} name
|
||||
* @param {Runtime} runtime
|
||||
* @param {string[]} execute
|
||||
* @param {string[]} events
|
||||
* @param {string} schedule
|
||||
* @param {number} timeout
|
||||
* @param {boolean} enabled
|
||||
* @param {boolean} logging
|
||||
* @param {string} entrypoint
|
||||
* @param {string} commands
|
||||
* @param {string} installationId
|
||||
* @param {string} providerRepositoryId
|
||||
* @param {string} providerBranch
|
||||
* @param {boolean} providerSilentMode
|
||||
* @param {string} providerRootDirectory
|
||||
* @param {number} cpus
|
||||
* @param {number} memory
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async update(
|
||||
functionId: string,
|
||||
name: string,
|
||||
runtime?: Runtime,
|
||||
execute?: string[],
|
||||
events?: string[],
|
||||
schedule?: string,
|
||||
timeout?: number,
|
||||
enabled?: boolean,
|
||||
logging?: boolean,
|
||||
entrypoint?: string,
|
||||
commands?: string,
|
||||
installationId?: string,
|
||||
providerRepositoryId?: string,
|
||||
providerBranch?: string,
|
||||
providerSilentMode?: boolean,
|
||||
providerRootDirectory?: string,
|
||||
cpus?: number,
|
||||
memory?: number
|
||||
): Promise<Func> {
|
||||
if (typeof functionId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "functionId"');
|
||||
}
|
||||
|
||||
if (typeof name === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "name"');
|
||||
}
|
||||
|
||||
const apiPath = '/functions/{functionId}'.replace('{functionId}', functionId);
|
||||
const payload: Payload = {};
|
||||
|
||||
if (typeof name !== 'undefined') {
|
||||
payload['name'] = name;
|
||||
}
|
||||
|
||||
if (typeof runtime !== 'undefined') {
|
||||
payload['runtime'] = runtime;
|
||||
}
|
||||
|
||||
if (typeof execute !== 'undefined') {
|
||||
payload['execute'] = execute;
|
||||
}
|
||||
|
||||
if (typeof events !== 'undefined') {
|
||||
payload['events'] = events;
|
||||
}
|
||||
|
||||
if (typeof schedule !== 'undefined') {
|
||||
payload['schedule'] = schedule;
|
||||
}
|
||||
|
||||
if (typeof timeout !== 'undefined') {
|
||||
payload['timeout'] = timeout;
|
||||
}
|
||||
|
||||
if (typeof enabled !== 'undefined') {
|
||||
payload['enabled'] = enabled;
|
||||
}
|
||||
|
||||
if (typeof logging !== 'undefined') {
|
||||
payload['logging'] = logging;
|
||||
}
|
||||
|
||||
if (typeof entrypoint !== 'undefined') {
|
||||
payload['entrypoint'] = entrypoint;
|
||||
}
|
||||
|
||||
if (typeof commands !== 'undefined') {
|
||||
payload['commands'] = commands;
|
||||
}
|
||||
|
||||
if (typeof installationId !== 'undefined') {
|
||||
payload['installationId'] = installationId;
|
||||
}
|
||||
|
||||
if (typeof providerRepositoryId !== 'undefined') {
|
||||
payload['providerRepositoryId'] = providerRepositoryId;
|
||||
}
|
||||
|
||||
if (typeof providerBranch !== 'undefined') {
|
||||
payload['providerBranch'] = providerBranch;
|
||||
}
|
||||
|
||||
if (typeof providerSilentMode !== 'undefined') {
|
||||
payload['providerSilentMode'] = providerSilentMode;
|
||||
}
|
||||
|
||||
if (typeof providerRootDirectory !== 'undefined') {
|
||||
payload['providerRootDirectory'] = providerRootDirectory;
|
||||
}
|
||||
|
||||
if (typeof cpus !== 'undefined') {
|
||||
payload['cpus'] = cpus;
|
||||
}
|
||||
|
||||
if (typeof memory !== 'undefined') {
|
||||
payload['memory'] = memory;
|
||||
}
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + apiPath);
|
||||
return await this.client.call(
|
||||
'put',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,119 @@
|
||||
//campaign welcome and startup
|
||||
|
||||
type CampaignData = {
|
||||
export type CampaignData = {
|
||||
title: string;
|
||||
description: string;
|
||||
template: 'card' | 'review';
|
||||
data?: Record<string, unknown>;
|
||||
onlyNewOrgs?: boolean;
|
||||
footer?: boolean;
|
||||
};
|
||||
|
||||
export const campaigns: Map<string, CampaignData> = new Map();
|
||||
|
||||
campaigns
|
||||
.set('welcome', {
|
||||
template: 'card',
|
||||
title: "You've received $VALUE in credits",
|
||||
description:
|
||||
'Get $VALUE in credits when you upgrade or create an organization with a Pro plan.'
|
||||
})
|
||||
.set('startup', {
|
||||
template: 'card',
|
||||
title: 'Welcome to the Startups program!',
|
||||
description:
|
||||
"We're excited to have you on board. Add the coupon code to your Appwrite Pro account to join."
|
||||
})
|
||||
.set('RenderATL2024', {
|
||||
template: 'card',
|
||||
title: 'Claim your $100 RenderATL credits',
|
||||
description:
|
||||
'Get $100 in Cloud credits when you upgrade or create an organization with a Pro plan.'
|
||||
})
|
||||
.set('dealsfordevs', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Deals For Devs credits',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan.'
|
||||
})
|
||||
.set('WelcomeManual', {
|
||||
template: 'card',
|
||||
title: 'Here is your $VALUE welcome credit!',
|
||||
description:
|
||||
'Upgrade to Appwrite Pro and add the credits to enjoy the full capabilities of Cloud.'
|
||||
})
|
||||
.set('InactiveAccounts', {
|
||||
template: 'card',
|
||||
title: 'Get everything out of Cloud. Claim your $VALUE credits.',
|
||||
description:
|
||||
'Get $VALUE in Cloud credits when you upgrade or create an organization with a Pro plan.'
|
||||
})
|
||||
.set('StartupsProgram', {
|
||||
template: 'review',
|
||||
title: 'Welcome to the Startups program',
|
||||
description:
|
||||
"We're excited to have you on board. Add your credit code to your Appwrite Pro account to join.",
|
||||
onlyNewOrgs: true,
|
||||
data: {
|
||||
cta: 'Get everything out of Cloud with Pro',
|
||||
claimed: 'Your credits will be valid for 12 months.',
|
||||
unclaimed: 'Apply your code to join the Startups program.',
|
||||
reviews: [
|
||||
{
|
||||
name: 'David Foster',
|
||||
img: '1.jpeg',
|
||||
desc: 'Managing director',
|
||||
review: 'We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. It was saving us a lot of money in comparison to Firebase since the amount of users grew quite fast and we needed a quick switch.'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.set('JSMastery', {
|
||||
template: 'review',
|
||||
title: 'Claim your JavaScript Mastery $50 credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan',
|
||||
data: {
|
||||
reviews: [
|
||||
{
|
||||
name: 'Marius Bloki',
|
||||
desc: 'CTO // mySHOEFITTER',
|
||||
img: 'js_mastery_reviewer_1.png',
|
||||
review: 'Appwrite has been a tremendous asset in implementing our IT infrastructure. Not only is the software an absolute game-changer, but the team is always there when you need them.'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.set('FusionVC', {
|
||||
template: 'review',
|
||||
title: 'Welcome to Appwrite!',
|
||||
description:
|
||||
'Upgrade to Appwrite Pro and add the credits to enjoy the full capabilities of Cloud. Provided to you by Fusion VC.',
|
||||
footer: true,
|
||||
data: {
|
||||
reviews: [
|
||||
{
|
||||
name: 'David Foster',
|
||||
img: '1.jpeg',
|
||||
desc: 'Managing director',
|
||||
review: 'We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. It was saving us a lot of money in comparison to Firebase since the amount of users grew quite fast and we needed a quick switch.'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.set('carbon', {
|
||||
template: 'review',
|
||||
title: 'Build your entire backend within minutes.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan.',
|
||||
data: {
|
||||
reviews: [
|
||||
{
|
||||
name: 'David Foster',
|
||||
img: '1.jpeg',
|
||||
desc: 'Managing director',
|
||||
review: 'We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. It was saving us a lot of money in comparison to Firebase since the amount of users grew quite fast and we needed a quick switch.'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { Billing } from '../sdk/billing';
|
||||
import { Sources } from '$lib/sdk/sources';
|
||||
import { Account as WebauthnAccount } from '$lib/sdk/account';
|
||||
import { SpecsFunctions } from '$lib/sdk/functionsSpec';
|
||||
|
||||
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
|
||||
|
||||
@@ -40,6 +41,7 @@ const sdkForProject = {
|
||||
avatars: new Avatars(clientProject),
|
||||
databases: new Databases(clientProject),
|
||||
functions: new Functions(clientProject),
|
||||
specsFunctions: new SpecsFunctions(clientProject),
|
||||
health: new Health(clientProject),
|
||||
locale: new Locale(clientProject),
|
||||
messaging: new Messaging(clientProject),
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$installation.$id,
|
||||
$repository.id,
|
||||
$choices.branch,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
undefined,
|
||||
$createFunction.entrypoint,
|
||||
$createFunction.commands || undefined,
|
||||
undefined,
|
||||
$installation.$id,
|
||||
$repository.id,
|
||||
$choices.branch,
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
undefined,
|
||||
runtimeDetail.entrypoint,
|
||||
runtimeDetail.commands || undefined,
|
||||
undefined,
|
||||
$installation.$id,
|
||||
$repository.id,
|
||||
$choices.branch,
|
||||
|
||||
@@ -67,10 +67,11 @@
|
||||
let name: string;
|
||||
let coupon: string;
|
||||
let couponData = data?.couponData;
|
||||
let campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
|
||||
|
||||
onMount(async () => {
|
||||
await loadPaymentMethods();
|
||||
if (!$organizationList?.total) {
|
||||
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
|
||||
selectedOrgId = newOrgId;
|
||||
}
|
||||
});
|
||||
@@ -183,7 +184,6 @@
|
||||
$: isButtonDisabled =
|
||||
checkButtonDisabled(selectedOrgId, name, paymentMethodId) ||
|
||||
couponData?.status !== 'active';
|
||||
$: campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
|
||||
|
||||
function checkButtonDisabled(id: string | null, name: string | null, method: string | null) {
|
||||
if (id === newOrgId) {
|
||||
@@ -207,7 +207,7 @@
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<FormList>
|
||||
{#if $organizationList?.total}
|
||||
{#if $organizationList?.total && !campaign?.onlyNewOrgs}
|
||||
<InputSelect
|
||||
bind:value={selectedOrgId}
|
||||
label="Select organization"
|
||||
@@ -254,24 +254,26 @@
|
||||
</FormList>
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
<div
|
||||
class="box card-container u-position-relative"
|
||||
style:--box-border-radius="var(--border-radius-small)">
|
||||
<div class="card-bg"></div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
|
||||
<img
|
||||
src={`/images/campaigns/${data?.couponData?.campaign ?? data?.campaign}/${$app.themeInUse}.png`}
|
||||
class="u-block u-image-object-fit-cover card-img"
|
||||
alt="promo" />
|
||||
<p class="text">
|
||||
{#if couponData?.credits}
|
||||
{campaign.title.replace('VALUE', couponData.credits.toString())}
|
||||
{:else}
|
||||
{campaign.title}
|
||||
{/if}
|
||||
</p>
|
||||
{#if campaign?.template === 'card'}
|
||||
<div
|
||||
class="box card-container u-position-relative"
|
||||
style:--box-border-radius="var(--border-radius-small)">
|
||||
<div class="card-bg"></div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
|
||||
<img
|
||||
src={`/images/campaigns/${data?.couponData?.campaign ?? data?.campaign}/${$app.themeInUse}.png`}
|
||||
class="u-block u-image-object-fit-cover card-img"
|
||||
alt="promo" />
|
||||
<p class="text">
|
||||
{#if couponData?.credits}
|
||||
{campaign.title.replace('VALUE', couponData.credits.toString())}
|
||||
{:else}
|
||||
{campaign.title}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedOrg?.billingPlan === BillingPlan.PRO}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
@@ -288,12 +290,27 @@
|
||||
{/if}
|
||||
</section>
|
||||
{:else if selectedOrgId}
|
||||
<EstimatedTotalBox
|
||||
fixedCoupon={!!data?.couponData?.code}
|
||||
billingPlan={BillingPlan.PRO}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget />
|
||||
<div class:u-margin-block-start={campaign?.template === 'card'}>
|
||||
<EstimatedTotalBox
|
||||
fixedCoupon={!!data?.couponData?.code}
|
||||
billingPlan={BillingPlan.PRO}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget>
|
||||
{#if campaign?.template === 'review' && (campaign?.data?.cta || campaign?.data?.claimed || campaign?.data?.unclaimed)}
|
||||
<div class="u-margin-block-end-24">
|
||||
<p class="body-text-1 u-bold">{campaign?.data?.cta}</p>
|
||||
<p class="text u-margin-block-start-8">
|
||||
{#if couponData?.code && couponData?.status === 'active' && campaign?.data?.claimed}
|
||||
{campaign?.data?.claimed}
|
||||
{:else if campaign?.data?.unclaimed}
|
||||
{campaign?.data?.unclaimed}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</EstimatedTotalBox>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import UpdateMockNumbers from './updateMockNumbers.svelte';
|
||||
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
|
||||
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
|
||||
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
<Container>
|
||||
<UpdateUsersLimit />
|
||||
<UpdateMockNumbers />
|
||||
<UpdateSessionLength />
|
||||
<UpdateSessionsLimit />
|
||||
<UpdatePasswordHistory />
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { InputPhone, InputText } from '$lib/elements/forms';
|
||||
import { Button, Form, FormItem, FormItemPart } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Empty from '$lib/components/empty.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let numbers: Models.MockNumber[] = $project?.authMockNumbers ?? [];
|
||||
let initialNumbers = [];
|
||||
let projectId: string = $project.$id;
|
||||
|
||||
$: initialNumbers = $project?.authMockNumbers?.map((num) => ({ ...num })) ?? [];
|
||||
$: submitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
|
||||
|
||||
async function updateMockNumbers() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateMockNumbers(projectId, numbers);
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated mock phone numbers successfully'
|
||||
});
|
||||
trackEvent(Submit.AuthMockNumbersUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthMockNumbersUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
function addPhoneNumber(number: Models.MockNumber) {
|
||||
numbers.push({
|
||||
phone: number.phone,
|
||||
otp: number.otp
|
||||
});
|
||||
numbers = numbers;
|
||||
}
|
||||
|
||||
function deletePhoneNumber(index: number) {
|
||||
numbers.splice(index, 1);
|
||||
numbers = numbers;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateMockNumbers}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Mock Phone Numbers</Heading>
|
||||
<p>
|
||||
Generate <b>fictional</b> numbers to simulate phone verification while testing demo accounts.
|
||||
A maximum of 10 phone numbers can be generated.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if numbers?.length > 0}
|
||||
<ul class="form-list">
|
||||
{#each numbers as number, index}
|
||||
<FormItem isMultiple>
|
||||
<InputPhone
|
||||
id={`key-${index}`}
|
||||
bind:value={number.phone}
|
||||
fullWidth
|
||||
placeholder="Enter Phone Number"
|
||||
label="Phone Number"
|
||||
showLabel={index === 0 ? true : false}
|
||||
minlength={8}
|
||||
maxlength={16}
|
||||
required />
|
||||
<InputText
|
||||
id={`value-${index}`}
|
||||
bind:value={number.otp}
|
||||
fullWidth
|
||||
placeholder="Enter value"
|
||||
label="Verification Code"
|
||||
showLabel={index === 0 ? true : false}
|
||||
maxlength={6}
|
||||
required />
|
||||
<FormItemPart alignEnd>
|
||||
<Button
|
||||
text
|
||||
disabled={numbers.length === 0}
|
||||
on:click={() => {
|
||||
deletePhoneNumber(index);
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</FormItemPart>
|
||||
</FormItem>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if numbers?.length < 10}
|
||||
<Button
|
||||
noMargin
|
||||
text
|
||||
on:click={() =>
|
||||
addPhoneNumber({
|
||||
phone: '',
|
||||
otp: ''
|
||||
})}
|
||||
disabled={numbers.length >= 10}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add number</span>
|
||||
</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty
|
||||
on:click={() => {
|
||||
addPhoneNumber({
|
||||
phone: '',
|
||||
otp: ''
|
||||
});
|
||||
}}>Create a mock phone number</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={submitDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -8,15 +8,17 @@ import type { LayoutLoad } from './$types';
|
||||
export const load: LayoutLoad = async ({ depends }) => {
|
||||
depends(Dependencies.FUNCTION_INSTALLATIONS);
|
||||
|
||||
const [runtimesList, installations] = await Promise.all([
|
||||
const [runtimesList, installations, specs] = await Promise.all([
|
||||
sdk.forProject.functions.listRuntimes(),
|
||||
sdk.forProject.vcs.listInstallations([Query.limit(100)])
|
||||
sdk.forProject.vcs.listInstallations([Query.limit(100)]),
|
||||
sdk.forProject.specsFunctions.getSpecs()
|
||||
]);
|
||||
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
runtimesList,
|
||||
installations
|
||||
installations,
|
||||
specs
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
import { page } from '$app/stores';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
import { CardContainer, Empty, GridItem1, Id, PaginationWithLimit } from '$lib/components';
|
||||
import {
|
||||
CardContainer,
|
||||
Empty,
|
||||
GridItem1,
|
||||
Id,
|
||||
PaginationWithLimit,
|
||||
SvgIcon
|
||||
} from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
import { marketplace } from '$lib/stores/marketplace.js';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
@@ -90,11 +96,7 @@
|
||||
<svelte:fragment slot="title">
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
<div class="avatar is-medium">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${
|
||||
func.runtime.split('-')[0]
|
||||
}.svg`}
|
||||
alt="technology" />
|
||||
<SvgIcon name={func.runtime.split('-')[0]}></SvgIcon>
|
||||
</div>
|
||||
<span class="text">{func.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,72 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import {
|
||||
CardGrid,
|
||||
DropList,
|
||||
DropListItem,
|
||||
DropListLink,
|
||||
Empty,
|
||||
Heading,
|
||||
Id,
|
||||
PaginationWithLimit,
|
||||
Alert
|
||||
Alert,
|
||||
ViewSelector,
|
||||
EmptySearch
|
||||
} from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { deploymentList, execute, func, proxyRuleList, showFunctionExecute } from './store';
|
||||
import { deploymentList, func } from './store';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { calculateSize, humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import Delete from './delete.svelte';
|
||||
import Create from './create.svelte';
|
||||
import Activate from './activate.svelte';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import { Pill } from '$lib/elements';
|
||||
import RedeployModal from './redeployModal.svelte';
|
||||
import DeploymentSource from './deploymentSource.svelte';
|
||||
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
|
||||
import DeploymentDomains from './deploymentDomains.svelte';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { project } from '../../store';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import Table from './table.svelte';
|
||||
import { Filters, TagList } from '$lib/components/filters';
|
||||
import { queries, tags } from '$lib/components/filters/store';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import DeploymentCard from './deploymentCard.svelte';
|
||||
import RedeployModal from './redeployModal.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
let showActivate = false;
|
||||
let showRedeploy = false;
|
||||
let showAlert = true;
|
||||
|
||||
let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
function handleActivate() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Deployment ID', type: 'string', show: true, width: 150 },
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status',
|
||||
type: 'enum',
|
||||
show: true,
|
||||
width: 110,
|
||||
array: true,
|
||||
format: 'enum',
|
||||
elements: ['ready', 'processing', 'building', 'waiting', 'cancelled', 'failed']
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
title: 'Source',
|
||||
type: 'string',
|
||||
show: true,
|
||||
width: 90,
|
||||
array: true,
|
||||
format: 'enum',
|
||||
elements: [
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
{ value: 'cli', label: 'CLI' },
|
||||
{ value: 'vcs', label: 'Git' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '$updatedAt',
|
||||
title: 'Updated',
|
||||
type: 'datetime',
|
||||
show: true,
|
||||
width: 150,
|
||||
format: 'datetime'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'buildTime',
|
||||
title: 'Build time',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Size',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
width: 80
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<ContainerHeader title="Deployments">
|
||||
<Create main />
|
||||
</ContainerHeader>
|
||||
{#if $deploymentList?.total}
|
||||
{#if !data?.deploymentList?.total && !data?.query}
|
||||
<Empty single target="deployment">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first deployment to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Learn more about deployments in our Documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/functions/deployment"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create deployment`}>Documentation</Button>
|
||||
<Create secondary />
|
||||
</div>
|
||||
</Empty>
|
||||
{:else}
|
||||
{@const activeDeployment = data.activeDeployment}
|
||||
|
||||
<div class="common-section">
|
||||
<Heading tag="h3" size="7">Active</Heading>
|
||||
</div>
|
||||
{#if activeDeployment && !$func.live && showAlert}
|
||||
{#if data?.activeDeployment && !$func.live && showAlert}
|
||||
<Alert
|
||||
type="warning"
|
||||
class="u-margin-block-start-8"
|
||||
@@ -78,60 +125,7 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if activeDeployment}
|
||||
<CardGrid>
|
||||
<div class="u-flex u-cross-start u-gap-16">
|
||||
<div class="avatar is-medium" aria-hidden="true">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${
|
||||
$func.runtime.split('-')[0]
|
||||
}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
|
||||
<p><b>Deployment ID</b></p>
|
||||
|
||||
<Id value={activeDeployment.$id}>
|
||||
{activeDeployment.$id}
|
||||
</Id>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const status = activeDeployment.status}
|
||||
{@const fileSize = humanFileSize(activeDeployment.size)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
|
||||
<p><b>Build time:</b> {calculateTime(activeDeployment.buildTime)}</p>
|
||||
<p>
|
||||
<b>Updated:</b>
|
||||
<DeploymentCreatedBy deployment={activeDeployment} />
|
||||
</p>
|
||||
<p><b>Size:</b> {fileSize.value + fileSize.unit}</p>
|
||||
<p class="u-flex u-gap-4 u-cross-center">
|
||||
<b>Source:</b>
|
||||
<DeploymentSource deployment={activeDeployment} />
|
||||
</p>
|
||||
{#if $proxyRuleList?.rules?.length}
|
||||
<p class="u-flex u-gap-4 u-cross-center">
|
||||
<b>Domains:</b>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<Pill
|
||||
danger={status === 'failed'}
|
||||
warning={status === 'building'}
|
||||
success={status === 'ready'}>
|
||||
<span class="text u-trim">
|
||||
{activeDeployment.status === 'ready'
|
||||
? 'active'
|
||||
: activeDeployment.status}
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<DeploymentCard deployment={activeDeployment}>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="u-flex u-flex-wrap">
|
||||
<Button
|
||||
@@ -148,20 +142,35 @@
|
||||
}}>
|
||||
Redeploy
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
$execute = $func;
|
||||
$showFunctionExecute = true;
|
||||
}}
|
||||
href={`${base}/console/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}
|
||||
disabled={isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE}>
|
||||
Execute now
|
||||
Execute
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{:else if $deploymentList.total}
|
||||
</DeploymentCard>
|
||||
{:else if !$deploymentList.total && !data?.query}
|
||||
<Empty single target="deployment">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2"
|
||||
>Create your first deployment to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Learn more about deployments in our Documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/functions/deployment"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create deployment`}>Documentation</Button>
|
||||
<Create secondary />
|
||||
</div>
|
||||
</Empty>
|
||||
{:else}
|
||||
<Empty noMedia single>
|
||||
<Create secondary round>
|
||||
<i class="icon-plus" />
|
||||
@@ -180,174 +189,81 @@
|
||||
</p>
|
||||
</div>
|
||||
</Empty>
|
||||
{:else}
|
||||
<Empty single target="deployment">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">
|
||||
Create your first deployment to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Learn more about deployments in our Documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/functions/deployment"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<Create secondary />
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
<div class="common-section">
|
||||
<Heading tag="h3" size="7">All</Heading>
|
||||
<div class="u-margin-block-start-48">
|
||||
<Heading tag="h3" size="7">All deployments</Heading>
|
||||
<div class="u-flex u-main-space-between is-not-mobile u-margin-block-start-16">
|
||||
<div class="u-flex u-gap-8 u-cross-center u-flex-wrap">
|
||||
<TagList />
|
||||
|
||||
<Filters query={data.query} {columns} let:disabled let:toggle singleCondition>
|
||||
<div class="u-flex u-gap-4">
|
||||
<Button
|
||||
text
|
||||
on:click={toggle}
|
||||
{disabled}
|
||||
ariaLabel="open filter"
|
||||
noMargin={!$tags?.length}>
|
||||
<span class="icon-filter-line" />
|
||||
{#if !$tags?.length}
|
||||
<span class="text">Filters</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if $tags?.length}
|
||||
<div
|
||||
style="flex-basis:1px; background-color:hsl(var(--color-border)); width: 1px">
|
||||
</div>
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Filters>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns hideText />
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-main-space-between u-margin-block-start-16 is-only-mobile">
|
||||
<Filters query={data.query} {columns}>
|
||||
<svelte:fragment slot="mobile" let:disabled let:toggle>
|
||||
<Button text on:click={toggle} {disabled} ariaLabel="open filter" noMargin>
|
||||
<span class="icon-filter-line" />
|
||||
<span class="text">Filters</span>
|
||||
{#if $tags?.length}
|
||||
<span class="inline-tag">{$tags?.length}</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Filters>
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
</div>
|
||||
</div>
|
||||
{#if $deploymentList.total}
|
||||
<div class="u-margin-block-start-24">
|
||||
<TableScroll noMargin>
|
||||
<TableHeader>
|
||||
<TableCellHead width={150}>Deployment ID</TableCellHead>
|
||||
<TableCellHead width={100}>Status</TableCellHead>
|
||||
<TableCellHead width={80}>Source</TableCellHead>
|
||||
<TableCellHead width={180}>Updated</TableCellHead>
|
||||
<TableCellHead width={80}>Build Time</TableCellHead>
|
||||
<TableCellHead width={80}>Size</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $deploymentList.deployments as deployment, index (deployment.$id)}
|
||||
{@const status = deployment.status}
|
||||
<TableRow>
|
||||
<TableCell width={150} title="Deployment ID">
|
||||
<Id value={deployment.$id}>{deployment.$id}</Id>
|
||||
</TableCell>
|
||||
<TableCell title="Status">
|
||||
{#if activeDeployment?.$id === deployment?.$id}
|
||||
<Pill success>active</Pill>
|
||||
{:else}
|
||||
<Pill
|
||||
danger={status === 'failed'}
|
||||
warning={status === 'building'}
|
||||
info={status === 'ready'}>
|
||||
{status}
|
||||
</Pill>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellText title="Source">
|
||||
<DeploymentSource {deployment} /></TableCellText>
|
||||
<TableCellText width={180} title="Updated">
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="Build Time">
|
||||
{#if ['processing', 'building'].includes(deployment.status)}
|
||||
<span use:timer={{ start: deployment.$createdAt }} />
|
||||
{:else}
|
||||
{calculateTime(deployment.buildTime)}
|
||||
{/if}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="Size">
|
||||
{calculateSize(deployment.size)}
|
||||
</TableCellText>
|
||||
|
||||
<TableCell width={40} showOverflow>
|
||||
<DropList
|
||||
bind:show={showDropdown[index]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="More options"
|
||||
on:click|preventDefault={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showRedeploy = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Redeploy
|
||||
</DropListItem>
|
||||
{#if deployment.status === 'ready' && deployment.$id !== $func.deployment}
|
||||
<DropListItem
|
||||
icon="lightning-bolt"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showActivate = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Activate
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListLink
|
||||
icon="terminal"
|
||||
href={`/console/project-${$page.params.project}/functions/function-${$page.params.function}/deployment-${deployment.$id}`}>
|
||||
Logs
|
||||
</DropListLink>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showDropdown = [];
|
||||
showDelete = true;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty single target="deployment" on:click>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">
|
||||
You have no deployments. Create one to see it here.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Learn more about deployments in our Documentation.
|
||||
</p>
|
||||
<Table columns={$columns} {data} />
|
||||
{:else if data?.query}
|
||||
<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 any deployments</b>
|
||||
<p>There are no deployments that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>Clear filters</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/functions/deployment"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<Create />
|
||||
</div>
|
||||
</Empty>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty single target="deployment">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first deployment to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Learn more about deployments in our Documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/functions/deployment"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<Create secondary />
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
<PaginationWithLimit
|
||||
@@ -358,7 +274,5 @@
|
||||
</Container>
|
||||
|
||||
{#if selectedDeployment}
|
||||
<Delete {selectedDeployment} bind:showDelete />
|
||||
<Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} />
|
||||
<RedeployModal {selectedDeployment} bind:show={showRedeploy} />
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { getLimit, getPage, getQuery, pageToOffset } from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
|
||||
const data = await parent();
|
||||
@@ -10,10 +11,14 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
const query = getQuery(url);
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
query,
|
||||
activeDeployment: data.function.deployment
|
||||
? await sdk.forProject.functions.getDeployment(
|
||||
params.function,
|
||||
@@ -23,7 +28,8 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
deploymentList: await sdk.forProject.functions.listDeployments(params.function, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc('')
|
||||
Query.orderDesc(''),
|
||||
...parsedQueries.values()
|
||||
])
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let showCancel = false;
|
||||
export let selectedDeployment: Models.Deployment;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await sdk.forProject.functions.updateDeploymentBuild(
|
||||
selectedDeployment.resourceId,
|
||||
selectedDeployment.$id
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
showCancel = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Deployment has been deleted`
|
||||
});
|
||||
trackEvent(Submit.DeploymentCancel);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.DeploymentCancel);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Cancel deployment"
|
||||
bind:show={showCancel}
|
||||
onSubmit={handleSubmit}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>Are you sure you want to cancel this deployment?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showCancel = false)}>Cancel</Button>
|
||||
<Button secondary submit>Continue</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,5 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { InputChoice, Button, InputFile, FormList, InputText } from '$lib/elements/forms';
|
||||
import {
|
||||
InputChoice,
|
||||
Button,
|
||||
InputFile,
|
||||
FormList,
|
||||
InputText,
|
||||
InputTextarea
|
||||
} from '$lib/elements/forms';
|
||||
import { Alert, Collapsible, CollapsibleItem, Modal } from '$lib/components';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@@ -11,10 +18,10 @@
|
||||
|
||||
export let show = false;
|
||||
|
||||
let active = false;
|
||||
let active = true;
|
||||
let files: FileList;
|
||||
let entrypoint: string = null;
|
||||
let buildCommand: string = null;
|
||||
let entrypoint: string = $func.entrypoint;
|
||||
let buildCommand: string = $func.commands;
|
||||
let error: string = null;
|
||||
|
||||
const functionId = $page.params.function;
|
||||
@@ -31,7 +38,9 @@
|
||||
);
|
||||
await invalidate(Dependencies.DEPLOYMENTS);
|
||||
files = undefined;
|
||||
active = false;
|
||||
active = true;
|
||||
entrypoint = $func.entrypoint;
|
||||
buildCommand = $func.commands;
|
||||
show = false;
|
||||
dispatch('created');
|
||||
trackEvent(Submit.DeploymentCreate);
|
||||
@@ -43,7 +52,10 @@
|
||||
|
||||
$: if (!show) {
|
||||
files = undefined;
|
||||
active = false;
|
||||
active = true;
|
||||
entrypoint = $func.entrypoint;
|
||||
buildCommand = $func.commands;
|
||||
show = false;
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
@@ -71,13 +83,8 @@
|
||||
allowedFileExtensions={['gz']}
|
||||
bind:files
|
||||
required={true} />
|
||||
<InputText
|
||||
label="Entrypoint"
|
||||
id="entrypoint"
|
||||
placeholder="Entrypoint"
|
||||
bind:value={entrypoint}
|
||||
required />
|
||||
{#if $func.version !== 'v3'}
|
||||
|
||||
{#if $func.version === 'v2'}
|
||||
<Alert type="info">
|
||||
<svelte:fragment slot="title">
|
||||
Build commands now available for functions v3.0
|
||||
@@ -93,26 +100,35 @@
|
||||
{:else}
|
||||
<Collapsible>
|
||||
<CollapsibleItem>
|
||||
<svelte:fragment slot="title">Build settings</svelte:fragment>
|
||||
<svelte:fragment slot="title">Overwrite settings</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">(optional)</svelte:fragment>
|
||||
|
||||
<FormList gap={16}>
|
||||
<p class="text">
|
||||
Overwrite your function configuration for a single deployment or save
|
||||
commands for future use.
|
||||
Overwrite your function entrypoint or build commands for a single
|
||||
deployment or save commands for future use.
|
||||
</p>
|
||||
<InputText
|
||||
label="Entrypoint"
|
||||
id="entrypoint"
|
||||
placeholder="Entrypoint"
|
||||
bind:value={entrypoint} />
|
||||
<InputTextarea
|
||||
label="Commands"
|
||||
placeholder="Enter a build commad (e.g. 'npm install')"
|
||||
tooltip="Enter a single command or chain multiple commands with the && operator"
|
||||
id="build"
|
||||
bind:value={buildCommand} />
|
||||
<InputChoice
|
||||
label="Activate deployment after build"
|
||||
id="activate"
|
||||
bind:value={active}>
|
||||
This deployment will be activated after the build is completed.
|
||||
</InputChoice>
|
||||
</FormList>
|
||||
</CollapsibleItem>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
<InputChoice label="Activate deployment after build" id="activate" bind:value={active}>
|
||||
This deployment will be activated after the build is completed.
|
||||
</InputChoice>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Close</Button>
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Card, CardGrid, Id } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { func, proxyRuleList } from '../store';
|
||||
import { func } from '../store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import DeploymentSource from '../deploymentSource.svelte';
|
||||
import { deployment } from './store';
|
||||
import DeploymentCreatedBy from '../deploymentCreatedBy.svelte';
|
||||
import DeploymentDomains from '../deploymentDomains.svelte';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import BoxAvatar from '$lib/components/boxAvatar.svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import Delete from '../delete.svelte';
|
||||
import Activate from '../activate.svelte';
|
||||
import Cancel from '../cancel.svelte';
|
||||
import DeploymentCard from '../deploymentCard.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let logs = '';
|
||||
let showDelete = false;
|
||||
let showCancel = false;
|
||||
let showActivate = false;
|
||||
|
||||
function handleActivate() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
function getDownload(deploymentId: string) {
|
||||
return (
|
||||
sdk.forProject.functions.getDeploymentDownload($func.$id, deploymentId).toString() +
|
||||
'&mode=admin'
|
||||
);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
logs = $deployment.buildLogs;
|
||||
@@ -44,100 +57,81 @@
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-medium" aria-hidden="true">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${
|
||||
$func.runtime.split('-')[0]
|
||||
}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
|
||||
<p><b>Deployment ID</b></p>
|
||||
<DeploymentCard deployment={$deployment}>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="u-flex u-flex-wrap">
|
||||
{#if $deployment.status === 'processing'}
|
||||
<Button
|
||||
text
|
||||
class="u-margin-inline-end-16"
|
||||
on:click={() => {
|
||||
showCancel = true;
|
||||
}}>Cancel</Button>
|
||||
{/if}
|
||||
|
||||
<Id value={$deployment.$id}>
|
||||
{$deployment.$id}
|
||||
</Id>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const status = $deployment.status}
|
||||
{@const fileSize = humanFileSize($deployment.size)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
|
||||
<p>
|
||||
<b>Build time:</b>
|
||||
{#if ['processing', 'building'].includes($deployment.status)}
|
||||
<span use:timer={{ start: $deployment.$createdAt }} />
|
||||
{:else}
|
||||
{calculateTime($deployment.buildTime)}
|
||||
{/if}
|
||||
</p>
|
||||
<p><b>Updated:</b> <DeploymentCreatedBy deployment={$deployment} /></p>
|
||||
<p>
|
||||
<b>Size:</b>
|
||||
{fileSize.value + fileSize.unit}
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)"
|
||||
use:tooltip={{
|
||||
content:
|
||||
'Build size is the size of your code that will run. If your code has been transpiled, it will include your library source code. If it has been compiled, it will include your build output file.'
|
||||
}} />
|
||||
</p>
|
||||
<p class="u-flex u-gap-4 u-cross-center">
|
||||
<b>Source:</b>
|
||||
<DeploymentSource deployment={$deployment} />
|
||||
</p>
|
||||
{#if $proxyRuleList?.rules?.length}
|
||||
<p class="u-flex u-gap-4 u-cross-center">
|
||||
<b>Domains:</b>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<Pill
|
||||
danger={status === 'failed'}
|
||||
warning={status === 'building'}
|
||||
info={status === 'ready'}>
|
||||
<span class="text u-trim">{$deployment.status}</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{#if $deployment.status === 'ready'}
|
||||
<Button text href={getDownload($deployment.$id)} class="u-margin-inline-end-16">
|
||||
Download code
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
secondary
|
||||
disabled={data.activeDeployment}
|
||||
on:click={() => {
|
||||
showDelete = true;
|
||||
}}>Activate</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</DeploymentCard>
|
||||
|
||||
<Card>
|
||||
<div class="u-stretch u-overflow-hidden">
|
||||
<section class="code-panel">
|
||||
<header
|
||||
class="code-panel-header u-flex u-main-space-between u-width-full-line u-flex-wrap u-gap-16">
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<h4 class="u-bold">Build {$func.name}</h4>
|
||||
{#if $deployment.status === 'building'}
|
||||
<span>Building...</span>
|
||||
{:else}
|
||||
<span class="u-capitalize">{$deployment.status}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-gap-16">
|
||||
<!-- TODO: add button once function is implemented -->
|
||||
<!-- <Button disabled text>
|
||||
<span class="icon-external-link" aria-hidden="true" /> Raw data</Button> -->
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
<span class="icon-arrow-sm-up" aria-hidden="true" /> Scroll to top</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="code-panel-content">
|
||||
<pre><code>{logs}</code></pre>
|
||||
<div class="u-stretch u-overflow-hidden u-padding-block-start-24">
|
||||
<section class="code-panel" style="border-radius: var(--border-radius-medium);">
|
||||
<header
|
||||
class="code-panel-header u-flex u-main-space-between u-width-full-line u-flex-wrap u-gap-16">
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<h4 class="u-bold">Build {$func.name}</h4>
|
||||
{#if $deployment.status === 'building'}
|
||||
<span>Building...</span>
|
||||
{:else}
|
||||
<span class="u-capitalize">{$deployment.status}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div class="u-flex u-gap-16">
|
||||
<!-- TODO: add button once function is implemented -->
|
||||
<!-- <Button disabled text>
|
||||
<span class="icon-external-link" aria-hidden="true" /> Raw data</Button> -->
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
<span class="icon-arrow-sm-up" aria-hidden="true" /> Scroll to top</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="code-panel-content">
|
||||
<pre><code>{logs}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{#if !data.activeDeployment}
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete deployment</Heading>
|
||||
<p>
|
||||
The deployment will be permanently deleted, including all data associated with it.
|
||||
This action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<p>Last updated: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Delete selectedDeployment={$deployment} bind:showDelete />
|
||||
<Cancel selectedDeployment={$deployment} bind:showCancel />
|
||||
<Activate selectedDeployment={$deployment} bind:showActivate on:activated={handleActivate} />
|
||||
|
||||
@@ -2,10 +2,17 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
export const load: PageLoad = async ({ params, depends, parent }) => {
|
||||
const { function: func } = await parent();
|
||||
depends(Dependencies.DEPLOYMENT);
|
||||
|
||||
const deployment = await sdk.forProject.functions.getDeployment(
|
||||
params.function,
|
||||
params.deployment
|
||||
);
|
||||
|
||||
return {
|
||||
deployment: await sdk.forProject.functions.getDeployment(params.function, params.deployment)
|
||||
deployment,
|
||||
activeDeployment: func.deployment === params.deployment
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Id, SvgIcon } from '$lib/components';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { func, proxyRuleList } from './store';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
|
||||
import DeploymentSource from './deploymentSource.svelte';
|
||||
|
||||
import DeploymentDomains from './deploymentDomains.svelte';
|
||||
|
||||
export let deployment: Models.Deployment;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="u-flex u-cross-start u-gap-16">
|
||||
<div class="avatar is-medium" aria-hidden="true">
|
||||
<SvgIcon name={$func.runtime.split('-')[0]}></SvgIcon>
|
||||
</div>
|
||||
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
|
||||
<p><b>Deployment ID</b></p>
|
||||
|
||||
<Id value={deployment.$id}>
|
||||
{deployment.$id}
|
||||
</Id>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const status = deployment.status}
|
||||
{@const fileSize = humanFileSize(deployment.size)}
|
||||
<ul class="stats-grid-box u-gap-16">
|
||||
<li class="u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Status</p>
|
||||
<span>
|
||||
<Pill
|
||||
danger={status === 'failed'}
|
||||
warning={status === 'building'}
|
||||
success={status === 'ready'}>
|
||||
<span class="icon-lightning-bolt" aria-hidden="true" />
|
||||
<span class="text u-trim">
|
||||
{status === 'ready' ? 'active' : status}
|
||||
</span>
|
||||
</Pill>
|
||||
</span>
|
||||
</li>
|
||||
<li class="u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Build time</p>
|
||||
<p class="u-line-height-2">
|
||||
{calculateTime(deployment.buildTime)}
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Build size</p>
|
||||
<p class="u-line-height-2">
|
||||
{fileSize.value + fileSize.unit}
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Updated</p>
|
||||
<p class="u-line-height-2">
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Source</p>
|
||||
<div>
|
||||
<DeploymentSource {deployment} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $proxyRuleList?.rules?.length}
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<p class="u-color-text-offline">Domains</p>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<slot name="actions" />
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
.stats-grid-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media #{$break3open} {
|
||||
.stats-grid-box {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -12,9 +12,10 @@
|
||||
<a href={`http://${domain.rules[0].domain}`} target="_blank">
|
||||
<span class="link">
|
||||
{domain.rules[0].domain}
|
||||
</span> <span class="icon-external-link" aria-hidden="true" />
|
||||
</span>
|
||||
</a>
|
||||
</Trim>
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
{#if domain.rules.length > 1}
|
||||
<DropList bind:show={showDropdown} scrollable>
|
||||
<Pill button on:click={() => (showDropdown = !showDropdown)}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
//TODO remove
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
@@ -98,7 +99,7 @@
|
||||
</p>
|
||||
|
||||
<FormList>
|
||||
{#if $execute?.version !== 'v3'}
|
||||
{#if $execute?.version === 'v2'}
|
||||
<Alert type="info">
|
||||
<svelte:fragment slot="title">
|
||||
Customizable execution data now available for functions v3.0
|
||||
|
||||
@@ -1,34 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Alert, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
|
||||
import { Alert, EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowButton,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { hoursToDays, timeFromNow } from '$lib/helpers/date';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import { hoursToDays } from '$lib/helpers/date';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func, execute, showFunctionExecute } from '../store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { func } from '../store';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { getServiceLimit, showUsageRatesModal } from '$lib/stores/billing';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import Create from '../create.svelte';
|
||||
import { abbreviateNumber } from '$lib/helpers/numbers';
|
||||
import { base } from '$app/paths';
|
||||
import { Filters, queries, TagList } from '$lib/components/filters';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import Table from './table.svelte';
|
||||
import { tags } from '$lib/components/filters/store';
|
||||
|
||||
export let data;
|
||||
|
||||
const logs = getServiceLimit('logs');
|
||||
|
||||
const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Execution ID', type: 'string', show: true, width: 150 },
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status',
|
||||
type: 'enum',
|
||||
show: true,
|
||||
width: 110,
|
||||
array: true,
|
||||
format: 'enum',
|
||||
elements: ['completed', 'scheduled', 'waiting', 'processing', 'cancelled', 'failed']
|
||||
},
|
||||
{
|
||||
id: '$createdAt',
|
||||
title: 'Created',
|
||||
type: 'datetime',
|
||||
show: true,
|
||||
width: 120,
|
||||
format: 'datetime'
|
||||
},
|
||||
{
|
||||
id: 'trigger',
|
||||
title: 'Trigger',
|
||||
type: 'string',
|
||||
show: true,
|
||||
width: 90,
|
||||
array: true,
|
||||
format: 'enum',
|
||||
elements: ['http', 'scheduled', 'event']
|
||||
},
|
||||
{
|
||||
id: 'requestMethod',
|
||||
title: 'Method',
|
||||
type: 'string',
|
||||
show: true,
|
||||
width: 70,
|
||||
array: true,
|
||||
format: 'enum',
|
||||
elements: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
},
|
||||
{
|
||||
id: 'responseStatusCode',
|
||||
title: 'Status code',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
width: 100,
|
||||
format: 'integer'
|
||||
},
|
||||
{
|
||||
id: 'requestPath',
|
||||
title: 'Path',
|
||||
type: 'string',
|
||||
show: true,
|
||||
width: 90,
|
||||
format: 'string'
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
title: 'Duration',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
width: 80,
|
||||
format: 'integer'
|
||||
}
|
||||
]);
|
||||
|
||||
onMount(() => {
|
||||
return sdk.forConsole.client.subscribe('console', (response) => {
|
||||
if (response.events.includes('functions.*.executions.*')) {
|
||||
@@ -36,25 +98,10 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showLogs(execution: Models.Execution) {
|
||||
$log.show = true;
|
||||
$log.func = $func;
|
||||
$log.data = execution;
|
||||
}
|
||||
|
||||
const logs = getServiceLimit('logs');
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<ContainerHeader
|
||||
title="Executions"
|
||||
buttonText="Execute now"
|
||||
buttonEvent="execute_function"
|
||||
buttonMethod={() => {
|
||||
$execute = $func;
|
||||
$showFunctionExecute = true;
|
||||
}}>
|
||||
<ContainerHeader title="Executions">
|
||||
<svelte:fragment slot="tooltip" let:tier let:limit let:upgradeMethod>
|
||||
<p class="u-bold">The {tier} plan has limits</p>
|
||||
<ul>
|
||||
@@ -83,6 +130,73 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ContainerHeader>
|
||||
<div class="u-flex u-main-space-between is-not-mobile u-margin-block-start-16">
|
||||
<div class="u-flex u-gap-8 u-cross-center u-flex-wrap">
|
||||
<TagList />
|
||||
|
||||
<Filters query={data.query} {columns} let:disabled let:toggle singleCondition>
|
||||
<div class="u-flex u-gap-4">
|
||||
<Button
|
||||
text
|
||||
on:click={toggle}
|
||||
{disabled}
|
||||
noMargin={!$tags?.length}
|
||||
ariaLabel="open filter">
|
||||
<span class="icon-filter-line" />
|
||||
{#if !$tags?.length}
|
||||
<span class="text">Filters</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if $tags?.length}
|
||||
<div
|
||||
style="flex-basis:1px; background-color:hsl(var(--color-border)); width: 1px">
|
||||
</div>
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Filters>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns hideText />
|
||||
<Button
|
||||
event="execute_function"
|
||||
href={`${base}/console/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}
|
||||
disabled={!$func.$id || !$func?.deployment}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Execute now</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-main-space-between u-margin-block-start-16 is-only-mobile">
|
||||
<Filters query={data.query} {columns}>
|
||||
<svelte:fragment slot="mobile" let:disabled let:toggle>
|
||||
<Button text on:click={toggle} {disabled} ariaLabel="open filter" noMargin>
|
||||
<span class="icon-filter-line" />
|
||||
<span class="text">Filters</span>
|
||||
{#if $tags?.length}
|
||||
<span class="inline-tag">{$tags?.length}</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Filters>
|
||||
<div class=" u-flex u-gap-16">
|
||||
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
|
||||
<Button
|
||||
event="execute_function"
|
||||
href={`${base}/console/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}
|
||||
disabled={!$func.$id || !$func?.deployment}>
|
||||
<span class="text">Execute</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$func.logging}
|
||||
<div class="common-section">
|
||||
@@ -98,60 +212,31 @@
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.executions.total}
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHead width={150}>Execution ID</TableCellHead>
|
||||
<TableCellHead width={110}>Status</TableCellHead>
|
||||
<TableCellHead width={140}>Created</TableCellHead>
|
||||
<TableCellHead width={90}>Trigger</TableCellHead>
|
||||
<TableCellHead width={70}>Method</TableCellHead>
|
||||
<TableCellHead width={90}>Path</TableCellHead>
|
||||
<TableCellHead width={80}>Duration</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.executions.executions as execution}
|
||||
<TableRowButton on:click={() => showLogs(execution)}>
|
||||
<TableCell width={150} title="Execution ID">
|
||||
<Id value={execution.$id}>{execution.$id}</Id>
|
||||
</TableCell>
|
||||
<TableCell width={110} title="Status">
|
||||
{@const status = execution.status}
|
||||
|
||||
<Pill
|
||||
warning={status === 'waiting' || status === 'building'}
|
||||
danger={status === 'failed'}
|
||||
info={status === 'completed' || status === 'ready'}>
|
||||
{status}
|
||||
</Pill>
|
||||
</TableCell>
|
||||
<TableCellText width={140} title="Created">
|
||||
{timeFromNow(execution.$createdAt)}
|
||||
</TableCellText>
|
||||
<TableCell width={90} title="Trigger">
|
||||
<Pill>
|
||||
<span class="text u-trim">{execution.trigger}</span>
|
||||
</Pill>
|
||||
</TableCell>
|
||||
<TableCellText width={70} title="Method">
|
||||
{execution.requestMethod}
|
||||
</TableCellText>
|
||||
<TableCellText width={90} title="Path">
|
||||
{execution.requestPath}
|
||||
</TableCellText>
|
||||
<TableCellText width={80} title="Duration">
|
||||
{calculateTime(execution.duration)}
|
||||
</TableCellText>
|
||||
</TableRowButton>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
{#if data?.executions?.total}
|
||||
<Table columns={$columns} {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Executions"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.executions.total} />
|
||||
{:else if data?.query}
|
||||
<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 any executions</b>
|
||||
<p>There are no executions that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>Clear filters</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { getLimit, getPage, getQuery, pageToOffset } from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, url, route }) => {
|
||||
depends(Dependencies.EXECUTIONS);
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
const query = getQuery(url);
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
query,
|
||||
executions: await sdk.forProject.functions.listExecutions(params.function, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc('')
|
||||
Query.orderDesc(''),
|
||||
...parsedQueries.values()
|
||||
])
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { func } from '../store';
|
||||
|
||||
export let showDelete = false;
|
||||
export let selectedExecution: Models.Execution;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await sdk.forProject.functions.deleteExecution($func.$id, selectedExecution.$id);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Execution has been deleted`
|
||||
});
|
||||
trackEvent(Submit.ExecutionDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.ExecutionDelete);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Delete execution"
|
||||
bind:show={showDelete}
|
||||
onSubmit={handleSubmit}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>Are you sure you want to delete this execution?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,382 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Alert, Card, Id } from '$lib/components';
|
||||
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
FormItemPart,
|
||||
FormList,
|
||||
Helper,
|
||||
InputDate,
|
||||
InputSelect,
|
||||
InputText,
|
||||
InputTextarea,
|
||||
InputTime
|
||||
} from '$lib/elements/forms';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter,
|
||||
WizardSecondaryHeader
|
||||
} from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ExecutionMethod, type Models } from '@appwrite.io/console';
|
||||
import { writable } from 'svelte/store';
|
||||
import DeploymentSource from '../../deploymentSource.svelte';
|
||||
import DeploymentDomains from '../../deploymentDomains.svelte';
|
||||
import { proxyRuleList } from '../../store';
|
||||
import DeploymentCreatedBy from '../../deploymentCreatedBy.svelte';
|
||||
import {
|
||||
isSameDay,
|
||||
toLocaleDateISO,
|
||||
toLocaleDateTime,
|
||||
toLocaleTimeISO
|
||||
} from '$lib/helpers/date';
|
||||
|
||||
let previousPage: string = `${base}/console`;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
previousPage = from?.url?.pathname || previousPage;
|
||||
});
|
||||
|
||||
export let data;
|
||||
|
||||
const func = data.function as Models.Function;
|
||||
const deployment = data.activeDeployment as Models.Deployment;
|
||||
|
||||
const keyList = [
|
||||
{ label: 'Authorization', value: 'Authorization' },
|
||||
{ label: 'Cache-Control', value: 'Cache-Control' },
|
||||
{ label: 'Content-Length', value: 'Content-Length' },
|
||||
{ label: 'Content-Type', value: 'Content-Type' },
|
||||
{ label: 'User-Agent', value: 'User-Agent' },
|
||||
{ label: 'X-Appwrite-Project', value: 'X-Appwrite-Project' },
|
||||
{ label: 'X-Appwrite-Key', value: 'X-Appwrite-Key' },
|
||||
{ label: 'X-Appwrite-JWT', value: 'X-Appwrite-JWT' },
|
||||
{ label: 'X-Appwrite-Response-Format', value: 'X-Appwrite-Response-Format' },
|
||||
{ label: 'X-Fallback-Cookies', value: 'X-Fallback-Cookies' }
|
||||
];
|
||||
|
||||
const methodOptions = [
|
||||
{ label: 'GET', value: ExecutionMethod.GET },
|
||||
{ label: 'POST', value: ExecutionMethod.POST },
|
||||
{ label: 'PUT', value: ExecutionMethod.PUT },
|
||||
{ label: 'PATCH', value: ExecutionMethod.PATCH },
|
||||
{ label: 'DELETE', value: ExecutionMethod.DELETE },
|
||||
{ label: 'OPTIONS', value: ExecutionMethod.OPTIONS }
|
||||
];
|
||||
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let path = '/';
|
||||
let method = ExecutionMethod.GET;
|
||||
let body = '';
|
||||
let headers: [string, string][] = [[null, '']];
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const headersObject = {};
|
||||
|
||||
for (const [name, value] of headers) {
|
||||
headersObject[name] = value;
|
||||
}
|
||||
|
||||
await sdk.forProject.functions.createExecution(
|
||||
func.$id,
|
||||
body,
|
||||
true,
|
||||
path,
|
||||
method,
|
||||
headersObject,
|
||||
isScheduled ? dateTime.toISOString() : undefined
|
||||
);
|
||||
await goto(
|
||||
`${base}/console/project-${$page.params.project}/functions/function-${func.$id}/executions`
|
||||
);
|
||||
invalidate(Dependencies.EXECUTIONS);
|
||||
close();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Function has been executed`
|
||||
});
|
||||
trackEvent(Submit.ExecutionCreate);
|
||||
} catch (e) {
|
||||
trackError(e, Submit.ExecutionCreate);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
let isScheduled: boolean = null;
|
||||
let now = new Date();
|
||||
let minDate: string;
|
||||
let date: string = toLocaleDateISO(now.getTime());
|
||||
let time: string = toLocaleTimeISO(now.getTime());
|
||||
$: minDate = toLocaleDateISO(now.getTime());
|
||||
$: minTime = isSameDay(new Date(date), new Date(minDate))
|
||||
? toLocaleTimeISO(now.getTime())
|
||||
: '00:00';
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Execute function - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<WizardSecondaryContainer>
|
||||
<WizardSecondaryHeader href={previousPage}>Execute function</WizardSecondaryHeader>
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<FormList>
|
||||
{#if func?.version === 'v2'}
|
||||
<Alert type="info">
|
||||
<svelte:fragment slot="title">
|
||||
Customizable execution data now available for functions v3.0
|
||||
</svelte:fragment>
|
||||
Update your function version to make use of new features including customizable
|
||||
HTTP data in your executions.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href="https://appwrite.io/docs/products/functions/development"
|
||||
external
|
||||
text>
|
||||
Learn more
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`Hello, World!`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
{:else}
|
||||
<FormItem isMultiple stackOnMobile style="gap: 1rem">
|
||||
<InputSelect
|
||||
required
|
||||
id="method"
|
||||
label="Method"
|
||||
options={methodOptions}
|
||||
bind:value={method} />
|
||||
<InputText
|
||||
label="Path"
|
||||
id="path"
|
||||
fullWidth
|
||||
placeholder="/"
|
||||
bind:value={path}
|
||||
required />
|
||||
</FormItem>
|
||||
<div>
|
||||
<h3>
|
||||
<span class="body-text-2 u-bold">Headers</span>
|
||||
<span class="optional">(optional)</span>
|
||||
<span
|
||||
use:tooltip={{
|
||||
content:
|
||||
'Headers should contain alphanumeric characters (a-z, A-Z, and 0-9) and hyphens only (- and _).'
|
||||
}}
|
||||
class="icon-info"></span>
|
||||
</h3>
|
||||
|
||||
<FormList class="u-gap-8 u-margin-block-start-8">
|
||||
{#if headers}
|
||||
{#each headers as [name, value], index}
|
||||
<FormItem isMultiple style="gap: 1rem">
|
||||
<InputSelect
|
||||
isMultiple
|
||||
fullWidth
|
||||
label="Key"
|
||||
placeholder="Select key"
|
||||
options={keyList}
|
||||
id={`key-${index}`}
|
||||
bind:value={name} />
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
label="Value"
|
||||
placeholder="Enter value"
|
||||
id={`value-${index}`}
|
||||
bind:value />
|
||||
<FormItemPart alignEnd>
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
disabled={(!name || !value) && index === 0}
|
||||
on:click={() => {
|
||||
if (index === 0) {
|
||||
headers = [['', '']];
|
||||
} else {
|
||||
headers.splice(index, 1);
|
||||
headers = headers;
|
||||
}
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</FormItemPart>
|
||||
</FormItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</FormList>
|
||||
<Button
|
||||
noMargin
|
||||
text
|
||||
disabled={headers?.length &&
|
||||
headers[headers.length - 1][0] &&
|
||||
headers[headers.length - 1][1]
|
||||
? false
|
||||
: true}
|
||||
on:click={() => {
|
||||
if (
|
||||
headers[headers.length - 1][0] &&
|
||||
headers[headers.length - 1][1]
|
||||
) {
|
||||
headers.push(['', '']);
|
||||
headers = headers;
|
||||
}
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add Header</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`Hello, World!`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
|
||||
<li>
|
||||
<InputSelect
|
||||
bind:value={isScheduled}
|
||||
id="schedule"
|
||||
label="Schedule"
|
||||
options={[
|
||||
{
|
||||
label: 'Now',
|
||||
value: null
|
||||
},
|
||||
{
|
||||
label: 'Schedule',
|
||||
value: true
|
||||
}
|
||||
]} />
|
||||
{#if isScheduled}
|
||||
<FormItem isMultiple tag="div" class="u-margin-block-start-16">
|
||||
<InputDate
|
||||
id="date"
|
||||
label="Date"
|
||||
required={true}
|
||||
min={minDate}
|
||||
bind:value={date}
|
||||
isMultiple
|
||||
fullWidth />
|
||||
<InputTime
|
||||
id="time"
|
||||
label="Time"
|
||||
required={true}
|
||||
min={minTime}
|
||||
bind:value={time}
|
||||
step={1}
|
||||
isMultiple
|
||||
fullWidth />
|
||||
</FormItem>
|
||||
{/if}
|
||||
<Helper type="neutral">
|
||||
{isScheduled
|
||||
? `Your function will be executed on ${toLocaleDateTime(dateTime?.toString())}`
|
||||
: 'Your function will be executed immediately'}
|
||||
</Helper>
|
||||
</li>
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
<Card class="u-flex-vertical u-gap-24" style="--p-card-padding: 1.5rem">
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Deployment ID</p>
|
||||
|
||||
<Id value={func.deployment}>
|
||||
{func.deployment}
|
||||
</Id>
|
||||
</div>
|
||||
<ul class="u-flex u-main-space-between">
|
||||
<li class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Status</p>
|
||||
<p>
|
||||
<Pill success>
|
||||
<span class="icon-lightning-bolt"></span><span>active</span>
|
||||
</Pill>
|
||||
<!-- {#if deployment?.$id === func.deployment && deployment?.status === 'active'}
|
||||
{:else}
|
||||
<Pill
|
||||
danger={deployment?.status === 'failed'}
|
||||
warning={deployment?.status === 'building'}
|
||||
info={deployment?.status === 'ready'}>
|
||||
{deployment?.status}
|
||||
</Pill>
|
||||
{/if} -->
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Build time</p>
|
||||
<p>
|
||||
{#if ['processing', 'building'].includes(deployment.status)}
|
||||
<span use:timer={{ start: deployment.$createdAt }} />
|
||||
{:else}
|
||||
{calculateTime(deployment.buildTime)}
|
||||
{/if}
|
||||
</p>
|
||||
</li>
|
||||
<li class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Size</p>
|
||||
<p>
|
||||
{humanFileSize(deployment.size).value +
|
||||
humanFileSize(deployment.size).unit}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Source</p>
|
||||
<span>
|
||||
<DeploymentSource {deployment} />
|
||||
</span>
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Domains</p>
|
||||
<span>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</span>
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Updated</p>
|
||||
<span>
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
<WizardSecondaryFooter>
|
||||
<Button fullWidthMobile secondary href={previousPage}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting}>
|
||||
Execute
|
||||
</Button>
|
||||
</WizardSecondaryFooter>
|
||||
</WizardSecondaryContainer>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, parent }) => {
|
||||
const data = await parent();
|
||||
depends(Dependencies.DEPLOYMENTS);
|
||||
|
||||
return {
|
||||
func: data.function,
|
||||
activeDeployment: data.function.deployment
|
||||
? await sdk.forProject.functions.getDeployment(
|
||||
params.function,
|
||||
data.function.deployment
|
||||
)
|
||||
: null
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem, Id } from '$lib/components';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { func } from '../store';
|
||||
import Delete from './delete.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
|
||||
let selectedExecution: Models.Execution = null;
|
||||
|
||||
export let columns: Column[];
|
||||
export let data;
|
||||
|
||||
let execution: Record<string, Models.Execution> = {};
|
||||
|
||||
$: data.executions.executions.forEach((s) => {
|
||||
execution[s.$id] = s;
|
||||
});
|
||||
|
||||
function showLogs(execution: Models.Execution) {
|
||||
$log.show = true;
|
||||
$log.func = $func;
|
||||
$log.data = execution;
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
{/each}
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.executions.executions as execution, index (execution.$id)}
|
||||
<TableRow>
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key column.id}
|
||||
<TableCell width={column.width} title="Execution ID">
|
||||
<Id value={execution.$id}>{execution.$id}</Id>
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'status'}
|
||||
<TableCell width={column.width} title={column.title}>
|
||||
{@const status = execution.status}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Scheduled to execute on ${toLocaleDateTime(execution?.schedule)}`,
|
||||
disabled: !execution?.schedule
|
||||
}}>
|
||||
<Pill
|
||||
warning={status === 'waiting' || status === 'building'}
|
||||
danger={status === 'failed'}
|
||||
info={status === 'completed' || status === 'ready'}>
|
||||
{#if status === 'scheduled'}
|
||||
<span class="icon-clock" aria-hidden="true" />
|
||||
{/if}
|
||||
{status}
|
||||
</Pill>
|
||||
</div>
|
||||
</TableCell>
|
||||
{:else if column.id === '$createdAt'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{timeFromNow(execution.$createdAt)}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'trigger'}
|
||||
<TableCell width={column.width} title={column.title}>
|
||||
<Pill>
|
||||
<span class="text u-trim">{execution.trigger}</span>
|
||||
</Pill>
|
||||
</TableCell>
|
||||
{:else if column.id === 'requestMethod'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{execution.requestMethod}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'responseStatusCode'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{execution.responseStatusCode}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'requestPath'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{execution.requestPath}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'duration'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{calculateTime(execution.duration)}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
<TableCell width={40} showOverflow>
|
||||
<DropList bind:show={showDropdown[index]} placement="bottom-start" noArrow>
|
||||
<Button
|
||||
round
|
||||
text
|
||||
ariaLabel="More options"
|
||||
on:click={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="terminal"
|
||||
on:click={() => {
|
||||
showDropdown = [];
|
||||
showLogs(execution);
|
||||
}}>
|
||||
Logs
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
event="execution_delete"
|
||||
on:click={() => {
|
||||
selectedExecution = execution;
|
||||
showDropdown = [];
|
||||
showDelete = true;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
|
||||
{#if selectedExecution}
|
||||
<Delete {selectedExecution} bind:showDelete />
|
||||
{/if}
|
||||
@@ -1,23 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
|
||||
import UpdateEvents from './updateEvents.svelte';
|
||||
import ExecuteFunction from './executeFunction.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdateRuntime from './updateRuntime.svelte';
|
||||
import UpdatePermissions from './updatePermissions.svelte';
|
||||
import UpdateSchedule from './updateSchedule.svelte';
|
||||
import UpdateTimeout from './updateTimeout.svelte';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
import ExecuteFunction from './executeFunction.svelte';
|
||||
import UpdateConfiguration from './updateConfiguration.svelte';
|
||||
import UpdateEvents from './updateEvents.svelte';
|
||||
import UpdateLogging from './updateLogging.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdatePermissions from './updatePermissions.svelte';
|
||||
import UpdateRuntime from './updateRuntime.svelte';
|
||||
import UpdateSchedule from './updateSchedule.svelte';
|
||||
import UpdateScopes from './updateScopes.svelte';
|
||||
import UpdateTimeout from './updateTimeout.svelte';
|
||||
import UpdateVariables from '../../../updateVariables.svelte';
|
||||
|
||||
import { func } from '../store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import UpdateLogging from './updateLogging.svelte';
|
||||
import UpdateConfiguration from './updateConfiguration.svelte';
|
||||
import { Alert, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let data;
|
||||
let showAlert = true;
|
||||
@@ -40,7 +43,7 @@
|
||||
|
||||
<Container>
|
||||
<Heading tag="h2" size="5">Settings</Heading>
|
||||
{#if $func.version !== 'v3' && showAlert}
|
||||
{#if $func.version === 'v2' && showAlert}
|
||||
<Alert
|
||||
type="warning"
|
||||
dismissible
|
||||
@@ -57,6 +60,12 @@
|
||||
>.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
on:click={() =>
|
||||
trackEvent('click_open_website', {
|
||||
from: 'button',
|
||||
source: 'function_keys_card',
|
||||
destination: 'docs'
|
||||
})}
|
||||
href="https://appwrite.io/docs/products/functions/development"
|
||||
external
|
||||
text>
|
||||
@@ -81,5 +90,6 @@
|
||||
globalVariableList={data.globalVariables}
|
||||
variableList={data.variables} />
|
||||
<UpdateTimeout />
|
||||
<UpdateScopes />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { CardGrid, Heading, SvgIcon } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { execute, func, showFunctionExecute } from '../store';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { func } from '../store';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -28,9 +30,8 @@
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
$execute = $func;
|
||||
$showFunctionExecute = true;
|
||||
}}>Execute now</Button>
|
||||
href={`${base}/console/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}>
|
||||
Execute now
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
selectedInstallationId,
|
||||
selectedRepoId,
|
||||
selectedBranch,
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
$func.logging || undefined,
|
||||
entrypoint,
|
||||
commands,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
selectedBranch,
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
functionLogging,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -10,23 +10,47 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import { runtimesList } from '../../store';
|
||||
import { runtimesList, specs } from '../../store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { Runtime } from '@appwrite.io/console';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let runtime: string = null;
|
||||
let memory: number = null;
|
||||
let cpus: number = null;
|
||||
|
||||
let options = [];
|
||||
let cpuOptions = [];
|
||||
let memoryOptions = [];
|
||||
|
||||
onMount(async () => {
|
||||
runtime ??= $func.runtime;
|
||||
memory ??= $func.memory;
|
||||
cpus ??= $func.cpus;
|
||||
|
||||
let runtimes = await $runtimesList;
|
||||
let allowedSpecs = await $specs;
|
||||
options = runtimes.runtimes.map((runtime) => ({
|
||||
label: `${runtime.name} - ${runtime.version}`,
|
||||
value: runtime.$id
|
||||
}));
|
||||
|
||||
memoryOptions = allowedSpecs.memory.map((memory) =>
|
||||
memory > 1024
|
||||
? {
|
||||
label: `${memory / 1024} GB`,
|
||||
value: memory
|
||||
}
|
||||
: {
|
||||
label: `${memory} MB`,
|
||||
value: memory
|
||||
}
|
||||
);
|
||||
|
||||
cpuOptions = allowedSpecs.cpus.map((cpu) => ({
|
||||
label: cpu === 1 ? `${cpu} CPU Cores` : `${cpu} CPU Core`,
|
||||
value: cpu
|
||||
}));
|
||||
});
|
||||
|
||||
async function updateRuntime() {
|
||||
@@ -34,7 +58,7 @@
|
||||
if (!isValueOfStringEnum(Runtime, runtime)) {
|
||||
throw new Error(`Invalid runtime: ${runtime}`);
|
||||
}
|
||||
await sdk.forProject.functions.update(
|
||||
await sdk.forProject.specsFunctions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
runtime,
|
||||
@@ -46,15 +70,18 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
$func.providerSilentMode || undefined,
|
||||
$func.providerRootDirectory || undefined
|
||||
$func.providerRootDirectory || undefined,
|
||||
cpus,
|
||||
memory
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Runtime has been updated',
|
||||
message: 'Runtime settings have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateName);
|
||||
@@ -66,6 +93,9 @@
|
||||
trackError(error, Submit.FunctionUpdateName);
|
||||
}
|
||||
}
|
||||
|
||||
$: isUpdateButtonEnabled =
|
||||
runtime !== $func?.runtime || memory !== $func?.memory || cpus !== $func?.cpus;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateRuntime}>
|
||||
@@ -82,11 +112,27 @@
|
||||
{options}
|
||||
required
|
||||
hideRequired />
|
||||
<InputSelect
|
||||
label="Memory Limit"
|
||||
id="memory"
|
||||
placeholder="Select memory"
|
||||
bind:value={memory}
|
||||
options={memoryOptions}
|
||||
required
|
||||
hideRequired />
|
||||
<InputSelect
|
||||
label="CPU Limit"
|
||||
id="cpus"
|
||||
placeholder="Select cpu cores"
|
||||
bind:value={cpus}
|
||||
options={cpuOptions}
|
||||
required
|
||||
hideRequired />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={runtime === $func.runtime} submit>Update</Button>
|
||||
<Button disabled={!isUpdateButtonEnabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { Runtime } from '@appwrite.io/console';
|
||||
import Scopes from '$routes/console/project-[project]/overview/keys/scopes.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let functionScopes: string[] = null;
|
||||
|
||||
onMount(async () => {
|
||||
functionScopes ??= $func.scopes;
|
||||
});
|
||||
|
||||
async function updateScopes() {
|
||||
try {
|
||||
if (!isValueOfStringEnum(Runtime, $func.runtime)) {
|
||||
throw new Error(`Invalid runtime: ${$func.runtime}`);
|
||||
}
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.runtime,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled || undefined,
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
functionScopes,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
$func.providerSilentMode || undefined,
|
||||
$func.providerRootDirectory || undefined
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Function scopes have been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateScopes, {
|
||||
scopes: functionScopes
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateScopes);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateScopes}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Scopes</Heading>
|
||||
<p class="text">
|
||||
Select scopes to grant the dynamic key generated temporarily for your function. It is
|
||||
best practice to allow only necessary permissions. <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more</a
|
||||
>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if functionScopes !== null}
|
||||
<Scopes bind:scopes={functionScopes} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
submit
|
||||
disabled={functionScopes &&
|
||||
$func?.scopes &&
|
||||
!symmetricDifference(functionScopes, $func?.scopes).length}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -36,6 +36,7 @@
|
||||
$func.logging || undefined,
|
||||
$func.entrypoint || undefined,
|
||||
$func.commands || undefined,
|
||||
$func.scopes || undefined,
|
||||
$func.installationId || undefined,
|
||||
$func.providerRepositoryId || undefined,
|
||||
$func.providerBranch || undefined,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived, writable, type Writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Func } from '$lib/sdk/functionsSpec';
|
||||
|
||||
export const func = derived(page, ($page) => $page.data.function as Models.Function);
|
||||
export const func = derived(page, ($page) => $page.data.function as Func);
|
||||
export const deploymentList = derived(
|
||||
page,
|
||||
($page) => $page.data.deploymentList as Models.DeploymentList
|
||||
@@ -11,8 +12,8 @@ export const proxyRuleList = derived(
|
||||
page,
|
||||
($page) => $page.data.proxyRuleList as Models.ProxyRuleList
|
||||
);
|
||||
export const execute: Writable<Models.Function> = writable();
|
||||
export const showFunctionExecute: Writable<boolean> = writable(false);
|
||||
export const execute: Writable<Models.Function> = writable(); //TODO Remove
|
||||
export const showFunctionExecute: Writable<boolean> = writable(false); //TODO Remove
|
||||
|
||||
export const repositories: Writable<{
|
||||
search: string;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem, DropListLink, Id } from '$lib/components';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import type { PageData } from './$types';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import DeploymentSource from './deploymentSource.svelte';
|
||||
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
|
||||
import { timer } from '$lib/actions/timer';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import { func } from './store';
|
||||
import { page } from '$app/stores';
|
||||
import Delete from './delete.svelte';
|
||||
import Activate from './activate.svelte';
|
||||
import RedeployModal from './redeployModal.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Cancel from './cancel.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export let columns: Column[];
|
||||
export let data: PageData;
|
||||
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
let showActivate = false;
|
||||
let showRedeploy = false;
|
||||
let showCancel = false;
|
||||
|
||||
let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
function handleActivate() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
function getDownload(deploymentId: string) {
|
||||
return (
|
||||
sdk.forProject.functions.getDeploymentDownload($func.$id, deploymentId).toString() +
|
||||
'&mode=admin'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
{/each}
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.deploymentList.deployments as deployment, index (deployment.$id)}
|
||||
<TableRow>
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key column.id}
|
||||
<TableCell width={column.width} title="Deployment ID">
|
||||
<Id value={deployment.$id}>{deployment.$id}</Id>
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'status'}
|
||||
<TableCell width={column.width} title={column.title}>
|
||||
{@const status = deployment.status}
|
||||
{#if data?.activeDeployment?.$id === deployment?.$id}
|
||||
<Pill success>
|
||||
<span class="icon-lightning-bolt" aria-hidden="true" />
|
||||
<span class="text u-trim">active</span>
|
||||
</Pill>
|
||||
{:else}
|
||||
<Pill
|
||||
danger={status === 'failed'}
|
||||
warning={status === 'building'}
|
||||
info={status === 'ready'}>
|
||||
{status}
|
||||
</Pill>
|
||||
{/if}
|
||||
</TableCell>
|
||||
{:else if column.id === 'type'}
|
||||
<TableCell width={column.width} title={column.title}>
|
||||
<DeploymentSource {deployment} />
|
||||
</TableCell>
|
||||
{:else if column.id === '$updatedAt'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
<DeploymentCreatedBy {deployment} />
|
||||
</TableCellText>
|
||||
{:else if column.id === 'buildTime'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{#if ['processing', 'building'].includes(deployment.status)}
|
||||
<span use:timer={{ start: deployment.$createdAt }} />
|
||||
{:else}
|
||||
{calculateTime(deployment.buildTime)}
|
||||
{/if}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'size'}
|
||||
<TableCellText width={column.width} title={column.title}>
|
||||
{calculateSize(deployment.size)}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
<TableCell width={40} showOverflow>
|
||||
<DropList bind:show={showDropdown[index]} placement="bottom-start" noArrow>
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="More options"
|
||||
on:click|preventDefault={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showRedeploy = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Redeploy
|
||||
</DropListItem>
|
||||
{#if deployment.status === 'ready' && deployment.$id !== $func.deployment}
|
||||
<DropListItem
|
||||
icon="lightning-bolt"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showActivate = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Activate
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListLink
|
||||
icon="terminal"
|
||||
href={`/console/project-${$page.params.project}/functions/function-${$page.params.function}/deployment-${deployment.$id}`}>
|
||||
Logs
|
||||
</DropListLink>
|
||||
{#if deployment.status === 'ready'}
|
||||
<DropListLink
|
||||
icon="download"
|
||||
href={getDownload(deployment.$id)}
|
||||
on:click={() => (showDropdown[index] = false)}>
|
||||
Download
|
||||
</DropListLink>
|
||||
{/if}
|
||||
{#if deployment.status === 'processing' || deployment.status === 'building'}
|
||||
<DropListItem
|
||||
icon="x-circle"
|
||||
event="deployment_cancel"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showDropdown = [];
|
||||
showCancel = true;
|
||||
}}>
|
||||
Cancel
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showDropdown = [];
|
||||
showDelete = true;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
|
||||
{#if selectedDeployment}
|
||||
<Delete {selectedDeployment} bind:showDelete />
|
||||
<Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} />
|
||||
<Cancel {selectedDeployment} bind:showCancel />
|
||||
<RedeployModal {selectedDeployment} bind:show={showRedeploy} />
|
||||
{/if}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Specs } from '$lib/sdk/functionsSpec';
|
||||
|
||||
export const runtimesList = derived(
|
||||
page,
|
||||
async ($page) => (await $page.data.runtimesList) as Models.RuntimeList
|
||||
);
|
||||
|
||||
export const specs = derived(page, async ($page) => (await $page.data.specs) as Specs);
|
||||
|
||||
export const baseRuntimesList = derived(runtimesList, async ($runtimesList) => {
|
||||
const baseRuntimes = new Map<string, Models.Runtime>();
|
||||
for (const runtime of (await $runtimesList).runtimes) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16681_3306)">
|
||||
<path d="M2.94884 11.7839C2.90955 11.7839 2.89972 11.7646 2.91937 11.7355L3.12566 11.4742C3.1453 11.4452 3.19442 11.4258 3.23371 11.4258H6.74059C6.77988 11.4258 6.7897 11.4549 6.77006 11.4839L6.60306 11.7355C6.58342 11.7646 6.5343 11.7936 6.50483 11.7936L2.94884 11.7839Z" fill="#00ACD7"/>
|
||||
<path d="M1.46556 12.6742C1.42627 12.6742 1.41645 12.6549 1.43609 12.6258L1.64238 12.3645C1.66203 12.3355 1.71114 12.3162 1.75043 12.3162H6.22981C6.2691 12.3162 6.28874 12.3452 6.27892 12.3742L6.20034 12.6065C6.19051 12.6452 6.15122 12.6645 6.11193 12.6645L1.46556 12.6742Z" fill="#00ACD7"/>
|
||||
<path d="M3.84276 13.5645C3.80347 13.5645 3.79364 13.5355 3.81329 13.5065L3.95081 13.2645C3.97046 13.2355 4.00975 13.2065 4.04905 13.2065H6.01368C6.05298 13.2065 6.07262 13.2355 6.07262 13.2742L6.05298 13.5065C6.05298 13.5452 6.01368 13.5742 5.98421 13.5742L3.84276 13.5645Z" fill="#00ACD7"/>
|
||||
<path d="M14.0392 11.6097C13.4204 11.7645 12.998 11.8807 12.3889 12.0355C12.2416 12.0742 12.2318 12.0839 12.1041 11.9387C11.9567 11.7742 11.8487 11.6678 11.6424 11.571C11.0235 11.271 10.4243 11.3581 9.86438 11.7161C9.19641 12.1419 8.8526 12.771 8.86242 13.5549C8.87224 14.329 9.41252 14.9678 10.1886 15.0742C10.8565 15.1613 11.4164 14.929 11.8585 14.4355C11.9469 14.329 12.0255 14.2129 12.1237 14.0774C11.7701 14.0774 11.328 14.0774 10.2278 14.0774C10.0216 14.0774 9.97244 13.9516 10.0412 13.7871C10.1689 13.4871 10.4047 12.9839 10.5422 12.7323C10.5717 12.6742 10.6404 12.5774 10.7878 12.5774C11.2887 12.5774 13.1355 12.5774 14.3634 12.5774C14.3438 12.8387 14.3438 13.1 14.3045 13.3613C14.1964 14.0581 13.9312 14.6968 13.499 15.2581C12.7917 16.1774 11.8683 16.7484 10.6994 16.9032C9.73668 17.029 8.84277 16.8452 8.05692 16.2645C7.33 15.7226 6.91743 15.0065 6.80938 14.1161C6.68167 13.0613 6.99602 12.1129 7.64435 11.2807C8.34179 10.3807 9.26517 9.80969 10.3948 9.60646C11.3182 9.44195 12.2023 9.5484 12.998 10.0807C13.5186 10.4194 13.8919 10.8839 14.1375 11.4452C14.1964 11.5323 14.1571 11.5807 14.0392 11.6097Z" fill="#00ACD7"/>
|
||||
<path d="M17.2907 16.9613C16.3968 16.9419 15.5814 16.6903 14.8938 16.1097C14.3143 15.6161 13.9508 14.9871 13.8329 14.2419C13.6561 13.1484 13.9606 12.1806 14.6286 11.3194C15.3457 10.3903 16.2101 9.90645 17.3791 9.70323C18.3811 9.52903 19.3241 9.62581 20.1787 10.1968C20.9547 10.7194 21.4361 11.4258 21.5638 12.3548C21.7308 13.6613 21.3477 14.7258 20.4341 15.6355C19.7858 16.2839 18.9901 16.6903 18.0765 16.8742C17.8113 16.9226 17.5461 16.9323 17.2907 16.9613ZM19.6286 13.0516C19.6188 12.9258 19.6188 12.829 19.5991 12.7323C19.4223 11.7742 18.5284 11.2323 17.5952 11.4452C16.6816 11.6484 16.0923 12.2194 15.8761 13.129C15.6993 13.8839 16.0726 14.6484 16.7799 14.9581C17.3201 15.1903 17.8604 15.1613 18.3811 14.9C19.1571 14.5032 19.5795 13.8839 19.6286 13.0516Z" fill="#00ACD7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16681_3306">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0 0.817017)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16681_3251)">
|
||||
<path d="M2.63021 10.083C2.58813 10.083 2.57762 10.063 2.59865 10.033L2.81952 9.76322C2.84056 9.73324 2.89315 9.71326 2.93522 9.71326H6.69C6.73207 9.71326 6.74259 9.74324 6.72156 9.77322L6.54276 10.033C6.52172 10.063 6.46913 10.093 6.43758 10.093L2.63021 10.083Z" fill="#414146"/>
|
||||
<path d="M1.04207 11.0023C1 11.0023 0.989482 10.9823 1.01052 10.9523L1.23139 10.6825C1.25242 10.6526 1.30501 10.6326 1.34708 10.6326H6.14311C6.18518 10.6326 6.20622 10.6625 6.1957 10.6925L6.11156 10.9324C6.10104 10.9723 6.05897 10.9923 6.0169 10.9923L1.04207 11.0023Z" fill="#414146"/>
|
||||
<path d="M3.58736 11.9216C3.54529 11.9216 3.53477 11.8916 3.55581 11.8617L3.70305 11.6118C3.72409 11.5819 3.76616 11.5519 3.80823 11.5519H5.91175C5.95382 11.5519 5.97486 11.5819 5.97486 11.6218L5.95382 11.8617C5.95382 11.9016 5.91175 11.9316 5.8802 11.9316L3.58736 11.9216Z" fill="#414146"/>
|
||||
<path d="M14.5046 9.90307C13.842 10.063 13.3897 10.1829 12.7376 10.3428C12.5799 10.3827 12.5693 10.3927 12.4326 10.2428C12.2749 10.073 12.1592 9.96303 11.9383 9.8631C11.2757 9.55331 10.6341 9.64325 10.0346 10.013C9.3194 10.4527 8.95129 11.1022 8.96181 11.9117C8.97232 12.7111 9.55079 13.3707 10.3817 13.4806C11.0969 13.5705 11.6964 13.3307 12.1697 12.8211C12.2643 12.7111 12.3485 12.5912 12.4537 12.4513C12.075 12.4513 11.6017 12.4513 10.4238 12.4513C10.2029 12.4513 10.1503 12.3214 10.2239 12.1515C10.3606 11.8417 10.6131 11.3221 10.7603 11.0623C10.7919 11.0023 10.8655 10.9024 11.0233 10.9024C11.5597 10.9024 13.537 10.9024 14.8517 10.9024C14.8306 11.1722 14.8306 11.442 14.7886 11.7118C14.6729 12.4313 14.3889 13.0909 13.9261 13.6705C13.1689 14.6198 12.1802 15.2094 10.9286 15.3693C9.89787 15.4992 8.94077 15.3094 8.09936 14.7098C7.32106 14.1501 6.87932 13.4107 6.76363 12.4913C6.6269 11.402 6.96346 10.4227 7.65762 9.5633C8.40437 8.63394 9.39303 8.04435 10.6026 7.83449C11.5912 7.66461 12.5378 7.77453 13.3897 8.32415C13.9472 8.67391 14.3468 9.15358 14.6098 9.73319C14.6729 9.82312 14.6308 9.87309 14.5046 9.90307Z" fill="#414146"/>
|
||||
<path d="M17.9859 15.4294C17.0288 15.4094 16.1558 15.1496 15.4196 14.55C14.799 14.0403 14.4099 13.3908 14.2837 12.6213C14.0944 11.4921 14.4204 10.4928 15.1356 9.60339C15.9034 8.64405 16.8289 8.14439 18.0805 7.93454C19.1533 7.75466 20.163 7.85459 21.0781 8.44419C21.909 8.98382 22.4243 9.71332 22.561 10.6727C22.7398 12.0217 22.3297 13.121 21.3515 14.0603C20.6574 14.7299 19.8054 15.1496 18.8273 15.3394C18.5433 15.3894 18.2593 15.3994 17.9859 15.4294ZM20.4891 11.3922C20.4786 11.2623 20.4786 11.1623 20.4575 11.0624C20.2682 10.0731 19.3111 9.51345 18.3119 9.7333C17.3338 9.94316 16.7027 10.5328 16.4713 11.4721C16.282 12.2516 16.6817 13.041 17.439 13.3608C18.0174 13.6006 18.5959 13.5707 19.1533 13.3009C19.9842 12.8911 20.4365 12.2516 20.4891 11.3922Z" fill="#414146"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16681_3251">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16681_3306)">
|
||||
<path d="M2.94884 11.7839C2.90955 11.7839 2.89972 11.7646 2.91937 11.7355L3.12566 11.4742C3.1453 11.4452 3.19442 11.4258 3.23371 11.4258H6.74059C6.77988 11.4258 6.7897 11.4549 6.77006 11.4839L6.60306 11.7355C6.58342 11.7646 6.5343 11.7936 6.50483 11.7936L2.94884 11.7839Z" fill="#00ACD7"/>
|
||||
<path d="M1.46556 12.6742C1.42627 12.6742 1.41645 12.6549 1.43609 12.6258L1.64238 12.3645C1.66203 12.3355 1.71114 12.3162 1.75043 12.3162H6.22981C6.2691 12.3162 6.28874 12.3452 6.27892 12.3742L6.20034 12.6065C6.19051 12.6452 6.15122 12.6645 6.11193 12.6645L1.46556 12.6742Z" fill="#00ACD7"/>
|
||||
<path d="M3.84276 13.5645C3.80347 13.5645 3.79364 13.5355 3.81329 13.5065L3.95081 13.2645C3.97046 13.2355 4.00975 13.2065 4.04905 13.2065H6.01368C6.05298 13.2065 6.07262 13.2355 6.07262 13.2742L6.05298 13.5065C6.05298 13.5452 6.01368 13.5742 5.98421 13.5742L3.84276 13.5645Z" fill="#00ACD7"/>
|
||||
<path d="M14.0392 11.6097C13.4204 11.7645 12.998 11.8807 12.3889 12.0355C12.2416 12.0742 12.2318 12.0839 12.1041 11.9387C11.9567 11.7742 11.8487 11.6678 11.6424 11.571C11.0235 11.271 10.4243 11.3581 9.86438 11.7161C9.19641 12.1419 8.8526 12.771 8.86242 13.5549C8.87224 14.329 9.41252 14.9678 10.1886 15.0742C10.8565 15.1613 11.4164 14.929 11.8585 14.4355C11.9469 14.329 12.0255 14.2129 12.1237 14.0774C11.7701 14.0774 11.328 14.0774 10.2278 14.0774C10.0216 14.0774 9.97244 13.9516 10.0412 13.7871C10.1689 13.4871 10.4047 12.9839 10.5422 12.7323C10.5717 12.6742 10.6404 12.5774 10.7878 12.5774C11.2887 12.5774 13.1355 12.5774 14.3634 12.5774C14.3438 12.8387 14.3438 13.1 14.3045 13.3613C14.1964 14.0581 13.9312 14.6968 13.499 15.2581C12.7917 16.1774 11.8683 16.7484 10.6994 16.9032C9.73668 17.029 8.84277 16.8452 8.05692 16.2645C7.33 15.7226 6.91743 15.0065 6.80938 14.1161C6.68167 13.0613 6.99602 12.1129 7.64435 11.2807C8.34179 10.3807 9.26517 9.80969 10.3948 9.60646C11.3182 9.44195 12.2023 9.5484 12.998 10.0807C13.5186 10.4194 13.8919 10.8839 14.1375 11.4452C14.1964 11.5323 14.1571 11.5807 14.0392 11.6097Z" fill="#00ACD7"/>
|
||||
<path d="M17.2907 16.9613C16.3968 16.9419 15.5814 16.6903 14.8938 16.1097C14.3143 15.6161 13.9508 14.9871 13.8329 14.2419C13.6561 13.1484 13.9606 12.1806 14.6286 11.3194C15.3457 10.3903 16.2101 9.90645 17.3791 9.70323C18.3811 9.52903 19.3241 9.62581 20.1787 10.1968C20.9547 10.7194 21.4361 11.4258 21.5638 12.3548C21.7308 13.6613 21.3477 14.7258 20.4341 15.6355C19.7858 16.2839 18.9901 16.6903 18.0765 16.8742C17.8113 16.9226 17.5461 16.9323 17.2907 16.9613ZM19.6286 13.0516C19.6188 12.9258 19.6188 12.829 19.5991 12.7323C19.4223 11.7742 18.5284 11.2323 17.5952 11.4452C16.6816 11.6484 16.0923 12.2194 15.8761 13.129C15.6993 13.8839 16.0726 14.6484 16.7799 14.9581C17.3201 15.1903 17.8604 15.1613 18.3811 14.9C19.1571 14.5032 19.5795 13.8839 19.6286 13.0516Z" fill="#00ACD7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16681_3306">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0 0.817017)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_16681_3251)">
|
||||
<path d="M2.63021 10.083C2.58813 10.083 2.57762 10.063 2.59865 10.033L2.81952 9.76322C2.84056 9.73324 2.89315 9.71326 2.93522 9.71326H6.69C6.73207 9.71326 6.74259 9.74324 6.72156 9.77322L6.54276 10.033C6.52172 10.063 6.46913 10.093 6.43758 10.093L2.63021 10.083Z" fill="#414146"/>
|
||||
<path d="M1.04207 11.0023C1 11.0023 0.989482 10.9823 1.01052 10.9523L1.23139 10.6825C1.25242 10.6526 1.30501 10.6326 1.34708 10.6326H6.14311C6.18518 10.6326 6.20622 10.6625 6.1957 10.6925L6.11156 10.9324C6.10104 10.9723 6.05897 10.9923 6.0169 10.9923L1.04207 11.0023Z" fill="#414146"/>
|
||||
<path d="M3.58736 11.9216C3.54529 11.9216 3.53477 11.8916 3.55581 11.8617L3.70305 11.6118C3.72409 11.5819 3.76616 11.5519 3.80823 11.5519H5.91175C5.95382 11.5519 5.97486 11.5819 5.97486 11.6218L5.95382 11.8617C5.95382 11.9016 5.91175 11.9316 5.8802 11.9316L3.58736 11.9216Z" fill="#414146"/>
|
||||
<path d="M14.5046 9.90307C13.842 10.063 13.3897 10.1829 12.7376 10.3428C12.5799 10.3827 12.5693 10.3927 12.4326 10.2428C12.2749 10.073 12.1592 9.96303 11.9383 9.8631C11.2757 9.55331 10.6341 9.64325 10.0346 10.013C9.3194 10.4527 8.95129 11.1022 8.96181 11.9117C8.97232 12.7111 9.55079 13.3707 10.3817 13.4806C11.0969 13.5705 11.6964 13.3307 12.1697 12.8211C12.2643 12.7111 12.3485 12.5912 12.4537 12.4513C12.075 12.4513 11.6017 12.4513 10.4238 12.4513C10.2029 12.4513 10.1503 12.3214 10.2239 12.1515C10.3606 11.8417 10.6131 11.3221 10.7603 11.0623C10.7919 11.0023 10.8655 10.9024 11.0233 10.9024C11.5597 10.9024 13.537 10.9024 14.8517 10.9024C14.8306 11.1722 14.8306 11.442 14.7886 11.7118C14.6729 12.4313 14.3889 13.0909 13.9261 13.6705C13.1689 14.6198 12.1802 15.2094 10.9286 15.3693C9.89787 15.4992 8.94077 15.3094 8.09936 14.7098C7.32106 14.1501 6.87932 13.4107 6.76363 12.4913C6.6269 11.402 6.96346 10.4227 7.65762 9.5633C8.40437 8.63394 9.39303 8.04435 10.6026 7.83449C11.5912 7.66461 12.5378 7.77453 13.3897 8.32415C13.9472 8.67391 14.3468 9.15358 14.6098 9.73319C14.6729 9.82312 14.6308 9.87309 14.5046 9.90307Z" fill="#414146"/>
|
||||
<path d="M17.9859 15.4294C17.0288 15.4094 16.1558 15.1496 15.4196 14.55C14.799 14.0403 14.4099 13.3908 14.2837 12.6213C14.0944 11.4921 14.4204 10.4928 15.1356 9.60339C15.9034 8.64405 16.8289 8.14439 18.0805 7.93454C19.1533 7.75466 20.163 7.85459 21.0781 8.44419C21.909 8.98382 22.4243 9.71332 22.561 10.6727C22.7398 12.0217 22.3297 13.121 21.3515 14.0603C20.6574 14.7299 19.8054 15.1496 18.8273 15.3394C18.5433 15.3894 18.2593 15.3994 17.9859 15.4294ZM20.4891 11.3922C20.4786 11.2623 20.4786 11.1623 20.4575 11.0624C20.2682 10.0731 19.3111 9.51345 18.3119 9.7333C17.3338 9.94316 16.7027 10.5328 16.4713 11.4721C16.282 12.2516 16.6817 13.041 17.439 13.3608C18.0174 13.6006 18.5959 13.5707 19.1533 13.3009C19.9842 12.8911 20.4365 12.2516 20.4891 11.3922Z" fill="#414146"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16681_3251">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |