refactor: quick filters, centrilize logic, remove duplicated code

This commit is contained in:
Arman
2025-03-11 18:42:42 +01:00
parent 008240df2f
commit 3472e9907e
14 changed files with 343 additions and 865 deletions
+1
View File
@@ -3,4 +3,5 @@ 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 { default as QuickFilters } from './quickFilters.svelte';
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
@@ -0,0 +1,67 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import MenuItem from '$lib/components/filters/subMenu.svelte';
import { queryParamToMap } from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
import Menu from '$lib/components/filters/menu.svelte';
import { CustomFilters } from '$lib/components/filters';
import { setFiltersOnNavigate } from '$lib/components/filters/setFilters';
import { addFilterAndApply, buildFilterCol } from '$lib/components/filters/quickFilters';
export let columns: Writable<Column[]>;
export let analyticsSource: string;
let filterCols = $columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options);
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
const localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
setFiltersOnNavigate(localTags, filterCols, $columns);
});
</script>
<Menu>
{#each filterCols as filter}
{#if filter.options}
<MenuItem
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
filter.tag = null;
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
@@ -0,0 +1,83 @@
import type { Column } from '$lib/helpers/types';
import { get } from 'svelte/store';
import { addFilter, queries, tags, ValidOperators } from './store';
import { Submit, trackEvent } from '$lib/actions/analytics';
export type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
export function buildFilterCol(col: Column, customOperator = null): FilterData {
return {
title: col.title,
id: col.id,
show: false,
array: col?.array,
tag: null,
operator: customOperator ?? ValidOperators.Equal,
options: col?.elements?.map((element) => {
return {
value: (element?.value ?? element) as string,
label: (element?.label ?? element) as string,
checked: false
};
})
};
}
export function addFilterAndApply(
colId: string,
colTitle: string,
operator: ValidOperators,
value: string,
arrayValues: string[] = [],
columns: Column[],
analyticsSource: string
) {
const tagList = get(tags).filter((tag) => tag.tag.includes(colTitle));
tagList.forEach((tag) => queries.removeFilter(tag));
if (value || arrayValues?.length) {
if (colId === 'sourceSize' || colId === 'buildSize') {
addSizeFilter(value, colId, columns);
} else if (colId === 'statusCode') {
addStatusCodeFilter(value, colId, columns);
} else if (colId === '$createdAt' || colId === '$updatedAt' || colId === 'buildDuration') {
addDateFilter(value, colId, columns);
} else {
addFilter(columns, colId, operator, value, arrayValues);
}
}
queries.apply();
trackEvent(Submit.ApplyQuickFilter, {
source: analyticsSource,
column: colId,
value: value || arrayValues.join(', ')
});
}
export function resetOptions(filter: FilterData) {
filter.options.forEach((option) => {
option.checked = false;
});
}
export function addStatusCodeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.LessThanOrEqual, parseInt(value));
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, parseInt(value) - 99);
}
export function addDateFilter(value: string, colId: string, columns: Column[]) {
const now = new Date();
const isoValue = new Date(now.getTime() - parseInt(value));
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter(columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
export function addSizeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
+133
View File
@@ -0,0 +1,133 @@
import type { Column } from '$lib/helpers/types';
import { resetOptions, type FilterData } from './quickFilters';
import type { TagValue } from './store';
export function setFiltersOnNavigate(
tags: TagValue[],
filterCols: FilterData[],
$columns: Column[]
) {
if (!tags?.length) {
filterCols.forEach((filter) => {
resetOptions(filter);
});
} else {
filterCols.forEach((filter) => {
if (filter.id === 'buildDuration') {
setTimeFilter(tags, filter, $columns);
} else if (filter.id.includes('size')) {
setSizeFilter(tags, filter, $columns);
} else if (filter.id === 'statusCode') {
setStatusCodeFilter(tags, filter, $columns);
} else if (filter.id === '$createdAt' || filter.id === '$updatedAt') {
setDateFilter(tags, filter, $columns);
} else {
setFilterData(filter, tags);
}
});
// Reasinging the filters to trigger reactivity
filterCols = filterCols;
}
}
export function setFilterData(filter: FilterData, list: TagValue[]) {
const tagData = list.find((tag) => tag.tag.includes(`**${filter.title}**`));
if (tagData) {
filter.tag = tagData.tag;
if (Array.isArray(tagData.value) && tagData.value?.length) {
const values = tagData.value as string[];
filter.options.forEach((option) => {
option.checked = values.includes(option.value);
});
}
} else {
filter.tag = null;
resetOptions(filter);
}
}
export function setTimeFilter(localTags: TagValue[], filter: FilterData, columns: Column[]) {
const col = columns.find((c) => c.id === filter.id);
const timeTag = localTags.find((tag) => tag.tag.includes(`**${filter.title}**`));
if (timeTag) {
const now = new Date();
const diff = now.getTime() - new Date(timeTag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
filter.tag = `**${filter.title}** is **${dateRange.label}**`;
filter = filter;
}
} else {
filter.tag = null;
}
}
export function setSizeFilter(localTags: TagValue[], filter: FilterData, columns: Column[]) {
const sizeTag = localTags.find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (sizeTag) {
const size = sizeTag.value as string;
const ranges = col.elements as { value: string; label: string }[];
// find smallest range that is bigger than size
const sizeRange = ranges.reduce((prev, curr) => {
if (parseInt(size) >= parseInt(curr.value)) {
return curr;
}
return prev;
});
if (sizeRange) {
filter.tag = `**${filter.title}** is **${sizeRange.label}**`;
filter = filter;
}
} else {
filter.tag = null;
}
}
export function setStatusCodeFilter(localTags: TagValue[], filter: FilterData, columns: Column[]) {
const statusCodeTag = localTags.find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (statusCodeTag) {
const ranges = col.elements as { value: number; label: string }[];
const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value);
if (codeRange) {
filter.tag = `**${filter.title}** is **${codeRange.label}**`;
filter = filter;
}
} else {
filter.tag = null;
}
}
export function setDateFilter(localTags: TagValue[], filter: FilterData, columns: Column[]) {
const dateTeag = localTags.find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (dateTeag) {
const now = new Date();
const diff = now.getTime() - new Date(dateTeag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
filter.tag = `**${filter.title}** is **${dateRange.label}**`;
filter = filter;
}
} else {
filter.tag = null;
}
}
-1
View File
@@ -1,5 +1,4 @@
import { goto } from '$app/navigation';
import { derived, get, writable } from 'svelte/store';
import { page } from '$app/stores';
import deepEqual from 'deep-equal';
+3
View File
@@ -39,6 +39,9 @@ export type Column = {
array?: boolean;
format?: string;
elements?: string[] | { value: string | number; label: string }[];
/**
* Set to true to hide this column by default
*/
hide?: boolean;
};
@@ -9,12 +9,9 @@
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { readOnly } from '$lib/stores/billing';
import Table from './table.svelte';
import { Filters } from '$lib/components/filters';
import { queries, tags } from '$lib/components/filters/store';
import { View } from '$lib/helpers/load';
import DeploymentCard from './(components)/deploymentCard.svelte';
import RedeployModal from './(modals)/redeployModal.svelte';
import QuickFilters from './quickFilters.svelte';
import { canWriteFunctions } from '$lib/stores/roles';
import {
ActionMenu,
@@ -28,13 +25,13 @@
import { Click, trackEvent } from '$lib/actions/analytics';
import {
IconDotsHorizontal,
IconFilterLine,
IconPlus,
IconRefresh,
IconTerminal
} from '@appwrite.io/pink-icons-svelte';
import { app } from '$lib/stores/app';
import CreateActionMenu from './(components)/createActionMenu.svelte';
import { QuickFilters } from '$lib/components/filters';
export let data;
@@ -42,11 +39,6 @@
let showAlert = true;
let selectedDeployment: Models.Deployment = null;
function clearAll() {
queries.clearAll();
queries.apply();
}
</script>
<Container>
@@ -182,7 +174,7 @@
<Layout.Stack direction="row" alignItems="center">
<Layout.Stack direction="row" gap="s" wrap="wrap">
{#if data.deploymentList.total}
<QuickFilters {columns} />
<QuickFilters {columns} analyticsSource="function_deployments" />
{/if}
</Layout.Stack>
<Layout.Stack direction="row" gap="s" inline>
@@ -9,11 +9,11 @@
import { project } from '$routes/(console)/project-[project]/store';
import { base } from '$app/paths';
import { View } from '$lib/helpers/load';
import QuickFilters from './quickFilters.svelte';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Table from './table.svelte';
import { columns } from './store';
import { QuickFilters } from '$lib/components/filters';
export let data;
@@ -30,7 +30,7 @@
<Container>
<Layout.Stack direction="row" alignItems="center" justifyContent="space-between">
<QuickFilters {columns} />
<QuickFilters {columns} analyticsSource="function_executions" />
<Layout.Stack gap="s" inline direction="row" alignItems="center">
{#if data?.executions?.total}
@@ -1,233 +0,0 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { Submit, trackEvent } from '$lib/actions/analytics';
import MenuItem from '$lib/components/filters/subMenu.svelte';
import {
addFilter,
queries,
queryParamToMap,
tags,
ValidOperators,
type TagValue
} from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
import Menu from '$lib/components/filters/menu.svelte';
import { CustomFilters } from '$lib/components/filters';
export let columns: Writable<Column[]>;
type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
function buildFilterCol(col: Column, customOperator = null): FilterData {
return {
title: col.title,
id: col.id,
show: false,
array: col?.array,
tag: null,
operator: customOperator ?? ValidOperators.Equal,
options: col?.elements?.map((element) => {
return {
value: (element?.value ?? element) as string,
label: (element?.label ?? element) as string,
checked: false
};
})
};
}
const statusCol = $columns.find((col) => col.id === 'status');
let statusFilter = buildFilterCol(statusCol);
const triggerCol = $columns.find((col) => col.id === 'trigger');
let triggerFilter = buildFilterCol(triggerCol);
const methodCol = $columns.find((col) => col.id === 'requestMethod');
let methodFilter = buildFilterCol(methodCol);
const statusCodeCol = $columns.find((col) => col.id === 'responseStatusCode');
let statusCodeFilter = buildFilterCol(statusCodeCol);
const createdAtCol = $columns.find((col) => col.id === '$createdAt');
let createdAtFilter = buildFilterCol(createdAtCol);
let localQueries = new Map<TagValue, string>();
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
if (!localTags?.length) {
statusFilter.tag = null;
triggerFilter.tag = null;
methodFilter.tag = null;
statusCodeFilter.tag = null;
createdAtFilter.tag = null;
[statusFilter, triggerFilter, methodFilter].forEach((filter) => {
resetOptions(filter);
});
} else {
[statusFilter, triggerFilter, methodFilter].forEach((filter) => {
setFilterData(filter, localTags);
});
const statusCodeTag = localTags.find((tag) =>
tag.tag.includes(`**${statusCodeFilter.title}**`)
);
if (statusCodeTag) {
const ranges = statusCodeCol.elements as { value: number; label: string }[];
const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value);
if (codeRange) {
statusCodeFilter.tag = `**${statusCodeFilter.title}** is **${codeRange.label}**`;
statusCodeFilter = statusCodeFilter;
}
} else {
statusCodeFilter.tag = null;
}
const createdAtTag = localTags.find((tag) =>
tag.tag.includes(`**${createdAtFilter.title}**`)
);
if (createdAtTag) {
const now = new Date();
const diff = now.getTime() - new Date(createdAtTag.value as string).getTime();
const ranges = createdAtCol.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
createdAtFilter.tag = `**${createdAtFilter.title}** is **${dateRange.label}**`;
createdAtFilter = createdAtFilter;
}
} else {
createdAtFilter.tag = null;
}
// Reasinging the filters to trigger reactivity
statusFilter = statusFilter;
triggerFilter = triggerFilter;
methodFilter = methodFilter;
statusCodeFilter = statusCodeFilter;
createdAtFilter = createdAtFilter;
}
});
function setFilterData(filter: FilterData, list: TagValue[]) {
const tagData = list.find((tag) => tag.tag.includes(`**${filter.title}**`));
if (tagData) {
filter.tag = tagData.tag;
if (Array.isArray(tagData.value) && tagData.value?.length) {
const values = tagData.value as string[];
filter.options.forEach((option) => {
option.checked = values.includes(option.value);
});
}
} else {
filter.tag = null;
resetOptions(filter);
}
}
function resetOptions(filter: FilterData) {
filter.options.forEach((option) => {
option.checked = false;
});
}
function addFilterAndApply(
colId: string,
colTitle: string,
operator: ValidOperators,
value: string,
arrayValues: string[] = []
) {
const tagList = $tags.filter((tag) => tag.tag.includes(colTitle));
tagList.forEach((tag) => queries.removeFilter(tag));
if (value || arrayValues?.length) {
if (colId === statusCodeFilter.id) {
addStatusCodeFilter(value, colId);
} else if (colId === createdAtFilter.id) {
addCreatedAtFilter(value, colId);
} else {
addFilter($columns, colId, operator, value, arrayValues);
}
}
queries.apply();
trackEvent(Submit.ApplyQuickFilter, {
source: 'function_executions',
column: colId,
value: value || arrayValues.join(', ')
});
}
function addStatusCodeFilter(value: string, colId: string) {
addFilter($columns, colId, ValidOperators.LessThanOrEqual, parseInt(value));
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, parseInt(value) - 99);
}
function addCreatedAtFilter(value: string, colId: string) {
const now = new Date();
const isoValue = new Date(now.getTime() - parseInt(value));
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter($columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
</script>
<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>
@@ -1,231 +0,0 @@
<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,
tags,
ValidOperators,
type TagValue
} from '$lib/components/filters/store';
import SubMenu from '$lib/components/filters/subMenu.svelte';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
export let columns: Writable<Column[]>;
type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
function buildFilterCol(col: Column, customOperator = null): FilterData {
return {
title: col.title,
id: col.id,
show: false,
array: col?.array,
tag: null,
operator: customOperator ?? ValidOperators.Equal,
options: col?.elements?.map((element) => {
return {
value: (element?.value ?? element) as string,
label: (element?.label ?? element) as string,
checked: false
};
})
};
}
const statusCol = $columns.find((col) => col.id === 'status');
let statusFilter = buildFilterCol(statusCol);
const typeCol = $columns.find((col) => col.id === 'type');
let typeFilter = buildFilterCol(typeCol);
const sizeCol = $columns.find((col) => col.id === 'sourceSize');
let sizeFilter = buildFilterCol(sizeCol);
const buildTimeCol = $columns.find((col) => col.id === 'buildDuration');
let buildTimeFilter = buildFilterCol(buildTimeCol);
let localQueries = new Map<TagValue, string>();
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
if (!localTags?.length) {
statusFilter.tag = null;
typeFilter.tag = null;
buildTimeFilter.tag = null;
[statusFilter, typeFilter].forEach((filter) => {
resetOptions(filter);
});
} else {
[statusFilter, typeFilter].forEach((filter) => {
setFilterData(filter, localTags);
});
const buildTimeTag = localTags.find((tag) =>
tag.tag.includes(`**${buildTimeFilter.title}**`)
);
if (buildTimeTag) {
const now = new Date();
const diff = now.getTime() - new Date(buildTimeTag.value as string).getTime();
const ranges = buildTimeCol.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
buildTimeFilter.tag = `**${buildTimeFilter.title}** is **${dateRange.label}**`;
buildTimeFilter = buildTimeFilter;
}
} else {
buildTimeFilter.tag = null;
}
const sizeTag = localTags.find((tag) => tag.tag.includes(`**${sizeFilter.title}**`));
if (sizeTag) {
const size = sizeTag.value as string;
const ranges = sizeCol.elements as { value: string; label: string }[];
// find smallest range that is bigger than size
const sizeRange = ranges.reduce((prev, curr) => {
if (parseInt(size) >= parseInt(curr.value)) {
return curr;
}
return prev;
});
if (sizeRange) {
sizeFilter.tag = `**${sizeFilter.title}** is **${sizeRange.label}**`;
sizeFilter = sizeFilter;
}
} else {
sizeFilter.tag = null;
}
// Reasinging the filters to trigger reactivity
statusFilter = statusFilter;
typeFilter = typeFilter;
sizeFilter = sizeFilter;
buildTimeFilter = buildTimeFilter;
}
});
function setFilterData(filter: FilterData, list: TagValue[]) {
const tagData = list.find((tag) => tag.tag.includes(`**${filter.title}**`));
if (tagData) {
filter.tag = tagData.tag;
if (Array.isArray(tagData.value) && tagData.value?.length) {
const values = tagData.value as string[];
filter.options.forEach((option) => {
option.checked = values.includes(option.value);
});
}
} else {
filter.tag = null;
resetOptions(filter);
}
}
function resetOptions(filter: FilterData) {
filter.options.forEach((option) => {
option.checked = false;
});
}
function addFilterAndApply(
colId: string,
colTitle: string,
operator: ValidOperators,
value: string,
arrayValues: string[] = []
) {
const tagList = $tags.filter((tag) => tag.tag.includes(colTitle));
tagList.forEach((tag) => queries.removeFilter(tag));
if (value || arrayValues?.length) {
if (colId === buildTimeFilter.id) {
addBuildTimeFilter(value, colId);
} else if (colId === sizeFilter.id) {
addSizeFilter(value, colId);
} else {
addFilter($columns, colId, operator, value, arrayValues);
}
}
queries.apply();
trackEvent(Submit.ApplyQuickFilter, {
source: 'function_deployments',
column: colId,
value: value || arrayValues.join(', ')
});
}
function addSizeFilter(value: string, colId: string) {
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
function addBuildTimeFilter(value: string, colId: string) {
const now = new Date();
const isoValue = new Date(now.getTime() - parseInt(value));
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter($columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
</script>
<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>
@@ -3,24 +3,22 @@
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { type Models } from '@appwrite.io/console';
import { Filters } from '$lib/components/filters';
import { queries, tags } from '$lib/components/filters/store';
import { View } from '$lib/helpers/load';
import { ActionMenu, Icon, Layout, Popover, Tag } from '@appwrite.io/pink-svelte';
import { ActionMenu, Icon, Layout, Popover } from '@appwrite.io/pink-svelte';
import Table from './table.svelte';
import QuickFilters from './quickFilters.svelte';
import RedeployModal from '../../redeployModal.svelte';
import CreateGitDeploymentModal from './createGitDeploymentModal.svelte';
import ConnectRepoModal from '../../(components)/connectRepoModal.svelte';
import { columns } from './store';
import CreateManualDeploymentModal from './createManualDeploymentModal.svelte';
import DeploymentMetrics from './deploymentMetrics.svelte';
import { IconFilterLine, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import CreateCliModal from './createCliModal.svelte';
import { QuickFilters } from '$lib/components/filters';
export let data;
@@ -33,11 +31,6 @@
let showConnectManual = false;
let showMobileFilters = false;
function clearAll() {
queries.clearAll();
queries.apply();
}
onMount(() => {
data?.query ? (showMobileFilters = true) : (showMobileFilters = false);
return sdk.forConsole.client.subscribe('console', (response) => {
@@ -53,106 +46,53 @@
<DeploymentMetrics deploymentList={data.deploymentList} />
<Layout.Stack gap="l">
<Layout.Stack justifyContent="space-between" direction="row">
<div class="is-not-mobile">
<Layout.Stack alignItems="center" direction="row">
{#if data.deploymentList.total}
<QuickFilters {columns} />
<Filters
query={data.query}
{columns}
let:disabled
let:toggle
singleCondition>
<Layout.Stack alignItems="center" direction="row" gap="xs">
<Button
compact
on:click={toggle}
{disabled}
size="s"
ariaLabel="open filter">
<Icon icon={IconFilterLine} size="s" slot="start" />
More filters
</Button>
{#if $tags?.length}
<!-- TODO: add vertical divider to pink 2 -->
<div
style="flex-basis:1px; background-color:hsl(var(--border)); width: 1px">
</div>
<Button text on:click={clearAll} size="s">Clear all</Button>
{/if}
</Layout.Stack>
</Filters>
{/if}
</Layout.Stack>
</div>
<div class="is-only-mobile">
<Button
secondary
size="s"
on:click={() => (showMobileFilters = !showMobileFilters)}
ariaLabel="toggle filters">
<span class="icon-filter-line" />
<span class="text">Filters</span>
</Button>
<div
class:u-hide={!showMobileFilters}
class:u-flex={showMobileFilters}
class=" u-gap-8 u-flex-wrap u-margin-block-start-16">
<QuickFilters {columns} />
<Layout.Stack alignItems="center" direction="row">
{#if data.deploymentList.total || data?.query}
<QuickFilters {columns} analyticsSource="site_deployments" />
{/if}
</Layout.Stack>
<Filters query={data.query} {columns} clearOnClick>
<svelte:fragment slot="mobile" let:disabled let:toggle>
<Tag size="m" on:click={toggle} {disabled}>
<span class="text">More filters</span>
<span class="icon-cheveron-down" />
</Tag>
</svelte:fragment>
</Filters>
</div>
</div>
<div>
<Layout.Stack direction="row" inline>
{#if data.deploymentList.total}
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
{/if}
<Popover padding="none" let:toggle>
<Button size="s" on:click={toggle}>
<Icon size="s" icon={IconPlus} />
Create deployment
</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
<ActionMenu.Item.Button
badge="Recommended"
on:click={(e) => {
toggle(e);
if (!hasInstallation) {
showConnectRepo = true;
} else {
showCreateDeployment = true;
}
}}>
Git
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectCLI = true;
}}>
CLI
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectManual = true;
}}>
Manual
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Layout.Stack>
</div>
<Layout.Stack direction="row" inline>
{#if data.deploymentList.total}
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
{/if}
<Popover padding="none" let:toggle>
<Button size="s" on:click={toggle}>
<Icon size="s" icon={IconPlus} />
Create deployment
</Button>
<svelte:fragment slot="tooltip" let:toggle>
<ActionMenu.Root>
<ActionMenu.Item.Button
badge="Recommended"
on:click={(e) => {
toggle(e);
if (!hasInstallation) {
showConnectRepo = true;
} else {
showCreateDeployment = true;
}
}}>
Git
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectCLI = true;
}}>
CLI
</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={(e) => {
toggle(e);
showConnectManual = true;
}}>
Manual
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Layout.Stack>
</Layout.Stack>
{#if data.deploymentList.total}
@@ -1,276 +0,0 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { Submit, trackEvent } from '$lib/actions/analytics';
import {
addFilter,
queries,
queryParamToMap,
tagFormat,
tags,
ValidOperators,
type TagValue
} from '$lib/components/filters/store';
import { SelectSearchCheckbox } from '$lib/elements';
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[]>;
type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
function buildFilterCol(col: Column, customOperator = null): FilterData {
return {
title: col.title,
id: col.id,
show: false,
array: col?.array,
tag: null,
operator: customOperator ?? ValidOperators.Equal,
options: col?.elements?.map((element) => {
return {
value: (element?.value ?? element) as string,
label: (element?.label ?? element) as string,
checked: false
};
})
};
}
const statusCol = $columns.find((col) => col.id === 'status');
let statusFilter = buildFilterCol(statusCol);
const typeCol = $columns.find((col) => col.id === 'type');
let typeFilter = buildFilterCol(typeCol);
const sizeCol = $columns.find((col) => col.id === 'sourceSize');
let sizeFilter = buildFilterCol(sizeCol);
const buildTimeCol = $columns.find((col) => col.id === 'buildDuration');
let buildTimeFilter = buildFilterCol(buildTimeCol);
let localQueries = new Map<TagValue, string>();
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
if (!localTags?.length) {
statusFilter.tag = null;
typeFilter.tag = null;
buildTimeFilter.tag = null;
[statusFilter, typeFilter].forEach((filter) => {
resetOptions(filter);
});
} else {
[statusFilter, typeFilter].forEach((filter) => {
setFilterData(filter, localTags);
});
const buildTimeTag = localTags.find((tag) =>
tag.tag.includes(`**${buildTimeFilter.title}**`)
);
if (buildTimeTag) {
const now = new Date();
const diff = now.getTime() - new Date(buildTimeTag.value as string).getTime();
const ranges = buildTimeCol.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
buildTimeFilter.tag = `**${buildTimeFilter.title}** is **${dateRange.label}**`;
buildTimeFilter = buildTimeFilter;
}
} else {
buildTimeFilter.tag = null;
}
const sizeTag = localTags.find((tag) => tag.tag.includes(`**${sizeFilter.title}**`));
if (sizeTag) {
const size = sizeTag.value as string;
const ranges = sizeCol.elements as { value: string; label: string }[];
// find smallest range that is bigger than size
const sizeRange = ranges.reduce((prev, curr) => {
if (parseInt(size) >= parseInt(curr.value)) {
return curr;
}
return prev;
});
if (sizeRange) {
sizeFilter.tag = `**${sizeFilter.title}** is **${sizeRange.label}**`;
sizeFilter = sizeFilter;
}
} else {
sizeFilter.tag = null;
}
// Reasinging the filters to trigger reactivity
statusFilter = statusFilter;
typeFilter = typeFilter;
sizeFilter = sizeFilter;
buildTimeFilter = buildTimeFilter;
}
});
function setFilterData(filter: FilterData, list: TagValue[]) {
const tagData = list.find((tag) => tag.tag.includes(`**${filter.title}**`));
if (tagData) {
filter.tag = tagData.tag;
if (Array.isArray(tagData.value) && tagData.value?.length) {
const values = tagData.value as string[];
filter.options.forEach((option) => {
option.checked = values.includes(option.value);
});
}
} else {
filter.tag = null;
resetOptions(filter);
}
}
function resetOptions(filter: FilterData) {
filter.options.forEach((option) => {
option.checked = false;
});
}
function addFilterAndApply(
colId: string,
colTitle: string,
operator: ValidOperators,
value: string,
arrayValues: string[] = []
) {
const tagList = $tags.filter((tag) => tag.tag.includes(colTitle));
tagList.forEach((tag) => queries.removeFilter(tag));
if (value || arrayValues?.length) {
if (colId === buildTimeFilter.id) {
addBuildTimeFilter(value, colId);
} else if (colId === sizeFilter.id) {
addSizeFilter(value, colId);
} else {
addFilter($columns, colId, operator, value, arrayValues);
}
}
queries.apply();
trackEvent(Submit.ApplyQuickFilter, {
source: 'function_deployments',
column: colId,
value: value || arrayValues.join(', ')
});
}
function addSizeFilter(value: string, colId: string) {
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
function addBuildTimeFilter(value: string, colId: string) {
const now = new Date();
const isoValue = new Date(now.getTime() - parseInt(value));
addFilter($columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter($columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
</script>
{#each [typeFilter] as filter}
<Popover padding="none" let:toggle let:showing>
<!--TODO: add tracking back event="apply_quick_filter" -->
<Tag on:click={toggle}>
{#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
padding={8}
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) => {
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}
{#each [sizeFilter] as filter}
<Popover padding="none" let:toggle let:showing>
<!--TODO: add tracking back event="apply_quick_filter" -->
<Tag on:click={toggle}>
{#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={() => {
filter.show = false;
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</svelte:fragment>
</Popover>
{/each}
@@ -13,8 +13,7 @@ export const columns = writable<Column[]>([
width: 110,
array: true,
format: 'enum',
elements: ['ready', 'processing', 'building', 'waiting', 'cancelled', 'failed'],
filter: false
elements: ['ready', 'processing', 'building', 'waiting', 'cancelled', 'failed']
},
{
@@ -4,6 +4,7 @@
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { capitalize } from '$lib/helpers/string';
import { iconPath } from '$lib/stores/app';
import { addNotification } from '$lib/stores/notifications';
import { getIconFromRuntime } from '$lib/stores/runtimes';
@@ -16,7 +17,7 @@
let buildRuntime = site?.buildRuntime;
let buildRuntimeOptions = framework.runtimes.map((runtime) => ({
label: runtime,
label: capitalize(runtime),
value: runtime,
leadingHtml: `<img src='${$iconPath(getIconFromRuntime(runtime), 'color')}' style='inline-size: var(--icon-size-m)' />`
}));