mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1160 from appwrite/feat-execution-filter
feat: execution filter
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
}`}>
|
||||
{#if $$slots.list}
|
||||
<section
|
||||
class:u-max-width-none={noMaxWidthList}
|
||||
class:u-overflow-y-auto={scrollable}
|
||||
class:u-max-height-200={scrollable}
|
||||
class="drop-section">
|
||||
class="drop-section"
|
||||
style={noMaxWidthList ? 'max-inline-size: 100%' : ''}>
|
||||
<ul class="drop-list">
|
||||
<slot name="list" />
|
||||
</ul>
|
||||
|
||||
@@ -6,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">
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
@@ -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,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>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let disabled: boolean;
|
||||
export let buttonText: string;
|
||||
export let buttonMethod: () => void | Promise<void>;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let icon = 'plus';
|
||||
export let showIcon = true;
|
||||
@@ -31,7 +32,8 @@
|
||||
secondary={buttonType === 'secondary'}
|
||||
on:click={buttonMethod}
|
||||
event={buttonEvent}
|
||||
{disabled}>
|
||||
{disabled}
|
||||
href={buttonHref}>
|
||||
{#if showIcon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
export let buttonText: string = null;
|
||||
export let buttonMethod: () => void = null;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let buttonDisabled = false;
|
||||
|
||||
@@ -177,7 +178,8 @@
|
||||
disabled={isButtonDisabled}
|
||||
{buttonText}
|
||||
{buttonEvent}
|
||||
{buttonMethod} />
|
||||
{buttonMethod}
|
||||
{buttonHref} />
|
||||
{/if}
|
||||
</slot>
|
||||
</header>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { getServiceLimit, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
@@ -163,9 +162,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<div class="code-panel-content grid-1-2" style="u-grid">
|
||||
<div
|
||||
class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16"
|
||||
class:theme-dark={$app.themeInUse === 'light'}>
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h3" size="6">Request</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
@@ -276,9 +273,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0"
|
||||
class:theme-dark={$app.themeInUse === 'light'}>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<Heading tag="h3" size="6">Response</Heading>
|
||||
<div class="u-sep-block-end">
|
||||
<Tabs>
|
||||
|
||||
@@ -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';
|
||||
|
||||
+140
-136
@@ -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}
|
||||
|
||||
+9
-2
@@ -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()
|
||||
])
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+380
@@ -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>
|
||||
+18
@@ -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
|
||||
};
|
||||
};
|
||||
+151
@@ -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}
|
||||
+6
-5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user