mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: executions logs
This commit is contained in:
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
+114
@@ -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>
|
||||
+127
@@ -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>
|
||||
+1
-25
@@ -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 />
|
||||
|
||||
+50
-85
@@ -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>
|
||||
|
||||
+105
-58
@@ -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}
|
||||
|
||||
+50
-28
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
+2
-29
@@ -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">
|
||||
|
||||
+51
-97
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user