feat: executions logs

This commit is contained in:
Arman
2025-03-10 17:26:47 +01:00
parent 40a3386a61
commit ca23b1f0bb
15 changed files with 652 additions and 357 deletions
@@ -0,0 +1,17 @@
<script lang="ts">
import { ActionMenu } from '@appwrite.io/pink-svelte';
import FiltersModal from './filtersModal.svelte';
import type { Writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
export let columns: Writable<Column[]>;
let show = false;
</script>
<ActionMenu.Root>
<ActionMenu.Item.Button on:click={() => (show = true)}>Custom filters</ActionMenu.Item.Button>
</ActionMenu.Root>
{#if show}
<FiltersModal bind:show {columns} analyticsSource="custom-filters" />
{/if}
@@ -0,0 +1,73 @@
<script lang="ts">
import { Submit, trackEvent } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import type { Writable } from 'svelte/store';
import Modal from '../modal.svelte';
import Content from './content.svelte';
import { addFilter, queries, queriesAreDirty, tags, ValidOperators } from './store';
import type { Column } from '$lib/helpers/types';
export let show = false;
export let columns: Writable<Column[]>;
export let analyticsSource = '';
export let clearOnClick = false;
/* eslint @typescript-eslint/no-explicit-any: 'off' */
let value: any = null;
let selectedColumn: string | null = null;
let operatorKey: string | null = null;
let arrayValues: string[] = [];
let applied = $tags.length;
function clearAll() {
selectedColumn = null;
queries.clearAll();
if (clearOnClick) {
trackEvent(Submit.FilterClear, { source: analyticsSource });
queries.apply();
}
}
function apply() {
if (
selectedColumn &&
operatorKey &&
(operatorKey === ValidOperators.IsNotNull ||
operatorKey === ValidOperators.IsNull ||
value ||
arrayValues.length)
) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
selectedColumn = null;
value = null;
operatorKey = null;
arrayValues = [];
}
queries.apply();
}
$: isButtonDisabled = $queriesAreDirty
? false
: !selectedColumn ||
!operatorKey ||
(!value &&
!arrayValues.length &&
operatorKey !== ValidOperators.IsNotNull &&
operatorKey !== ValidOperators.IsNull);
</script>
<Modal title="Filters" bind:show>
<span slot="description"> Apply filter rules to refine the table view </span>
<Content
{columns}
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
on:clear={() => (applied = 0)} />
<svelte:fragment slot="footer">
<Button size="s" text on:click={clearAll}>Clear all</Button>
<Button size="s" on:click={apply} disabled={isButtonDisabled}>Apply</Button>
</svelte:fragment>
</Modal>
+3
View File
@@ -1,3 +1,6 @@
export { default as Filters } from './filters.svelte';
export { default as TagList } from './tagList.svelte';
export { default as FilterMenu } from './menu.svelte';
export { default as FilterSubMenu } from './subMenu.svelte';
export { default as CustomFilters } from './customFilters.svelte';
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { Card, Divider, Icon } from '@appwrite.io/pink-svelte';
import { tags } from './store';
import { IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
const {
elements: { trigger: trigger, menu: menu, separator: separator }
} = createMenu();
</script>
<div use:melt={$menubar}>
<div use:melt={$trigger}>
{#if $tags.length}
<Button secondary badge={`${$tags.length}`}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
</div>
<div class="menu" use:melt={$menu}>
<Card.Base padding="xxxs">
<slot />
<div use:melt={$separator}>
<Divider />
</div>
<slot name="end" />
</Card.Base>
</div>
</div>
<style>
.menu {
min-width: 244px;
z-index: 20;
}
</style>
@@ -183,31 +183,6 @@
<Layout.Stack direction="row" gap="s" wrap="wrap">
{#if data.deploymentList.total}
<QuickFilters {columns} />
<Filters
query={data.query}
{columns}
let:disabled
let:toggle
singleCondition
analyticsSource="function_filters">
<div class="u-flex u-gap-4">
<Button
text
on:click={toggle}
{disabled}
ariaLabel="open filter"
compact={!$tags?.length}>
<Icon icon={IconFilterLine} size="s" slot="start" />
<span class="text">More filters</span>
</Button>
{#if $tags?.length}
<div
style="flex-basis:1px; background-color:hsl(var(--border)); width: 1px">
</div>
<Button text on:click={clearAll}>Clear all</Button>
{/if}
</div>
</Filters>
{/if}
</Layout.Stack>
<Layout.Stack direction="row" gap="s" inline>
@@ -0,0 +1,114 @@
<script lang="ts">
import { Card } from '$lib/components';
import { Link } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import {
Badge,
Divider,
Input,
Layout,
Table,
Tabs,
Typography
} from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
export let selectedLog: Models.Execution;
let requestTab: 'parameters' | 'headers' = 'parameters';
let parameters = [];
onMount(() => {
try {
// Add dummy base URL to parse relative paths
const url = new URL(selectedLog.requestPath, 'http://dummy.local');
if (url.search) {
parameters = Array.from(url.searchParams.entries()).map(([name, value]) => ({
name,
value: decodeURIComponent(value)
}));
}
} catch (error) {
parameters = [];
}
});
onMount(() => {
if (parameters?.length) {
requestTab = 'parameters';
} else if (selectedLog.requestHeaders?.length) {
requestTab = 'headers';
}
});
</script>
<Layout.Stack>
<Layout.Stack gap="none">
<Tabs.Root variant="secondary" let:root>
<Tabs.Item.Button
{root}
active={requestTab === 'parameters'}
on:click={() => (requestTab = 'parameters')}>
Parameters
<Badge variant="secondary" size="s" content={parameters?.length?.toString()} />
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={requestTab === 'headers'}
on:click={() => (requestTab = 'headers')}>
Headers <Badge
variant="secondary"
size="s"
content={selectedLog?.requestHeaders?.length?.toString()} />
</Tabs.Item.Button>
</Tabs.Root>
<Divider />
</Layout.Stack>
{#if requestTab === 'parameters'}
{#if parameters?.length}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Key</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each parameters as parameter}
<Table.Row>
<Table.Cell>{parameter.name}</Table.Cell>
<Table.Cell>{parameter.value}</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Code>No parameters found.</Typography.Code>
</Card>
{/if}
{:else if requestTab === 'headers'}
{#if selectedLog.requestHeaders?.length}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Key</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each selectedLog.requestHeaders as request}
<Table.Row>
<Table.Cell>{request.name}</Table.Cell>
<Table.Cell>{request.value}</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
</span>
</Input.Helper>
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Code>No headers found.</Typography.Code>
</Card>
{/if}
{/if}
</Layout.Stack>
@@ -0,0 +1,127 @@
<script lang="ts">
import { Card } from '$lib/components';
import { Link } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import {
Badge,
Divider,
InlineCode,
Input,
Layout,
Logs,
Table,
Tabs,
Typography
} from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
export let selectedLog: Models.Execution;
let responseTab: 'logs' | 'errors' | 'headers' | 'body' = 'logs';
onMount(() => {
if (selectedLog?.logs) {
responseTab = 'logs';
} else if (selectedLog.requestHeaders?.length) {
responseTab = 'headers';
}
});
</script>
<Layout.Stack>
<Layout.Stack gap="none">
<Tabs.Root variant="secondary" let:root>
<Tabs.Item.Button
{root}
active={responseTab === 'logs'}
on:click={() => (responseTab = 'logs')}>
Logs
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={responseTab === 'errors'}
on:click={() => (responseTab = 'errors')}>
Errors
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={responseTab === 'headers'}
on:click={() => (responseTab = 'headers')}>
Headers <Badge
variant="secondary"
size="s"
content={selectedLog?.responseHeaders?.length?.toString()} />
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={responseTab === 'body'}
on:click={() => (responseTab = 'body')}>
Body
</Tabs.Item.Button>
</Tabs.Root>
<Divider />
</Layout.Stack>
{#if responseTab === 'logs'}
{#if selectedLog.logs}
<Logs logs={selectedLog.logs} />
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Code>No logs found.</Typography.Code>
</Card>
{/if}
{:else if responseTab === 'errors'}
{#if selectedLog.errors}
<Logs logs={selectedLog.errors} />
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Code>No errors found.</Typography.Code>
</Card>
{/if}
{:else if responseTab === 'headers'}
{#if selectedLog.responseHeaders?.length}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Key</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each selectedLog.responseHeaders as request}
<Table.Row>
<Table.Cell>{request.name}</Table.Cell>
<Table.Cell>{request.value}</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
<Input.Helper state="default">
<span>
Missing headers? Check the <Link variant="muted" href="#" external>docs</Link> to
see the supported data and how to log it.
</span>
</Input.Helper>
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Code>No headers found.</Typography.Code>
</Card>
{/if}
{:else if responseTab === 'body'}
{#if selectedLog.responseBody}
<Logs logs={selectedLog.responseBody} />
{:else}
<Card isTile padding="xs" radius="s">
<Typography.Text
>Body data is not captured by Appwrite for your user's security and privacy. To
display body data in the Logs tab, use <InlineCode
code="context.log()"
size="s" />. <Link
external
href="https://appwrite.io/docs/products/functions/develop#logging"
variant="muted">
Learn more</Link
>.
</Typography.Text>
</Card>
{/if}
{/if}
</Layout.Stack>
@@ -31,31 +31,7 @@
<Container>
<Layout.Stack direction="row" alignItems="center" justifyContent="space-between">
<QuickFilters {columns} />
<!-- <Filters
query={data.query}
{columns}
let:disabled
let:toggle
clearOnClick
analyticsSource="executions_filters">
<div class="u-flex u-gap-4">
<Button
text
on:click={toggle}
{disabled}
ariaLabel="open filter"
noMargin={!$tags?.length}>
<span class="icon-filter-line" />
<span class="text">More filters</span>
</Button>
{#if $tags?.length}
<div
style="flex-basis: 1px; background-color: hsl(var(--border)); width: 1px;">
</div>
<Button text on:click={clearAll}>Clear All</Button>
{/if}
</div>
</Filters> -->
<Layout.Stack gap="s" inline direction="row" alignItems="center">
{#if data?.executions?.total}
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
@@ -11,11 +11,9 @@
type TagValue
} from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { Card, Icon } from '@appwrite.io/pink-svelte';
import { melt, createMenubar } from '@melt-ui/svelte';
import { type Writable } from 'svelte/store';
import { Button } from '$lib/elements/forms';
import { IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import Menu from '$lib/components/filters/menu.svelte';
import { CustomFilters } from '$lib/components/filters';
export let columns: Writable<Column[]>;
@@ -183,86 +181,53 @@
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter($columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
const {
elements: { trigger: trigger, menu: menu }
} = createMenu();
</script>
<div use:melt={$menubar}>
<div use:melt={$trigger}>
{#if $tags.length}
<Button secondary badge={`${$tags.length}`}>
<Icon icon={IconFilterLine} slot="start" />
Filters
</Button>
{:else}
<Button secondary>
<Icon icon={IconFilterLine} slot="start" />
Filters
</Button>
{/if}
</div>
<div class="menu" use:melt={$menu}>
<Card.Base padding="xxxs" shadow={true}>
{#each [statusFilter, triggerFilter, methodFilter] as filter}
<MenuItem
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
{#each [statusCodeFilter, createdAtFilter] as filter}
<MenuItem
variant="radio"
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
</Card.Base>
</div>
</div>
<style>
.menu {
min-width: 244px;
z-index: 20;
}
</style>
<Menu>
{#each [statusFilter, triggerFilter, methodFilter] as filter}
<MenuItem
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options.filter((opt) => opt.checked).map((opt) => opt.value) ??
[])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
{#each [statusCodeFilter, createdAtFilter] as filter}
<MenuItem
variant="radio"
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options.filter((opt) => opt.checked).map((opt) => opt.value) ??
[])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
@@ -11,11 +11,16 @@
Layout,
Sheet,
Tag,
Typography
Typography,
Tooltip,
Status
} from '@appwrite.io/pink-svelte';
import { timeFromNow } from '$lib/helpers/date';
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
import { capitalize } from '$lib/helpers/string';
import { Copy } from '$lib/components';
import { logStatusConverter } from './store';
import LogsResponse from './(components)/LogsResponse.svelte';
import LogsRequest from './(components)/LogsRequest.svelte';
export let open = false;
export let selectedLogId: string;
@@ -40,14 +45,13 @@
$: selectedLog = logs?.find((log) => log.$id === selectedLogId);
$: isFirstLog = logs.findIndex((log) => log.$id === selectedLogId) === 0;
$: isLastLog = logs.findIndex((log) => log.$id === selectedLogId) === logs.length - 1;
$: console.log(selectedLog);
</script>
<Sheet bind:open>
<div slot="header" style:width="100%">
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" gap="m" alignItems="center">
<Typography.Text variant="m-400">Log ID</Typography.Text>
<Typography.Text variant="m-400">Execution ID</Typography.Text>
<Copy value={selectedLog?.$id}>
<Tag size="s" variant="code">{selectedLog?.$id}</Tag>
</Copy>
@@ -66,66 +70,109 @@
<Layout.Stack gap="xl">
<Accordion title="Details" open>
<Layout.Stack gap="xl">
<Layout.Stack direction="row">
<Layout.Grid columnsXS={2} columns={4} gap="xxxl" rowGap="xl">
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Method
</Typography.Text>
<Typography.Text variant="m-400">
{selectedLog.requestMethod}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Status
</Typography.Text>
<span>
<Badge
content={selectedLog.responseStatusCode.toString()}
variant="secondary"
type={selectedLog?.responseStatusCode >= 400
? 'error'
: 'success'} />
</span>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Created
</Typography.Text>
<Typography.Text variant="m-400">
{capitalize(timeFromNow(selectedLog.$createdAt))}
</Typography.Text>
</Layout.Stack>
</Layout.Grid>
</Layout.Stack>
<Layout.Grid
columnsXXS={1}
columnsXS={3}
columnsS={3}
columns={5}
gap="xxxl"
rowGap="xl">
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Method
</Typography.Text>
<Typography.Text variant="m-400">
{selectedLog.requestMethod}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Status code
</Typography.Text>
<span>
<Badge
content={selectedLog.responseStatusCode.toString()}
variant="secondary"
type={selectedLog?.responseStatusCode >= 400
? 'error'
: selectedLog.responseStatusCode === 0
? undefined
: 'success'} />
</span>
</Layout.Stack>
<Layout.Stack gap="xs" inline alignItems="flex-start">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Path
</Typography.Text>
<div>
<InteractiveText
text={selectedLog.requestPath}
variant="copy-code"
isVisible />
</div>
</Layout.Stack>
<Layout.Stack gap="xs" inline alignItems="flex-start">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Path
</Typography.Text>
<div>
<InteractiveText
text={selectedLog.requestPath}
variant="copy-code"
isVisible />
</div>
</Layout.Stack>
</Layout.Grid>
<Layout.Grid
columnsXXS={1}
columnsXS={3}
columnsS={4}
columns={4}
gap="xxxl"
rowGap="xl">
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Status
</Typography.Text>
<Tooltip
disabled={!selectedLog?.scheduledAt ||
selectedLog.status !== 'scheduled'}
maxWidth="400px">
<div>
<Status
status={logStatusConverter(selectedLog.status)}
label={capitalize(selectedLog.status)}>
{capitalize(selectedLog.status)}
</Status>
</div>
<span slot="tooltip">
{`Scheduled to execute on ${toLocaleDateTime(selectedLog.scheduledAt)}`}
</span>
</Tooltip>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Duration
</Typography.Text>
<Typography.Text variant="m-400">
{calculateTime(selectedLog.duration)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Triggered
</Typography.Text>
<Typography.Text variant="m-400">
{capitalize(selectedLog.trigger)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Created
</Typography.Text>
<Typography.Text variant="m-400">
{capitalize(timeFromNow(selectedLog.$createdAt))}
</Typography.Text>
</Layout.Stack>
</Layout.Grid>
</Layout.Stack>
</Accordion>
<Accordion title="Request" open>
<!-- <LogsRequest {selectedLog} /> -->
<LogsRequest {selectedLog} />
</Accordion>
<Accordion title="Response" open hideDivider>
<!-- <LogsResponse {selectedLog} /> -->
<LogsResponse {selectedLog} />
</Accordion>
</Layout.Stack>
{/if}
@@ -3,29 +3,15 @@ import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Execution ID', type: 'string', show: true, width: 150 },
{
id: '$createdAt',
title: 'Created',
type: 'datetime',
id: 'requestPath',
title: 'Path',
type: 'string',
show: true,
width: 120,
format: 'datetime',
elements: [
{
value: 5 * 60 * 1000,
label: 'last 5 minutes'
},
{
value: 60 * 60 * 1000,
label: 'last 1 hour'
},
{
value: 24 * 60 * 60 * 1000,
label: 'last 24 hours'
}
]
width: 90,
format: 'string'
},
{
id: 'trigger',
title: 'Trigger',
@@ -79,14 +65,7 @@ export const columns = writable<Column[]>([
],
hide: true
},
{
id: 'requestPath',
title: 'Path',
type: 'string',
show: true,
width: 90,
format: 'string'
},
{
id: 'status',
title: 'Status',
@@ -118,5 +97,48 @@ export const columns = writable<Column[]>([
label: 'more than 30 seconds'
}
]
},
{
id: '$createdAt',
title: 'Created',
type: 'datetime',
show: true,
width: 120,
format: 'datetime',
elements: [
{
value: 5 * 60 * 1000,
label: 'last 5 minutes'
},
{
value: 60 * 60 * 1000,
label: 'last 1 hour'
},
{
value: 24 * 60 * 60 * 1000,
label: 'last 24 hours'
}
]
}
]);
export function logStatusConverter(status: string) {
// ['completed', 'failed', 'waiting', 'scheduled', 'processing', 'cancelled']
switch (status) {
case 'completed':
return 'complete';
case 'scheduled':
return 'processing';
case 'processing':
return 'processing';
case 'waiting':
return 'waiting';
case 'peding':
return 'pending';
case 'cancelled':
return 'waiting';
case 'failed':
return 'failed';
}
}
@@ -7,35 +7,13 @@
import Sheet from './sheet.svelte';
import { capitalize } from '$lib/helpers/string';
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
import { logStatusConverter } from './store';
export let columns: Column[];
export let logs: Models.ExecutionList;
let openSheet = false;
let selectedLogId: string = null;
$: console.log(columns);
export function logStatusConverter(status: string) {
// ['completed', 'failed', 'waiting', 'scheduled', 'processing', 'cancelled']
switch (status) {
case 'completed':
return 'complete';
case 'scheduled':
return 'ready';
case 'processing':
return 'processing';
case 'waiting':
return 'waiting';
case 'peding':
return 'pending';
case 'cancelled':
return 'waiting';
case 'failed':
return 'failed';
}
}
</script>
<Table.Root>
@@ -97,12 +75,7 @@
<Status
status={logStatusConverter(status)}
label={capitalize(status)}>
{#if status === 'scheduled'}
<span class="icon-clock" aria-hidden="true" />
{timeFromNow(log.scheduledAt)}
{:else}
{status}
{/if}
{status}
</Status>
</div>
<span slot="tooltip">
@@ -1,19 +1,18 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { CustomFilters } from '$lib/components/filters';
import Menu from '$lib/components/filters/menu.svelte';
import {
addFilter,
queries,
queryParamToMap,
tagFormat,
tags,
ValidOperators,
type TagValue
} from '$lib/components/filters/store';
import { SelectSearchCheckbox } from '$lib/elements';
import SubMenu from '$lib/components/filters/subMenu.svelte';
import type { Column } from '$lib/helpers/types';
import { IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Icon, Popover, Tag } from '@appwrite.io/pink-svelte';
import { type Writable } from 'svelte/store';
export let columns: Writable<Column[]>;
@@ -182,96 +181,51 @@
}
</script>
{#each [typeFilter] as filter}
<Popover let:toggle let:showing padding="none">
<Tag
on:click={toggle}
event="apply_quick_filter"
eventData={{ source: 'function_deployments', column: filter.title }}>
{#key filter.tag}
<span use:tagFormat>
{filter?.tag ?? filter.title}
</span>
{/key}
<Icon icon={showing ? IconChevronUp : IconChevronDown} slot="end" size="s" />
</Tag>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#each filter.options as option (option.value + option.checked)}
<SelectSearchCheckbox
bind:value={option.checked}
on:click={() => {
option.checked = !option.checked;
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
filter?.array ? null : option.checked ? option.value : null,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: []
);
}}>
{option.label}
</SelectSearchCheckbox>
{/each}
{#if filter.options.some((option) => option.checked)}
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</svelte:fragment>
</Popover>
{/each}
{#each [sizeFilter] as filter}
<Popover padding="none" let:toggle let:showing>
<Tag
on:click={toggle}
event="apply_quick_filter"
eventData={{ source: 'function_deployments', column: filter.title }}>
{#key filter.tag}
<span use:tagFormat>
{filter?.tag ?? filter.title}
</span>
{/key}
<Icon icon={showing ? IconChevronUp : IconChevronDown} slot="end" size="s" />
</Tag>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
{#each filter.options as option (option.value + option.checked)}
<ActionMenu.Item.Button
on:click={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
filter?.array ? null : option.value,
[]
);
toggle(e);
}}>
{option.label}
</ActionMenu.Item.Button>
{/each}
{#if filter?.tag}
<ActionMenu.Item.Button
on:click={(e) => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
toggle(e);
}}>
Clear selection
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</svelte:fragment>
</Popover>
{/each}
<Menu>
{#each [typeFilter] as filter}
<SubMenu
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options.filter((opt) => opt.checked).map((opt) => opt.value) ??
[])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
{#each [sizeFilter] as filter}
<SubMenu
variant="radio"
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options.filter((opt) => opt.checked).map((opt) => opt.value) ??
[])
: []
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}} />
{/each}
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
@@ -20,6 +20,15 @@
const columns = writable<Column[]>([
{ id: '$id', title: 'Log ID', type: 'string', show: true, width: 150 },
{
id: 'requestPath',
title: 'Path',
type: 'string',
show: true,
width: 90,
format: 'string'
},
{
id: '$createdAt',
title: 'Created',
@@ -41,15 +50,6 @@
label: 'last 24 hours'
}
]
},
{
id: 'requestPath',
title: 'Path',
type: 'string',
show: true,
width: 90,
format: 'string'
}
]);
let search = '';
@@ -80,7 +80,7 @@
</Layout.Stack>
<Layout.Stack gap="xs" inline>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Status
Status code
</Typography.Text>
<span>
<Badge