Merge branch '1.6.x' of github.com:appwrite/console into mock-phone-numbers

This commit is contained in:
Arman
2024-07-03 12:56:06 +02:00
73 changed files with 2244 additions and 720 deletions
+25 -4
View File
@@ -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",
+2 -1
View File
@@ -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",
+3
View File
@@ -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>
+2 -2
View File
@@ -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>
+50 -162
View File
@@ -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">
+85 -33
View File
@@ -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
View File
@@ -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';
+194 -2
View File
@@ -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();
+51
View File
@@ -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}
+5 -1
View File
@@ -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">
+3 -1
View File
@@ -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>
+6 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
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>
+1 -1
View File
@@ -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}
+6 -3
View File
@@ -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>
+1 -1
View File
@@ -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>>(
+3 -1
View File
@@ -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}
+3 -1
View File
@@ -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>
+2 -7
View File
@@ -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>
+86 -12
View File
@@ -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>
+1 -3
View File
@@ -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>
+12 -2
View File
@@ -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>
+90 -1
View File
@@ -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,
+45 -27
View File
@@ -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}
+14
View File
@@ -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

+14
View File
@@ -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

+14
View File
@@ -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

+14
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB