Merge branch 'main' of github.com:appwrite/appwrite-console-poc into feat-grid-item-option-overflow-counter

This commit is contained in:
Arman
2022-09-14 11:59:19 +02:00
41 changed files with 1369 additions and 344 deletions
+14 -14
View File
@@ -9,8 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@aw-labs/appwrite-console": "^1.0.0-0",
"@aw-labs/icons": "0.0.0-53",
"@aw-labs/ui": "0.0.0-53",
"@aw-labs/icons": "0.0.0-54",
"@aw-labs/ui": "0.0.0-54",
"echarts": "^5.3.3",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
@@ -77,14 +77,14 @@
}
},
"node_modules/@aw-labs/icons": {
"version": "0.0.0-53",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-53.tgz",
"integrity": "sha512-9UhHTGajh2HJun2uYbcMyxRtPsgIQ5CXf8mn/sCiRyZJIw5FYtuxTSkP/35hv2zJsVq/mY09HvfdX3w4XVKItg=="
"version": "0.0.0-54",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-54.tgz",
"integrity": "sha512-lk+uikRag3Y5Rm4Ys5lHBhknhCVpA/oTaIBBDgB6nidqtJdqLpa+Nur4BdsBXOEZTlo2yOiWU2NNAl6ZzUzFGQ=="
},
"node_modules/@aw-labs/ui": {
"version": "0.0.0-53",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-53.tgz",
"integrity": "sha512-5ftq05SZ9lVdXGHgHIyxwtGznfKsCa1fKPpZhS+279IUTIB+H7GIq0g2U2EzRBj7ht0PXNZJNO4n9hO2ItDJwA==",
"version": "0.0.0-54",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-54.tgz",
"integrity": "sha512-1Dj216qPaVKdBScW+1XIp6IOVBdcv/kYjiWO4YiFtoQ/IaayoSO6njlAPTwyQzclmX5A/9suEGadOVa3NZqbgQ==",
"dependencies": {
"@aw-labs/icons": "*"
}
@@ -7698,14 +7698,14 @@
}
},
"@aw-labs/icons": {
"version": "0.0.0-53",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-53.tgz",
"integrity": "sha512-9UhHTGajh2HJun2uYbcMyxRtPsgIQ5CXf8mn/sCiRyZJIw5FYtuxTSkP/35hv2zJsVq/mY09HvfdX3w4XVKItg=="
"version": "0.0.0-54",
"resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-54.tgz",
"integrity": "sha512-lk+uikRag3Y5Rm4Ys5lHBhknhCVpA/oTaIBBDgB6nidqtJdqLpa+Nur4BdsBXOEZTlo2yOiWU2NNAl6ZzUzFGQ=="
},
"@aw-labs/ui": {
"version": "0.0.0-53",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-53.tgz",
"integrity": "sha512-5ftq05SZ9lVdXGHgHIyxwtGznfKsCa1fKPpZhS+279IUTIB+H7GIq0g2U2EzRBj7ht0PXNZJNO4n9hO2ItDJwA==",
"version": "0.0.0-54",
"resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-54.tgz",
"integrity": "sha512-1Dj216qPaVKdBScW+1XIp6IOVBdcv/kYjiWO4YiFtoQ/IaayoSO6njlAPTwyQzclmX5A/9suEGadOVa3NZqbgQ==",
"requires": {
"@aw-labs/icons": "*"
}
+2 -2
View File
@@ -19,8 +19,8 @@
},
"dependencies": {
"@aw-labs/appwrite-console": "^1.0.0-0",
"@aw-labs/icons": "0.0.0-53",
"@aw-labs/ui": "0.0.0-53",
"@aw-labs/icons": "0.0.0-54",
"@aw-labs/ui": "0.0.0-54",
"echarts": "^5.3.3",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
+18 -13
View File
@@ -4,6 +4,8 @@
import { addNotification } from '$lib/stores/notifications';
export let value: string;
export let label: string = null;
export let showLabel = false;
let content = 'Click to copy';
@@ -21,17 +23,20 @@
</script>
<div class="input-text-wrapper is-with-end-button">
<input {value} type="text" class="input-text" readonly />
<button
type="button"
class="input-button"
aria-label="Click to copy."
on:click={copy}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
content,
hideOnClick: false
}}>
<span class="icon-duplicate" aria-hidden="true" />
</button>
<label class:u-hide={!showLabel} class="label" for={label}>{label}</label>
<div class="input-text-wrapper">
<input {value} id={label} type="text" class="input-text" readonly />
<button
type="button"
class="input-button"
aria-label="Click to copy."
on:click={copy}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
content,
hideOnClick: false
}}>
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
</div>
+24 -14
View File
@@ -26,22 +26,32 @@
</script>
<a class="card" {href}>
<div class="grid-item-1" bind:offsetWidth={itemWidth}>
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
<h2 class="heading-level-6"><slot name="title" /></h2>
<div class="status">
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
<h2 class="heading-level-6"><slot name="title" /></h2>
</div>
<div class="grid-item-1-start-end">
<slot name="status" />
</div>
<div class="options" style="flex-wrap: nowrap !important;" bind:this={options}>
<slot />
{#if more}
<Pill>
+{more}
</Pill>
{/if}
<div class="grid-item-1-end-start">
<div
class="u-flex u-gap-16 u-flex-wrap"
style="flex-wrap: nowrap !important;"
bind:this={options}>
<slot />
{#if more}
<Pill>
+{more}
</Pill>
{/if}
</div>
</div>
<div class="grid-item-1-end-end">
<ul class="icons u-flex u-gap-8">
<slot name="icons" />
</ul>
</div>
<ul class="icons u-flex u-gap-8">
<slot name="icons" />
</ul>
</div>
</a>
+15 -8
View File
@@ -10,6 +10,7 @@
export let error: string = null;
export let closable = true;
let alert: HTMLElement;
const dispatch = createEventDispatcher();
const transitionFly: FlyParams = {
duration: 150,
@@ -48,6 +49,10 @@
document.body.classList.remove('u-overflow-hidden');
}
}
$: if (error) {
alert?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
}
</script>
<svelte:window on:keydown={handleKeydown} />
@@ -81,14 +86,16 @@
</header>
<div class="modal-content">
{#if error}
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
<div bind:this={alert}>
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</div>
{/if}
<slot />
</div>
+1
View File
@@ -15,5 +15,6 @@ export { default as InputSearch } from './inputSearch.svelte';
export { default as InputRadio } from './inputRadio.svelte';
export { default as InputSelect } from './inputSelect.svelte';
export { default as InputCheckbox } from './inputCheckbox.svelte';
export { default as InputChoice } from './inputChoice.svelte';
export { default as InputPhone } from './inputPhone.svelte';
export { default as Helper } from './helper.svelte';
+30
View File
@@ -0,0 +1,30 @@
<script lang="ts">
import { FormItem } from '.';
export let type: 'checkbox' | 'switchbox' = 'checkbox';
export let label: string;
export let showLabel = true;
export let id: string;
export let value = false;
export let required = false;
export let disabled = false;
</script>
<FormItem>
<label class="choice-item" for={id}>
<input
{id}
{disabled}
{required}
type="checkbox"
class:switch={type === 'switchbox'}
bind:checked={value} />
<div class="choice-item-content">
<div class:u-hide={!showLabel} class="choice-item-title">{label}</div>
{#if $$slots}
<p class="choice-item-paragraph"><slot /></p>
{/if}
</div>
</label>
</FormItem>
+2 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
export let title: string;
export let showOverflow = false;
let data: HTMLSpanElement;
</script>
<div class="table-col " class:u-overflow-visible={showOverflow} data-title={title} role="cell">
<span class="text u-trim"><slot /></span>
<span bind:this={data} class="text u-trim" title={data?.innerText}><slot /></span>
</div>
+20
View File
@@ -0,0 +1,20 @@
import { browser } from '$app/env';
import { writable, type Writable } from 'svelte/store';
export function cachedStore<TModel, TMethods = Record<string, unknown>>(
id: string,
callback: (store: Writable<TModel>) => TMethods
): TMethods & Writable<TModel> {
const store = writable<TModel>(browser ? JSON.parse(sessionStorage.getItem(id)) : null);
if (browser) {
store.subscribe((n) => sessionStorage?.setItem(id, JSON.stringify(n ?? '')));
}
return {
subscribe: store.subscribe,
set: store.set,
update: store.update,
...callback(store)
};
}
+3 -1
View File
@@ -86,7 +86,9 @@
showDropdown = false;
newOrgModal.set(true);
}}>New organization</DropListItem>
<DropListLink href="/console/$me">Your Account</DropListLink>
<DropListLink
href={`${base}/console/account`}
on:click={() => (showDropdown = false)}>Your Account</DropListLink>
</ul>
</section>
<section class="drop-section">
+2 -2
View File
@@ -4,8 +4,8 @@
</script>
{#if $notifications}
<section class="toaster">
<ul class="toaster-list">
<section>
<ul class="u-flex u-flex-vertical u-gap-16">
{#each $notifications as notification (notification.id)}
<Notification
type={notification.type}
+30 -2
View File
@@ -13,10 +13,14 @@
} from '$lib/stores/organization';
import { sdkForConsole } from '$lib/stores/sdk';
import { base } from '$app/paths';
import { user } from '$lib/stores/user';
import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let isOpen = false;
export let showSideNavigation = false;
let y: number;
let tabsList: HTMLUListElement;
let showLeft = false;
let showRight = false;
@@ -55,6 +59,11 @@
}
});
const logout = async () => {
await user.logout();
await goto(`${base}/login`);
};
const onScroll = () => {
if (!tabsList) {
return;
@@ -77,9 +86,21 @@
}
};
};
const toggleMenu = () => {
y = 0;
isOpen = !isOpen;
if (browser) {
if (isOpen) {
document.body.classList.add('u-overflow-hidden');
} else {
document.body.classList.remove('u-overflow-hidden');
}
}
};
</script>
<svelte:window on:resize={throttle(onScroll, 25)} />
<svelte:window bind:scrollY={y} on:resize={throttle(onScroll, 25)} />
<main
class:grid-with-side={showSideNavigation}
@@ -87,9 +108,10 @@
class:is-open={isOpen}>
<header class="main-header">
<button
class:u-hide={!showSideNavigation}
class="icon-button is-no-desktop"
aria-label="Open Menu"
on:click={() => (isOpen = !isOpen)}>
on:click={toggleMenu}>
<span class:icon-x={isOpen} class:icon-menu={!isOpen} aria-hidden="true" />
</button>
<slot name="header" />
@@ -159,6 +181,12 @@
<span class="text"> {$title}</span>
</h1>
{/if}
{#if $page.url.pathname.includes('/console/account')}
<div class="u-margin-inline-start-auto">
<Button secondary on:click={logout}>Logout</Button>
</div>
{/if}
{#if $copyData?.value}
<Copy value={$copyData.value}>
<Pill button>
+10 -6
View File
@@ -3,6 +3,7 @@
import LoginLight from '$lib/images/login/login-light-mode.svg';
import LoginDark from '$lib/images/login/login-dark-mode.svg';
import { app } from '$lib/stores/app';
import { base } from '$app/paths';
const technologies = [
'js',
@@ -31,8 +32,9 @@
<div class="u-margin-block-start-auto" />
<div class="u-margin-block-start-auto is-no-mobile" />
<div
class="container u-margin-block-start-20"
class="container u-margin-block-start-20 is-no-mobile"
style="--p-container-max-size: var(--container-size-large);">
{#if $app.themeInUse === 'dark'}
<img src={LoginDark} alt="" class="u-only-dark" />
@@ -43,16 +45,18 @@
<div class="u-margin-block-start-auto" />
<div class="u-margin-block-start-auto is-no-mobile" />
<div
class="container u-text-color-light-gray is-only-desktop"
class="container u-text-color-light-gray is-no-mobile"
style="--p-container-max-size:var(--container-size-small); --p-container-padding-inline:1rem;">
<p>Integrate with your favourite technologies</p>
<ul
class="u-flex u-main-center u-flex-wrap u-gap-16 u-margin-block-start-32 u-line-height-1 ">
class="u-flex u-main-center u-flex-wrap u-gap-16 u-margin-block-start-32 u-line-height-1 u-opacity-0-5">
{#each technologies as tech}
<li>
<span
class={`icon-${tech} u-font-size-32`}
<img
src={`${base}/icons/${$app.themeInUse}/grayscale/${tech}.svg`}
alt={tech}
aria-hidden="true"
aria-label={tech} />
</li>
@@ -62,7 +66,7 @@
<div class="u-margin-block-start-40" />
</section>
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center">
<div class="container u-flex u-flex-vertical u-cross-center">
<div class="container u-flex u-flex-vertical u-cross-center u-height-100-percents">
<div class="u-margin-block-start-auto" />
<div class="u-max-width-500 u-width-full-line">
+34 -53
View File
@@ -1,52 +1,50 @@
import { sdkForConsole } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { browser } from '$app/env';
import { get } from 'svelte/store';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { cachedStore } from '$lib/helpers/cache';
function createOrganizationList() {
const { subscribe, set } = writable<Models.TeamList>(
browser ? JSON.parse(sessionStorage.getItem('organizationList')) : null
);
export const newOrgModal = writable<boolean>(false);
export const newMemberModal = writable<boolean>(false);
export const organizationList = cachedStore<
Models.TeamList,
{
load: () => Promise<void>;
}
>('organizationList', function ({ set }) {
return {
subscribe,
set,
load: async () => {
const response = await sdkForConsole.teams.list();
set(response);
}
};
}
function createOrganization() {
const { subscribe, set } = writable<Models.Team>(
browser ? JSON.parse(sessionStorage.getItem('organization')) : null
);
});
export const organization = cachedStore<
Models.Team,
{
load: (teamId: string) => Promise<void>;
}
>('organization', function ({ set }) {
return {
subscribe,
set,
load: async (teamId: string) => {
load: async (teamId) => {
const response = await sdkForConsole.teams.get(teamId);
set(response);
},
deleteCache: () => {
sessionStorage.removeItem('organization');
}
};
}
function createProjectList() {
const { subscribe, set } = writable<Models.ProjectList>(
browser ? JSON.parse(sessionStorage.getItem('projectList')) : null
);
});
export const projectList = cachedStore<
Models.ProjectList,
{
load: (search: string, limit: number, offset: number) => Promise<void>;
}
>('projectList', function ({ set }) {
return {
subscribe,
set,
load: async (search: string, limit: number, offset: number) => {
load: async (search, limit, offset) => {
const response = await sdkForConsole.projects.list(
search,
limit,
@@ -58,17 +56,16 @@ function createProjectList() {
set(response);
}
};
}
function createMemberList() {
const { subscribe, set } = writable<Models.MembershipList>(
browser ? JSON.parse(sessionStorage.getItem('memberList')) : null
);
});
export const memberList = cachedStore<
Models.MembershipList,
{
load: (teamId: string, search: string, limit: number, offset: number) => Promise<void>;
}
>('memberList', function ({ set }) {
return {
subscribe,
set,
load: async (teamId: string, search: string, limit: number, offset: number) => {
load: async (teamId, search, limit, offset) => {
const response = await sdkForConsole.teams.getMemberships(
teamId,
search,
@@ -78,23 +75,7 @@ function createMemberList() {
set(response);
}
};
}
export const organizationList = createOrganizationList();
export const organization = createOrganization();
export const projectList = createProjectList();
export const memberList = createMemberList();
export const newOrgModal = writable<boolean>(false);
export const newMemberModal = writable<boolean>(false);
if (browser) {
organizationList.subscribe((n) =>
sessionStorage?.setItem('organizationList', JSON.stringify(n ?? ''))
);
organization.subscribe((n) => sessionStorage?.setItem('organization', JSON.stringify(n ?? '')));
projectList.subscribe((n) => sessionStorage?.setItem('projectList', JSON.stringify(n ?? '')));
memberList.subscribe((n) => sessionStorage?.setItem('memberList', JSON.stringify(n ?? '')));
}
});
export const redirectTo = async () => {
let org = get(organization);
+117
View File
@@ -0,0 +1,117 @@
import { writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
export type Service = {
label: string;
method: string;
value: boolean | null;
};
function createServices() {
const { subscribe, set } = writable({
list: [
{
label: 'Account',
method: 'account',
value: null
},
{
label: 'Avatars',
method: 'avatars',
value: null
},
{
label: 'Databases',
method: 'databases',
value: null
},
{
label: 'Functions',
method: 'functions',
value: null
},
{
label: 'Health',
method: 'health',
value: null
},
{
label: 'Locale',
method: 'locale',
value: null
},
{
label: 'Storage',
method: 'storage',
value: null
},
{
label: 'Teams',
method: 'teams',
value: null
},
{
label: 'Users',
method: 'users',
value: null
}
]
});
return {
subscribe,
set,
load: (project: Models.Project) => {
const list = [
{
label: 'Account',
method: 'account',
value: project.serviceStatusForAccount
},
{
label: 'Avatars',
method: 'avatars',
value: project.serviceStatusForAvatars
},
{
label: 'Databases',
method: 'databases',
value: project.serviceStatusForDatabases
},
{
label: 'Functions',
method: 'functions',
value: project.serviceStatusForFunctions
},
{
label: 'Health',
method: 'health',
value: project.serviceStatusForHealth
},
{
label: 'Locale',
method: 'locale',
value: project.serviceStatusForLocale
},
{
label: 'Storage',
method: 'storage',
value: project.serviceStatusForStorage
},
{
label: 'Teams',
method: 'teams',
value: project.serviceStatusForTeams
},
{
label: 'Users',
method: 'users',
value: project.serviceStatusForUsers
}
];
set({ list });
}
};
}
export const services = createServices();
+3
View File
@@ -10,6 +10,9 @@ function createUserStore() {
set,
fetchUser: async () => {
set(await sdkForConsole.account.get());
},
logout: async () => {
await sdkForConsole.account.deleteSession('current');
}
};
}
+5 -1
View File
@@ -23,6 +23,8 @@
window.GOOGLE_ANALYTICS = import.meta.env.VITE_GOOGLE_ANALYTICS?.toString() ?? false;
}
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
onMount(async () => {
try {
if (!$user) {
@@ -33,7 +35,9 @@
await redirectTo();
}
} catch (error) {
await goto(`${base}/login`);
if (acceptedRoutes.includes($page.url.pathname)) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
} else await goto(`${base}/login`);
} finally {
loaded = true;
}
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Button } from '$lib/elements/forms';
import { app } from '$lib/stores/app';
import { sdkForConsole } from '$lib/stores/sdk';
const logout = async () => {
await sdkForConsole.account.deleteSession('current');
await goto(`${base}/login`);
};
const toggleTheme = () => {
$app.theme = $app.theme === 'light' ? 'dark' : 'light';
};
</script>
<h1>hey</h1>
<Button on:click={toggleTheme}>toggle Theme</Button>
<Button on:click={logout}>Logout</Button>
+7 -2
View File
@@ -36,7 +36,10 @@
<title>Appwrite - Console</title>
</svelte:head>
<Shell showSideNavigation={$page.url.pathname !== '/console' && !$page?.params.organization}>
<Shell
showSideNavigation={$page.url.pathname !== '/console' &&
!$page?.params.organization &&
!$page.url.pathname.includes('/console/account')}>
<svelte:fragment slot="header">
<Header />
</svelte:fragment>
@@ -47,4 +50,6 @@
<footer class="main-footer" />
</Shell>
<Create bind:show={$newOrgModal} closable={!!$organizationList?.total} />
{#if $newOrgModal}
<Create bind:show={$newOrgModal} closable={!!$organizationList?.total} />
{/if}
@@ -0,0 +1,51 @@
<script lang="ts">
import { updateLayout } from '$lib/stores/layout';
import { user } from '$lib/stores/user';
import { afterNavigate } from '$app/navigation';
import { onMount } from 'svelte';
const path = 'console/account';
onMount(handle);
afterNavigate(handle);
async function handle(event = null) {
if (!$user) {
await user.fetchUser();
}
updateLayout({
navigate: event,
title: $user?.name,
level: 0,
customBase: '',
breadcrumbs: {
title: `${$user.name}`,
href: path
},
tabs: [
{
href: path,
title: 'Overview'
},
{
href: `${path}/sessions`,
title: 'Sessions'
},
{
href: `${path}/activity`,
title: 'Activity'
},
{
href: `${path}/organizations`,
title: 'Organizations'
}
]
});
}
</script>
<svelte:head>
<title>Appwrite - User</title>
</svelte:head>
<slot />
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
export let showDelete = false;
const deleteAccount = async () => {
try {
await sdkForConsole.users.delete($user.$id);
showDelete = false;
addNotification({
type: 'success',
message: `Account was deleted `
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form on:submit={deleteAccount}>
<Modal bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
</Form>
+108
View File
@@ -0,0 +1,108 @@
<script lang="ts">
import { Empty, Pagination } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableHeader,
TableRow,
TableCellHead,
TableCell,
TableCellText
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageLimit } from '$lib/stores/layout';
import { onMount } from 'svelte';
import { accountActivity } from './store';
let offset = 0;
onMount(async () => {
await accountActivity.load($pageLimit, offset);
});
$: accountActivity.load($pageLimit, offset);
const getBrowser = (clientCode: string) => {
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
};
</script>
<Container>
{#if $accountActivity?.total}
<Table>
<TableHeader>
<TableCellHead>Date</TableCellHead>
<TableCellHead>Event</TableCellHead>
<TableCellHead>Client</TableCellHead>
<TableCellHead>Location</TableCellHead>
<TableCellHead>IP</TableCellHead>
</TableHeader>
<TableBody>
{#each $accountActivity.logs as log}
<TableRow>
<TableCellText title="Date">{toLocaleDateTime(log.time)}</TableCellText>
<TableCellText title="Event">{log.event}</TableCellText>
<TableCell title="Client">
{#if log.clientName}
<div class="u-flex u-cross-center u-gap-12">
<div class="avatar is-small">
<img
height="20"
width="20"
src={getBrowser(log.clientCode).toString()}
alt={log.clientName} />
</div>
<p
class="text u-trim"
title={`${log.clientName} ${log.clientVersion} on ${log.osName} ${log.osVersion}`}>
{log.clientName}
{log.clientVersion}
on {log.osName}
{log.osVersion}
</p>
</div>
{:else}
<div class="u-flex u-cross-center u-gap-12">
<span class="avatar is-color-empty" />
<p class="text u-trim">Unknown</p>
</div>
{/if}
</TableCell>
<TableCellText title="Location">
{#if log.countryCode !== '--'}
{log.countryName}
{:else}
Unknown
{/if}
</TableCellText>
<TableCellText title="IP">{log.ip}</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<Empty centered>
<div class="u-flex u-flex-vertical u-cross-center">
<div class="common-section">
<p>No logs available</p>
</div>
<div class="common-section">
<Button
external
secondary
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetLogs"
>Documentation</Button>
</div>
</div>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$accountActivity?.total}</p>
<Pagination limit={$pageLimit} bind:offset sum={$accountActivity?.total} />
</div>
</Container>
+187
View File
@@ -0,0 +1,187 @@
<script lang="ts">
import { Button, Form, FormList, InputText, InputPassword } from '$lib/elements/forms';
import { CardGrid, Box, Avatar } from '$lib/components';
import { Container } from '$lib/layout';
import { onMount } from 'svelte';
import { user } from '$lib/stores/user';
import { sdkForConsole } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { title, breadcrumbs } from '$lib/stores/layout';
import { base } from '$app/paths';
import Delete from './_delete.svelte';
let name: string = null,
email: string = null,
emailPassword: string = null,
newPassword: string = null,
oldPassword: string = null;
let showDelete = false;
onMount(async () => {
name ??= $user.name;
email ??= $user.email;
});
const getAvatar = (name: string) => sdkForConsole.avatars.getInitials(name, 96, 96).toString();
async function updateName() {
try {
await sdkForConsole.account.updateName(name);
$user.name = name;
title.set(name);
const breadcrumb = $breadcrumbs.get(0);
breadcrumb.title = name;
$breadcrumbs = $breadcrumbs.set($breadcrumbs.size, breadcrumb);
addNotification({
message: 'Name has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updateEmail() {
try {
await sdkForConsole.account.updateEmail(email, emailPassword);
$user.email = email;
addNotification({
message: 'Email has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
async function updatePassword() {
try {
await sdkForConsole.account.updatePassword(newPassword, oldPassword);
addNotification({
message: 'Password has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
}
}
</script>
<Container>
<Form on:submit={updateName}>
<CardGrid>
<h6 class="heading-level-7">Update Name</h6>
<svelte:fragment slot="aside">
<ul>
<InputText
id="name"
label="Name"
placeholder="Enter name"
autocomplete={false}
autofocus={true}
bind:value={name} />
</ul>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $user.name || !name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateEmail}>
<CardGrid>
<h6 class="heading-level-7">Update Email</h6>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="email"
label="Email"
placeholder="Enter email"
autocomplete={false}
bind:value={email} />
{#if email !== $user.email && email}
<InputPassword
id="emailPassword"
label="Password"
placeholder="Enter password"
autocomplete={false}
showPasswordButton={true}
bind:value={emailPassword} />
{/if}
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={email === $user.email || !email || !emailPassword} submit
>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePassword}>
<CardGrid>
<h6 class="heading-level-7">Update Password</h6>
<p class="text">
Forgot your password? <a class="link" href={`${base}/recover`}
>Recover your password</a>
</p>
<svelte:fragment slot="aside">
<FormList>
<InputPassword
id="newPassword"
label="New password"
placeholder="Enter password"
autocomplete={false}
showPasswordButton={true}
bind:value={newPassword} />
<InputPassword
id="oldPassword"
label="Old password"
placeholder="Enter password"
autocomplete={false}
showPasswordButton={true}
bind:value={oldPassword} />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={!newPassword || !oldPassword} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid>
<div>
<h6 class="heading-level-7">Delete Account</h6>
</div>
<p>
Your account will be permanently deleted and access will be lost to any of your teams
and data. This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="image">
<Avatar size={48} name={$user.name} src={getAvatar($user.name)} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold">{$user.name}</h6>
</svelte:fragment>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
</Container>
<Delete bind:showDelete />
@@ -0,0 +1,110 @@
<script lang="ts">
import { base } from '$app/paths';
import { GridItem1, EmptyGridItem, Empty, Pagination, AvatarGroup } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateOrganization from '../_createOrganization.svelte';
import { organizationList } from '$lib/stores/organization';
import { onMount } from 'svelte';
import { sdkForConsole } from '$lib/stores/sdk';
onMount(async () => {
await organizationList.load();
});
const getAvatar = (name: string) => {
return sdkForConsole.avatars.getInitials(name, 80, 80).toString();
};
const getMemberships = async (teamId: string) => {
const memberships = await sdkForConsole.teams.getMemberships(teamId);
return memberships.memberships.map((team) => {
return {
name: team.userName,
img: getAvatar(team.userName)
};
});
};
let addOrganization = false;
let offset = 0;
const limit = 6;
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<h2 class="heading-level-5">Organizations</h2>
<Button
on:click={() => {
addOrganization = true;
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create organization</span>
</Button>
</div>
{#if $organizationList?.teams?.length}
<ul
class="grid-box common-section u-margin-block-start-32"
style={`--grid-gap:1.5rem; --grid-item-size:${
$organizationList.total > 3 ? '22rem' : '25rem'
};`}>
{#each $organizationList.teams as organization, index}
{@const avatarList = getMemberships(organization.$id)}
{#if index >= offset && index < limit + offset}
<GridItem1 href={`${base}/console/organization-${organization.$id}`}>
<svelte:fragment slot="eyebrow"
>{organization?.total ? organization?.total : 'No'} projects</svelte:fragment>
<svelte:fragment slot="title">
{organization.name}
</svelte:fragment>
{#await avatarList}
<span class="avatar is-color-empty" />
{:then avatars}
<AvatarGroup size={40} {avatars} />
{/await}
</GridItem1>
{/if}
{/each}
{#if $organizationList?.total < limit + offset && ($organizationList?.total % 2 !== 0 || $organizationList?.total % 4 === 0)}
<EmptyGridItem on:click={() => (addOrganization = true)}>
<div class="common-section">
<Button secondary round>
<i class="icon-plus" />
</Button>
</div>
<div class="common-section">
<p>Create a new organization</p>
</div>
</EmptyGridItem>
{/if}
</ul>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$organizationList?.total}</p>
<Pagination {limit} bind:offset sum={$organizationList?.total} />
</div>
{:else}
<Empty dashed centered>
<div class="grid-item-1">
<div class="u-flex u-flex-vertical u-cross-center ">
<div class="common-section">
<Button secondary round on:click={() => (addOrganization = true)}>
<i class="icon-plus" />
</Button>
</div>
<div class="common-section">
<p>Create a new organization</p>
</div>
</div>
</div>
</Empty>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$organizationList?.total}</p>
<Pagination {limit} bind:offset sum={$organizationList?.total} />
</div>
{/if}
</Container>
<CreateOrganization bind:show={addOrganization} />
+125
View File
@@ -0,0 +1,125 @@
<script lang="ts">
import { Empty, Pagination } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import {
Table,
TableBody,
TableHeader,
TableRow,
TableCellHead,
TableCell,
TableCellText
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageLimit } from '$lib/stores/layout';
import type { Models } from '@aw-labs/appwrite-console';
import { accountSession } from './store';
import { onMount } from 'svelte';
let offset = 0;
onMount(async () => {
await accountSession.load();
});
const getBrowser = (clientCode: string) => {
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
};
const logout = async (session: Models.Session) => {
await sdkForConsole.account.deleteSession(session.$id);
await accountSession.load();
};
const logoutAll = async () => {
await sdkForConsole.account.deleteSessions();
await accountSession.load();
};
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<h2 class="heading-level-5">Sessions</h2>
<Button secondary on:click={logoutAll}>
<span class="text">Logout all sessions</span>
</Button>
</div>
{#if $accountSession?.total}
<Table>
<TableHeader>
<TableCellHead>Client</TableCellHead>
<TableCellHead width={100}>Location</TableCellHead>
<TableCellHead width={150}>IP</TableCellHead>
<TableCellHead width={100} />
</TableHeader>
<TableBody>
{#each $accountSession.sessions as session}
<TableRow>
<TableCell title="Client">
<div class="u-flex u-gap-12">
<div class="u-flex u-cross-center u-gap-12">
{#if session.clientName}
<div class="avatar is-small">
<img
height="20"
width="20"
src={getBrowser(session.clientCode).toString()}
alt={session.clientName} />
</div>
<p class="text u-trim">
{session.clientName}
{session.clientVersion}
on {session.osName}
{session.osVersion}
</p>
{:else}
<span class="avatar is-color-empty" />
<p class="text u-trim">Unknown</p>
{/if}
</div>
<Pill>{session.provider}</Pill>
{#if session.current}
<Pill success>current session</Pill>
{/if}
</div>
</TableCell>
<TableCellText title="Location">
{#if session.countryCode !== '--'}
{session.countryName}
{:else}
Unknown
{/if}
</TableCellText>
<TableCellText title="IP">{session.ip}</TableCellText>
<TableCell title="Client">
<Button secondary on:click={() => logout(session)}>Logout</Button>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<Empty centered>
<div class="u-flex u-flex-vertical u-cross-center">
<div class="common-section">
<p>No sessions available</p>
</div>
<div class="common-section">
<Button
external
secondary
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetsessions"
>Documentation</Button>
</div>
</div>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$accountSession?.total}</p>
<Pagination limit={$pageLimit} bind:offset sum={$accountSession?.total} />
</div>
</Container>
+30
View File
@@ -0,0 +1,30 @@
import { cachedStore } from '$lib/helpers/cache';
import { sdkForConsole } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
export const accountSession = cachedStore<
Models.SessionList,
{
load: () => Promise<void>;
}
>('accountSession', function ({ set }) {
return {
load: async () => {
const response = await sdkForConsole.account.getSessions();
set(response);
}
};
});
export const accountActivity = cachedStore<
Models.LogList,
{
load: (limit: number, offset: number) => Promise<void>;
}
>('accountActivity', function ({ set }) {
return {
load: async (limit, offset) => {
const response = await sdkForConsole.account.getLogs(limit, offset);
set(response);
}
};
});
@@ -20,7 +20,7 @@
afterNavigate(handle);
async function handle(event = null) {
if ($organization.$id !== organizationId) {
if ($organization?.$id !== organizationId) {
await organization.load(organizationId);
await memberList.load(organizationId, '', 12, 0);
}
@@ -14,7 +14,7 @@
const dispatch = createEventDispatcher();
let email: string, name: string, error: string;
const url = `${$page.url.origin}/console/`;
const url = `${$page.url.origin}/invite`;
const create = async () => {
try {
@@ -1,4 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
@@ -15,8 +17,14 @@
const deleteMembership = async () => {
try {
await sdkForConsole.teams.deleteMembership(selectedMember.teamId, selectedMember.$id);
if (isUser) {
await user.logout();
await goto(`${base}/login`);
} else {
dispatch('deleted');
}
showDelete = false;
dispatch('deleted');
addNotification({
type: 'success',
message: `${selectedMember.userName} was deleted from ${selectedMember.teamName}`
@@ -18,6 +18,7 @@
import type { Models } from '@aw-labs/appwrite-console';
import { pageLimit } from '$lib/stores/layout';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
let search = '';
let offset: number = null;
@@ -29,14 +30,26 @@
const getAvatar = (name: string) =>
sdkForConsole.avatars.getInitials(name, 120, 120).toString();
const deleted = () => memberList.load($organization.$id, search, $pageLimit, offset ?? 0);
const resend = async (member: Models.Membership) =>
await sdkForConsole.teams.createMembership(
$organization.$id,
member.userEmail,
member.roles,
url,
member.userName
);
const resend = async (member: Models.Membership) => {
try {
await sdkForConsole.teams.createMembership(
$organization.$id,
member.userEmail,
member.roles,
url,
member.userName
);
addNotification({
type: 'success',
message: `Invite has been sent to ${member.userEmail}`
});
} catch (error) {
addNotification({
type: 'error',
message: error
});
}
};
$: if (search) offset = 0;
$: memberList.load($organization.$id, search, $pageLimit, offset ?? 0);
@@ -1,17 +1,15 @@
import { sdkForProject } from '$lib/stores/sdk';
import { writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
import { browser } from '$app/env';
function createUsersListStore() {
const { subscribe, set } = writable<Models.UserList<Record<string, unknown>>>(
browser ? JSON.parse(sessionStorage.getItem('users')) : null
);
import { cachedStore } from '$lib/helpers/cache';
export const usersList = cachedStore<
Models.UserList<Record<string, unknown>>,
{
load: (search: string, limit: number, offset: number) => Promise<void>;
}
>('users', function ({ set }) {
return {
subscribe,
set,
load: async (search: string, limit: number, offset: number) => {
load: async (search, limit, offset) => {
const response = await sdkForProject.users.list(
search,
limit,
@@ -23,17 +21,16 @@ function createUsersListStore() {
set(response);
}
};
}
function createTeamsListStore() {
const { subscribe, set } = writable<Models.TeamList>(
browser ? JSON.parse(sessionStorage.getItem('teams')) : null
);
});
export const teamsList = cachedStore<
Models.TeamList,
{
load: (search: string, limit: number, offset: number) => Promise<void>;
}
>('teams', function ({ set }) {
return {
subscribe,
set,
load: async (search: string, limit: number, offset: number) => {
load: async (search, limit, offset) => {
const response = await sdkForProject.teams.list(
search,
limit,
@@ -45,28 +42,18 @@ function createTeamsListStore() {
set(response);
}
};
}
function createUsersUsageStore() {
const { subscribe, set } = writable(
browser ? JSON.parse(sessionStorage.getItem('usersUsage')) : null
);
});
export const usersUsage = cachedStore<
Models.UsageUsers,
{
load: (range: string) => Promise<void>;
}
>('usersUsage', function ({ set }) {
return {
subscribe,
set,
load: async (range: string) => {
load: async (range) => {
const response = await sdkForProject.users.getUsage(range);
set(response);
}
};
}
export const usersList = createUsersListStore();
export const teamsList = createTeamsListStore();
export const usersUsage = createUsersUsageStore();
if (browser) {
usersList.subscribe((n) => sessionStorage?.setItem('users', JSON.stringify(n ?? '')));
teamsList.subscribe((n) => sessionStorage?.setItem('teams', JSON.stringify(n ?? '')));
usersUsage.subscribe((n) => sessionStorage?.setItem('usersUsage', JSON.stringify(n ?? '')));
}
});
@@ -1,31 +1,28 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { browser } from '$app/env';
function createTeamStore() {
const { subscribe, set } = writable<Models.Team>(
browser ? JSON.parse(sessionStorage.getItem('team')) : null
);
import { cachedStore } from '$lib/helpers/cache';
export const team = cachedStore<
Models.Team,
{
load: (teamId: string) => Promise<void>;
}
>('team', function ({ set }) {
return {
subscribe,
set,
load: async (teamId: string) => {
const response = await sdkForProject.teams.get(teamId);
set(response);
}
};
}
function createMembershipStore() {
const { subscribe, set } = writable<Models.MembershipList>(
browser ? JSON.parse(sessionStorage.getItem('memberships')) : null
);
});
export const memberships = cachedStore<
Models.MembershipList,
{
load: (teamId: string, search: string, limit: number, offset: number) => Promise<void>;
}
>('memberships', function ({ set }) {
return {
subscribe,
set,
load: async (teamId: string, search: string, limit: number, offset: number) => {
load: async (teamId, search, limit, offset) => {
const response = await sdkForProject.teams.getMemberships(
teamId,
search,
@@ -38,12 +35,4 @@ function createMembershipStore() {
set(response);
}
};
}
export const memberships = createMembershipStore();
export const team = createTeamStore();
if (browser) {
team.subscribe((n) => sessionStorage?.setItem('team', JSON.stringify(n ?? '')));
memberships.subscribe((n) => sessionStorage?.setItem('memberships', JSON.stringify(n ?? '')));
}
});
@@ -1,25 +1,17 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { browser } from '$app/env';
function createUserStore() {
const { subscribe, set } = writable<Models.User<Record<string, unknown>>>(
browser ? JSON.parse(sessionStorage.getItem('selectedUser')) : null
);
import { cachedStore } from '$lib/helpers/cache';
export const user = cachedStore<
Models.User<Record<string, unknown>>,
{
load: (userId: string) => Promise<void>;
}
>('selectedUser', function ({ set }) {
return {
subscribe,
set,
load: async (userId: string) => {
const response = await sdkForProject.users.get(userId);
set(response);
}
};
}
export const user = createUserStore();
if (browser) {
user.subscribe((n) => sessionStorage?.setItem('selectedUser', JSON.stringify(n ?? '')));
}
});
@@ -29,10 +29,6 @@
{
href: `${path}/domains`,
title: 'Custom Domains'
},
{
href: `${path}/members`,
title: 'Members'
}
]
});
@@ -0,0 +1,70 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Modal } from '$lib/components';
import { Button, Form, FormList, InputPassword, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { project } from '../store';
export let showDelete = false;
const projectId = $page.params.project;
let password: string = null;
let name: string = null;
onMount(async () => {
if (projectId !== $project.$id) await project.load(projectId);
});
const handleDelete = async () => {
try {
await sdkForConsole.projects.delete($project.$id, password);
showDelete = false;
addNotification({
type: 'success',
message: `${$project.name} has been deleted`
});
await goto(`${base}/console`);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<Form on:submit={handleDelete}>
<Modal bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Project</svelte:fragment>
<p>
<b>This project will be deleted</b>, along with all of its metadata, stats, and other
resources. <b>This action is irreversible</b>.
</p>
<FormList>
<InputText
label={`Enter "${$project.name}" to continue`}
placeholder="Enter name"
id="name"
autofocus
required
bind:value={name} />
<InputPassword
label="To verify, enter your password"
placeholder="Enter password"
id="password"
required
showPasswordButton={true}
bind:value={password} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button disabled={!name || !password || name !== $project.name} secondary submit
>Delete</Button>
</svelte:fragment>
</Modal>
</Form>
@@ -1,15 +1,34 @@
<script lang="ts">
import { Card } from '$lib/components';
import { Button, Form, FormItem, InputText } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
import { project } from '../store';
import { services, type Service } from '$lib/stores/project-services';
import { CardGrid, CopyInput, Box } from '$lib/components';
import { Button, Form, FormList, InputText, InputSwitch } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import Delete from './_deleteProject.svelte';
const update = async () => {
let name: string = null;
let showDelete = false;
const endpoint = sdkForConsole.client.config.endpoint;
onMount(async () => {
await project.load($page.params.project);
name ??= $project.name;
});
const updateName = async () => {
try {
await sdkForConsole.projects.update($project.$id, $project.name);
await project.load($project.$id);
await sdkForConsole.projects.update($project.$id, name);
$project.name = name;
addNotification({
type: 'success',
message: 'Project name has been updated'
});
} catch (error) {
addNotification({
type: 'error',
@@ -17,18 +36,115 @@
});
}
};
const serviceUpdate = async (service: Service) => {
try {
await sdkForConsole.projects.updateServiceStatus(
$project.$id,
service.method,
service.value
);
addNotification({
type: 'success',
message: `${service.label} service has been ${
service.value ? 'enabled' : 'disabled'
}`
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
$: services.load($project);
</script>
<Container>
<h1>Overview</h1>
{#if $project}
<Card>
<Form on:submit={update}>
<InputText id="name" label="Name" bind:value={$project.name} required />
<FormItem>
<Button submit>Update</Button>
</FormItem>
</Form>
</Card>
<Form on:submit={updateName}>
<CardGrid>
<h6 class="heading-level-7">Update Name</h6>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="name"
label="Name"
bind:value={name}
required
placeholder="Enter name" />
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={name === $project.name} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid>
<h6 class="heading-level-7">API Credentials</h6>
<p class="text">
Access Appwrite services using your API Endpoint and Project ID. You can connect
Appwrite to your applications and server-side code by <a href="#/" class="link"
>integrating a new platform</a>
or
<a href="#/" class="link">creating an API key</a>.
</p>
<svelte:fragment slot="aside">
<FormList>
<CopyInput label="Project ID" showLabel={true} value={$project.$id} />
<CopyInput label="API Endpoint" showLabel={true} value={endpoint} />
</FormList>
</svelte:fragment>
</CardGrid>
<CardGrid>
<h6 class="heading-level-7">Services</h6>
<p class="text">Choose services you wish to enable or disable.</p>
<svelte:fragment slot="aside">
<FormList>
<form class="form">
<ul class="form-list is-multiple">
{#each $services.list as service}
<InputSwitch
label={service.label}
id={service.method}
bind:value={service.value}
on:change={() => {
serviceUpdate(service);
}} />
{/each}
</ul>
</form>
</FormList>
</svelte:fragment>
</CardGrid>
<CardGrid>
<div>
<h6 class="heading-level-7">Delete Project</h6>
</div>
<p>
The project will be permanently deleted, including all the metadata, resources and
stats within it. This action is irreversible.
</p>
<svelte:fragment slot="aside">
<Box>
<svelte:fragment slot="title">
<h6 class="u-bold">{$project.name}</h6>
</svelte:fragment>
<p>Last update: {toLocaleDateTime($project.$updatedAt)}</p>
</Box>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</CardGrid>
{/if}
</Container>
<Delete bind:showDelete />
@@ -1,50 +0,0 @@
<script lang="ts">
import { List, ListItem } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { project } from '../store';
$: request = sdkForConsole.teams.getMemberships($project?.teamId ?? undefined);
</script>
<Container>
<h1>Members</h1>
{#await request}
<div aria-busy="true" />
{:then response}
{#if response}
<List>
{#each response.memberships as membership}
<ListItem
avatar={sdkForConsole.avatars
.getInitials(membership.userName, 64, 64)
.toString()}>
<svelte:fragment slot="header">
<h2 class="sessions-item-title">
<span class="text">
{membership.userName}
</span>
</h2>
{#each membership.roles as role}
<Pill>{role}</Pill>
{/each}
{#if !membership.confirm}
<Pill danger>Pending Approval</Pill>
{/if}
</svelte:fragment>
<svelte:fragment slot="info">
{membership.userEmail}
</svelte:fragment>
<svelte:fragment slot="action">
<Button danger>Remove</Button>
</svelte:fragment>
</ListItem>
{:else}
<p>No sessions available.</p>
{/each}
</List>
{/if}
{/await}
</Container>
@@ -1,25 +1,17 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { browser } from '$app/env';
function createFileStore() {
const { subscribe, set } = writable<Models.File>(
browser ? JSON.parse(sessionStorage.getItem('file')) : null
);
import { cachedStore } from '$lib/helpers/cache';
export const file = cachedStore<
Models.File,
{
load: (bucketId: string, fileId: string) => Promise<void>;
}
>('file', function ({ set }) {
return {
subscribe,
set,
load: async (bucketId: string, fileId: string) => {
load: async (bucketId, fileId) => {
const response = await sdkForProject.storage.getFile(bucketId, fileId);
set(response);
}
};
}
export const file = createFileStore();
if (browser) {
file.subscribe((n) => sessionStorage?.setItem('file', JSON.stringify(n ?? '')));
}
});
@@ -1,25 +1,17 @@
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { browser } from '$app/env';
function createBucketListStore() {
const { subscribe, set } = writable<Models.BucketList>(
browser ? JSON.parse(sessionStorage.getItem('bucketList')) : null
);
import { cachedStore } from '$lib/helpers/cache';
export const bucketList = cachedStore<
Models.BucketList,
{
load: (search: string, limit: number, offset: number) => Promise<void>;
}
>('bucketList', function ({ set }) {
return {
subscribe,
set,
load: async (search: string, limit: number, offset: number) => {
load: async (search, limit, offset) => {
const response = await sdkForProject.storage.listBuckets(search, limit, offset);
set(response);
}
};
}
export const bucketList = createBucketListStore();
if (browser) {
bucketList.subscribe((n) => sessionStorage?.setItem('bucketList', JSON.stringify(n ?? '')));
}
});
@@ -1,18 +1,17 @@
import { sdkForConsole } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
function createProject() {
const { subscribe, set } = writable<Models.Project>();
import { cachedStore } from '$lib/helpers/cache';
export const project = cachedStore<
Models.Project,
{
load: (projectId: string) => Promise<void>;
}
>('project', function ({ set }) {
return {
subscribe,
set,
load: async (projectId: string) => {
const project = await sdkForConsole.projects.get(projectId);
set(project);
}
};
}
export const project = createProject();
});
+76
View File
@@ -0,0 +1,76 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Button, Form, FormList, InputChoice } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { Unauthenticated } from '$lib/layout';
import { page } from '$app/stores';
import { onMount } from 'svelte';
let teamId: string, membershipId: string, userId: string, secret: string;
let terms = false;
onMount(() => {
userId = $page.url.searchParams.get('userId');
secret = $page.url.searchParams.get('secret');
teamId = $page.url.searchParams.get('teamId');
membershipId = $page.url.searchParams.get('membershipId');
});
const acceptInvite = async () => {
try {
await sdkForConsole.teams.updateMembershipStatus(teamId, membershipId, userId, secret);
await user.fetchUser();
addNotification({
type: 'success',
message: 'Successfully logged in.'
});
await goto(`${base}/console`);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
};
</script>
<svelte:head>
<title>Appwrite - Accept invite</title>
</svelte:head>
<Unauthenticated>
<svelte:fragment slot="title">Invite</svelte:fragment>
<svelte:fragment>
{#if !userId || !secret || !membershipId || !teamId}
<p class="text">Invalid invite link.</p>
{:else}
<p class="text">You have been invited to join a team project on Appwrite</p>
<Form on:submit={acceptInvite}>
<FormList>
<InputChoice
required
bind:value={terms}
id="terms"
label="terms"
showLabel={false}>
By accepting the invitation, you agree to the <a
class="link"
href="https://appwrite.io/policy/terms"
target="_blank">Terms and Conditions</a>
and
<a class="link" href="https://appwrite.io/policy/privacy" target="_blank">
Privacy Policy</a
>.</InputChoice>
<div class="u-flex u-main-end u-gap-12">
<Button secondary href={`${base}/login`}>Cancel</Button>
<Button submit>Accept</Button>
</div>
</FormList>
</Form>
{/if}
</svelte:fragment>
</Unauthenticated>