Merge pull request #1160 from appwrite/feat-execution-filter

feat: execution filter
This commit is contained in:
Arman
2024-07-02 17:01:55 +02:00
committed by GitHub
23 changed files with 1044 additions and 315 deletions
+1 -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:khushboo-console@0.0.3",
"@appwrite.io/pink": "0.23.0",
"@appwrite.io/pink-icons": "0.23.0",
"@popperjs/core": "^2.11.8",
+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>
+33 -52
View File
@@ -6,13 +6,14 @@
InputText,
InputTags,
FormList,
InputSelectCheckbox
InputSelectCheckbox,
InputDateTime
} from '$lib/elements/forms';
import { createEventDispatcher, onMount } from 'svelte';
import { tags, queries, type TagValue, operators, addFilter } 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' */
@@ -21,6 +22,7 @@
export let columnId: string | null = null;
export let arrayValues: string[] = [];
export let operatorKey: string | null = null;
export let singleCondition = false;
$: column = $columns.find((c) => c.id === columnId) as Column;
@@ -41,6 +43,12 @@
onMount(() => {
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
const today = new Date();
console.log(today.toISOString());
value = today.toISOString();
console.log(value);
}
});
function addFilterAndReset() {
@@ -51,26 +59,18 @@
arrayValues = [];
}
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))
);
}
const dispatch = createEventDispatcher<{
clear: void;
apply: { applied: number };
}>();
dispatch('apply', { applied: $tags.length });
// $: if (column?.type === 'datetime' && !value) {
// const today = new Date();
// console.log(today.toISOString());
// value = today.toISOString();
// console.log('value', value);
// }
</script>
<div>
@@ -138,48 +138,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" noMargin submit>
<i class="icon-plus" />
Add condition
</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">
+49 -22
View File
@@ -11,6 +11,7 @@
export let columns: Writable<Column[]>;
export let disabled = false;
export let fullWidthMobile = false;
export let singleCondition = false;
const parsedQueries = queryParamToMap(query);
queries.set(parsedQueries);
@@ -39,7 +40,7 @@
}
function apply() {
if (selectedColumn && operatorKey && value) {
if (selectedColumn && operatorKey && (value || arrayValues.length)) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
selectedColumn = null;
value = null;
@@ -53,20 +54,31 @@
selectedColumn = null;
}
$: isButtonDisabled = $queriesAreDirty ? false : !selectedColumn || !operatorKey || !value;
$: 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}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
<span class="inline-tag">
{applied}
</span>
{/if}
</Button>
<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>
@@ -76,11 +88,16 @@
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">
<Button text on:click={clearAll}>Clear all</Button>
{#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>
@@ -89,15 +106,20 @@
</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 name="mobile" {disabled} toggle={toggleMobileModal}>
<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"
@@ -110,10 +132,15 @@
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>
{#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>
+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';
+164 -70
View File
@@ -5,6 +5,7 @@ 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;
@@ -12,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;
@@ -39,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;
});
}
@@ -96,7 +104,6 @@ export function addFilter(
) {
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 });
@@ -105,74 +112,133 @@ export function addFilter(
}
}
export 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']
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;
@@ -182,3 +248,31 @@ function formatArray(array: string[]) {
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}
+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>
+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>
+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>
+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>
@@ -25,7 +25,7 @@
TableRow,
TableScroll
} from '$lib/elements/table';
import { deploymentList, execute, func, proxyRuleList, showFunctionExecute } from './store';
import { deploymentList, func, proxyRuleList } from './store';
import { Container, ContainerHeader } from '$lib/layout';
import { app } from '$lib/stores/app';
import { calculateSize, humanFileSize } from '$lib/helpers/sizeConvertion';
@@ -42,6 +42,7 @@
import DeploymentDomains from './deploymentDomains.svelte';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { readOnly } from '$lib/stores/billing';
import { project } from '../../store';
export let data;
@@ -168,10 +169,7 @@
</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
</Button>
@@ -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';
@@ -1,46 +1,96 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import {
Alert,
DropList,
DropListItem,
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 Delete from './delete.svelte';
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;
let showDropdown = [];
let showDelete = false;
const logs = getServiceLimit('logs');
let selectedExecution: Models.Execution = null;
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: 'string',
show: true,
width: 100,
array: true,
format: 'integer'
},
{
id: 'requestPath',
title: 'Path',
type: 'string',
show: true,
width: 90,
format: 'string'
},
{
id: 'duration',
title: 'Duration',
type: 'string',
show: true,
width: 80,
format: 'integer'
}
]);
onMount(() => {
return sdk.forConsole.client.subscribe('console', (response) => {
@@ -49,25 +99,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>
@@ -96,6 +131,66 @@
{/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} 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 showColsTextMobile />
<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">
@@ -111,95 +206,8 @@
</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>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each data.executions.executions as execution, index (execution.$id)}
<TableRowButton>
<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 === 'scheduled' ||
status === 'processing' ||
status === 'pending'}
danger={status === 'failed'}
info={status === 'completed'}>
{#if status === 'scheduled'}
<span class="icon-clock" aria-hidden="true" />
{/if}
{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>
<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>
</TableRowButton>
{/each}
</TableBody>
</TableScroll>
{#if data?.executions?.total}
<Table columns={$columns} {data} />
<PaginationWithLimit
name="Executions"
@@ -223,7 +231,3 @@
</EmptySearch>
{/if}
</Container>
{#if selectedExecution}
<Delete {selectedExecution} bind:showDelete />
{/if}
@@ -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()
])
};
};
@@ -9,7 +9,7 @@
import type { Models } from '@appwrite.io/console';
export let showDelete = false;
export let selectedExecution: Models.Execution = null;
export let selectedExecution: Models.Execution;
async function handleSubmit() {
try {
@@ -0,0 +1,380 @@
<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 } 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][] = [['', '']];
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 : null
);
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 !== 'v3'}
<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>
<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>
<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>
<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">
<div class="u-flex-vertical u-gap-8">
<p class="u-color-text-offline">Deployment ID</p>
<span>
<Pill>{func.deployment}</Pill>
</span>
</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,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>
@@ -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;