feat: function overview and templates

This commit is contained in:
Arman
2025-03-07 11:22:19 +01:00
parent c8e756953b
commit 5b80c9d1fc
6 changed files with 241 additions and 239 deletions
+5 -5
View File
@@ -1,15 +1,15 @@
<script>
<script lang="ts">
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
export let href;
export let href: string;
</script>
<Card.Link class="card" {href}>
<Layout.Stack height="calc(182 / 16 * 1rem)" justifyContent="space-between">
<Layout.Stack direction="row">
<Layout.Stack gap="xs">
<Typography.Caption color="--fgcolor-neutral-tertiary"
<Typography.Caption variant="400" color="--fgcolor-neutral-tertiary"
><slot name="eyebrow" /></Typography.Caption>
<Typography.Title size="s"><slot name="title" /></Typography.Title>
<Typography.Title size="s" truncate><slot name="title" /></Typography.Title>
<div>
<slot name="subtitle" />
</div>
@@ -22,7 +22,7 @@
<Layout.Stack direction="row">
<slot />
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end">
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center">
<slot name="icons" />
</Layout.Stack>
</Layout.Stack>
+1 -1
View File
@@ -36,6 +36,6 @@
<Layout.Stack direction="row" alignItems="center" inline>
<InputSelect id="rows" {options} bind:value={limit} on:change={limitChange} />
<p class="text" style:white-space="nowrap">
{name} per page. Total results: {sum >= 5000 ? `${sum}+` : sum}
{name} per page. Total: {sum >= 5000 ? `${sum}+` : sum}
</p>
</Layout.Stack>
+1 -1
View File
@@ -14,7 +14,7 @@
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void>;
export let buttonMethod: () => void | Promise<void> = () => {};
export let buttonHref: string = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonEventData: Record<string, unknown> = {};
@@ -11,7 +11,7 @@
SvgIcon
} from '$lib/components';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ContainerHeader } from '$lib/layout';
import { Container } from '$lib/layout';
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
@@ -21,9 +21,11 @@
import { functionsList } from './store';
import { canWriteFunctions } from '$lib/stores/roles';
import type { Models } from '@appwrite.io/console';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { IconClock, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import Avatar from '$lib/components/avatar.svelte';
export let data;
@@ -73,13 +75,12 @@
</script>
<Container>
<ContainerHeader
title="Functions"
buttonText={$canWriteFunctions ? 'Create function' : ''}
buttonEvent="create_function"
buttonEventData={{ source: 'functions_overview' }}
buttonHref={`${base}/project-${project}/functions/create-function`}
total={data.functions.total} />
<Layout.Stack direction="row" justifyContent="flex-end">
<Button href={`${base}/project-${project}/functions/create-function`}>
<Icon icon={IconPlus} slot="start" />
Create function
</Button>
</Layout.Stack>
{#if data.functions.total}
<CardContainer
@@ -91,23 +92,21 @@
{#each data.functions.functions as func}
<GridItem1 href={`${base}/project-${project}/functions/function-${func.$id}`}>
<svelte:fragment slot="title">
<div class="u-flex u-gap-16 u-cross-center">
<div class="avatar is-medium">
<SvgIcon name={func.runtime.split('-')[0]}></SvgIcon>
</div>
<span class="text">{func.name}</span>
</div>
<Layout.Stack gap="l" alignItems="center" direction="row" inline>
<Avatar alt={func.name} size="m">
<SvgIcon name={func.runtime.split('-')[0]} />
</Avatar>
{func.name}
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="icons">
{#if func.schedule}
<li>
<Tooltip>
<span class="icon-clock" aria-hidden="true" />
<span slot="tooltip"
>{`Next execution:
{#if !func.schedule}
<Tooltip>
<Icon icon={IconClock} size="s" />
<span slot="tooltip"
>{`Next execution:
${getNextScheduledExecution(func)}`}</span>
</Tooltip>
</li>
</Tooltip>
{/if}
</svelte:fragment>
<Id value={func.$id} event="function">{func.$id}</Id>
@@ -2,32 +2,42 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import {
Collapsible,
CollapsibleItem,
EmptySearch,
Pagination,
SvgIcon
} from '$lib/components';
import { EmptySearch, SvgIcon } from '$lib/components';
import { Button, InputSearch } from '$lib/elements/forms';
import { Container, ContainerButton } from '$lib/layout';
import { app } from '$lib/stores/app';
import { isServiceLimited } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { canWriteFunctions } from '$lib/stores/roles';
import type { Models } from '@appwrite.io/console';
import { functionsList } from '../store';
import { debounce } from '$lib/helpers/debounce';
import { Tooltip, Typography } from '@appwrite.io/pink-svelte';
import {
Accordion,
AvatarGroup,
Layout,
Selector,
Tooltip,
Typography,
Card as PinkCard
} from '@appwrite.io/pink-svelte';
import { capitalize } from '$lib/helpers/string';
import Card from '$lib/components/card.svelte';
import Link from '$lib/elements/link.svelte';
import Avatar from '$lib/components/avatar.svelte';
import PaginationWithLimit from '$lib/components/paginationWithLimit.svelte';
export let data;
function applyFilter(filter: string, value: string, event: Event) {
const add = (event.target as EventTarget & HTMLInputElement).checked;
function applyFilter(filter: string, value: string, event: CustomEvent) {
const target = new URL($page.url);
if (add) {
target.searchParams.append(filter, value);
if (event?.detail) {
if (
!target.searchParams
.getAll(filter)
.some((n) => n.toLowerCase() === value.toLowerCase())
) {
target.searchParams.append(filter, value);
}
} else {
const previous = target.searchParams
.getAll(filter)
@@ -121,155 +131,143 @@
</script>
<Container>
<div class="u-flex u-gap-8 u-cross-center">
<Typography.Title>Templates</Typography.Title>
<div class="tag eyebrow-heading-3">
<span class="text u-x-small">Experimental</span>
</div>
</div>
<div class="grid-300px-1fr u-margin-block-start-24">
<section>
<Layout.GridFraction start={1} end={3} gap="xxl">
<Layout.Stack gap="xl">
<InputSearch
placeholder="Search templates"
value={$page.url.searchParams.get('search')}
on:clear={clearSearch}
on:change={applySearch} />
<div class="u-margin-block-start-24">
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">Use case</svelte:fragment>
<ul class="form-list u-row-gap-16">
{#each [...data.useCases] as useCase}
<li class="form-item">
<label class="u-flex u-cross-center u-gap-16">
<input
type="checkbox"
class="is-small"
checked={isChecked(useCase)}
on:change={(e) => applyFilter('useCase', useCase, e)} />
<span class="u-trim-1">{capitalize(useCase)}</span>
</label>
</li>
{/each}
</ul>
</CollapsibleItem>
<CollapsibleItem>
<svelte:fragment slot="title">Runtime</svelte:fragment>
<ul class="form-list u-row-gap-16">
{#each [...data.runtimes] as runtime}
{@const icon = getIconFromRuntime(runtime)}
<li class="form-item">
<label class="u-flex u-cross-center u-gap-16">
<input
type="checkbox"
class="is-small"
checked={$page.url.searchParams
.getAll('runtime')
.includes(runtime)}
on:change={(e) => applyFilter('runtime', runtime, e)} />
<div
class="u-flex u-cross-center u-gap-8 functions-avatar-holder">
<div class="avatar is-size-x-small">
<SvgIcon name={icon} iconSize="small" />
</div>
<div class="u-trim-1 u-capitalize">
{runtime?.split('-')?.join(' ')}
</div>
</div>
</label>
</li>
{/each}
</ul>
</CollapsibleItem>
</Collapsible>
</div>
<Layout.Stack>
<Accordion title="Use case">
<Layout.Stack>
{#each [...data.useCases] as useCase}
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox
id={useCase}
size="s"
label={useCase === 'ai'
? useCase.toUpperCase()
: capitalize(useCase)}
checked={isChecked(useCase)}
on:change={(e) => {
console.log(useCase, e);
applyFilter('useCase', useCase, e);
}} />
</Layout.Stack>
{/each}
</Layout.Stack>
</Accordion>
<Accordion title="Runtime">
<Layout.Stack>
{#each [...data.runtimes] as runtime}
<!-- {@const icon = getIconFromRuntime(runtime)} -->
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox
id={runtime}
size="s"
label={runtime?.split('-')?.join(' ')}
checked={$page.url.searchParams
.getAll('runtime')
.includes(runtime)}
on:change={(e) => applyFilter('runtime', runtime, e)} />
</Layout.Stack>
{/each}
</Layout.Stack>
</Accordion>
</Layout.Stack>
<section class="card u-margin-block-start-24">
<h4 class="body-text-1 u-bold">Contribute</h4>
<p class="u-margin-block-start-16">
Have an idea for a function template? View our <a
class="link"
href="https://github.com/appwrite/templates/blob/main/CONTRIBUTING.md"
target="_blank">contribution guidelines</a
>.
</p>
</section>
</section>
<section>
<Card isTile radius="s" padding="xs">
<Layout.Stack gap="xxxs">
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
Contribute
</Typography.Text>
<Typography.Text variant="m-400">
Got a function template idea? <Link
variant="muted"
external
href="https://github.com/appwrite/templates/blob/main/CONTRIBUTING.md"
>View the contribution guidelines.
</Link>
</Typography.Text>
</Layout.Stack>
</Card>
</Layout.Stack>
<Layout.Stack gap="l">
{#if data.templates.length > 0}
<ul
class="grid-box"
style="--grid-item-size:22rem; --grid-item-size-small-screens:19rem">
<Layout.Grid columns={2} columnsXL={3} columnsXS={1}>
{#each data.templates as template}
{@const baseRuntimes = getBaseRuntimes(template.runtimes)}
{@const displayed = baseRuntimes.slice(0, 2)}
{@const hidden = baseRuntimes.slice(1, -1)}
<li>
<article class="card u-min-height-100-percent">
<div class="u-flex u-gap-16 u-cross-center u-main-space-between">
<h2
class="body-text-1 u-bold u-trim-1 functions-template-title">
<PinkCard.Base radius="m" padding="xs">
<Layout.Stack height="100%" justifyContent="space-between" gap="xl">
<Layout.Stack gap="xxxs">
<Typography.Text
variant="m-500"
color="--fgcolor-neutral-primary">
{template.name}
</h2>
<ul class="avatars-group is-with-border">
</Typography.Text>
<Typography.Text variant="m-400">
{template.tagline}
</Typography.Text>
</Layout.Stack>
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center">
<AvatarGroup>
{#each displayed as runtime}
{@const icon = getIconFromRuntime(runtime.name)}
{#if icon}
<li
class="avatars-group-item functions-avatar-holder">
<div class="avatar is-size-small">
<img
style:--p-text-size="20px"
src={`${base}/icons/${$app.themeInUse}/color/${icon}.svg`}
alt={icon}
aria-hidden="true"
aria-label={icon} />
</div>
</li>
<Avatar alt={runtime.name} size="xs">
<SvgIcon name={icon} iconSize="small" />
</Avatar>
{/if}
{/each}
{#if hidden.length}
<li class="avatars-group-item functions-avatar-holder">
<Tooltip>
<div class="avatar is-size-small">
<Tooltip>
<Avatar alt="hidden runtime number" size="xs">
<span style:font-size="10px">
+{hidden.length}
</div>
<span slot="tooltip"
>{hidden
.map((n) => n.name)
.join(', ')}</span>
</Tooltip>
</li>
</span>
</Avatar>
<span slot="tooltip"
>{hidden.map((n) => n.name).join(', ')}</span>
</Tooltip>
{/if}
</ul>
</div>
<p class="u-margin-block-start-20 u-trim-2 u-break-word">
{template.tagline}
</p>
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24">
<Button
href={`${base}/project-${$page.params.project}/functions/templates/template-${template.id}`}
text>
<span class="text">View details</span>
</Button>
{#if $canWriteFunctions}
<ContainerButton
title="functions"
disabled={buttonDisabled}
buttonType="secondary"
buttonMethod={() => connectTemplate(template)}
showIcon={false}
buttonText="Create function"
buttonEventData={{ source: 'functions_template' }}
buttonEvent="create_function" />
{/if}
</div>
</article>
</li>
</AvatarGroup>
<Layout.Stack
direction="row"
gap="s"
alignItems="center"
inline>
<Button
href={`${base}/project-${$page.params.project}/functions/templates/template-${template.id}`}
text>
<span class="text">Details</span>
</Button>
{#if $canWriteFunctions}
<ContainerButton
title="functions"
disabled={buttonDisabled}
buttonType="secondary"
buttonHref={`${base}/project-${$page.params.project}/functions/create-function/template-${template.id}`}
showIcon={false}
buttonText="Create"
buttonEventData={{
source: 'functions_template'
}}
buttonEvent="create_function" />
{/if}
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</PinkCard.Base>
{/each}
</ul>
</Layout.Grid>
{:else}
<EmptySearch hidePagination>
<div class="common-section">
@@ -283,12 +281,13 @@
</div>
</EmptySearch>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between u-cross-center">
<p class="text">Total templates: {data.sum}</p>
<Pagination limit={data.limit} offset={data.offset} sum={data.sum} />
</div>
</section>
</div>
<PaginationWithLimit
name="Templates"
limit={data.limit}
offset={data.offset}
total={data.sum} />
</Layout.Stack>
</Layout.GridFraction>
</Container>
<style>
@@ -1,6 +1,5 @@
<script lang="ts">
import { Card, Collapsible } from '$lib/components';
import { Pill } from '$lib/elements';
import { Card } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container, ContainerButton } from '$lib/layout';
import { isCloud } from '$lib/system';
@@ -12,71 +11,76 @@
import { organization } from '$lib/stores/organization';
import { functionsList } from '../../store';
import { canWriteFunctions } from '$lib/stores/roles';
import { Typography } from '@appwrite.io/pink-svelte';
import {
Layout,
Typography,
Card as PinkCard,
Image,
Badge,
Divider
} from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { capitalize } from '$lib/helpers/string';
$: buttonDisabled =
isCloud && isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total);
</script>
<Container>
<div class="grid-300px-1fr">
<section>
<Collapsible>
<li class="collapsible-item-divider collapsible-item">
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
Use cases
<span class="inline-tag">{$template.useCases.length}</span>
</h3>
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
{#each $template.useCases as useCase}
<Pill>{useCase}</Pill>
{/each}
</div>
</li>
<li class="collapsible-item-divider collapsible-item">
<h3 class="u-flex u-gap-16 body-text-2 u-bold u-padding-block-12">
Runtimes
<span class="inline-tag">{$template.runtimes.length}</span>
</h3>
<div class="collapsible-content u-flex u-flex-wrap u-gap-8">
{#each $template.runtimes as runtime}
<Pill>{runtime.name}</Pill>
{/each}
</div>
</li>
<li class="collapsible-item-divider collapsible-item">
<section class="card u-margin-block-start-24">
<h4 class="body-text-1 u-bold">Published by</h4>
<img
class="u-margin-block-start-8"
src={$app.themeInUse === 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
width="120"
height="22"
alt="Appwrite" />
</section>
</li>
</Collapsible>
</section>
<section>
<Card>
<Typography.Title size="s">
<span class="u-flex u-cross-center u-gap-16 functions-avatar-holder">
<div class="avatar is-size-small">
<span
style:--p-text-size="20px"
class={$template.icon}
aria-hidden="true" />
</div>
{$template.name}
</span>
</Typography.Title>
<p class="u-margin-block-start-24">{$template.tagline}</p>
<Layout.GridFraction start={1} end={3} gap="xxl">
<div>
<PinkCard.Base radius="s" padding="s">
<Layout.Stack gap="s">
<Typography.Text color="--fgcolor-neutral-tertiary"
>Published by</Typography.Text>
<Image
fit="contain"
src={$app.themeInUse === 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
width={120}
height={22}
alt="Appwrite" />
</Layout.Stack>
</PinkCard.Base>
</div>
<Card radius="m" padding="s">
<Layout.Stack gap="xl">
<Layout.Stack gap="l">
<Layout.Stack gap="xxs">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
About
</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
{$template.tagline}
</Typography.Text>
</Layout.Stack>
<Layout.Stack gap="xxs">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Use cases
</Typography.Text>
<Layout.Stack direction="row" gap="xs" wrap="wrap">
{#each $template.useCases as useCase}
<Badge variant="secondary" size="s" content={capitalize(useCase)} />
{/each}
</Layout.Stack>
</Layout.Stack>
<Layout.Stack gap="xxs">
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
Runtimes
</Typography.Text>
<Layout.Stack direction="row" gap="xs" wrap="wrap">
{#each $template.runtimes as runtime}
<Badge variant="secondary" size="s" content={runtime.name} />
{/each}
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24 u-flex-wrap">
<Divider />
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button
text
secondary
href={`https://github.com/${$template.providerOwner}/${$template.providerRepositoryId}`}
external>
View source
@@ -91,10 +95,10 @@
buttonText="Create function"
buttonEvent="create_function" />
{/if}
</div>
</Card>
</section>
</div>
</Layout.Stack>
</Layout.Stack>
</Card>
</Layout.GridFraction>
</Container>
<style>