feat: quick filter refactor PoC

This commit is contained in:
Arman
2025-03-10 11:44:34 +01:00
parent ae38e15c10
commit 1e071dfc34
5 changed files with 246 additions and 220 deletions
+120
View File
@@ -0,0 +1,120 @@
<script lang="ts">
import { capitalize } from '$lib/helpers/string';
import { IconChevronRight } from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Card, Layout, Selector } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
import { createEventDispatcher } from 'svelte';
import type { ValidOperators } from './store';
type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
export let filter: FilterData;
export let variant: 'checkbox' | 'radio' = 'checkbox';
const {
builders: { createMenu }
} = createMenubar();
const {
elements: { item: item, separator: separator },
builders: { createSubmenu: createSubmenu, createMenuRadioGroup, createCheckboxItem }
} = createMenu();
const {
elements: { checkboxItem: checkboxItem }
} = createCheckboxItem();
const {
elements: { radioGroup: radioGroup }
} = createMenuRadioGroup({});
const {
elements: { subMenu: subMenu, subTrigger: subTrigger }
} = createSubmenu();
const dispatch = createEventDispatcher();
</script>
<div use:melt={$subTrigger}>
<ActionMenu.Root>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
>{filter.title}</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
<div class="menu subMenu" use:melt={$subMenu}>
<Card.Base padding="xxxs">
<div use:melt={$radioGroup}>
{#each filter.options as option (option.value + option.checked)}
{#if variant === 'radio'}
<div use:melt={$item}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
option.checked = !option.checked;
dispatch('add', {
value: filter?.array
? null
: option.checked
? option.value
: null
});
}}>
{capitalize(option.label)}
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{:else}
<div use:melt={$checkboxItem}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
option.checked = !option.checked;
dispatch('add', {
value: filter?.array
? null
: option.checked
? option.value
: null
});
}}>
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox bind:checked={option.checked} />
{capitalize(option.label)}
</Layout.Stack>
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{/if}
{/each}
{#if filter.options.some((option) => option.checked)}
<div class="separator" use:melt={$separator} />
<div use:melt={$item}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
dispatch('clear');
}}>
Clear all
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{/if}
</div>
</Card.Base>
</div>
<style>
.menu {
min-width: 244px;
z-index: 20;
}
</style>
@@ -16,11 +16,11 @@
$: totalSize = humanFileSize((deployment?.buildSize ?? 0) + (deployment?.sourceSize ?? 0));
</script>
<Card isTile padding="s" radius="m" {variant}>
<Layout.Stack gap="l">
<Card isTile padding="m" radius="m" {variant}>
<Layout.Stack gap="xxl">
<Layout.GridFraction start={4} end={6}>
<Layout.Stack direction="row">
<Layout.Stack gap="xs">
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if activeDeployment}
<Typography.Title
size="s"
@@ -28,10 +28,11 @@
color="--fgcolor-neutral-primary">
Active deployment
</Typography.Title>
<Id value={deployment.$id}>
{deployment.$id}
</Id>
<div>
<Id value={deployment.$id}>
{deployment.$id}
</Id>
</div>
{:else}
<Typography.Title
size="s"
@@ -127,7 +128,7 @@
{#if $$slots.footer}
<span
style="margin-left: calc(-1* var(--space-7));margin-right: calc(-1* var(--space-7));width:auto;">
style="margin-left: calc(-1* var(--space-9));margin-right: calc(-1* var(--space-9));width:auto;">
<Divider />
</span>
<Layout.Stack direction="row-reverse">
@@ -1,18 +1,13 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Alert, EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { goto, invalidate } from '$app/navigation';
import { Alert, Empty, EmptySearch, PaginationWithLimit, ViewSelector } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { hoursToDays } from '$lib/helpers/date';
import { Container, ContainerHeader } from '$lib/layout';
import { Container } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
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 '../(components)/createActionMenu.svelte';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { base } from '$app/paths';
import { Filters, queries } from '$lib/components/filters';
import { writable } from 'svelte/store';
@@ -20,13 +15,11 @@
import { View } from '$lib/helpers/load';
import Table from './table.svelte';
import QuickFilters from './quickFilters.svelte';
import { Pill } from '$lib/elements';
import { tags } from '$lib/components/filters/store';
import { Layout } from '@appwrite.io/pink-svelte';
export let data;
const logs = getServiceLimit('logs');
const columns = writable<Column[]>([
{ id: '$id', title: 'Execution ID', type: 'string', show: true, width: 150 },
{
@@ -160,116 +153,43 @@
</script>
<Container>
<ContainerHeader title="Executions">
<svelte:fragment slot="tooltip" let:tier let:limit let:upgradeMethod>
<p class="u-bold">The {tier} plan has limits</p>
<ul>
<li>
{abbreviateNumber(limit)} function executions
</li>
<li>
{hoursToDays(logs)} of logs
</li>
</ul>
{#if $organization?.billingPlan === BillingPlan.FREE}
<p class="text">
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
>Upgrade</button>
to increase your resource limits.
</p>
{:else}
<p class="text">
After this amount, <button
class="link"
type="button"
on:click|preventDefault={() => ($showUsageRatesModal = true)}
>usage fees will apply</button
>.
</p>
{/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">
<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}
<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>
</Filters>
</div>
<div class="u-flex u-gap-16">
<ViewSelector view={View.Table} {columns} hideView allowNoColumns hideText />
<Button text on:click={clearAll}>Clear All</Button>
{/if}
</div>
</Filters> -->
<Layout.Stack gap="s" inline direction="row" alignItems="center">
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
<Button
event="execute_function"
href={`${base}/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}
disabled={!$func.$id || !$func?.deployment}>
<span class="text">Execute</span>
Execute
</Button>
</div>
</div>
<div class="is-only-mobile">
<div class="u-flex u-main-space-between u-margin-block-start-16">
<Button
text
on:click={() => (showMobileFilters = !showMobileFilters)}
ariaLabel="toggle filters">
<span class="icon-filter-line" />
<span class="text">Filters</span>
</Button>
<div class=" u-flex u-gap-16">
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
<Button
event="execute_function"
href={`${base}/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`}
disabled={!$func.$id || !$func?.deployment}>
<span class="text">Execute</span>
</Button>
</div>
</div>
<div
class:u-hide={!showMobileFilters}
class:u-flex={showMobileFilters}
class=" u-gap-8 u-flex-wrap u-margin-block-start-16">
<QuickFilters {columns} />
<Filters query={data.query} {columns} clearOnClick analyticsSource="executions_filters">
<svelte:fragment slot="mobile" let:disabled let:toggle>
<Pill
button
on:click={toggle}
{disabled}
class="u-flex u-gap-4 u-cross-center"
style="--p-tag-content-height: auto">
<!-- <span class="icon-filter-line" /> -->
<span class="text">More filters</span>
<span class="icon-cheveron-down" />
</Pill>
</svelte:fragment>
</Filters>
</div>
</div>
</Layout.Stack>
</Layout.Stack>
{#if !$func.logging}
<div class="common-section">
@@ -310,19 +230,14 @@
</div>
</EmptySearch>
{:else}
<EmptySearch>
<div class="u-text-center">
<p class="text u-line-height-1-5">
You have no execution logs. Create and activate a deployment to see it here.
</p>
<p class="text u-line-height-1-5">Need a hand? Learn more in our documentation.</p>
</div>
<div class="u-flex u-gap-16">
<Button text external href="https://appwrite.io/docs/products/functions/execution">
Documentation
</Button>
<Create secondary />
</div>
</EmptySearch>
<Empty
single
target="execution"
href="https://appwrite.io/docs/products/functions/execution"
on:click={() =>
goto(
`${base}/project-${$project.$id}/functions/function-${$func.$id}/executions/execute-function`
)}>
</Empty>
{/if}
</Container>
@@ -2,6 +2,7 @@
import { afterNavigate } from '$app/navigation';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { DropList, DropListItem } from '$lib/components';
import MenuItem from '$lib/components/filters/subMenu.svelte';
import {
addFilter,
queries,
@@ -13,7 +14,11 @@
} from '$lib/components/filters/store';
import { Pill, SelectSearchCheckbox } from '$lib/elements';
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';
export let columns: Writable<Column[]>;
@@ -181,103 +186,86 @@
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>
{#each [statusFilter, triggerFilter, methodFilter] as filter}
<DropList bind:show={filter.show} noArrow class="u-margin-block-start-16">
<Pill
button
on:click={() => (filter.show = !filter.show)}
event="apply_quick_filter"
eventData={{ source: 'function_execution', column: filter.title }}>
{#key filter.tag}
<span use:tagFormat>
{filter?.tag ?? filter.title}
</span>
{/key}
<span class:icon-cheveron-down={!filter.show} class:icon-cheveron-up={filter.show}>
</span>
</Pill>
<svelte:fragment slot="list">
{#each filter.options as option (option.value + option.checked)}
<SelectSearchCheckbox
padding={8}
bind:value={option.checked}
on:click={() => {
option.checked = !option.checked;
<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,
filter?.array ? null : option.checked ? option.value : null,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: []
);
}}>
{option.label}
</SelectSearchCheckbox>
{/each}
{#if filter.options.some((option) => option.checked)}
<DropListItem
padding={8}
on:click={() => {
filter.show = false;
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
{/each}
{#each [statusCodeFilter, createdAtFilter] as filter}
<DropList bind:show={filter.show} noArrow class="u-margin-block-start-16">
<Pill
button
on:click={() => (filter.show = !filter.show)}
event="apply_quick_filter"
eventData={{ source: 'function_execution', column: filter.title }}>
{#key filter.tag}
<span use:tagFormat>
{filter?.tag ?? filter.title}
</span>
{/key}
<span class:icon-cheveron-down={!filter.show} class:icon-cheveron-up={filter.show}>
</span>
</Pill>
<svelte:fragment slot="list">
{#each filter.options as option (option.value + option.checked)}
<DropListItem
padding={8}
on:click={() => {
filter.show = false;
}} />
{/each}
{#each [statusCodeFilter, createdAtFilter] as filter}
<MenuItem
variant="radio"
{filter}
on:add={(e) => {
console.log('test');
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
filter?.array ? null : option.value,
[]
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: []
);
}}>
{option.label}
</DropListItem>
{/each}
{#if filter?.tag}
<DropListItem
padding={8}
on:click={() => {
filter.show = false;
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
{/each}
}} />
{/each}
</Card.Base>
</div>
</div>
<style>
.menu {
min-width: 244px;
z-index: 20;
}
</style>
@@ -17,11 +17,13 @@
Card as PinkCard,
Image,
Badge,
Divider
Divider,
Icon
} from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { capitalize } from '$lib/helpers/string';
import { IconExternalLink } from '@appwrite.io/pink-icons-svelte';
$: buttonDisabled =
isCloud && isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total);
@@ -84,7 +86,7 @@
href={`https://github.com/${$template.providerOwner}/${$template.providerRepositoryId}`}
external>
View source
<span class="icon-external-link" />
<Icon icon={IconExternalLink} size="s" slot="end" />
</Button>
{#if $canWriteFunctions}
<ContainerButton