Merge branch '1.6.x' of github.com:appwrite/console into mock-phone-numbers
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.4",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@stripe/stripe-js": "^3.4.0",
|
||||
"ai": "^2.2.11",
|
||||
"analytics": "^0.8.9",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"deep-equal": "^2.2.2",
|
||||
"dotenv": "^16.3.1",
|
||||
@@ -148,9 +149,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.4",
|
||||
"resolved": "https://registry.npmjs.org/matej-appwrite-console-16x/-/matej-appwrite-console-16x-0.6.4.tgz",
|
||||
"integrity": "sha512-hovc+6HFyC20lUmlD0R5kAsDMBLXXJWEES2mUAaKG56aQ1wOwQRhVOIUGmvX/1EasiUOq5dH/5uzrU+fl5C+Dw=="
|
||||
},
|
||||
"node_modules/@appwrite.io/pink": {
|
||||
"version": "0.23.0",
|
||||
@@ -4020,6 +4022,17 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -7019,6 +7032,14 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
|
||||
@@ -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.4",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -28,6 +28,7 @@
|
||||
"@stripe/stripe-js": "^3.4.0",
|
||||
"ai": "^2.2.11",
|
||||
"analytics": "^0.8.9",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"deep-equal": "^2.2.2",
|
||||
"dotenv": "^16.3.1",
|
||||
|
||||
@@ -227,13 +227,16 @@ export enum Submit {
|
||||
FunctionUpdateLogging = 'submit_function_update_logging',
|
||||
FunctionUpdateTimeout = 'submit_function_update_timeout',
|
||||
FunctionUpdateEvents = 'submit_function_update_events',
|
||||
FunctionUpdateScopes = 'submit_function_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 } 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,64 +33,39 @@
|
||||
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 = [];
|
||||
}
|
||||
|
||||
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}>
|
||||
<form on:submit|preventDefault={addFilterAndReset}>
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="column"
|
||||
@@ -188,10 +92,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 +114,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 +132,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,23 @@
|
||||
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 } 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 +39,78 @@
|
||||
queries.clearAll();
|
||||
}
|
||||
|
||||
function apply() {
|
||||
if (selectedColumn && operatorKey && (value || arrayValues.length)) {
|
||||
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
|
||||
selectedColumn = null;
|
||||
value = null;
|
||||
operatorKey = null;
|
||||
arrayValues = [];
|
||||
}
|
||||
queries.apply();
|
||||
}
|
||||
|
||||
$: if (!showFiltersDesktop && !showFiltersMobile) {
|
||||
selectedColumn = null;
|
||||
}
|
||||
|
||||
$: isButtonDisabled = $queriesAreDirty
|
||||
? false
|
||||
: !selectedColumn || !operatorKey || (!value && !arrayValues.length);
|
||||
|
||||
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={(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">
|
||||
{#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 +119,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 +128,21 @@
|
||||
size="big">
|
||||
<Content
|
||||
{columns}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{singleCondition}
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -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,51 @@
|
||||
<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>');
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
@@ -38,9 +40,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>
|
||||
@@ -68,4 +71,4 @@
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
</svelte:component>
|
||||
|
||||
@@ -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,14 +77,14 @@
|
||||
<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="u-text-center auth-container">
|
||||
<div class="is-only-mobile u-width-full-line">
|
||||
<Heading
|
||||
size="5"
|
||||
@@ -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>
|
||||
|
||||
@@ -1,30 +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.'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -140,7 +141,8 @@
|
||||
await sdk.forConsole.billing.updateTaxId(org.$id, taxId);
|
||||
}
|
||||
trackEvent(Submit.CreditRedeem, {
|
||||
coupon: couponData.code
|
||||
coupon: couponData.code,
|
||||
campaign: couponData?.campaign
|
||||
});
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
@@ -182,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) {
|
||||
@@ -206,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"
|
||||
@@ -253,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"
|
||||
@@ -287,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,24 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { CardContainer, Empty, GridItem1, Id, PaginationWithLimit } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { onMount } from 'svelte';
|
||||
import Initial from '$lib/wizards/functions/cover.svelte';
|
||||
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
import {
|
||||
CardContainer,
|
||||
Empty,
|
||||
GridItem1,
|
||||
Id,
|
||||
PaginationWithLimit,
|
||||
SvgIcon
|
||||
} from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
import { marketplace } from '$lib/stores/marketplace.js';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Initial from '$lib/wizards/functions/cover.svelte';
|
||||
import CreateTemplate from '$lib/wizards/functions/createTemplate.svelte';
|
||||
import {
|
||||
templateConfig as templateConfigStore,
|
||||
template as templateStore
|
||||
} from '$lib/wizards/functions/store.js';
|
||||
import { marketplace } from '$lib/stores/marketplace.js';
|
||||
import { parseExpression } from 'cron-parser';
|
||||
import { onMount } from 'svelte';
|
||||
import { functionsList } from './store';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -89,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>
|
||||
@@ -106,7 +109,7 @@
|
||||
aria-hidden="true"
|
||||
use:tooltip={{
|
||||
content: `Next execution:
|
||||
${toLocaleDateTime(func.schedule)}`
|
||||
${toLocaleDateTime(parseExpression(func.schedule, { utc: true }).next().toString())}`
|
||||
}} />
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
@@ -1,67 +1,95 @@
|
||||
<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?.activeDeployment}
|
||||
{@const activeDeployment = data.activeDeployment}
|
||||
<div class="common-section">
|
||||
<Heading tag="h3" size="7">Active</Heading>
|
||||
@@ -78,60 +106,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,19 +123,15 @@
|
||||
}}>
|
||||
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>
|
||||
</DeploymentCard>
|
||||
{:else if $deploymentList.total}
|
||||
<Empty noMedia single>
|
||||
<Create secondary round>
|
||||
@@ -201,114 +172,79 @@
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
<div class="common-section">
|
||||
<Heading tag="h3" size="7">All</Heading>
|
||||
<div class="u-margin-block-start-40">
|
||||
<Heading tag="h3" size="7">All deployments</Heading>
|
||||
<div class="u-flex u-main-space-between is-not-mobile u-margin-block-start-24">
|
||||
<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>
|
||||
<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>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty single target="deployment" on:click>
|
||||
<div class="u-text-center">
|
||||
@@ -358,7 +294,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>
|
||||
@@ -11,10 +11,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 +31,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 +45,10 @@
|
||||
|
||||
$: if (!show) {
|
||||
files = undefined;
|
||||
active = false;
|
||||
active = true;
|
||||
entrypoint = $func.entrypoint;
|
||||
buildCommand = $func.commands;
|
||||
show = false;
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
@@ -71,13 +76,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 +93,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} />
|
||||
<InputText
|
||||
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,33 @@
|
||||
<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);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
logs = $deployment.buildLogs;
|
||||
@@ -44,100 +50,75 @@
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<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}
|
||||
<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,71 @@
|
||||
{/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`}>
|
||||
<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`}>
|
||||
<span class="text">Execute</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$func.logging}
|
||||
<div class="common-section">
|
||||
@@ -98,60 +210,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,381 @@
|
||||
<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.toString() : 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}
|
||||
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,151 @@
|
||||
<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"
|
||||
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,21 +1,23 @@
|
||||
<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';
|
||||
|
||||
@@ -40,7 +42,7 @@
|
||||
|
||||
<Container>
|
||||
<Heading tag="h2" size="5">Settings</Heading>
|
||||
{#if $func.version !== 'v3' && showAlert}
|
||||
{#if $func.version === 'v2' && showAlert}
|
||||
<Alert
|
||||
type="warning"
|
||||
dismissible
|
||||
@@ -81,5 +83,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,90 @@
|
||||
<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);
|
||||
} 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,
|
||||
|
||||
@@ -11,8 +11,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,175 @@
|
||||
<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';
|
||||
|
||||
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);
|
||||
}
|
||||
</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 === 'processing'}
|
||||
<DropListItem
|
||||
icon="x-circle"
|
||||
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}
|
||||
@@ -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: 416 KiB |
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 384 KiB |
|
After Width: | Height: | Size: 2.5 KiB |