mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-pause-project
This commit is contained in:
@@ -22,11 +22,11 @@ jobs:
|
||||
# run: npm audit --audit-level low
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
- name: Svelte Diagnostics
|
||||
run: npm run check
|
||||
- name: Linter
|
||||
run: npm run lint
|
||||
- name: Unit Tests
|
||||
run: npm test
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
|
||||
@@ -131,6 +131,7 @@ export enum Submit {
|
||||
ProjectCreate = 'submit_project_create',
|
||||
ProjectDelete = 'submit_project_delete',
|
||||
ProjectUpdateName = 'submit_project_update_name',
|
||||
ProjectUpdateTeam = 'submit_project_update_team',
|
||||
ProjectService = 'submit_project_service',
|
||||
MemberCreate = 'submit_member_create',
|
||||
MemberDelete = 'submit_member_delete',
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
{#if single}
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
on:click|preventDefault
|
||||
@@ -40,12 +41,14 @@
|
||||
</button>
|
||||
<slot>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first {target} to get started.</Heading>
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
Create your first {target} to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
{href}
|
||||
|
||||
@@ -1,19 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { getElementDir } from '$lib/helpers/style';
|
||||
import { waitUntil } from '$lib/helpers/waitUntil';
|
||||
|
||||
export let selected = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
|
||||
function track() {
|
||||
if (!event) return;
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
async function handleClick(e: Event) {
|
||||
if (event) {
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
let el = e.target as HTMLElement;
|
||||
if (el.tagName !== 'A') {
|
||||
el = el.closest('a');
|
||||
}
|
||||
|
||||
await goto(href);
|
||||
await waitUntil(() => {
|
||||
console.log('tickUntil', el);
|
||||
return el.classList.contains('is-selected');
|
||||
}, 1000);
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const keysMap = {
|
||||
ltr: {
|
||||
next: 'ArrowRight',
|
||||
prev: 'ArrowLeft'
|
||||
},
|
||||
rtl: {
|
||||
next: 'ArrowLeft',
|
||||
prev: 'ArrowRight'
|
||||
}
|
||||
};
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const tabBtn = e.target as HTMLElement;
|
||||
const tabItem = tabBtn.closest('.tabs-item') as HTMLElement;
|
||||
const tabsList = tabItem.closest('.tabs') as HTMLElement;
|
||||
const tabItems = Array.from(tabsList.querySelectorAll('.tabs-item'));
|
||||
const currentIdx = tabItems.indexOf(tabItem);
|
||||
|
||||
const dir = getElementDir(tabsList);
|
||||
|
||||
switch (e.key) {
|
||||
case 'Home': {
|
||||
e.preventDefault();
|
||||
const firstTabBtn = tabItems[0].querySelector('.tabs-button') as HTMLElement;
|
||||
firstTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case 'End': {
|
||||
e.preventDefault();
|
||||
const lastTabBtn = last(tabItems).querySelector('.tabs-button') as HTMLElement;
|
||||
lastTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case keysMap[dir].next: {
|
||||
e.preventDefault();
|
||||
const nextIdx = currentIdx === tabItems.length - 1 ? 0 : currentIdx + 1;
|
||||
const nextTabBtn = tabItems[nextIdx].querySelector('.tabs-button') as HTMLElement;
|
||||
nextTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case keysMap[dir].prev: {
|
||||
e.preventDefault();
|
||||
const prevIdx = currentIdx === 0 ? tabItems.length - 1 : currentIdx - 1;
|
||||
const prevTabBtn = tabItems[prevIdx].querySelector('.tabs-button') as HTMLElement;
|
||||
prevTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="tabs-item">
|
||||
{#if href}
|
||||
<a class="tabs-button" {href} class:is-selected={selected} on:click={track}>
|
||||
<a
|
||||
class="tabs-button"
|
||||
{href}
|
||||
class:is-selected={selected}
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{:else}
|
||||
@@ -22,7 +98,10 @@
|
||||
class="tabs-button"
|
||||
class:is-selected={selected}
|
||||
on:click|preventDefault
|
||||
on:click={track}>
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -2,11 +2,28 @@
|
||||
import type { Action } from 'svelte/action';
|
||||
|
||||
export let isSticky = false;
|
||||
let isOverflowing = false;
|
||||
|
||||
const hasOverflow: Action<HTMLDivElement, (value: boolean) => void> = (node, callback) => {
|
||||
const hasOverflow: Action<HTMLDivElement> = (node) => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
callback(entry.contentRect.width < entry.target.scrollWidth);
|
||||
let overflowing = false;
|
||||
if (entry.contentRect.width < entry.target.scrollWidth) {
|
||||
overflowing = true;
|
||||
}
|
||||
|
||||
const cols = entry.target.querySelectorAll('.table-thead-col');
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const col = cols[i];
|
||||
const cs = getComputedStyle(col);
|
||||
const innerWidth =
|
||||
col.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
||||
if (innerWidth < 32) {
|
||||
overflowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
isOverflowing = overflowing;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,12 +35,10 @@
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let isOverflowing = false;
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll u-margin-block-start-32" data-private>
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<div class="table-wrapper" use:hasOverflow>
|
||||
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
|
||||
<slot />
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
type Direction = 'rtl' | 'ltr';
|
||||
|
||||
function isDirection(dir: string): dir is Direction {
|
||||
return dir === 'rtl' || dir === 'ltr';
|
||||
}
|
||||
|
||||
function parseDirection(dir: string): Direction {
|
||||
return isDirection(dir) ? dir : 'ltr';
|
||||
}
|
||||
|
||||
export function getElementDir(el: HTMLElement): Direction {
|
||||
if (window.getComputedStyle) {
|
||||
return parseDirection(window.getComputedStyle(el, null).getPropertyValue('direction'));
|
||||
}
|
||||
return parseDirection(el.style.direction);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export async function waitUntil(condition: () => boolean, timeout = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(interval);
|
||||
resolve(undefined);
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Timeout'));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
|
||||
if ('buildId' in data) {
|
||||
selectedTab = 'logs';
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployment/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployments/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
function isExecution(data: Models.Deployment | Models.Execution): data is Models.Execution {
|
||||
if ('trigger' in data) {
|
||||
selectedTab = 'response';
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/executions/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte';
|
||||
import Auth0 from '../../routes/console/project-[project]/auth/auth0OAuth.svelte';
|
||||
import Authentik from '../../routes/console/project-[project]/auth/authentikOAuth.svelte';
|
||||
import GitLab from '../../routes/console/project-[project]/auth/gitlabOAuth.svelte';
|
||||
import Google from '../../routes/console/project-[project]/auth/googleOAuth.svelte';
|
||||
import Main from '../../routes/console/project-[project]/auth/mainOAuth.svelte';
|
||||
|
||||
export type Provider = Models.Provider & {
|
||||
@@ -81,6 +82,7 @@ const setProviders = (project: Models.Project): Provider[] => {
|
||||
break;
|
||||
case 'google':
|
||||
docs = 'https://support.google.com/googleapi/answer/6158849';
|
||||
component = Google;
|
||||
break;
|
||||
case 'linkedin':
|
||||
docs = 'https://developer.linkedin.com/';
|
||||
|
||||
@@ -2,20 +2,56 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Heading } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
const project = $page.url.searchParams.get('project');
|
||||
if (project) {
|
||||
await goto(`appwrite-callback-${project}://${$page.url.search}`);
|
||||
const project = $page.url.searchParams.get('project');
|
||||
const link = `appwrite-callback-${project}://${$page.url.search}`;
|
||||
|
||||
const redirect = new Promise((resolve, reject) => {
|
||||
if (!project) {
|
||||
reject('no-project');
|
||||
}
|
||||
// this timeout is needed because goto does not
|
||||
// throw an exception if the redirect does not work
|
||||
setTimeout(() => reject('timeout'), 500);
|
||||
// goto will resolve on successful redirect
|
||||
goto(link).then(resolve);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Heading tag="h1" size="1">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a class="link" href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
{#await redirect then}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Login failed</Heading>
|
||||
<p class="text">You will be automatically redirected back to your app shortly.</p>
|
||||
<p class="text">
|
||||
If you are not redirected, please click on the following
|
||||
<a class="link" href={`appwrite-callback-${project}://${$page.url.search}`}>link</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{:catch}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
// override padding for screens bigger than mobile
|
||||
@media #{$break2open} {
|
||||
article.card {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,20 +2,56 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Heading } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
const project = $page.url.searchParams.get('project');
|
||||
if (project) {
|
||||
await goto(`appwrite-callback-${project}://${$page.url.search}`);
|
||||
const project = $page.url.searchParams.get('project');
|
||||
const link = `appwrite-callback-${project}://${$page.url.search}`;
|
||||
|
||||
const redirect = new Promise((resolve, reject) => {
|
||||
if (!project) {
|
||||
reject('no-project');
|
||||
}
|
||||
// this timeout is needed because goto does not
|
||||
// throw an exception if the redirect does not work
|
||||
setTimeout(() => reject('timeout'), 500);
|
||||
// goto will resolve on successful redirect
|
||||
goto(link).then(resolve);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Heading tag="h1" size="1">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a class="link" href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
{#await redirect then}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">You're now logged in</Heading>
|
||||
<p class="text">You will be automatically redirected back to your app shortly.</p>
|
||||
<p class="text">
|
||||
If you are not redirected, please click on the following
|
||||
<a class="link" href={`appwrite-callback-${project}://${$page.url.search}`}>link</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{:catch}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
// override padding for screens bigger than mobile
|
||||
@media #{$break2open} {
|
||||
article.card {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
disabled={(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId) ||
|
||||
!(appId && clientSecret && endpoint)}
|
||||
!(appId && clientSecret)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from './updateOAuth';
|
||||
|
||||
export let provider: Provider;
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
});
|
||||
|
||||
let error: string;
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal {error} size="big" show onSubmit={update} on:close>
|
||||
<svelte:fragment slot="header">{provider.name} OAuth2 Settings</svelte:fragment>
|
||||
<FormList>
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="App ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId} />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="App Secret"
|
||||
placeholder="Enter App Secret"
|
||||
minlength={0}
|
||||
showPasswordButton
|
||||
bind:value={secret} />
|
||||
<Alert type="info">
|
||||
To complete the setup, create an OAuth2 client ID with "Web application" as the
|
||||
application type, then add this redirect URI to your {provider.name} configuration.
|
||||
</Alert>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!secret ||
|
||||
(appId === provider.appId &&
|
||||
secret === provider.secret &&
|
||||
enabled === provider.enabled)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputSwitch } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { OAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { OAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { page } from '$app/stores';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
provider: provider.name.toLowerCase()
|
||||
});
|
||||
}}>
|
||||
<div class="image-item">
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
try {
|
||||
await sdk.forProject.users.deleteSession($page.params.user, selectedSessionId);
|
||||
await invalidate(Dependencies.SESSIONS);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Session has been deleted'
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { EmptySearch } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCellHead,
|
||||
TableCell,
|
||||
TableCellText
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import DeleteAllSessions from '../deleteAllSessions.svelte';
|
||||
@@ -47,7 +47,7 @@
|
||||
<TableRow>
|
||||
<TableCell title="Client">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<div class="image-item">
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { PageData } from './$types';
|
||||
@@ -20,7 +20,7 @@
|
||||
const databaseId = $page.params.database;
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
@@ -54,4 +54,4 @@
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { PageData } from './$types';
|
||||
@@ -19,7 +19,7 @@
|
||||
const projectId = $page.params.project;
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
@@ -55,4 +55,4 @@
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import Delete from './delete.svelte';
|
||||
import Create from './create.svelte';
|
||||
import Rebuild from './rebuild.svelte';
|
||||
import Activate from './activate.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -44,6 +45,7 @@
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
let showActivate = false;
|
||||
let showRebuild = false;
|
||||
|
||||
let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
@@ -51,6 +53,10 @@
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
function handleRebuild() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
$: activeDeployment = data.deployments.deployments.find((d) => d.$id === $func?.deployment);
|
||||
|
||||
if (browser) {
|
||||
@@ -215,6 +221,17 @@
|
||||
}}>
|
||||
Activate
|
||||
</DropListItem>
|
||||
{#if 'failed' === deployment.status}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showRebuild = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Retry Build
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="terminal"
|
||||
on:click={() => {
|
||||
@@ -281,4 +298,5 @@
|
||||
{#if selectedDeployment}
|
||||
<Delete {selectedDeployment} bind:showDelete />
|
||||
<Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} />
|
||||
<Rebuild {selectedDeployment} bind:showRebuild on:rebuild={handleRebuild} />
|
||||
{/if}
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
function setCodeSnippets(lang: string) {
|
||||
return {
|
||||
Unix: {
|
||||
code: `appwrite functions createDeployment \\
|
||||
--functionId=${functionId} \\
|
||||
--entrypoint='index.${lang}' \\
|
||||
--code="." \\
|
||||
code: `appwrite functions createDeployment \\
|
||||
--functionId=${functionId} \\
|
||||
--entrypoint='index.${lang}' \\
|
||||
--code="." \\
|
||||
--activate=true`,
|
||||
language: 'bash'
|
||||
},
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
<CoverTitle href={`/console/project-${projectId}/functions`}>
|
||||
{$func?.name}
|
||||
</CoverTitle>
|
||||
<Id value={$func?.$id} event="function">Function ID</Id>
|
||||
{#if $func?.$id}
|
||||
<Id value={$func.$id} event="function">{$func.$id}</Id>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
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';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let showRebuild = false;
|
||||
export let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await sdk.forProject.functions.createBuild(
|
||||
selectedDeployment.resourceId,
|
||||
selectedDeployment.$id,
|
||||
selectedDeployment.buildId
|
||||
);
|
||||
showRebuild = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Retrying build`
|
||||
});
|
||||
dispatch('rebuild');
|
||||
trackEvent(Submit.DeploymentUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.DeploymentUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showRebuild} onSubmit={handleSubmit}>
|
||||
<svelte:fragment slot="header">Retry build</svelte:fragment>
|
||||
<p>Are you sure you want to retry building this deployment?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showRebuild = false)}>Cancel</Button>
|
||||
<Button secondary submit>Retry build</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+16
-511
@@ -1,520 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import {
|
||||
Box,
|
||||
CardGrid,
|
||||
DropList,
|
||||
DropListItem,
|
||||
Empty,
|
||||
Output,
|
||||
PaginationInline,
|
||||
Secret
|
||||
} from '$lib/components';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputCron, InputNumber, InputText } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import { execute, func } from '../store';
|
||||
import UploadVariables from './uploadVariables.svelte';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import type { PageData } from './$types';
|
||||
import Delete from './delete.svelte';
|
||||
|
||||
import UpdateEvents from './updateEvents.svelte';
|
||||
import ExecuteFunction from './executeFunction.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdatePermissions from './updatePermissions.svelte';
|
||||
import UpdateSchedule from './updateSchedule.svelte';
|
||||
import UpdateVariables from './updateVariables.svelte';
|
||||
import UpdateTimeout from './updateTimeout.svelte';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let showDelete = false;
|
||||
let selectedVar: Models.Variable = null;
|
||||
let showVariablesUpload = false;
|
||||
let showVariablesModal = false;
|
||||
let showVariablesDropdown = [];
|
||||
let timeout: number = null;
|
||||
let functionName: string = null;
|
||||
let functionSchedule: string = null;
|
||||
let permissions: string[] = [];
|
||||
let arePermsDisabled = true;
|
||||
let offset = 0;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
functionName ??= $func.name;
|
||||
functionSchedule ??= $func.schedule;
|
||||
permissions = $func.execute;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
functionName,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Name has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateName);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateName);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
permissions,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSchedule() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
functionSchedule,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Cron Schedule has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateSchedule);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTimeout() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
timeout,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Timeout has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateTimeout);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
|
||||
try {
|
||||
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableCreate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
try {
|
||||
await sdk.forProject.functions.updateVariable(
|
||||
functionId,
|
||||
variable.$id,
|
||||
variable.key,
|
||||
variable.value
|
||||
);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
selectedVar = null;
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableUpdate);
|
||||
}
|
||||
}
|
||||
async function handleVariableDeleted(variable: Models.Variable) {
|
||||
try {
|
||||
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Variable has been deleted`
|
||||
});
|
||||
trackEvent(Submit.VariableDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadVariables() {
|
||||
if (data.variables.total) {
|
||||
let content = data.variables.variables
|
||||
.map((variable) => `${variable.key}=${variable.value}`)
|
||||
.join('\n');
|
||||
const file = new File([content], '.env', {
|
||||
type: 'application/x-envoy'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $func.execute).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
}
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-medium">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${
|
||||
$func.runtime.split('-')[0]
|
||||
}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">{$func.name}</Heading>
|
||||
|
||||
<p class="text u-capitalize">{$func.runtime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div>
|
||||
<p>Function ID: {$func.$id}</p>
|
||||
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
|
||||
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($execute = $func)}>Execute now</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Name</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autocomplete={false}
|
||||
bind:value={functionName} />
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={functionName === $func.name || !functionName} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Execute Access</Heading>
|
||||
<p>
|
||||
Choose who can execute this function using the client API. For more information,
|
||||
check out the <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<ExecuteFunction />
|
||||
<UpdateName />
|
||||
<UpdatePermissions />
|
||||
<UpdateEvents />
|
||||
|
||||
<Form onSubmit={updateSchedule}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Schedule</Heading>
|
||||
<p>
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://en.wikipedia.org/wiki/Cron"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
More details on Cron syntax here.</a>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={functionSchedule}
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.schedule === functionSchedule} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Variables</Heading>
|
||||
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-margin-inline-start-auto u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
event="download_env"
|
||||
disabled={!data.variables.total}
|
||||
on:click={downloadVariables}>
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download .env file</span>
|
||||
</Button>
|
||||
<Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<span class="icon-upload" />
|
||||
<span class="text">Import .env file</span>
|
||||
</Button>
|
||||
</div>
|
||||
{@const limit = 10}
|
||||
{@const sum = data.variables.total}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Key</TableCellHead>
|
||||
<TableCellHead width={180}>Value</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.variables.variables.slice(offset, offset + limit) as variable, i}
|
||||
<TableRow>
|
||||
<TableCell title="key">
|
||||
<Output value={variable.key} hideCopyIcon>
|
||||
{variable.key}
|
||||
</Output>
|
||||
</TableCell>
|
||||
|
||||
<TableCell showOverflow title="value">
|
||||
<Secret copyEvent="variable" value={variable.value} />
|
||||
</TableCell>
|
||||
<TableCell showOverflow title="options">
|
||||
<DropList
|
||||
bind:show={showVariablesDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
text
|
||||
round
|
||||
ariaLabel="more options"
|
||||
on:click={() =>
|
||||
(showVariablesDropdown[i] =
|
||||
!showVariablesDropdown[i])}>
|
||||
<span
|
||||
class="icon-dots-horizontal"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
selectedVar = variable;
|
||||
showVariablesDropdown[i] = false;
|
||||
showVariablesModal = true;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={async () => {
|
||||
handleVariableDeleted(variable);
|
||||
showVariablesDropdown[i] = false;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button text noMargin on:click={() => (showVariablesModal = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create variable</span>
|
||||
</Button>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total variables: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
|
||||
Create a variable to get started
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateTimeout}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Timeout</Heading>
|
||||
<p>
|
||||
Limit the execution time of your function. Maximum value is 900 seconds (15
|
||||
minutes).
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={900}
|
||||
id="time"
|
||||
label="Time (in seconds)"
|
||||
bind:value={timeout} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.timeout === timeout || timeout < 1} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete Function</Heading>
|
||||
<p>
|
||||
The function will be permanently deleted, including all deployments associated with it.
|
||||
This action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$func.name}</h6>
|
||||
</svelte:fragment>
|
||||
<p>Last Updated: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<UpdateSchedule />
|
||||
<UpdateVariables variableList={data.variables} />
|
||||
<UpdateTimeout />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
{#if showVariablesModal}
|
||||
<Variable
|
||||
bind:selectedVar
|
||||
bind:showCreate={showVariablesModal}
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
{#if showVariablesUpload}
|
||||
<UploadVariables bind:show={showVariablesUpload} />
|
||||
{/if}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Box, CardGrid } from '$lib/components';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import Delete from './deleteModal.svelte';
|
||||
|
||||
import { func } from '../store';
|
||||
|
||||
let showDelete = false;
|
||||
</script>
|
||||
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete Function</Heading>
|
||||
<p>
|
||||
The function will be permanently deleted, including all deployments associated with it. This
|
||||
action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$func.name}</h6>
|
||||
</svelte:fragment>
|
||||
<p>Last Updated: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { execute, func } from '../store';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-medium">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${$func.runtime.split('-')[0]}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">{$func.name}</Heading>
|
||||
|
||||
<p class="text u-capitalize">{$func.runtime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div>
|
||||
<p>Function ID: {$func.$id}</p>
|
||||
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
|
||||
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($execute = $func)}>Execute now</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let functionName: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
functionName ??= $func.name;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
functionName,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Name has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateName);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateName);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Name</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autocomplete={false}
|
||||
bind:value={functionName} />
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={functionName === $func.name || !functionName} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
|
||||
let arePermsDisabled = true;
|
||||
let permissions: string[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
permissions = $func.execute;
|
||||
});
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
permissions,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $func.execute).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Execute Access</Heading>
|
||||
<p>
|
||||
Choose who can execute this function using the client API. For more information, check
|
||||
out the <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputCron } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let functionSchedule: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
functionSchedule ??= $func.schedule;
|
||||
});
|
||||
|
||||
async function updateSchedule() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
functionSchedule,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Cron Schedule has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateSchedule);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateSchedule);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSchedule}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Schedule</Heading>
|
||||
<p>
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://appwrite.io/docs/functions#scheduled-execution"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
More details on Cron syntax here.</a>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={functionSchedule}
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.schedule === functionSchedule} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputNumber } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let timeout: number = null;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
});
|
||||
|
||||
async function updateTimeout() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
timeout,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Timeout has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateTimeout);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateTimeout);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateTimeout}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Timeout</Heading>
|
||||
<p>Limit the execution time of your function. Maximum value is 900 seconds (15 minutes).</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={900}
|
||||
id="time"
|
||||
label="Time (in seconds)"
|
||||
bind:value={timeout} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.timeout === timeout || timeout < 1} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
CardGrid,
|
||||
Heading,
|
||||
DropList,
|
||||
DropListItem,
|
||||
Empty,
|
||||
Output,
|
||||
PaginationInline,
|
||||
Secret
|
||||
} from '$lib/components';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import UploadVariables from './uploadVariablesModal.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { func } from '../store';
|
||||
|
||||
export let variableList: Models.VariableList;
|
||||
const functionId = $page.params.function;
|
||||
let showVariablesDropdown = [];
|
||||
let selectedVar: Models.Variable = null;
|
||||
let showVariablesUpload = false;
|
||||
let showVariablesModal = false;
|
||||
let offset = 0;
|
||||
|
||||
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
|
||||
try {
|
||||
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableCreate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
try {
|
||||
await sdk.forProject.functions.updateVariable(
|
||||
functionId,
|
||||
variable.$id,
|
||||
variable.key,
|
||||
variable.value
|
||||
);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
selectedVar = null;
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableUpdate);
|
||||
}
|
||||
}
|
||||
async function handleVariableDeleted(variable: Models.Variable) {
|
||||
try {
|
||||
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Variable has been deleted`
|
||||
});
|
||||
trackEvent(Submit.VariableDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadVariables() {
|
||||
if (variableList.total) {
|
||||
let content = variableList.variables
|
||||
.map((variable) => `${variable.key}=${variable.value}`)
|
||||
.join('\n');
|
||||
const file = new File([content], '.env', {
|
||||
type: 'application/x-envoy'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Variables</Heading>
|
||||
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-margin-inline-start-auto u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
event="download_env"
|
||||
disabled={!variableList.total}
|
||||
on:click={downloadVariables}>
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download .env file</span>
|
||||
</Button>
|
||||
<Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<span class="icon-upload" />
|
||||
<span class="text">Import .env file</span>
|
||||
</Button>
|
||||
</div>
|
||||
{@const limit = 10}
|
||||
{@const sum = variableList.total}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Key</TableCellHead>
|
||||
<TableCellHead width={180}>Value</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each variableList.variables.slice(offset, offset + limit) as variable, i}
|
||||
<TableRow>
|
||||
<TableCell title="key">
|
||||
<Output value={variable.key} hideCopyIcon>
|
||||
{variable.key}
|
||||
</Output>
|
||||
</TableCell>
|
||||
|
||||
<TableCell showOverflow title="value">
|
||||
<Secret copyEvent="variable" value={variable.value} />
|
||||
</TableCell>
|
||||
<TableCell showOverflow title="options">
|
||||
<DropList
|
||||
bind:show={showVariablesDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
text
|
||||
round
|
||||
ariaLabel="more options"
|
||||
on:click={() =>
|
||||
(showVariablesDropdown[i] =
|
||||
!showVariablesDropdown[i])}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
selectedVar = variable;
|
||||
showVariablesDropdown[i] = false;
|
||||
showVariablesModal = true;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={async () => {
|
||||
handleVariableDeleted(variable);
|
||||
showVariablesDropdown[i] = false;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button text noMargin on:click={() => (showVariablesModal = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create variable</span>
|
||||
</Button>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total variables: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
|
||||
Create a variable to get started
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
{#if showVariablesModal}
|
||||
<Variable
|
||||
bind:selectedVar
|
||||
bind:showCreate={showVariablesModal}
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
{#if showVariablesUpload}
|
||||
<UploadVariables bind:show={showVariablesUpload} />
|
||||
{/if}
|
||||
@@ -65,7 +65,7 @@
|
||||
},
|
||||
[Platform.Web]: {
|
||||
name: 'My Web App',
|
||||
hostname: 'com.company.appname',
|
||||
hostname: 'localhost',
|
||||
tooltip:
|
||||
'The hostname that your website will use to interact with the Appwrite APIs in production or development environments. No protocol or port number required.'
|
||||
},
|
||||
|
||||
@@ -35,8 +35,12 @@
|
||||
</svelte:fragment>
|
||||
{#if method === Method.NPM}
|
||||
<p>
|
||||
Use <a href="https://npmjs.org" target="_blank" rel="noopener noreferrer" class="link"
|
||||
>NPM (node package manager)</a> from your command line to add Appwrite SDK to your project.
|
||||
Use <a
|
||||
href="https://npmjs.com/package/appwrite"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">NPM (node package manager)</a> from your command line to add Appwrite SDK
|
||||
to your project.
|
||||
</p>
|
||||
<Code label="Bash" language="sh" code="npm install appwrite" withCopy />
|
||||
<p class="common-section">
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { project } from '../store';
|
||||
import { services, type Service } from '$lib/stores/project-services';
|
||||
import { CardGrid, CopyInput, Box, Heading } from '$lib/components';
|
||||
import { Button, Form, FormList, InputText, InputSwitch } from '$lib/elements/forms';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormList,
|
||||
InputText,
|
||||
InputSwitch,
|
||||
InputSelect
|
||||
} from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -16,9 +24,12 @@
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import EnableAllServices from './enableAllServices.svelte';
|
||||
import DisableAllServices from './disableAllServices.svelte';
|
||||
import Transfer from './transferProject.svelte';
|
||||
|
||||
let name: string = null;
|
||||
let teamId: string = null;
|
||||
let showDelete = false;
|
||||
let showTransfer = false;
|
||||
const endpoint = sdk.forConsole.client.config.endpoint;
|
||||
const projectId = $page.params.project;
|
||||
let showDisableAll = false;
|
||||
@@ -26,6 +37,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
name ??= $project.name;
|
||||
teamId ??= $project.teamId;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
@@ -163,7 +175,10 @@
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Services</Heading>
|
||||
<p class="text">Choose services you wish to enable or disable.</p>
|
||||
<p class="text">
|
||||
Choose services you wish to enable or disable for the client API. When disabled, the
|
||||
services are not accessible to client SDKs but remain accessible to server SDKs.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<ul class="buttons-list u-main-end">
|
||||
<li class="buttons-list-item">
|
||||
@@ -192,7 +207,30 @@
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Transfer project</Heading>
|
||||
<p class="text">Transfer your project to another organization that you own.</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputSelect
|
||||
id="organization"
|
||||
label="Available organizations"
|
||||
bind:value={teamId}
|
||||
options={$organizationList.teams.map((team) => ({
|
||||
value: team.$id,
|
||||
label: team.name
|
||||
}))} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
secondary
|
||||
disabled={teamId === $project.teamId}
|
||||
on:click={() => (showTransfer = true)}>Transfer</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid danger>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Delete Project</Heading>
|
||||
@@ -220,3 +258,9 @@
|
||||
<Delete bind:showDelete />
|
||||
<DisableAllServices handleDisableAll={() => toggleAllServices(false)} bind:show={showDisableAll} />
|
||||
<EnableAllServices handleEnableAll={() => toggleAllServices(true)} bind:show={showEnableAll} />
|
||||
{#if teamId}
|
||||
<Transfer
|
||||
bind:teamId
|
||||
teamName={$organizationList.teams.find((t) => t.$id == teamId).name}
|
||||
bind:show={showTransfer} />
|
||||
{/if}
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../store';
|
||||
|
||||
export let show = false;
|
||||
export let teamName;
|
||||
export let teamId;
|
||||
|
||||
const handleTransfer = async () => {
|
||||
try {
|
||||
await sdk.forConsole.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forConsole.client.config.endpoint + '/projects/' + $project.$id + '/team'
|
||||
),
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
{
|
||||
teamId: teamId
|
||||
}
|
||||
);
|
||||
// await sdk.forConsole.projects.update($project.$id, password);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$project.name} has been transfered to ${teamName}`
|
||||
});
|
||||
trackEvent(Submit.ProjectUpdateTeam);
|
||||
await goto(`${base}/console/organization-${teamId}`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.ProjectUpdateTeam);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show onSubmit={handleTransfer} headerDivider={false}>
|
||||
<svelte:fragment slot="header">Transfer project</svelte:fragment>
|
||||
<p>Are you sure you want to transfer <b>{$project.name}</b> to <b>{teamName}</b>?</p>
|
||||
<p>
|
||||
Members who are not part of the destination organization must be invited to gain access to
|
||||
this project.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit>Transfer</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -6,13 +6,13 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableRowLink,
|
||||
TableCellHead,
|
||||
TableCell,
|
||||
TableCellText,
|
||||
TableHeader
|
||||
TableHeader,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { Container } from '$lib/layout';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
@@ -46,10 +46,10 @@
|
||||
</div>
|
||||
|
||||
{#if data.webhooks.total}
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>POST URL</TableCellHead>
|
||||
<TableCellHead width={200}>Name</TableCellHead>
|
||||
<TableCellHead width={180}>POST URL</TableCellHead>
|
||||
<TableCellHead width={80}>Events</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -57,7 +57,7 @@
|
||||
<TableRowLink
|
||||
href={`${base}/console/project-${projectId}/settings/webhooks/${webhook.$id}`}>
|
||||
<TableCell title="Name">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
{webhook.name}
|
||||
{#if webhook.security === false}
|
||||
<Pill>SLL/TLS disabled</Pill>
|
||||
@@ -69,7 +69,7 @@
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<CoverTitle href={`/console/project-${projectId}/storage`}>
|
||||
{$bucket?.name}
|
||||
</CoverTitle>
|
||||
<Id value={$bucket?.$id} event="bucket">Bucket ID</Id>
|
||||
{#if $bucket?.$id}
|
||||
<Id value={$bucket.$id} event="bucket">{$bucket.$id}</Id>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -6,12 +6,12 @@ import { Tab } from '../../../src/lib/components';
|
||||
test('shows tab', () => {
|
||||
const { getByRole } = render(Tab);
|
||||
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
expect(getByRole('tab')).toBeInTheDocument();
|
||||
});
|
||||
test('shows tab - is selected', () => {
|
||||
const { getByRole } = render(Tab, { selected: true });
|
||||
|
||||
expect(getByRole('button')).toHaveClass('is-selected');
|
||||
expect(getByRole('tab')).toHaveClass('is-selected');
|
||||
});
|
||||
|
||||
test('shows tab - is link', () => {
|
||||
@@ -23,7 +23,7 @@ test('shows tab - is link', () => {
|
||||
|
||||
test('shows tab - on:click', async () => {
|
||||
const { getByRole, component } = render(Tab);
|
||||
const tab = getByRole('button');
|
||||
const tab = getByRole('tab');
|
||||
const callback = vi.fn();
|
||||
component.$on('click', callback);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user