Merge branch 'main' into refactor/dismissible-self-hosted-alerts

This commit is contained in:
Torsten Dittmann
2023-03-23 16:47:04 +01:00
committed by GitHub
337 changed files with 4876 additions and 2917 deletions
+4 -8
View File
@@ -15,11 +15,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 16
- name: Audit dependencies
run: npm audit --audit-level low
node-version: 18
# - name: Audit dependencies
# run: npm audit --audit-level low
- name: Install dependencies
run: npm ci
- name: Build Console
@@ -30,7 +30,3 @@ jobs:
run: npm run lint
- name: Unit Tests
run: npm test
# - name: E2E Tests
# run: |
# npx playwright install
# npm run e2e
+15 -1
View File
@@ -33,7 +33,21 @@ If you are worried about or dont know where to start, check out the next sect
## Development
Once you've created a project and installed dependencies with `npm install`, create a `.env` file using `.env.example` as a template.
### 1. Clone the repository with git
```bash
git clone https://github.com/appwrite/console.git appwrite-console
```
### 2. Install dependencies with npm
```bash
npm install
```
### 3. Setup environment variables
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
Finally, start a development server:
+875 -842
View File
File diff suppressed because it is too large Load Diff
+24 -23
View File
@@ -1,6 +1,5 @@
{
"name": "@appwrite/console",
"version": "0.0.1",
"engines": {
"node": ">=16"
},
@@ -20,47 +19,49 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/pink": "^0.0.4",
"@appwrite.io/pink": "^0.0.6-rc.4",
"@aw-labs/appwrite-console": "^13.1.0",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.36.0",
"@sentry/tracing": "^7.36.0",
"@sentry/svelte": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"analytics": "^0.8.1",
"dotenv": "^16.0.3",
"echarts": "^5.4.1",
"logrocket": "^3.0.1",
"pretty-bytes": "^6.1.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
"web-vitals": "^3.1.1"
},
"devDependencies": {
"@playwright/test": "^1.30.0",
"@sveltejs/adapter-static": "1.0.6",
"@sveltejs/kit": "1.3.10",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@testing-library/dom": "^8.20.0",
"@playwright/test": "^1.31.2",
"@sveltejs/adapter-static": "2.0.1",
"@sveltejs/kit": "1.13.0",
"@sveltejs/vite-plugin-svelte": "^2.0.3",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
"@testing-library/user-event": "^14.4.3",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitest/ui": "^0.28.4",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vitest/ui": "^0.29.7",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.1.0",
"jsdom": "^21.1.1",
"pre-commit": "^1.2.2",
"prettier": "^2.8.3",
"prettier": "^2.8.6",
"prettier-plugin-svelte": "^2.9.0",
"sass": "^1.58.0",
"svelte": "^3.55.1",
"svelte-check": "^3.0.3",
"sass": "^1.59.3",
"svelte": "^3.57.0",
"svelte-check": "^3.1.4",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.3",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vitest": "^0.28.4"
"typescript": "^5.0.2",
"vite": "^4.2.1",
"vitest": "^0.29.7"
},
"type": "module",
"pre-commit": [
+6 -6
View File
@@ -1,16 +1,16 @@
import Analytics from 'analytics';
import googleAnalytics from '@analytics/google-analytics';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { AppwriteException } from '@aw-labs/appwrite-console';
import { ENV, MODE, VARS } from '$lib/system';
import googleAnalytics from '@analytics/google-analytics';
import { AppwriteException } from '@aw-labs/appwrite-console';
import Analytics from 'analytics';
import { get } from 'svelte/store';
const analytics = Analytics({
app: 'appwrite',
plugins: [
googleAnalytics({
measurementIds: [VARS.GOOGLE_ANALYTICS ?? 'G-R4YJ9JN8L4']
measurementIds: [VARS.GOOGLE_ANALYTICS || 'G-R4YJ9JN8L4']
})
]
});
@@ -88,7 +88,7 @@ function sendEventToGrowth(event: string, path: string, data: object = null): vo
});
}
function isTrackingAllowed() {
export function isTrackingAllowed() {
if (ENV.TEST) {
return;
}
+1 -1
View File
@@ -32,7 +32,7 @@
class:icon-exclamation={type === 'warning'}
class:icon-exclamation-circle={type === 'error'}
aria-hidden="true" />
<div class="alert-content">
<div class="alert-content" data-private>
{#if $$slots.title}
<h6 class="alert-title">
<slot name="title" />
+2 -2
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import Avatar from './avatar.svelte';
export let name: string;
export let size: number;
$: src = sdkForConsole.avatars.getInitials(name, size * 2, size * 2).toString();
$: src = sdk.forConsole.avatars.getInitials(name, size * 2, size * 2).toString();
</script>
<Avatar {name} {size} {src} />
+21 -5
View File
@@ -1,16 +1,31 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
type BaseProps = {
isTile?: boolean;
isDashed?: boolean;
danger?: boolean;
};
type ButtonProps = {
isButton: true;
href?: never;
};
type AnchorProps = {
href: string;
isButton?: never;
};
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps);
export let isTile = false;
export let isDashed = false;
export let isButton = false;
export let danger = false;
export let href: string = null;
//allows to add the href attribute without throwing an error
let attributes = { href } as Record<string, string>;
function getElement(): string {
function getElement() {
switch (true) {
case !!href:
return 'a';
@@ -28,8 +43,9 @@
class:common-section={!isTile}
class:is-border-dashed={isDashed}
class:is-danger={danger}
class:is-allowed-focus={href}
on:click
on:keyup={clickOnEnter}
{...attributes}>
{href}>
<slot />
</svelte:element>
+2 -1
View File
@@ -10,7 +10,8 @@
<ul
class="grid-box common-section u-margin-block-start-32"
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}>
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
data-private>
<slot />
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
+1
View File
@@ -33,6 +33,7 @@
</script>
<span
style:cursor="pointer"
on:click|preventDefault={handleClick}
on:keyup={clickOnEnter}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
+1 -4
View File
@@ -4,13 +4,10 @@
export let href: string;
export let icon: string = null;
export let disabled = false;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
</script>
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
<a {href} class="drop-button" {...attributes}>
<a {href} class="drop-button" class:is-disabled={disabled}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+1 -1
View File
@@ -245,7 +245,7 @@
}
</script>
<Modal bind:show on:submit={create} size="big">
<Modal bind:show onSubmit={create} size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<slot />
<div>
+1 -1
View File
@@ -51,7 +51,7 @@
Your feedback is important to us. Please be honest and tell us what you think.
</div>
<Form on:submit={handleSubmit}>
<Form onSubmit={handleSubmit}>
<FormList>
<InputText
label="name"
+1 -1
View File
@@ -49,7 +49,7 @@
think.
</div>
<Form on:submit={handleSubmit}>
<Form onSubmit={handleSubmit}>
<Evaluation bind:value>
How likely are you to recommend Appwrite to a friend or colleague?
</Evaluation>
+2
View File
@@ -46,3 +46,5 @@ export { default as EventModal } from './eventModal.svelte';
export { default as Status } from './status.svelte';
export { default as Heading } from './heading.svelte';
export { default as Output } from './output.svelte';
export { default as Limit } from './limit.svelte';
export { default as ViewSelector } from './viewSelector.svelte';
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page as pageStore } from '$app/stores';
import { InputSelect } from '$lib/elements/forms';
import { preferences } from '$lib/stores/preferences';
export let sum: number;
export let limit: number;
export let name: string;
const options = [
{ label: '6', value: 6 },
{ label: '12', value: 12 },
{ label: '24', value: 24 },
{ label: '48', value: 48 },
{ label: '96', value: 96 }
];
async function limitChange() {
const url = new URL($pageStore.url);
const previousLimit = Number(url.searchParams.get('limit'));
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
if (url.searchParams.has('page')) {
const page = Number(url.searchParams.get('page'));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
url.searchParams.set('page', newPage.toString());
}
await goto(url.toString());
}
</script>
<div class="u-flex u-gap-12 u-cross-center">
<InputSelect
id="rows"
label="Rows per page"
showLabel={false}
{options}
bind:value={limit}
on:change={limitChange} />
<p class="text">{name} per page. Total results: {sum}</p>
</div>
+7 -4
View File
@@ -2,12 +2,16 @@
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { Alert } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Form } from '$lib/elements/forms';
export let show = false;
export let size: 'small' | 'big' = null;
export let warning = false;
export let error: string = null;
export let closable = true;
export let onSubmit: () => Promise<void> | void = function () {
return;
};
let dialog: HTMLDialogElement;
let alert: HTMLElement;
@@ -77,11 +81,10 @@
class:is-big={size === 'big'}
bind:this={dialog}>
{#if show}
<!-- svelte-ignore a11y-no-redundant-roles -->
<form class="modal-form" role="form" on:submit|preventDefault>
<Form isModal {onSubmit}>
<header class="modal-header">
{#if warning}
<div class="avatar is-color-orange is-medium">
<div class="avatar is-warning is-medium">
<span class="icon-exclamation" aria-hidden="true" />
</div>
{/if}
@@ -127,6 +130,6 @@
</div>
</div>
{/if}
</form>
</Form>
{/if}
</dialog>
+12 -7
View File
@@ -2,16 +2,21 @@
import Copy from './copy.svelte';
export let value: string;
export let hideCopyIcon = false;
</script>
<Copy {value}>
<div class="interactive-text-output is-textarea">
<div
class="interactive-text-output is-textarea"
style:min-inline-size="0"
style:display="inline-block">
<span class="text u-line-height-1-5 u-break-all"><slot /></span>
<div class="u-flex u-cross-child-start u-gap-8">
<button class="interactive-text-output-button" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
{#if !hideCopyIcon}
<div class="u-flex u-cross-child-start u-gap-8">
<button class="interactive-text-output-button" aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
{/if}
</div>
</Copy>
+68 -49
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
import { page as pageStore } from '$app/stores';
import { Button } from '$lib/elements/forms';
export let sum: number;
export let limit: number;
export let offset: number;
export let path: string;
export let hidePages = false;
$: totalPages = Math.ceil(sum / limit);
@@ -27,68 +27,87 @@
...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
];
}
function getLink(page: number): string {
const url = new URL($pageStore.url);
if (page === 1) {
url.searchParams.delete('page');
} else {
url.searchParams.set('page', page.toString());
}
return url.toString();
}
</script>
{#if totalPages > 1}
{@const search = $page.url.search}
<nav class="pagination">
<a
class:is-disabled={currentPage <= 1}
class="button is-text"
aria-label="prev page"
href={`${path}/${currentPage - 1}${search}`}>
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</a>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
<a
href={`${path}/${page}${search}`}
class="button"
class:is-disabled={currentPage === page}
class:is-text={currentPage !== page}
aria-label="page">
<span class="text">{page}</span>
</a>
</li>
{:else}
<li class="li is-text">
<span class="icon">...</span>
</li>
{/if}
{/each}
</ol>
{/if}
<a
class:is-disabled={currentPage === totalPages}
class="button is-text"
href={`${path}/${currentPage + 1}${search}`}
aria-label="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</a>
</nav>
{#key $pageStore.url}
<nav class="pagination">
{#if currentPage <= 1}
<Button disabled text ariaLabel="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</Button>
{:else}
<Button text ariaLabel="prev page" href={getLink(currentPage - 1)}>
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</Button>
{/if}
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
{#if currentPage === page}
<Button ariaLabel="page" disabled>
<span class="text">{page}</span>
</Button>
{:else}
<Button text ariaLabel="page" href={getLink(page)}>
<span class="text">{page}</span>
</Button>
{/if}
</li>
{:else}
<li class="li is-text">
<span class="icon">...</span>
</li>
{/if}
{/each}
</ol>
{/if}
{#if currentPage >= totalPages}
<Button disabled text ariaLabel="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</Button>
{:else}
<Button text ariaLabel="next page" href={getLink(currentPage + 1)}>
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</Button>
{/if}
</nav>
{/key}
{:else}
<nav class="pagination">
<button type="button" class="button is-text is-disabled" aria-label="prev page">
<Button text disabled ariaLabel="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</button>
</Button>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
<li class="pagination-item">
<button type="button" class="button is-disabled" aria-label="page">
<Button disabled ariaLabel="page">
<span class="text">1</span>
</button>
</Button>
</li>
</ol>
{/if}
<button type="button" class="button is-text is-disabled" aria-label="next page">
<Button text disabled ariaLabel="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</button>
</Button>
</nav>
{/if}
+2 -2
View File
@@ -25,7 +25,7 @@
$: disabled = !value || $groups.has(value);
</script>
<Modal bind:show on:close={reset} on:submit={create}>
<Modal bind:show on:close={reset} onSubmit={create}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<p class="text">
Custom permissions allow you to grant access to specific users or teams using their ID and
@@ -44,6 +44,6 @@
</FormList>
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
<Button submit {disabled}>Add</Button>
</svelte:fragment>
</Modal>
+1 -1
View File
@@ -104,7 +104,7 @@
<Row {role} />
</TableCell>
<TableCellText title="Remove">
<div class="u-flex">
<div class="u-flex u-main-end">
<button
class="button is-text is-only-icon"
type="button"
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { sdkForProject } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { tick } from 'svelte';
import { AvatarInitials } from '../';
@@ -18,11 +18,11 @@
const role = permission.split(':')[0];
const id = permission.split(':')[1].split('/')[0];
if (role === 'user') {
const user = await sdkForProject.users.get(id);
const user = await sdk.forProject.users.get(id);
return user;
}
if (role === 'team') {
const team = await sdkForProject.teams.get(id);
const team = await sdk.forProject.teams.get(id);
return team;
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { sdkForProject } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import type { Writable } from 'svelte/store';
@@ -31,7 +31,7 @@
async function request() {
if (!show) return;
results = await sdkForProject.teams.list([Query.limit(5), Query.offset(offset)], search);
results = await sdk.forProject.teams.list([Query.limit(5), Query.offset(offset)], search);
}
function onSelection(event: Event, role: string) {
@@ -58,7 +58,7 @@
}
</script>
<Modal bind:show on:submit={create} on:close={reset} size="big">
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<p class="text">
Grant access to any member of a specific team. To grant access to team members with specific
@@ -144,6 +144,6 @@
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
<Button submit disabled={!hasSelection}>Add</Button>
</svelte:fragment>
</Modal>
+4 -4
View File
@@ -2,7 +2,7 @@
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import { sdkForProject } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import type { Writable } from 'svelte/store';
import type { Permission } from './permissions.svelte';
@@ -31,7 +31,7 @@
async function request() {
if (!show) return;
results = await sdkForProject.users.list([Query.limit(5), Query.offset(offset)], search);
results = await sdk.forProject.users.list([Query.limit(5), Query.offset(offset)], search);
}
function onSelection(event: Event, role: string) {
@@ -58,7 +58,7 @@
}
</script>
<Modal bind:show on:submit={create} on:close={reset} size="big">
<Modal bind:show onSubmit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<p class="text">Grant access to any authenticated or anonymous user.</p>
<InputSearch
@@ -163,6 +163,6 @@
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
<Button submit disabled={!hasSelection}>Add</Button>
</svelte:fragment>
</Modal>
+13 -8
View File
@@ -29,23 +29,28 @@
}
});
$: valueChange(search);
$: valueChange(search ?? '');
function valueChange(value: string) {
clearTimeout(timer);
timer = setTimeout(async () => {
timer = setTimeout(() => {
const url = new URL($page.url);
if (url.search === value) {
const previous = url.searchParams.get('search') ?? '';
if (previous === value) {
return;
}
/**
* Reset to first page if search changes.
*/
if ($page.data.page > 1) {
url.pathname = url.pathname.replace(new RegExp(`/${$page.data.page}*$`), '/');
url.searchParams.delete('page');
}
if (value === '') {
url.searchParams.delete('search');
} else {
url.searchParams.set('search', value);
}
url.search = value;
trackEvent('search');
goto(url, { keepFocus: true });
}, debounce);
+1 -1
View File
@@ -8,7 +8,7 @@
export let copyEvent: string = null;
</script>
<div class="interactive-text-output" class:is-textarea={show}>
<div class="interactive-text-output" class:is-textarea={show} data-private>
{#if show}
<span class="text u-line-height-1-5 u-break-all">{value}</span>
{:else}
+1 -1
View File
@@ -1,3 +1,3 @@
<ul class="grid-box common-section" style="--grid-item-size:25rem;">
<ul class="grid-box common-section" style="--grid-item-size:25rem;" data-private>
<slot />
</ul>
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { uploader } from '$lib/stores/uploader';
import { Pill } from '$lib/elements';
import { sdkForProject } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { Avatar } from '$lib/components';
import { base } from '$app/paths';
import { page } from '$app/stores';
@@ -9,7 +9,7 @@
let hovering = false;
const getPreview = (fileId: string, bucketId: string) =>
sdkForProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
sdk.forProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
</script>
{#if $uploader?.isOpen}
+129
View File
@@ -0,0 +1,129 @@
<script context="module" lang="ts">
export type Column = {
id: string;
title: string;
show: boolean;
width?: number;
};
</script>
<script lang="ts">
import { Button, InputChoice } from '$lib/elements/forms';
import { DropList } from '.';
import { page } from '$app/stores';
import type { Writable } from 'svelte/store';
import { preferences } from '$lib/stores/preferences';
import { onMount } from 'svelte';
import { View } from '$lib/helpers/load';
export let columns: Writable<Column[]>;
export let view: View;
export let isCustomCollection = false;
export let hideView = false;
export let hideColumns = false;
let showSelectColumns = false;
onMount(async () => {
if (isCustomCollection) {
const prefs = preferences.getCustomCollectionColumns($page.params.collection);
columns.set(
$columns.map((column) => {
column.show = prefs?.includes(column.id) ?? true;
return column;
})
);
} else {
const prefs = preferences.get($page.route);
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
}
return columns.subscribe((ctx) => {
const columns = ctx.filter((n) => n.show).map((n) => n.id);
if (isCustomCollection) {
preferences.setCustomCollectionColumns(columns);
} else {
preferences.setColumns(columns);
}
});
});
function getViewLink(view: View): string {
const url = new URL($page.url);
url.searchParams.set('view', view);
return url.toString();
}
function updateViewPreferences(view: View) {
preferences.setView(view);
}
$: selectedColumnsNumber = $columns.reduce((acc, column) => {
if (column.show) {
acc++;
}
return acc;
}, 0);
</script>
{#if !hideColumns && view === View.Table}
{#if $columns?.length}
<DropList bind:show={showSelectColumns} scrollable={true}>
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
<span
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text">Columns</span>
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
{#each $columns as column}
<InputChoice
id={column.id}
label={column.title}
bind:value={column.show}
disabled={selectedColumnsNumber <= 1 && column.show} />
{/each}
</svelte:fragment>
</DropList>
{/if}
{/if}
{#if !hideView}
<div class="toggle-button">
<ul class="toggle-button-list">
{#key $page.url}
<li class="toggle-button-item">
<a
href={getViewLink(View.Table)}
on:click={() => updateViewPreferences(View.Table)}
class="toggle-button-element"
aria-label="List View"
type="button"
class:is-selected={view === View.Table}>
<span class="icon-view-list" aria-hidden="true" />
</a>
</li>
<li class="toggle-button-item">
<a
href={getViewLink(View.Grid)}
on:click={() => updateViewPreferences(View.Grid)}
class="toggle-button-element"
aria-label="Grid View"
type="button"
class:is-selected={view === View.Grid}>
<span class="icon-view-grid" aria-hidden="true" />
</a>
</li>
{/key}
</ul>
</div>
{/if}
+10 -4
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { getContext, hasContext } from 'svelte';
import { readable } from 'svelte/store';
import type { FormContext } from './form.svelte';
export let submit = false;
export let secondary = false;
@@ -14,8 +17,11 @@
export let noMargin = false;
export let event: string = null;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
: readable(false);
$: internalDisabled = (submit && $isSubmitting) || disabled;
function track() {
if (!event) {
@@ -31,11 +37,11 @@
{#if href}
<a
on:click={track}
{...attributes}
{href}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class="button"
class:is-disabled={disabled}
class:is-only-icon={round}
class:is-secondary={secondary}
class:is-text={text}
@@ -49,7 +55,7 @@
<button
on:click
on:click={track}
{disabled}
disabled={internalDisabled}
class="button"
class:is-only-icon={round}
class:is-secondary={secondary}
+31 -5
View File
@@ -1,9 +1,35 @@
<script lang="ts">
export let noMargin = false;
export let noStyle = false;
<script context="module" lang="ts">
import type { Writable } from 'svelte/store';
export type FormContext = {
isSubmitting: Writable<boolean>;
};
</script>
<!-- svelte-ignore a11y-no-redundant-roles -->
<form role="form" class:form={!noStyle} class:common-section={!noMargin} on:submit|preventDefault>
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
export let noMargin = false;
export let noStyle = false;
export let isModal = false;
export let onSubmit: () => Promise<void> | void;
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
});
async function submit() {
isSubmitting.set(true);
await onSubmit();
isSubmitting.set(false);
}
</script>
<form
class:form={!noStyle}
class:common-section={!noMargin}
class:modal-form={isModal}
on:submit|preventDefault={submit}>
<slot />
</form>
+1 -1
View File
@@ -77,7 +77,7 @@
</div>
<div class="u-min-width-0 u-text-center">
<h5 class="upload-file-box-title heading-level-7 u-inline">
<span class="is-only-desktop">Drag and drop files here to upload</span>
<span class="is-only-desktop">Drag and drop a file here to upload</span>
<span class="is-not-desktop">Upload a File</span>
</h5>
{#if allowedFileExtensions?.length}
+2 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem } from '.';
import TextCounter from './textCounter.svelte';
export let value = '';
@@ -42,11 +43,7 @@
bind:value
bind:this={element}
on:invalid={handleInvalid} />
<span class="text-counter">
<span class="text-counter-count">{value?.length ?? 0}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">36</span>
</span>
<TextCounter count={value?.length ?? 0} max={36} />
</div>
</FormItem>
<div
+2 -1
View File
@@ -52,7 +52,8 @@
{disabled}
bind:this={element}
bind:value
on:invalid={handleInvalid}>
on:invalid={handleInvalid}
on:change>
{#if placeholder}
<option value={null} disabled selected hidden>{placeholder}</option>
{/if}
+3 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import TextCounter from './textCounter.svelte';
export let label: string;
export let optionalText: string | undefined = undefined;
@@ -58,14 +59,11 @@
type="text"
class="input-text"
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
{#if maxlength}
<span class="text-counter">
<span class="text-counter-count">{value?.length ?? 0}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">{maxlength}</span>
</span>
<TextCounter max={maxlength} count={value?.length ?? 0} />
{/if}
</div>
{#if error}
+7 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import TextCounter from './textCounter.svelte';
export let label: string;
export let showLabel = true;
@@ -12,6 +13,8 @@
export let readonly = false;
export let autofocus = false;
export let maxlength: number = null;
export let optionalText: string | undefined = undefined;
export let tooltip: string = null;
let element: HTMLTextAreaElement;
let error: string;
@@ -37,7 +40,7 @@
</script>
<FormItem>
<Label {required} hide={!showLabel} for={id}>
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
@@ -53,6 +56,9 @@
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{#if maxlength}
<TextCounter max={maxlength} count={value?.length ?? 0} />
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
export let count: number;
export let max: number;
</script>
<span class="text-counter" class:active={count > 0}>
<span class="text-counter-count">{count}</span>
<span class="text-counter-separator" />
<span class="text-counter-max">{max}</span>
</span>
<style lang="scss">
.text-counter {
--p-text-counter-color: var(--color-neutral-50);
&.active {
--p-text-counter-color: var(--color-neutral-70);
}
}
:global(.theme-dark) .text-counter {
--p-text-counter-color: var(--color-neutral-70);
&.active {
--p-text-counter-color: var(--color-neutral-50);
}
}
</style>
-4
View File
@@ -11,9 +11,6 @@
export let href: string = null;
export let trim = false;
//allows to add the disabled attribute to <a> tag without throwing an error
let attributes = { disabled } as Record<string, boolean>;
let element: HTMLButtonElement;
const isOverflowing = (elem: HTMLButtonElement, iterator = 1) => {
@@ -44,7 +41,6 @@
{#if href}
<a
{...attributes}
{href}
target={external ? '_blank' : '_self'}
rel={external ? 'noopener noreferrer' : ''}
+1 -1
View File
@@ -3,6 +3,6 @@
export let title: string;
</script>
<div class="table-col" data-title={title} role="cell">
<div class="table-col" data-title={title} role="cell" data-private>
<a role="button" tabindex="0" class="link" {href}><slot /></a>
</div>
+2 -1
View File
@@ -10,6 +10,7 @@
class:is-only-desktop={onlyDesktop}
class:u-overflow-visible={showOverflow}
data-title={title}
role="cell">
role="cell"
data-private>
<Trim><slot /></Trim>
</div>
+2 -1
View File
@@ -7,6 +7,7 @@
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
role="table">
role="table"
data-private>
<slot />
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
<!-- TODO: is 100% height needed? -->
<div class="table-with-scroll">
<div class="table-with-scroll" data-private>
<div class="table-wrapper">
<div class="table is-remove-outer-styles">
<ul class="table-thead">
+23 -3
View File
@@ -1,10 +1,30 @@
<script lang="ts">
import type { Action } from 'svelte/action';
export let isSticky = false;
const hasOverflow: Action<HTMLDivElement, (value: boolean) => void> = (node, callback) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width < entry.target.scrollWidth);
}
});
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
};
let isOverflowing = false;
</script>
<div class="table-with-scroll u-margin-block-start-32">
<div class="table-wrapper">
<table class="table" class:is-sticky-scroll={isSticky}>
<div class="table-with-scroll u-margin-block-start-32" data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
<slot />
</table>
</div>
+30
View File
@@ -1,7 +1,37 @@
import { preferences } from '$lib/stores/preferences';
import type { Page } from '@sveltejs/kit';
export function pageToOffset(page: number, limit: number): number {
return page ? page * limit - limit : 0;
}
export function getPage(url: URL): number {
return Number(url.searchParams.get('page'));
}
export function getLimit(url: URL, route: Page['route'], fallback: number): number {
return Number(url.searchParams.get('limit') ?? preferences.get(route).limit ?? fallback);
}
export enum View {
Table = 'table',
Grid = 'grid'
}
export function getView(url: URL, route: Page['route'], fallback: View): View {
return (url.searchParams.get('view') ?? preferences.get(route).view) === View.Grid
? View.Grid
: View.Table ?? fallback;
}
export function getColumns(route: Page['route'], fallback: string[]): string[] {
return preferences.get(route).columns ?? fallback;
}
export function getSearch(url: URL): string | undefined {
return url.searchParams.get('search') ?? undefined;
}
type TabElement = { href: string; title: string; hasChildren?: boolean };
export function isTabSelected(
+3
View File
@@ -0,0 +1,3 @@
export function isMac(): boolean {
return window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
}
+5
View File
@@ -0,0 +1,5 @@
export function getProjectId() {
const pathname = window.location.pathname;
const projectMatch = pathname.match(/\/project-(.*?)\//);
return projectMatch?.[1] || null;
}
+3
View File
@@ -0,0 +1,3 @@
export async function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
+48
View File
@@ -0,0 +1,48 @@
import { cubicOut } from 'svelte/easing';
import type { EasingFunction, TransitionConfig } from 'svelte/transition';
export interface SlideParams {
delay?: number;
duration?: number;
easing?: EasingFunction;
axis?: 'x' | 'y';
}
export function slide(
node: Element,
{ delay = 0, duration = 400, easing = cubicOut, axis = 'y' }: SlideParams = {}
): TransitionConfig {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const primary_property = axis === 'y' ? 'height' : 'width';
const primary_property_value = parseFloat(style[primary_property]);
const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right'];
const capitalized_secondary_properties = secondary_properties.map(
(e) => `${e[0].toUpperCase()}${e.slice(1)}`
);
const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]);
const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]);
const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]);
const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]);
const border_width_start_value = parseFloat(
style[`border${capitalized_secondary_properties[0]}Width`]
);
const border_width_end_value = parseFloat(
style[`border${capitalized_secondary_properties[1]}Width`]
);
return {
delay,
duration,
easing,
css: (t) =>
'overflow: hidden;' +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`${primary_property}: ${t * primary_property_value}px;` +
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;`
};
}
+4 -5
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { AvatarInitials, EmptySearch, Pagination, Trim } from '$lib/components';
import { AvatarInitials, EmptySearch, Limit, Pagination, Trim } from '$lib/components';
import {
TableBody,
TableHeader,
@@ -11,12 +11,11 @@
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { PAGE_LIMIT } from '$lib/constants';
import type { Models } from '@aw-labs/appwrite-console';
export let logs: Models.LogList;
export let path: string;
export let offset = 0;
export let limit = 0;
</script>
<Container>
@@ -66,8 +65,8 @@
</TableBody>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {logs.total}</p>
<Pagination limit={PAGE_LIMIT} {path} {offset} sum={logs.total} />
<Limit {limit} sum={logs.total} name="Logs" />
<Pagination {limit} {offset} sum={logs.total} />
</div>
{:else}
<EmptySearch>
+1 -1
View File
@@ -18,7 +18,7 @@
<nav class="breadcrumbs is-only-desktop" aria-label="breadcrumb">
<ol class="breadcrumbs-list">
{#each breadcrumbs as breadcrumb}
<li class="breadcrumbs-item">
<li class="breadcrumbs-item" data-private>
{#if breadcrumb.href}
<a href={breadcrumb.href} title={breadcrumb.title} on:click={track}>
{breadcrumb.title}
+10 -8
View File
@@ -5,12 +5,14 @@
</script>
<Heading size="4" tag="h1" trimmed={false}>
{#if href}
<a class="button is-text is-only-icon" {href} aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
{/if}
<span class="text u-trim-1">
<slot />
</span>
<div class="u-flex u-cross-center">
{#if href}
<a class="button is-text is-only-icon" {href} aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
{/if}
<span class="text u-trim-1" data-private>
<slot />
</span>
</div>
</Heading>
+55 -34
View File
@@ -1,47 +1,68 @@
<footer class="main-footer">
<div class="container">
<ul class="inline-links is-center">
<script>
import { Mode, MODE } from '$lib/system';
import { version } from '$routes/console/store';
const currentYear = new Date().getFullYear();
</script>
<footer class="main-footer u-cross-center">
<div class="main-footer-start">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<a
class="link"
href="https://github.com/appwrite/appwrite"
target="_blank"
rel="noopener noreferrer">
<span class="icon-github-circled" aria-hidden="true" />
<span class="text">GitHub</span>
<div class="u-flex u-cross-center u-gap-8">
{#if MODE !== Mode.CLOUD}
<span class="icon-cloud" />
{/if}
{#if $version}
<a
class="text"
href="https://github.com/appwrite/appwrite/releases"
target="_blank"
rel="noreferrer">
Version {$version}
</a>
{/if}
</div>
</li>
<li class="inline-links-item">
<a href="https://appwrite.io/policy/terms" target="_blank" rel="noreferrer">
<span class="text">Terms</span>
</a>
</li>
<li class="inline-links-item">
<a
class="link"
href="https://appwrite.io/discord"
target="_blank"
rel="noopener noreferrer">
<span class="icon-discord" aria-hidden="true" />
<span class="text">Discord</span>
<a href="https://appwrite.io/policy/privacy" target="_blank" rel="noreferrer">
<span class="text">Privacy</span>
</a>
</li>
</ul>
</div>
<div class="main-footer-end">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<li class="inline-links-item">
<a
class="link"
href="https://github.com/appwrite/appwrite/issues/new"
target="_blank"
rel="noopener noreferrer">
<span class="text">Open an Issue</span>
</a>
<span class="text">{currentYear} Appwrite. All rights reserved.</span>
</li>
<li class="inline-links-item">
<a
class="link"
href="https://appwrite.io/docs"
target="_blank"
rel="noopener noreferrer">
<span class="text">Docs</span>
<li class="inline-links-item u-flex u-gap-8">
<a href="https://github.com/appwrite/appwrite" target="_blank" rel="noreferrer">
<span class="icon-github" aria-hidden="true" aria-label="Appwrite on Github" />
</a>
<a href="https://appwrite.io/discord" target="_blank" rel="noreferrer">
<span
class="icon-discord"
aria-hidden="true"
aria-label="Appwrite on Discord" />
</a>
</li>
<li class="inline-links-item">
<span class="text">API:0.13.2</span>
</li>
</ul>
</div>
</footer>
<style lang="scss">
.main-footer {
margin-block-start: auto;
}
[class^='icon-']:not(.icon-cloud):not(:hover) {
color: hsl(var(--color-neutral-50));
}
</style>
+2 -2
View File
@@ -20,7 +20,7 @@
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
import { Submit, trackEvent } from '$lib/actions/analytics';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
let showDropdown = false;
@@ -35,7 +35,7 @@
}
async function logout() {
await sdkForConsole.account.deleteSession('current');
await sdk.forConsole.account.deleteSession('current');
trackEvent(Submit.AccountLogout);
await goto(`${base}/login`);
}
+4 -4
View File
@@ -6,7 +6,7 @@
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { calculateTime } from '$lib/helpers/timeConversion';
import type { Models } from '@aw-labs/appwrite-console';
@@ -17,7 +17,7 @@
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
if ('buildId' in data) {
selectedTab = 'logs';
rawData = `${sdkForConsole.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}/deployment/${$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 = `${sdkForConsole.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}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
return true;
}
}
@@ -47,7 +47,7 @@
<svelte:window on:keydown={handleKeydown} />
{#if $log.data}
<section class="cover-frame">
<section class="cover-frame" data-private>
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
<h1 class="body-text-1">Function ID: {$log.func.$id}</h1>
<button
+160 -70
View File
@@ -2,81 +2,171 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
$: project = $page.params.project;
$: path = `${base}/console/project-${project}`;
$: projectPath = `${base}/console/project-${project}`;
$: subNavigation = $page.data.subNavigation;
// We need to have this second variable, because we only want narrow
// to change automatically if we change from having a second side nav to
// not having one, not when the second side nav changes to a different value.
$: hasSubNavigation = !!subNavigation;
let narrow = false;
$: {
narrow = hasSubNavigation;
}
function handleKeyDown(event: KeyboardEvent) {
// If Alt + S is pressed
if (hasSubNavigation && event.altKey && event.keyCode === 83) {
event.preventDefault();
narrow = !narrow;
}
}
</script>
<div class="side-nav">
{#if project}
<div class="side-nav-main">
<section class="drop-section">
<ul class="drop-list">
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/overview`)}
on:click={() => trackEvent('click_menu_overview')}
href={path}>
<span class="icon-chart-bar" aria-hidden="true" />
<span class="text">Overview</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/auth`)}
on:click={() => trackEvent('click_menu_auth')}
href={`${path}/auth`}>
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/databases`)}
on:click={() => trackEvent('click_menu_databases')}
href={`${path}/databases`}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/functions`)}
on:click={() => trackEvent('click_menu_functions')}
href={`${path}/functions`}>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
on:click={() => trackEvent('click_menu_storage')}
href={`${path}/storage`}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
</ul>
</section>
</div>
<svelte:window on:keydown={handleKeyDown} />
<div class="side-nav-bottom">
<section class="drop-section">
<a
class="drop-button"
href={`${path}/settings`}
on:click={() => trackEvent('click_menu_settings')}
class:is-selected={$page.url.pathname.startsWith(`${path}/settings`)}>
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
</section>
<div class="side-nav" class:is-open-level-2={hasSubNavigation}>
<div class="side-nav-level-1" class:is-narrow={narrow}>
{#if project}
<div class="side-nav-main">
<section class="drop-section">
<ul class="drop-list">
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/overview`
)}
on:click={() => trackEvent('click_menu_overview')}
href={projectPath}
use:tooltip={{
content: 'Overview',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-chart-bar" aria-hidden="true" />
<span class="text">Overview</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/auth`
)}
on:click={() => trackEvent('click_menu_auth')}
href={`${projectPath}/auth`}
use:tooltip={{
content: 'Auth',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Auth</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/databases`
)}
on:click={() => trackEvent('click_menu_databases')}
href={`${projectPath}/databases`}
use:tooltip={{
content: 'Databases',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/functions`
)}
on:click={() => trackEvent('click_menu_functions')}
href={`${projectPath}/functions`}
use:tooltip={{
content: 'Functions',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-lightning-bolt" aria-hidden="true" />
<span class="text">Functions</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/storage`
)}
on:click={() => trackEvent('click_menu_storage')}
href={`${projectPath}/storage`}
use:tooltip={{
content: 'Storage',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
</ul>
</section>
</div>
<div class="side-nav-bottom">
<section class="drop-section">
<a
class="drop-button"
href={`${projectPath}/settings`}
on:click={() => trackEvent('click_menu_settings')}
class:is-selected={$page.url.pathname.startsWith(`${projectPath}/settings`)}
title="Settings"
use:tooltip={{
content: 'Settings',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-cog" aria-hidden="true" />
<span class="text">Settings</span>
</a>
</section>
</div>
{/if}
{#if subNavigation}
<button
class="side-nav-button button is-small is-secondary is-not-mobile"
aria-label="resize menu"
on:click={() => (narrow = !narrow)}
use:tooltip={{
content: isMac() ? '⌥ + S' : 'Alt + S'
}}>
<span
class:icon-cheveron-right={narrow}
class:icon-cheveron-left={!narrow}
aria-hidden="true" />
</button>
{/if}
</div>
{#if subNavigation}
<div class="side-nav-level-2 is-open" transition:slide={{ axis: 'x', duration: 250 }}>
<div class="side-nav-main">
<svelte:component this={subNavigation} />
</div>
</div>
{/if}
</div>
+5 -3
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { fly } from 'svelte/transition';
import type { Notification } from '../stores/notifications';
export let type: Notification['type'] = 'info';
@@ -9,12 +10,13 @@
const dispatch = createEventDispatcher();
</script>
<li
<div
class="alert-sticky"
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
class:is-info={type === 'info'}
transition:fly={{ x: 50 }}>
<button
class="button is-text is-only-icon"
style="--button-size:1.5rem;"
@@ -45,4 +47,4 @@
{/each}
</div>
{/if}
</li>
</div>
+19 -9
View File
@@ -1,19 +1,22 @@
<script lang="ts">
import Notification from './notification.svelte';
import { dismissNotification, notifications } from '../stores/notifications';
import { flip } from 'svelte/animate';
</script>
{#if $notifications}
<section>
<ul class="u-flex u-flex-vertical u-gap-16">
{#each $notifications as notification (notification.id)}
<Notification
type={notification.type}
title={notification.title}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
<li animate:flip={{ duration: 500 }}>
<Notification
type={notification.type}
title={notification.title}
on:dismiss={() => dismissNotification(notification.id)}
buttons={notification?.buttons}>
{notification.message}
</Notification>
</li>
{/each}
</ul>
</section>
@@ -22,8 +25,15 @@
<style lang="scss">
section {
position: fixed;
top: calc(var(--main-header-height) + 24px);
right: 24px;
top: calc(var(--main-header-height) + 12px);
right: 12px;
z-index: 1000;
}
@media (min-width: 768px) {
section {
top: calc(var(--main-header-height) + 24px);
right: 24px;
}
}
</style>
+23 -6
View File
@@ -41,7 +41,6 @@
<main
class:grid-with-side={showSideNavigation}
class:grid={!showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show}>
<header class="main-header u-padding-inline-end-0">
@@ -54,9 +53,11 @@
</button>
<slot name="header" />
</header>
<nav class="main-side">
<slot name="side" />
</nav>
{#if showSideNavigation}
<nav class="main-side">
<slot name="side" />
</nav>
{/if}
<section class="main-content">
{#if $page.data?.header}
<svelte:component this={$page.data.header} />
@@ -64,19 +65,35 @@
<slot />
</section>
<slot name="footer" />
</main>
<style>
.grid-with-side {
<style lang="scss">
main {
min-height: 100vh;
&:not(.grid-with-side) {
display: flex;
flex-direction: column;
}
}
.main-side {
z-index: 25;
}
@media (max-width: 550.99px), (min-width: 551px) and (max-width: 1198.99px) {
.main-side {
top: 4.5rem;
}
}
@media (min-width: 1199px) {
.grid-with-side {
grid-template-columns: auto 1fr !important;
}
}
.main-content {
overflow: hidden;
}
</style>
+5 -2
View File
@@ -6,6 +6,9 @@
import { base } from '$app/paths';
import { user } from '$lib/stores/user';
export let imgLight = LoginLight;
export let imgDark = LoginDark;
const technologies = [
'js',
'flutter',
@@ -38,9 +41,9 @@
class="container u-margin-block-start-20 is-not-mobile"
style="--p-container-max-size: var(--container-size-large);">
{#if $app.themeInUse === 'dark'}
<img src={LoginDark} alt="" class="u-only-dark" />
<img src={imgDark} alt="" class="u-only-dark" />
{:else}
<img src={LoginLight} alt="" class="u-only-light" />
<img src={imgLight} alt="" class="u-only-light" />
{/if}
</div>
+1 -1
View File
@@ -58,7 +58,7 @@
</div>
<Card>
{#if count}
<Heading tag="h6" size="6">{last(count).value}</Heading>
<Heading tag="h6" size="6">{total(count)}</Heading>
<p>{countMetadata.title}</p>
<div class="u-margin-block-start-16" />
<div class="chart-container">
+1 -1
View File
@@ -122,7 +122,7 @@
{/if}
</div>
<div class="wizard-main">
<Form noStyle on:submit={submit}>
<Form noStyle onSubmit={submit}>
{#each sortedSteps as [step, { component }]}
{#if currentStep === step}
<svelte:component this={component} />
+1 -1
View File
@@ -11,7 +11,7 @@
}
</script>
<Modal bind:show on:submit={handleSubmit} warning>
<Modal bind:show onSubmit={handleSubmit} warning>
<svelte:fragment slot="header">Exit Process</svelte:fragment>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import Form from '../elements/forms/form.svelte';
import Button from '../elements/forms/button.svelte';
export let onSubmit: () => Promise<void> | void;
</script>
<Form {onSubmit}>
<Button submit>Submit</Button>
<Button>Button</Button>
</Form>
+31
View File
@@ -0,0 +1,31 @@
import type { Models, Payload } from '@aw-labs/appwrite-console';
import { Service } from './service';
export class Project extends Service {
/**
* Get usage stats for a project
*
*
* @param {string} range
* @throws {AppwriteException}
* @returns {Promise}
*/
async getUsage(range?: string): Promise<Models.UsageProject> {
const path = '/project/usage';
const payload: Payload = {};
if (typeof range !== 'undefined') {
payload['range'] = range;
}
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'get',
uri,
{
'content-type': 'application/json'
},
payload
);
}
}
+28
View File
@@ -0,0 +1,28 @@
import type { Client, Payload } from '@aw-labs/appwrite-console';
export class Service {
static CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
client: Client;
constructor(client: Client) {
this.client = client;
}
static flatten(data: Payload, prefix = ''): Payload {
let output: Payload = {};
for (const key in data) {
const value = data[key];
const finalKey = prefix ? `${prefix}[${key}]` : key;
if (Array.isArray(value)) {
output = Object.assign(output, this.flatten(value, finalKey));
} else {
output[finalKey] = value;
}
}
return output;
}
}
+1 -1
View File
@@ -74,7 +74,7 @@ function createFeedbackStore() {
customFields: value ? [{ id: '40655', value }] : undefined
})
});
if (response.status !== 200) {
if (response.status >= 400) {
throw new Error('Failed to submit feedback');
}
}
+1 -5
View File
@@ -33,11 +33,7 @@ export const addNotification = (notification: Omit<Notification, 'id'>) => {
const n = { ...defaults, ...notification };
notifications.update((all) => {
if (all.length < 3) {
return [n, ...all];
} else {
return [n, ...all.slice(0, 2)];
}
return [n, ...all.slice(0, 4)];
});
if (n.timeout) setTimeout(() => dismissNotification(n.id), n.timeout);
+111
View File
@@ -0,0 +1,111 @@
import { browser } from '$app/environment';
import { page } from '$app/stores';
import type { View } from '$lib/helpers/load';
import type { Page } from '@sveltejs/kit';
import { get, writable } from 'svelte/store';
import { sdk } from './sdk';
type Preferences = {
limit?: number;
view?: View;
columns?: string[];
};
type PreferencesStore = {
[key: string]: {
[key: string]: Preferences;
collections?: {
[key: string]: Preferences['columns'];
};
};
};
function createPreferences() {
const { subscribe, set, update } = writable<PreferencesStore>({});
if (browser) {
set(JSON.parse(globalThis.localStorage.getItem('preferences') ?? '{}'));
}
return {
subscribe,
get: (route: Page['route']): Preferences => {
let preferences: PreferencesStore;
subscribe((n) => (preferences = n))();
return (
preferences[sdk.forProject.client.config.project]?.[route.id] ?? {
limit: null,
view: null,
columns: null
}
);
},
getCustomCollectionColumns: (collectionId: string): Preferences['columns'] => {
let preferences: PreferencesStore;
subscribe((n) => (preferences = n))();
return (
preferences[sdk.forProject.client.config.project]?.collections?.[collectionId] ??
null
);
},
setLimit: (limit: Preferences['limit']) =>
update((n) => {
const path = get(page).route.id;
const project = sdk.forProject.client.config.project;
if (!n[project]?.[path]) {
n[project] ??= {};
n[project][path] ??= {};
}
n[project][path].limit = limit;
return n;
}),
setView: (view: Preferences['view']) =>
update((n) => {
const path = get(page).route.id;
const project = sdk.forProject.client.config.project;
if (!n[project]?.[path]) {
n[project] ??= {};
n[project][path] ??= {};
}
n[project][path].view = view;
return n;
}),
setColumns: (columns: Preferences['columns']) =>
update((n) => {
const path = get(page).route.id;
const project = sdk.forProject.client.config.project;
if (!n[project]?.[path]) {
n[project] ??= {};
n[project][path] ??= {};
}
n[project][path].columns = columns;
return n;
}),
setCustomCollectionColumns: (columns: Preferences['columns']) =>
update((n) => {
const current = get(page);
const project = sdk.forProject.client.config.project;
const collection = current.params.collection;
if (!n[project]?.collections?.[collection]) {
n[project] ??= {};
n[project].collections ??= {};
}
n[project].collections[collection] = columns;
return n;
})
};
}
export const preferences = createPreferences();
if (browser) {
preferences.subscribe((n) => globalThis.localStorage.setItem('preferences', JSON.stringify(n)));
}
+25 -17
View File
@@ -1,3 +1,5 @@
import { getProjectId } from '$lib/helpers/project';
import { Project } from '$lib/sdk/project';
import { VARS } from '$lib/system';
import {
Account,
@@ -14,26 +16,12 @@ import {
} from '@aw-labs/appwrite-console';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
const clientConsole = new Client();
clientConsole.setEndpoint(endpoint).setProject('console');
const clientProject = new Client();
clientProject.setEndpoint(endpoint).setMode('admin');
const setProject = (projectId: string): Client => clientProject.setProject(projectId);
const sdkForConsole = {
client: clientConsole,
account: new Account(clientConsole),
avatars: new Avatars(clientConsole),
functions: new Functions(clientConsole),
health: new Health(clientConsole),
locale: new Locale(clientConsole),
projects: new Projects(clientConsole),
teams: new Teams(clientConsole),
users: new Users(clientConsole)
};
const sdkForProject = {
client: clientProject,
account: new Account(clientProject),
@@ -42,10 +30,30 @@ const sdkForProject = {
functions: new Functions(clientProject),
health: new Health(clientProject),
locale: new Locale(clientProject),
projects: new Projects(clientProject),
project: new Project(clientProject),
storage: new Storage(clientProject),
teams: new Teams(clientProject),
users: new Users(clientProject)
};
export { sdkForConsole, sdkForProject, setProject };
export const sdk = {
forConsole: {
client: clientConsole,
account: new Account(clientConsole),
avatars: new Avatars(clientConsole),
functions: new Functions(clientConsole),
health: new Health(clientConsole),
locale: new Locale(clientConsole),
projects: new Projects(clientConsole),
teams: new Teams(clientConsole),
users: new Users(clientConsole)
},
get forProject() {
const projectId = getProjectId();
if (projectId && projectId !== clientProject.config.project) {
clientProject.setProject(projectId);
}
return sdkForProject;
}
};
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { sdkForProject } from './sdk';
import { sdk } from './sdk';
type UploaderFile = {
$id: string;
@@ -71,7 +71,7 @@ const createUploader = () => {
n.files.unshift(newFile);
return n;
});
const uploadedFile = await sdkForProject.storage.createFile(
const uploadedFile = await sdk.forProject.storage.createFile(
bucketId,
id ?? 'unique()',
file,
+2
View File
@@ -21,3 +21,5 @@ export const ENV = {
};
export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED;
export const isCloud = MODE === Mode.CLOUD;
export const isSelfHosted = MODE === Mode.SELF_HOSTED;
+12
View File
@@ -0,0 +1,12 @@
<script>
import { page } from '$app/stores';
import { Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Unauthenticated } from '$lib/layout';
</script>
<Unauthenticated>
<Heading size="1" tag="h3">{$page.status}</Heading>
<Heading size="3" tag="h4">{$page.error.message}</Heading>
<Button href="/">Back to the console</Button>
</Unauthenticated>
+29 -16
View File
@@ -1,19 +1,21 @@
<script lang="ts">
import { browser } from '$app/environment';
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { onCLS, onFID, onLCP, onFCP, onINP, onTTFB } from 'web-vitals';
import { reportWebVitals } from '$lib/helpers/vitals';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { browser, dev } from '$app/environment';
import { page } from '$app/stores';
import { isTrackingAllowed, trackPageView } from '$lib/actions/analytics';
import { reportWebVitals } from '$lib/helpers/vitals';
import { Notifications, Progress } from '$lib/layout';
import { app } from '$lib/stores/app';
import { Progress, Notifications } from '$lib/layout';
import { loading } from './store';
import Loading from './loading.svelte';
import { trackPageView } from '$lib/actions/analytics';
import { user } from '$lib/stores/user';
import { ENV, isCloud } from '$lib/system';
import * as Sentry from '@sentry/svelte';
import LogRocket from 'logrocket';
import { BrowserTracing } from '@sentry/tracing';
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
import { loading } from './store';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
@@ -23,7 +25,7 @@
/**
* Reporting Web Vitals.
*/
if (!dev && window.VERCEL_ANALYTICS_ID) {
if (ENV.PROD && window.VERCEL_ANALYTICS_ID) {
onCLS(reportWebVitals);
onFID(reportWebVitals);
onLCP(reportWebVitals);
@@ -32,15 +34,26 @@
onTTFB(reportWebVitals);
}
/**
* Sentry Error Logging
*/
if (!dev) {
if (ENV.PROD) {
/**
* Sentry Error Logging
*/
Sentry.init({
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.sentry.io/4504158071422976',
integrations: [new BrowserTracing()],
tracesSampleRate: 1.0
});
/**
* LogRocket
*/
if (isCloud && isTrackingAllowed()) {
LogRocket.init('rgthvf/appwrite', {
dom: {
inputSanitizer: true
}
});
}
}
/**
@@ -59,7 +72,7 @@
}
loading.set(false);
} else {
if (acceptedRoutes.includes($page.url.pathname)) {
if (acceptedRoutes.some((n) => $page.url.pathname.startsWith(n))) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
} else {
await goto(`${base}/login`, {
+10 -4
View File
@@ -1,7 +1,8 @@
import '@appwrite.io/pink';
import '@appwrite.io/pink-icons';
import 'tippy.js/dist/tippy.css';
import { sdkForConsole } from '$lib/stores/sdk';
import LogRocket from 'logrocket';
import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
@@ -11,11 +12,16 @@ export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
try {
const account = await sdkForConsole.account.get();
const account = await sdk.forConsole.account.get();
LogRocket.identify(account.$id, {
name: account.name,
email: account.email
});
return {
account,
organizations: sdkForConsole.teams.list()
organizations: sdk.forConsole.teams.list()
};
} catch (error) {
const acceptedRoutes = [
@@ -28,7 +34,7 @@ export const load: LayoutLoad = async ({ depends, url }) => {
'/auth/oauth2/failure'
];
if (!acceptedRoutes.includes(url.pathname)) {
if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) {
throw redirect(303, '/login');
}
}
+13 -16
View File
@@ -1,18 +1,19 @@
<script lang="ts">
import Shell from '$lib/layout/shell.svelte';
import SideNavigation from '$lib/layout/navigation.svelte';
import Header from '$lib/layout/header.svelte';
import { newOrgModal } from '$lib/stores/organization';
import Create from './createOrganization.svelte';
import { page } from '$app/stores';
import { beforeNavigate } from '$app/navigation';
import { wizard } from '$lib/stores/wizard';
import { page } from '$app/stores';
import { INTERVAL } from '$lib/constants';
import { Logs } from '$lib/layout';
import Footer from '$lib/layout/footer.svelte';
import Header from '$lib/layout/header.svelte';
import SideNavigation from '$lib/layout/navigation.svelte';
import Shell from '$lib/layout/shell.svelte';
import { feedback } from '$lib/stores/app';
import { log } from '$lib/stores/logs';
import { newOrgModal } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { loading } from '../store';
import { feedback } from '$lib/stores/app';
import { INTERVAL } from '$lib/constants';
import Create from './createOrganization.svelte';
onMount(() => {
loading.set(false);
@@ -50,14 +51,10 @@
!$page?.params.organization &&
!$page.url.pathname.includes('/console/account') &&
!$page.url.pathname.includes('/console/onboarding')}>
<svelte:fragment slot="header">
<Header />
</svelte:fragment>
<svelte:fragment slot="side">
<SideNavigation />
</svelte:fragment>
<Header slot="header" />
<SideNavigation slot="side" />
<slot />
<footer class="main-footer" />
<Footer slot="footer" />
</Shell>
{#if $wizard.show && $wizard.component}
+11
View File
@@ -0,0 +1,11 @@
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
const response = await fetch(`${sdk.forConsole.client.config.endpoint}/health/version`);
const data = await response.json();
return {
version: data?.version ?? null
};
};
+10 -10
View File
@@ -4,7 +4,7 @@
import { Container } from '$lib/layout';
import { onMount } from 'svelte';
import { user } from '$lib/stores/user';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { base } from '$app/paths';
import Delete from './delete.svelte';
@@ -26,7 +26,7 @@
async function updateName() {
try {
await sdkForConsole.account.updateName(name);
await sdk.forConsole.account.updateName(name);
invalidate(Dependencies.ACCOUNT);
addNotification({
message: 'Name has been updated',
@@ -43,7 +43,7 @@
}
async function updateEmail() {
try {
await sdkForConsole.account.updateEmail(email, emailPassword);
await sdk.forConsole.account.updateEmail(email, emailPassword);
invalidate(Dependencies.ACCOUNT);
addNotification({
message: 'Email has been updated',
@@ -61,7 +61,7 @@
async function updatePassword() {
try {
await sdkForConsole.account.updatePassword(newPassword, oldPassword);
await sdk.forConsole.account.updatePassword(newPassword, oldPassword);
newPassword = oldPassword = null;
addNotification({
message: 'Password has been updated',
@@ -79,9 +79,9 @@
</script>
<Container>
<Form on:submit={updateName}>
<Form onSubmit={updateName}>
<CardGrid>
<Heading tag="h6" size="7">Update Name</Heading>
<Heading tag="h6" size="7">Name</Heading>
<svelte:fragment slot="aside">
<ul>
@@ -94,9 +94,9 @@
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updateEmail}>
<Form onSubmit={updateEmail}>
<CardGrid>
<Heading tag="h6" size="7">Update Email</Heading>
<Heading tag="h6" size="7">Email</Heading>
<svelte:fragment slot="aside">
<FormList>
@@ -122,9 +122,9 @@
</svelte:fragment>
</CardGrid>
</Form>
<Form on:submit={updatePassword}>
<Form onSubmit={updatePassword}>
<CardGrid>
<Heading tag="h6" size="7">Update Password</Heading>
<Heading tag="h6" size="7">Password</Heading>
<p class="text">
Forgot your password? <a class="link" href={`${base}/recover`}
>Recover your password</a>
@@ -3,7 +3,6 @@
import type { PageData } from './$types';
export let data: PageData;
const path = '/console/account/activity';
</script>
<Activity {path} logs={data.logs} offset={data.offset} />
<Activity {...data} />
@@ -0,0 +1,17 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route }) => {
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
logs: await sdk.forConsole.account.listLogs([Query.offset(offset), Query.limit(limit)])
};
};
@@ -1,15 +0,0 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const page = Number(params.page);
const offset = pageToOffset(page, PAGE_LIMIT);
return {
offset,
logs: await sdkForConsole.account.listLogs([Query.offset(offset), Query.limit(PAGE_LIMIT)])
};
};
+3 -3
View File
@@ -5,13 +5,13 @@
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
export let showDelete = false;
const deleteAccount = async () => {
try {
await sdkForConsole.account.updateStatus();
await sdk.forConsole.account.updateStatus();
showDelete = false;
invalidate(Dependencies.ACCOUNT);
addNotification({
@@ -29,7 +29,7 @@
};
</script>
<Modal bind:show={showDelete} on:submit={deleteAccount} warning>
<Modal bind:show={showDelete} onSubmit={deleteAccount} warning>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
+2 -2
View File
@@ -7,7 +7,7 @@
import { Button } from '$lib/elements/forms';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
const path = `/console/account`;
@@ -38,7 +38,7 @@
];
async function logout() {
await sdkForConsole.account.deleteSession('current');
await sdk.forConsole.account.deleteSession('current');
trackEvent(Submit.AccountLogout);
await goto(`${base}/login`);
}
@@ -6,19 +6,19 @@
Pagination,
AvatarGroup,
CardContainer,
Heading
Heading,
Limit
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateOrganization from '../../../createOrganization.svelte';
import { sdkForConsole } from '$lib/stores/sdk';
import { CARD_LIMIT } from '$lib/constants';
import CreateOrganization from '../../createOrganization.svelte';
import { sdk } from '$lib/stores/sdk';
import type { PageData } from './$types';
export let data: PageData;
const getMemberships = async (teamId: string) => {
const memberships = await sdkForConsole.teams.listMemberships(teamId);
const memberships = await sdk.forConsole.teams.listMemberships(teamId);
return memberships.memberships.map((team) => team.userName);
};
@@ -67,12 +67,8 @@
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {data.organizations.total}</p>
<Pagination
limit={CARD_LIMIT}
path="/console/account/organizations"
offset={data.offset}
sum={data.organizations.total} />
<Limit limit={data.limit} sum={data.organizations.total} name="Organizations" />
<Pagination limit={data.limit} offset={data.offset} sum={data.organizations.total} />
</div>
</Container>
@@ -0,0 +1,21 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route }) => {
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
organizations: await sdk.forConsole.teams.list([
Query.offset(offset),
Query.limit(limit),
Query.orderDesc('$createdAt')
])
};
};
@@ -1,19 +0,0 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const page = Number(params.page);
const offset = pageToOffset(page, CARD_LIMIT);
return {
offset,
organizations: await sdkForConsole.teams.list([
Query.offset(offset),
Query.limit(CARD_LIMIT),
Query.orderDesc('$createdAt')
])
};
};
@@ -12,7 +12,7 @@
TableScroll
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import type { Models } from '@aw-labs/appwrite-console';
@@ -23,11 +23,11 @@
export let data: PageData;
const getBrowser = (clientCode: string) => {
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
return sdk.forConsole.avatars.getBrowser(clientCode, 40, 40);
};
const logout = async (session: Models.Session) => {
await sdkForConsole.account.deleteSession(session.$id);
await sdk.forConsole.account.deleteSession(session.$id);
trackEvent(Submit.AccountDeleteSession);
if (session.current) {
await goto(`${base}/login`);
@@ -35,7 +35,7 @@
invalidate(Dependencies.ACCOUNT_SESSIONS);
};
const logoutAll = async () => {
await sdkForConsole.account.deleteSessions();
await sdk.forConsole.account.deleteSessions();
trackEvent(Submit.AccountDeleteAllSessions);
await goto(`${base}/login`);
invalidate(Dependencies.ACCOUNT_SESSIONS);
@@ -115,7 +115,7 @@
<Button
external
secondary
href="https://appwrite.io/docs/server/auth?sdk=nodejs-default#usersGetsessions">
href="https://appwrite.io/docs/client/account#accountCreateEmailSession">
Documentation
</Button>
</div>
@@ -124,6 +124,5 @@
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {data.sessions.total}</p>
<!-- <Pagination limit={PAGE_LIMIT} offset={data.offset} sum={data.sessions.total} /> -->
</div>
</Container>
@@ -1,11 +1,11 @@
import { Dependencies } from '$lib/constants';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ depends }) => {
depends(Dependencies.ACCOUNT_SESSIONS);
return {
sessions: await sdkForConsole.account.listSessions()
sessions: await sdk.forConsole.account.listSessions()
};
};
+3 -3
View File
@@ -3,7 +3,7 @@
import { Pill } from '$lib/elements';
import { InputText, Button, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -21,7 +21,7 @@
const create = async () => {
try {
const org = await sdkForConsole.teams.create(id ?? ID.unique(), name);
const org = await sdk.forConsole.teams.create(id ?? ID.unique(), name);
await invalidate(Dependencies.ACCOUNT);
dispatch('created');
await goto(`/console/organization-${org.$id}`);
@@ -42,7 +42,7 @@
};
</script>
<Modal {error} on:submit={create} size="big" bind:show>
<Modal {error} onSubmit={create} size="big" bind:show>
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
<FormList>
<InputText
+5 -9
View File
@@ -9,19 +9,17 @@
import FormList from '$lib/elements/forms/formList.svelte';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { ID } from '@aw-labs/appwrite-console';
let name: string;
let id: string;
let showCustomId = false;
let loading = false;
async function createProject() {
try {
loading = true;
const org = await createOrganization();
const project = await sdkForConsole.projects.create(
const project = await sdk.forConsole.projects.create(
id ?? ID.unique(),
name,
org.$id,
@@ -34,7 +32,6 @@
teamId: org.$id
});
} catch (error) {
loading = false;
addNotification({
message: error.message,
type: 'error'
@@ -44,20 +41,19 @@
}
async function createOrganization() {
return await sdkForConsole.teams.create(ID.unique(), 'Personal Projects');
return await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
}
</script>
<Container overlapCover size="large">
<Card>
<Form on:submit={createProject}>
<Form onSubmit={createProject}>
<FormList>
<InputText
id="name"
label="Project name"
placeholder="First Appwrite Project"
required
disabled={loading}
bind:value={name} />
{#if !showCustomId}
<div>
@@ -70,7 +66,7 @@
{:else}
<CustomId bind:show={showCustomId} name="Project" bind:id />
{/if}
<Button fullWidth submit disabled={loading || name === ''} event="create_project">
<Button fullWidth submit disabled={name === ''} event="create_project">
Create project
</Button>
</FormList>
@@ -1,20 +1,19 @@
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
import { error } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
export const load: LayoutLoad = async ({ params, parent, depends }) => {
await parent();
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
try {
return {
header: Header,
breadcrumbs: Breadcrumbs,
organization: await sdkForConsole.teams.get(params.organization),
members: await sdkForConsole.teams.listMemberships(params.organization)
organization: await sdk.forConsole.teams.get(params.organization),
members: await sdk.forConsole.teams.listMemberships(params.organization)
};
} catch (e) {
localStorage.removeItem('organization');
@@ -1,13 +1,12 @@
<script lang="ts">
import { base } from '$app/paths';
import { Pill } from '$lib/elements';
import { GridItem1, Heading, Empty, CardContainer, Pagination } from '$lib/components';
import { GridItem1, Heading, Empty, CardContainer, Pagination, Limit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateProject from '../createProject.svelte';
import CreateOrganization from '../../createOrganization.svelte';
import CreateProject from './createProject.svelte';
import CreateOrganization from '../createOrganization.svelte';
import type { PageData } from './$types';
import { CARD_LIMIT } from '$lib/constants';
import { page } from '$app/stores';
export let data: PageData;
@@ -96,12 +95,8 @@
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {data.projects.total}</p>
<Pagination
path={`/console/organization-${$page.params.organization}`}
limit={CARD_LIMIT}
offset={data.offset}
sum={data.projects.total} />
<Limit limit={data.limit} sum={data.projects.total} name="Projects" />
<Pagination limit={data.limit} offset={data.offset} sum={data.projects.total} />
</div>
</Container>
@@ -0,0 +1,22 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, url, route }) => {
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
projects: await sdk.forConsole.projects.list([
Query.offset(offset),
Query.limit(limit),
Query.equal('teamId', params.organization),
Query.orderDesc('$createdAt')
])
};
};
@@ -1,20 +0,0 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const page = Number(params.page);
const offset = pageToOffset(page, CARD_LIMIT);
return {
offset,
projects: await sdkForConsole.projects.list([
Query.offset(offset),
Query.limit(CARD_LIMIT),
Query.equal('teamId', params.organization),
Query.orderDesc('$createdAt')
])
};
};
@@ -3,7 +3,7 @@
import { Modal } from '$lib/components';
import { InputText, InputEmail, Button, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { organization } from '$lib/stores/organization';
import { invalidate } from '$app/navigation';
@@ -19,7 +19,7 @@
const create = async () => {
try {
const team = await sdkForConsole.teams.createMembership(
const team = await sdk.forConsole.teams.createMembership(
$organization.$id,
email,
['owner'],
@@ -47,7 +47,7 @@
}
</script>
<Modal {error} size="big" bind:show={showCreate} on:submit={create}>
<Modal {error} size="big" bind:show={showCreate} onSubmit={create}>
<svelte:fragment slot="header">Invite Member</svelte:fragment>
<FormList>
<InputEmail
@@ -5,7 +5,7 @@
import { Pill } from '$lib/elements';
import { InputText, Button, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { ID } from '@aw-labs/appwrite-console';
import { createEventDispatcher } from 'svelte';
@@ -18,12 +18,10 @@
let name: string;
let showCustomId = false;
let error: string;
let isCreating = false;
async function create() {
try {
isCreating = true;
const project = await sdkForConsole.projects.create(
const project = await sdk.forConsole.projects.create(
id ?? ID.unique(),
name,
teamId,
@@ -40,23 +38,16 @@
});
await goto(`/console/project-${project.$id}`);
} catch (e) {
isCreating = false;
error = e.message;
trackError(e, Submit.ProjectCreate);
}
}
</script>
<Modal {error} on:submit={create} size="big" bind:show>
<Modal {error} onSubmit={create} size="big" bind:show>
<svelte:fragment slot="header">Create Project</svelte:fragment>
<FormList>
<InputText
id="name"
label="Name"
bind:value={name}
required
autofocus={true}
disabled={isCreating} />
<InputText id="name" label="Name" bind:value={name} required autofocus={true} />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
@@ -71,6 +62,6 @@
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit disabled={isCreating}>Create</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
@@ -4,7 +4,7 @@
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { createEventDispatcher } from 'svelte';
import { user } from '$lib/stores/user';
@@ -17,10 +17,10 @@
const deleteMembership = async () => {
try {
await sdkForConsole.teams.deleteMembership(selectedMember.teamId, selectedMember.$id);
await sdk.forConsole.teams.deleteMembership(selectedMember.teamId, selectedMember.$id);
if (isUser) {
await sdkForConsole.account.deleteSession('current');
await sdk.forConsole.account.deleteSession('current');
await goto(`${base}/login`);
} else {
dispatch('deleted');
@@ -43,11 +43,11 @@
$: isUser = selectedMember?.userId === $user?.$id;
</script>
<Modal bind:show={showDelete} on:submit={deleteMembership} warning>
<Modal bind:show={showDelete} onSubmit={deleteMembership} warning>
<svelte:fragment slot="header">
{isUser ? 'Leave Organization' : 'Delete Member'}
</svelte:fragment>
<p>
<p data-private>
{isUser
? `Are you sure you want to leave '${selectedMember?.teamName}'?`
: `Are you sure you want to delete ${selectedMember?.userName} from '${selectedMember?.teamName}'?`}
@@ -2,7 +2,7 @@
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { sdk } from '$lib/stores/sdk';
import { organization, organizationList } from '$lib/stores/organization';
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
@@ -13,7 +13,7 @@
const deleteOrg = async () => {
try {
await sdkForConsole.teams.delete($organization.$id);
await sdk.forConsole.teams.delete($organization.$id);
addNotification({
type: 'success',
message: `${$organization.name} has been deleted`
@@ -37,7 +37,7 @@
};
</script>
<Modal on:submit={deleteOrg} bind:show={showDelete} warning>
<Modal onSubmit={deleteOrg} bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Organization</svelte:fragment>
<p>
Are you sure you want to delete <b>{$organization.name}</b>? All projects ({$organization.total})
@@ -72,7 +72,9 @@
</DropList>
<div class="u-margin-inline-start-auto">
<div class="u-flex u-gap-16">
<AvatarGroup size={40} {avatars} total={$members?.total ?? 0} />
<a href={`${path}/members`}>
<AvatarGroup size={40} {avatars} total={$members?.total ?? 0} />
</a>
<Button secondary on:click={() => newMemberModal.set(true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Invite</span>
@@ -1,38 +1,37 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { AvatarInitials, Heading, Limit, Pagination } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
TableHeader,
TableBody,
TableCellHead,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { AvatarInitials, Heading, Pagination } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { organization, members, newMemberModal } from '$lib/stores/organization';
import { sdkForConsole } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import Delete from '../../deleteMember.svelte';
import { members, newMemberModal, organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import type { PageData } from './$types';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import Delete from '../deleteMember.svelte';
export let data: PageData;
let selectedMember: Models.Membership;
let showDelete = false;
const url = `${$page.url.origin}/console/`;
const deleted = () => invalidate(Dependencies.ACCOUNT);
const resend = async (member: Models.Membership) => {
try {
await sdkForConsole.teams.createMembership(
await sdk.forConsole.teams.createMembership(
$organization.$id,
member.userEmail,
member.roles,
@@ -69,7 +68,7 @@
<TableHeader>
<TableCellHead width={140}>Name</TableCellHead>
<TableCellHead width={120}>Email</TableCellHead>
<TableCellHead width={90}>Role</TableCellHead>
<TableCellHead width={90} />
<TableCellHead width={30} />
</TableHeader>
<TableBody>
@@ -87,17 +86,14 @@
</div>
</TableCell>
<TableCellText title="Email">{member.userEmail}</TableCellText>
<TableCell title="Roles">
<TableCell>
{#if member.invited && !member.joined}
<Button
secondary
event="invite_resend"
on:click={() => resend(member)}>Resend</Button>
{:else}
{member.roles}
{/if}
</TableCell>
<TableCell>
<button
class="button is-only-icon is-text"
@@ -115,12 +111,8 @@
</TableBody>
</TableScroll>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {data.organizationMembers.total}</p>
<Pagination
offset={data.offset}
limit={PAGE_LIMIT}
path={`/console/organization-${$page.params.organization}/members`}
sum={data.members.total} />
<Limit limit={data.limit} sum={data.members.total} name="Members" />
<Pagination limit={data.limit} offset={data.offset} sum={data.members.total} />
</div>
{/if}
</Container>
@@ -0,0 +1,20 @@
import { PAGE_LIMIT } from '$lib/constants';
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@aw-labs/appwrite-console';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, params, route }) => {
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
organizationMembers: await sdk.forConsole.teams.listMemberships(params.organization, [
Query.limit(limit),
Query.offset(offset)
])
};
};

Some files were not shown because too many files have changed in this diff Show More