feat: deployments

This commit is contained in:
Arman
2024-11-13 15:30:44 +01:00
parent b0bb470162
commit b15d7cbb56
23 changed files with 1369 additions and 28 deletions
+21 -19
View File
@@ -1,26 +1,28 @@
<script lang="ts">
import { Card, Empty, Layout } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { EmptySearch } from '.';
import { queries } from './filters';
export let resource: string;
</script>
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2 u-bold">Sorry, we couldn't find any {resource}.</b>
<p>There are no {resource} that match your filters.</p>
</div>
<div class="u-flex common-section u-main-center">
<Button
secondary
on:click={() => {
queries.clearAll();
queries.apply();
}}>
Clear filters
</Button>
</div>
</div>
</EmptySearch>
<Card.Base padding="none">
<Empty
title={`Sorry, we couldn't find any ${resource}`}
description={`There are no ${resource} that match your filters.`}
type="secondary">
<svelte:fragment slot="actions">
<Layout.Stack direction="row" justifyContent="center">
<Button
secondary
on:click={() => {
queries.clearAll();
queries.apply();
}}>
Clear filters
</Button>
<slot />
</Layout.Stack>
</svelte:fragment>
</Empty>
</Card.Base>
+1 -1
View File
@@ -9,7 +9,7 @@
<Card.Base padding="none">
<Empty
title={`Sorry, we couldn't find ${search}`}
title={`Sorry, we couldn't find ${search ? `${search}` : `any ${target}`}`}
description={`There are no ${target} that match your search.`}
type="secondary">
<svelte:fragment slot="actions">
+8 -7
View File
@@ -158,13 +158,13 @@
{/if}
<div class="u-flex u-gap-8">
{#if singleCondition}
<Button text on:click={toggleDropdown}>Cancel</Button>
<Button size="s" text on:click={toggleDropdown}>Cancel</Button>
{:else}
<Button disabled={applied === 0} text on:click={clearAll}>
<Button size="s" disabled={applied === 0} text on:click={clearAll}>
Clear all
</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
<Button size="s" on:click={apply} disabled={isButtonDisabled}>Apply</Button>
</div>
</div>
</div>
@@ -174,7 +174,7 @@
<div class="is-only-mobile">
<slot name="mobile" {disabled} toggle={toggleMobileModal}>
<Button secondary on:click={toggleMobileModal} {fullWidthMobile}>
<Button size="s" secondary on:click={toggleMobileModal} {fullWidthMobile}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
@@ -219,11 +219,12 @@
{/if}
<div class="u-flex u-gap-8">
{#if singleCondition}
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
<Button size="s" text on:click={() => (showFiltersMobile = false)}
>Cancel</Button>
{:else}
<Button text on:click={clearAll}>Clear all</Button>
<Button size="s" text on:click={clearAll}>Clear all</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
<Button size="s" on:click={apply} disabled={isButtonDisabled}>Apply</Button>
</div>
</div>
</svelte:fragment>
+1
View File
@@ -78,6 +78,7 @@
{#if $columns?.length}
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
<Button
size="s"
secondary
on:click={() => (showSelectColumns = !showSelectColumns)}
{fullWidthMobile}>
+1 -1
View File
@@ -14,7 +14,7 @@
export let danger = false;
export let icon = false;
export let link = false;
export let size: Props['size'] = 'medium';
export let size: Props['size'] = 'm';
export let disabled = false;
export let external = false;
export let href: string = null;
@@ -1,5 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Breadcrumbs } from '$lib/layout';
import { organization } from '$lib/stores/organization';
import { project } from '../../store';
@@ -16,6 +17,10 @@
{
href: `${base}/project-${$project?.$id}/sites`,
title: 'Sites'
},
{
href: `${base}/project-${$project?.$id}/sites/site-${$page.params.site}`,
title: $page.params.site
}
];
</script>
@@ -0,0 +1,153 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { invalidate, goto } from '$app/navigation';
import { registerCommands } from '$lib/commandCenter';
import { project } from '../../../store';
import type { Models } from '@appwrite.io/console';
import { base } from '$app/paths';
import { canWriteSites } from '$lib/stores/roles';
onMount(() => {
let previousStatus = null;
return sdk.forConsole.client.subscribe<Models.Deployment>('console', (message) => {
if (previousStatus === message.payload.status) {
return;
}
previousStatus = message.payload.status;
if (message.events.includes('sites.*.deployments.*.create')) {
invalidate(Dependencies.DEPLOYMENTS);
return;
}
if (message.events.includes('sites.*.deployments.*.update')) {
invalidate(Dependencies.DEPLOYMENTS);
invalidate(Dependencies.FUNCTION);
return;
}
if (message.events.includes('sites.*.deployments.*.delete')) {
invalidate(Dependencies.DEPLOYMENTS);
return;
}
});
});
$: $registerCommands([
{
label: 'Create deployment',
async callback() {
if (!$page.url.pathname.endsWith($page.params.site)) {
await goto(`${base}/project-${$project.$id}/sites/site-${$page.params.site}`);
}
},
keys: $page.url.pathname.endsWith($page.params.site) ? ['c'] : ['c', 'd'],
group: 'functions',
icon: 'plus',
disabled: !$canWriteSites
},
{
label: 'Permissions',
async callback() {
await goto(
`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings#permissions`
);
scrollBy({ top: -100 });
},
icon: 'search',
group: 'functions',
disabled: !$canWriteSites
},
{
label: 'Events',
async callback() {
await goto(
`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings#events`
);
scrollBy({ top: -100 });
},
icon: 'calendar',
group: 'functions',
disabled: !$canWriteSites
},
{
label: 'Variables',
async callback() {
await goto(
`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings#variables`
);
},
icon: 'list',
group: 'functions',
disabled: !$canWriteSites
},
{
label: 'Timeout',
callback() {
goto(
`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings#timeout`
);
},
icon: 'x-circle',
group: 'functions',
disabled: !$canWriteSites
},
{
label: 'Schedule',
async callback() {
await goto(
`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings#schedule`
);
scrollBy({ top: -100 });
},
icon: 'clock',
group: 'functions',
disabled: !$canWriteSites
},
{
label: 'Go to deployments',
callback() {
goto(`${base}/project-${$project.$id}/sites/site-${$page.params.site}`);
},
keys: ['g', 'd'],
group: 'navigation',
rank: 10,
disabled: $page.url.pathname.endsWith($page.params.site)
},
{
label: 'Go to usage',
callback() {
goto(`${base}/project-${$project.$id}/sites/site-${$page.params.site}/usage`);
},
keys: ['g', 'u'],
group: 'navigation',
rank: 10,
disabled: $page.url.pathname.endsWith('usage')
},
{
label: 'Go to executions',
callback() {
goto(`${base}/project-${$project.$id}/sites/site-${$page.params.site}/executions`);
},
keys: ['g', 'e'],
group: 'navigation',
rank: 10,
disabled: $page.url.pathname.endsWith('executions')
},
{
label: 'Go to settings',
callback() {
goto(`${base}/project-${$project.$id}/sites/site-${$page.params.site}/settings`);
},
keys: ['g', 's'],
group: 'navigation',
rank: 10,
disabled: $page.url.pathname.endsWith('settings') || !$canWriteSites
}
]);
</script>
<slot />
@@ -0,0 +1,28 @@
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { error } from '@sveltejs/kit';
import { Query } from '@appwrite.io/console';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.SITE);
depends(Dependencies.DEPLOYMENTS);
try {
const [site, proxyRuleList] = await Promise.all([
sdk.forProject.sites.get(params.site),
sdk.forProject.proxy.listRules([
Query.equal('resourceType', 'sites'),
Query.equal('resourceId', params.site),
Query.limit(1)
])
]);
return {
site,
proxyRuleList
};
} catch (e) {
error(e.code, e.message);
}
};
@@ -0,0 +1,198 @@
<script lang="ts">
import { PaginationWithLimit, ViewSelector, EmptyFilter } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { deploymentList } from './store';
import { Container, ContainerHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
import { Filters } from '$lib/components/filters';
import { queries, tags } from '$lib/components/filters/store';
import { View } from '$lib/helpers/load';
import RedeployModal from './redeployModal.svelte';
import QuickFilters from './quickFilters.svelte';
import { Pill } from '$lib/elements';
import { onMount } from 'svelte';
import Table from './table.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
export let data;
let showRedeploy = false;
let selectedDeployment: Models.Deployment = null;
const columns = writable<Column[]>([
{ id: '$id', title: 'Deployment ID', type: 'string', show: true, width: 150 },
{
id: 'status',
title: 'Status',
type: 'enum',
show: true,
width: 110,
array: true,
format: 'enum',
elements: ['ready', 'processing', 'building', 'waiting', 'cancelled', 'failed'],
filter: false
},
{
id: 'type',
title: 'Source',
type: 'string',
show: true,
width: 90,
array: true,
format: 'enum',
elements: [
{ value: 'manual', label: 'Manual' },
{ value: 'cli', label: 'CLI' },
{ value: 'vcs', label: 'Git' }
]
},
{
id: '$updatedAt',
title: 'Updated',
type: 'datetime',
show: true,
width: 150,
format: 'datetime'
},
{
id: 'buildTime',
title: 'Build time',
type: 'integer',
show: true,
width: 90,
elements: [
{
value: 15,
label: 'more than 15 seconds'
},
{
value: 60,
label: 'more than 1 minute'
},
{
value: 5 * 60,
label: 'more than 5 minutes'
}
],
filter: false
},
{
id: 'size',
title: 'Size',
type: 'integer',
show: true,
width: 140,
elements: [
{
value: 2 * 1000 * 1000,
label: 'more than 2MB'
},
{
value: 10 * 1000 * 1000,
label: 'more than 10MB'
},
{
value: 50 * 1000 * 1000,
label: 'more than 50MB'
}
]
},
{
id: 'buildSize',
title: 'Build size',
type: 'integer',
show: false,
filter: false,
width: 80
}
]);
let showMobileFilters = false;
onMount(() => {
data?.query ? (showMobileFilters = true) : (showMobileFilters = false);
});
function clearAll() {
queries.clearAll();
queries.apply();
}
</script>
<Container>
<ContainerHeader title="Deployments"></ContainerHeader>
<Layout.Stack justifyContent="space-between" direction="row">
<Layout.Stack alignItems="center" direction="row" gap="s">
<QuickFilters {columns} />
<Filters query={data.query} {columns} let:disabled let:toggle singleCondition>
<Layout.Stack alignItems="center" direction="row" gap="xs">
<Button text on:click={toggle} {disabled} size="s" ariaLabel="open filter">
<span class="icon-filter-line" />
<span class="text">More filters</span>
</Button>
{#if $tags?.length}
<!-- TODO: add vertical divider to pink 2 -->
<div
style="flex-basis:1px; background-color:hsl(var(--color-border)); width: 1px">
</div>
<Button text on:click={clearAll} size="s">Clear all</Button>
{/if}
</Layout.Stack>
</Filters>
</Layout.Stack>
<ViewSelector view={View.Table} {columns} hideView allowNoColumns hideText />
</Layout.Stack>
<div class="is-only-mobile">
<Layout.Stack justifyContent="space-between" direction="row">
<Button
text
size="s"
on:click={() => (showMobileFilters = !showMobileFilters)}
ariaLabel="toggle filters">
<span class="icon-filter-line" />
<span class="text">Filters</span>
</Button>
<ViewSelector view={View.Table} {columns} hideView allowNoColumns />
</Layout.Stack>
<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>
<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="text">More filters</span>
<span class="icon-cheveron-down" />
</Pill>
</svelte:fragment>
</Filters>
</div>
</div>
{#if $deploymentList.total}
<Table columns={$columns} {data} />
{:else if data?.query}
<EmptyFilter resource="deployments" />
{/if}
<PaginationWithLimit
name="Deployments"
limit={data.limit}
offset={data.offset}
total={$deploymentList?.total} />
</Container>
{#if selectedDeployment}
<RedeployModal {selectedDeployment} bind:show={showRedeploy} />
{/if}
@@ -0,0 +1,32 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getQuery, pageToOffset } from '$lib/helpers/load';
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
import { queries, queryParamToMap } from '$lib/components/filters';
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
const data = await parent();
depends(Dependencies.DEPLOYMENTS);
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
const query = getQuery(url);
const parsedQueries = queryParamToMap(query || '[]');
queries.set(parsedQueries);
return {
offset,
limit,
query,
activeDeployment: data.site.deploymentId
? await sdk.forProject.sites.getDeployment(params.site, data.site.deploymentId)
: null,
deploymentList: await sdk.forProject.sites.listDeployments(params.site, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...parsedQueries.values()
])
};
};
@@ -0,0 +1,49 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
export let showCancel = false;
export let selectedDeployment: Models.Deployment;
async function handleSubmit() {
try {
await sdk.forProject.functions.updateDeploymentBuild(
selectedDeployment.resourceId,
selectedDeployment.$id
);
await invalidate(Dependencies.FUNCTION);
showCancel = false;
addNotification({
type: 'success',
message: `Deployment has been deleted`
});
trackEvent(Submit.DeploymentCancel);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DeploymentCancel);
}
}
</script>
<Modal
title="Cancel deployment"
bind:show={showCancel}
onSubmit={handleSubmit}
icon="exclamation"
state="warning"
headerDivider={false}>
<p data-private>Are you sure you want to cancel this deployment?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showCancel = false)}>Cancel</Button>
<Button secondary submit>Continue</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,49 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
export let showDelete = false;
export let selectedDeployment: Models.Deployment = null;
async function handleSubmit() {
try {
await sdk.forProject.functions.deleteDeployment(
selectedDeployment.resourceId,
selectedDeployment.$id
);
await invalidate(Dependencies.FUNCTION);
showDelete = false;
addNotification({
type: 'success',
message: `Deployment has been deleted`
});
trackEvent(Submit.DeploymentDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.DeploymentDelete);
}
}
</script>
<Modal
title="Delete deployment"
bind:show={showDelete}
onSubmit={handleSubmit}
icon="exclamation"
state="warning"
headerDivider={false}>
<p data-private>Are you sure you want to delete this deployment?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,36 @@
<script lang="ts">
import { CardGrid } from '$lib/components';
import { Container } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { deployment } from './store';
export let data;
let logs = '';
onMount(() => {
logs = $deployment.buildLogs;
if ($deployment.status === 'ready') {
return;
}
return sdk.forConsole.client.subscribe<Models.Deployment>('console', (message) => {
if (
message.events.includes(
`functions.${$page.params.site}.deployments.${$page.params.deployment}.update`
)
) {
logs = message.payload['logs'];
if (message.payload.status === 'ready') {
invalidate(Dependencies.DEPLOYMENT);
}
}
});
});
</script>
<Container></Container>
@@ -0,0 +1,13 @@
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, depends }) => {
depends(Dependencies.DEPLOYMENT);
const deployment = await sdk.forProject.sites.getDeployment(params.site, params.deployment);
return {
deployment
};
};
@@ -0,0 +1,5 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { derived } from 'svelte/store';
export const deployment = derived(page, ($page) => $page.data.deployment as Models.Deployment);
@@ -0,0 +1,121 @@
<script lang="ts">
import { CardGrid, Id, SvgIcon } from '$lib/components';
import type { Models } from '@appwrite.io/console';
import { func, proxyRuleList } from './store';
import { Pill } from '$lib/elements';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { calculateTime } from '$lib/helpers/timeConversion';
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
import DeploymentSource from './deploymentSource.svelte';
import DeploymentDomains from './deploymentDomains.svelte';
import { tooltip } from '$lib/actions/tooltip';
export let deployment: Models.Deployment;
</script>
<CardGrid>
<div class="u-flex u-cross-start u-gap-16">
<div class="avatar" style={`--p-image-size: ${32 / 16}rem`} aria-hidden="true">
<SvgIcon size={64} iconSize="large" name={$func.runtime.split('-')[0]}></SvgIcon>
</div>
<div class="u-grid-equal-row-size u-gap-4 u-line-height-1">
<p><b>Deployment ID</b></p>
<Id value={deployment.$id}>
{deployment.$id}
</Id>
</div>
</div>
<svelte:fragment slot="aside">
{@const status = deployment.status}
{@const deploymentSize = humanFileSize(deployment.size)}
{@const buildSize = humanFileSize(deployment.buildSize)}
{@const totalSize = humanFileSize(deployment.buildSize + deployment.size)}
<ul class="stats-grid-box u-gap-16">
<li class="u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Status</p>
<span>
<Pill
danger={status === 'failed'}
warning={status === 'building'}
success={status === 'ready'}>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text u-trim">
{status === 'ready' ? 'active' : status}
</span>
</Pill>
</span>
</li>
<li class="u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Build time</p>
<p class="u-line-height-2">
{calculateTime(deployment.buildTime)}
</p>
</li>
<li class="u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Total size</p>
<p class="u-line-height-2">
{totalSize.value + totalSize.unit}
<button
type="button"
on:click|preventDefault
class="tooltip"
aria-label="input tooltip"
use:tooltip={{
content: `
<p><b>Deployment size:</b> ${deploymentSize.value + deploymentSize.unit}</p>
<p><b>Build size:</b> ${buildSize.value + buildSize.unit}</p>
`,
allowHTML: true,
appendTo: 'parent'
}}>
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
</p>
</li>
<li class="u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Updated</p>
<p class="u-line-height-2">
<DeploymentCreatedBy {deployment} />
</p>
</li>
</ul>
<div class="u-flex u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Source</p>
<div>
<DeploymentSource {deployment} />
</div>
</div>
{#if $proxyRuleList?.rules?.length}
<div class="u-flex u-flex-vertical u-gap-4">
<p class="u-color-text-offline">Domains</p>
<DeploymentDomains domain={$proxyRuleList} />
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<slot name="actions" />
</svelte:fragment>
</CardGrid>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
.stats-grid-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@media #{$break3open} {
.stats-grid-box {
grid-template-columns: repeat(4, 1fr);
}
}
</style>
@@ -0,0 +1,14 @@
<script lang="ts">
import { timeFromNow } from '$lib/helpers/date';
import type { Models } from '@appwrite.io/console';
export let deployment: Models.Deployment;
</script>
{timeFromNow(deployment.$updatedAt)}
{#if deployment.providerCommitAuthor}
by <a
class="u-underline u-cursor-pointer"
target="_blank"
href={deployment.providerCommitAuthorUrl}>{deployment.providerCommitAuthor}</a>
{/if}
@@ -0,0 +1,38 @@
<script lang="ts">
import { DropList, DropListLink, Trim } from '$lib/components';
import { Pill } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
export let domain: Models.ProxyRuleList;
let showDropdown = false;
</script>
<div class="u-flex u-gap-4 u-cross-center">
<a
href={`http://${domain.rules[0].domain}`}
target="_blank"
class="u-flex u-gap-4 u-cross-center">
<Trim alternativeTrim>
<span class="link">
{domain.rules[0].domain}
</span>
</Trim>
<span class="icon-external-link" aria-hidden="true" />
</a>
{#if domain.rules.length > 1}
<DropList bind:show={showDropdown} scrollable>
<Pill button on:click={() => (showDropdown = !showDropdown)}>
+{domain.rules.length - 1}
</Pill>
<svelte:fragment slot="list">
{#each domain.rules as rule, i}
{#if i !== 0}
<DropListLink href={`http://${rule.domain}`} external icon="external-link">
{rule.domain}
</DropListLink>
{/if}
{/each}
</svelte:fragment>
</DropList>
{/if}
</div>
@@ -0,0 +1,38 @@
<script lang="ts">
import { DropList, DropListLink, Trim } from '$lib/components';
import type { Models } from '@appwrite.io/console';
export let deployment: Models.Deployment;
let showDropdown = false;
</script>
{#if deployment.type === 'vcs'}
<DropList bind:show={showDropdown}>
<button
on:click={() => (showDropdown = !showDropdown)}
type="button"
class="u-flex u-gap-4 u-cross-center">
<span class="icon-github" aria-hidden="true" /> <span class="link">GitHub</span>
</button>
<svelte:fragment slot="list">
<DropListLink href={deployment.providerRepositoryUrl} external icon="github">
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
</DropListLink>
<DropListLink href={deployment.providerBranchUrl} external icon="git-branch">
{deployment.providerBranch}
</DropListLink>
{#if deployment?.providerCommitMessage && deployment?.providerCommitHash && deployment?.providerCommitUrl}
<DropListLink href={deployment.providerCommitUrl} external icon="git-commit">
<Trim alternativeTrim>
{deployment?.providerCommitHash?.substring(0, 7)}
{deployment.providerCommitMessage}
</Trim>
</DropListLink>
{/if}
</svelte:fragment>
</DropList>
{:else if deployment.type === 'manual'}
<span class="icon-code" aria-hidden="true" /> <span>Manual</span>
{:else if deployment.type === 'cli'}
<span class="icon-terminal" aria-hidden="true" /> <span>CLI</span>
{/if}
@@ -0,0 +1,281 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { DropList, DropListItem } from '$lib/components';
import {
addFilter,
queries,
queryParamToMap,
tagFormat,
tags,
ValidOperators,
type TagValue
} from '$lib/components/filters/store';
import { Pill, SelectSearchCheckbox } from '$lib/elements';
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 === 'size');
let sizeFilter = buildFilterCol(sizeCol);
const buildTimeCol = $columns.find((col) => col.id === 'buildTime');
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}
<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_deployments', 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;
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)}
<DropListItem
padding={8}
on:click={() => {
filter.show = false;
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
{/each}
{#each [sizeFilter] 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_deployments', 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;
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
filter?.array ? null : option.value,
[]
);
}}>
{option.label}
</DropListItem>
{/each}
{#if filter?.tag}
<DropListItem
padding={8}
on:click={() => {
filter.show = false;
filter.tag = null;
addFilterAndApply(filter.id, filter.title, filter.operator, null, []);
}}>
Clear selection
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
{/each}
@@ -0,0 +1,56 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { Modal } from '$lib/components';
import { func } from './store';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
export let show = false;
export let selectedDeployment: Models.Deployment = null;
let error: string;
async function redeploy() {
try {
await sdk.forProject.functions.createBuild(
$func.$id,
selectedDeployment.$id,
selectedDeployment.buildId
);
addNotification({
type: 'success',
message: `Redeploying ${$func.name}`
});
trackEvent(Submit.FunctionRedeploy);
invalidate(Dependencies.FUNCTION);
invalidate(Dependencies.DEPLOYMENTS);
show = false;
} catch (e) {
error = e.message;
trackError(e, Submit.FunctionRedeploy);
}
}
</script>
<Modal
title="Redeploy function"
size="big"
bind:show
bind:error
onSubmit={redeploy}
headerDivider={false}>
<p class="text">
Are you sure you want to redeploy <b>{$func.name}</b>? Redeploying may affect your
production code.
</p>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>Redeploy</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,25 @@
import { page } from '$app/stores';
import { derived, writable, type Writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
export const func = derived(page, ($page) => $page.data.function as Models.Function);
export const deploymentList = derived(
page,
($page) => $page.data.deploymentList as Models.DeploymentList
);
export const proxyRuleList = derived(
page,
($page) => $page.data.proxyRuleList as Models.ProxyRuleList
);
export const repositories: Writable<{
search: string;
installationId: string;
repositories: Models.ProviderRepository[];
}> = writable({
search: '',
installationId: '',
repositories: []
});
export const showCreateDeployment: Writable<boolean> = writable(false);
@@ -0,0 +1,196 @@
<script lang="ts">
import { DropList, DropListItem, DropListLink, Id } from '$lib/components';
import {
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import type { PageData } from './$types';
import { type Models } from '@appwrite.io/console';
import type { Column } from '$lib/helpers/types';
import { Pill } from '$lib/elements';
import { calculateTime } from '$lib/helpers/timeConversion';
import DeploymentSource from './deploymentSource.svelte';
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
import { timer } from '$lib/actions/timer';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { func } from './store';
import { page } from '$app/stores';
import Delete from './delete.svelte';
import RedeployModal from './redeployModal.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import Cancel from './cancel.svelte';
import { sdk } from '$lib/stores/sdk';
import { base } from '$app/paths';
export let columns: Column[];
export let data: PageData;
let showDropdown = [];
let showDelete = false;
let showActivate = false;
let showRedeploy = false;
let showCancel = false;
let selectedDeployment: Models.Deployment = null;
function handleActivate() {
invalidate(Dependencies.DEPLOYMENTS);
}
function getDownload(deploymentId: string) {
return (
sdk.forProject.functions.getDeploymentDownload($func.$id, deploymentId).toString() +
'&mode=admin'
);
}
</script>
<TableScroll noMargin>
<TableHeader>
{#each columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
{/if}
{/each}
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each data.deploymentList.deployments as deployment, index (deployment.$id)}
<TableRow>
{#each columns as column}
{#if column.show}
{#if column.id === '$id'}
{#key column.id}
<TableCell width={column.width} title="Deployment ID">
<Id value={deployment.$id}>{deployment.$id}</Id>
</TableCell>
{/key}
{:else if column.id === 'status'}
<TableCell width={column.width} title={column.title}>
{@const status = deployment.status}
{#if data?.activeDeployment?.$id === deployment?.$id}
<Pill success>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text u-trim">active</span>
</Pill>
{:else}
<Pill
danger={status === 'failed'}
warning={status === 'building'}
info={status === 'ready'}>
{status}
</Pill>
{/if}
</TableCell>
{:else if column.id === 'type'}
<TableCell width={column.width} title={column.title}>
<DeploymentSource {deployment} />
</TableCell>
{:else if column.id === '$updatedAt'}
<TableCellText width={column.width} title={column.title}>
<DeploymentCreatedBy {deployment} />
</TableCellText>
{:else if column.id === 'buildTime'}
<TableCellText width={column.width} title={column.title}>
{#if ['processing', 'building'].includes(deployment.status)}
<span use:timer={{ start: deployment.$createdAt }} />
{:else}
{calculateTime(deployment.buildTime)}
{/if}
</TableCellText>
{:else if column.id === 'size'}
<TableCellText width={column.width} title={column.title}>
{calculateSize(deployment.size)}
</TableCellText>
{:else if column.id === 'buildSize'}
<TableCellText width={column.width} title={column.title}>
{calculateSize(deployment.buildSize)}
</TableCellText>
{/if}
{/if}
{/each}
<TableCell width={40} showOverflow>
<DropList bind:show={showDropdown[index]} placement="bottom-start" noArrow>
<button
class="button is-only-icon is-text"
aria-label="More options"
on:click|preventDefault={() => {
showDropdown[index] = !showDropdown[index];
}}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</button>
<svelte:fragment slot="list">
<DropListItem
icon="refresh"
on:click={() => {
selectedDeployment = deployment;
showRedeploy = true;
showDropdown = [];
}}>
Redeploy
</DropListItem>
{#if deployment.status === 'ready' && deployment.$id !== $func.deployment}
<DropListItem
icon="lightning-bolt"
on:click={() => {
selectedDeployment = deployment;
showActivate = true;
showDropdown = [];
}}>
Activate
</DropListItem>
{/if}
<DropListLink
icon="terminal"
href={`${base}/project-${$page.params.project}/functions/function-${$page.params.function}/deployment-${deployment.$id}`}>
Logs
</DropListLink>
<DropListLink
icon="download"
href={getDownload(deployment.$id)}
on:click={() => (showDropdown[index] = false)}>
Download
</DropListLink>
{#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'}
<DropListItem
icon="x-circle"
event="deployment_cancel"
on:click={() => {
selectedDeployment = deployment;
showDropdown = [];
showCancel = true;
}}>
Cancel
</DropListItem>
{/if}
{#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'}
<DropListItem
icon="trash"
on:click={() => {
selectedDeployment = deployment;
showDropdown = [];
showDelete = true;
}}>
Delete
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</TableScroll>
{#if selectedDeployment}
<Delete {selectedDeployment} bind:showDelete />
<!-- <Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} /> -->
<Cancel {selectedDeployment} bind:showCancel />
<RedeployModal {selectedDeployment} bind:show={showRedeploy} />
{/if}