Add embedded route detection and styling

Here's a concise commit message that summarizes the changes: Add
embedded route detection and UI styling

Check if the current route is in the /embed/ path and style
appropriately. Add embedded views for auth, messaging, databases, and
storage sections.
This commit is contained in:
Torsten Dittmann
2025-09-02 19:29:52 +02:00
parent b0b2fe8ffd
commit 8175f6ffe9
45 changed files with 690 additions and 283 deletions
+6
View File
@@ -13,6 +13,12 @@
</head>
<body data-sveltekit-preload-data="hover" data-loading="true">
<script>
// Check if this is an embedded route
if (window.location.pathname.includes('/embed/')) {
document.body.setAttribute('data-embed', 'true');
document.documentElement.setAttribute('data-embed', 'true');
}
let themeInUse = 'auto';
const appwrite = localStorage.getItem('appwrite');
+1 -1
View File
@@ -11,7 +11,7 @@ export type Breadcrumb = {
title: string;
};
export type View = 'list' | 'grid';
export type View = 'table' | 'grid';
export type updateLayoutArguments = {
header?: Component;
+1 -1
View File
@@ -174,7 +174,7 @@ export async function confirmPayment(
if (error) {
throw error.message;
}
} catch (e) {
} catch {
addNotification({
title: 'Error',
message:
+1 -1
View File
@@ -58,7 +58,7 @@
} from '@appwrite.io/pink-icons-svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
export let data: LayoutData & { currentOrgId?: string; allProjectsCount?: number };
let emailBannerClosed = false;
function kebabToSentenceCase(str: string) {
@@ -15,7 +15,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
const { plansInfo, organizations, preferences: prefs } = await parent();
depends(Dependencies.PROJECT);
const project = await sdk.forConsole.projects.get({ projectId: params.project });
const project = await sdk.forConsole.projects.get(params.project);
project.region ??= 'default';
// fast path without a network call!
@@ -27,7 +27,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
const [org, regionalConsoleVariables, rolesResult, organizationPlan] = await Promise.all([
!organization
? (sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise<Organization>)
? (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>)
: organization,
sdk.forConsoleIn(project.region).console.variables(),
isCloud ? sdk.forConsole.billing.getRoles(project.teamId) : null,
@@ -4,15 +4,23 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
const createUserUrl = (user: Models.User<{}>) => `${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`;
</script>
<View
users={{ total: data.users.total, users: data.users.users }}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
<Container>
<View
users={{ total: data.users.total, users: data.users.users }}
limit={data.limit}
offset={data.offset}
search={data.search}
{createUserUrl}
/>
</Container>
@@ -4,14 +4,22 @@
</script>
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
const createTeamUrl = (team: Models.Team<Record<string, unknown>>) => `${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${team.$id}`;
</script>
<View
teams={{ total: data.teams.total, teams: data.teams.teams }}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
<Container>
<View
teams={{ total: data.teams.total, teams: data.teams.teams }}
limit={data.limit}
offset={data.offset}
search={data.search}
{createTeamUrl}
/>
</Container>
@@ -1,6 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
@@ -12,24 +11,29 @@
import DualTimeView from '$lib/components/dualTimeView.svelte';
import CreateTeam from '../createTeam.svelte';
export let teams: { total: number; teams: Models.Team<Record<string, unknown>>[] };
export let limit: number;
export let offset: number;
export const pageNum: number = 0;
export let search: string | null;
let {
teams,
limit,
offset,
search,
createTeamUrl
}: {
teams: { total: number; teams: Models.Team<Record<string, unknown>>[] };
limit: number;
offset: number;
search: string | null;
createTeamUrl: (team: Models.Team<Record<string, unknown>>) => string;
} = $props();
const clearSearchHref = `${base}/project-${page.params.region}-${page.params.project}/auth/teams`;
const getTeamUrl = (t: Models.Team<Record<string, unknown>>) =>
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${t.$id}`;
const clearSearchHref = page.url.pathname;
let showCreateTeam = false;
let showCreateTeam = $state(false);
async function onTeamCreated(e: CustomEvent<Models.Team<Record<string, unknown>>>) {
await goto(getTeamUrl(e.detail));
await goto(createTeamUrl(e.detail));
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name" />
</Layout.Stack>
@@ -49,7 +53,7 @@
<Table.Header.Cell {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each teams.teams as team}
{@const href = getTeamUrl(team)}
{@const href = createTeamUrl(team)}
{#if href}
<Table.Row.Link {root} href={href}>
<Table.Cell {root}>
@@ -99,6 +103,5 @@
href="https://appwrite.io/docs/references/cloud/client-web/teams"
target="team" />
{/if}
</Container>
<CreateTeam bind:showCreate={showCreateTeam} on:created={onTeamCreated} />
@@ -1,6 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { AvatarInitials, Copy, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components';
@@ -16,19 +15,25 @@
import { writable } from 'svelte/store';
import CreateUser from './createUser.svelte';
export let users: { total: number; users: Models.User<{}>[] };
export let limit: number;
export let offset: number;
export const pageNum: number = 0;
export let search: string | null;
let {
users,
limit,
offset,
search,
createUserUrl
}: {
users: { total: number; users: Models.User<{}>[] };
limit: number;
offset: number;
search: string | null;
createUserUrl: (user: Models.User<{}>) => string;
} = $props();
const clearSearchHref = `${base}/project-${page.params.region}-${page.params.project}/auth`;
const getUserUrl = (u: Models.User<{}>) =>
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${u.$id}`;
const clearSearchHref = page.url.pathname;
let showCreateUser = false;
let showCreateUser = $state(false);
async function onUserCreated(e: CustomEvent<Models.User<{}>>) {
await goto(getUserUrl(e.detail));
await goto(createUserUrl(e.detail));
}
const columns = writable<Column[]>([
@@ -42,8 +47,7 @@
]);
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name, email, phone, or ID" />
</Layout.Stack>
@@ -65,7 +69,7 @@
</svelte:fragment>
{#each users.users as user}
{@const href = getUserUrl(user)}
{@const href = createUserUrl(user)}
{#if href}
<Table.Row.Link href={href} {root}>
{#each $columns as { id } (id)}
@@ -213,6 +217,5 @@
allowCreate={true}
on:click={() => (showCreateUser = true)} />
{/if}
</Container>
<CreateUser bind:showCreate={showCreateUser} on:created={onUserCreated} />
@@ -1,18 +1,27 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
import { base } from "\$app/paths";
import { page } from "\$app/state";
const getDatabaseUrl = (database: Models.Database, firstTableId?: string | null) => {
const tableSegment = firstTableId ? `/table-${firstTableId}` : '';
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${database.$id}${tableSegment}`;
};
</script>
<View
databases={data.databases}
tables={data.tables}
policies={data.policies}
lastBackups={data.lastBackups}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
clearSearchHref={`${base}/project-${page.params.region}-${page.params.project}/databases`}
/>
<Container>
<View
databases={data.databases}
tables={data.tables}
policies={data.policies}
lastBackups={data.lastBackups}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
{getDatabaseUrl}
/>
</Container>
@@ -1,6 +1,23 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
const createTableUrl = (table: Models.Table) => {
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${table.$id}`;
};
</script>
<View tables={data.tables} limit={data.limit} offset={data.offset} view={data.view} search={data.search} />
<Container>
<View
tables={data.tables}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
{createTableUrl}
/>
</Container>
@@ -1,26 +1,36 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { EmptySearch, PaginationWithLimit, SearchQuery, ViewSelector, Id, CardContainer, GridItem1 } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { showCreate, tableViewColumns } from './store';
import { Card, Empty, Icon, Layout, Table as PinkTable } from '@appwrite.io/pink-svelte';
import { canWriteTables } from '$lib/stores/roles';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import { app } from '$lib/stores/app';
import { View } from '$lib/helpers/load';
export let tables: { total: number; tables: Models.Table[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
let {
tables,
limit,
offset,
view,
search,
createTableUrl
}: {
tables: { total: number; tables: Models.Table[] };
limit: number;
offset: number;
view: View;
search: string | null;
createTableUrl: (table: Models.Table) => string;
} = $props();
const clearSearchHref = page.url.pathname;
const databaseId = page.params.database;
</script>
<Container paddingInlineEnd={false}>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
@@ -41,7 +51,7 @@
{#if view === 'grid'}
<CardContainer disableEmpty={!$canWriteTables} total={tables.total} {offset} event="table" service="tables" on:click={() => ($showCreate = true)}>
{#each tables.tables as table}
<GridItem1 href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`}>
<GridItem1 href={createTableUrl(table)}>
<svelte:fragment slot="title">{table.name}</svelte:fragment>
<Id value={table.$id}>{table.$id}</Id>
</GridItem1>
@@ -55,7 +65,7 @@
{/each}
</svelte:fragment>
{#each tables.tables as table (table.$id)}
<PinkTable.Row.Link {root} href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`}>
<PinkTable.Row.Link {root} href={createTableUrl(table)}>
{#each $tableViewColumns as column}
<PinkTable.Cell column={column.id} {root}>
{#if column.id === '$id'}
@@ -77,7 +87,7 @@
<PaginationWithLimit name="Tables" {limit} {offset} total={tables.total} />
{:else if search}
<EmptySearch target="tables" hidePagination>
<Button size="s" secondary href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}`}>Clear Search</Button>
<Button size="s" secondary href={clearSearchHref}>Clear Search</Button>
</EmptySearch>
{:else}
<Card.Base padding="none">
@@ -92,4 +102,3 @@
</Empty>
</Card.Base>
{/if}
</Container>
@@ -1,39 +1,48 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, ViewSelector, Id, CardContainer, GridItem1 } from '$lib/components';
import { canWriteDatabases } from '$lib/stores/roles';
import { columns } from './store';
import { Icon, Layout, Table as PinkTable, Badge, Tooltip, Button } from '@appwrite.io/pink-svelte';
import { Icon, Layout, Table as PinkTable, Tooltip, Button } from '@appwrite.io/pink-svelte';
import Create from './create.svelte';
import { goto } from '$app/navigation';
import { IconPlus, IconExclamation } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import EmptySearch from '$lib/components/emptySearch.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import { View } from '$lib/helpers/load';
export let databases: { total: number; databases: Models.Database[] };
export let tables: Record<string, string>;
export let policies: Record<string, any>;
export let lastBackups: Record<string, string>;
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
export let clearSearchHref: string | null = null;
export let getDatabaseUrl: (db: Models.Database, firstTableId?: string | null) => string | null = (db, t) => db ? dbHref(db.$id, t ?? null) : null;
let {
databases,
tables,
policies,
lastBackups,
limit,
offset,
view,
search,
getDatabaseUrl
}: {
databases: { total: number; databases: Models.Database[] };
tables: Record<string, string>;
policies: Record<string, Array<{ schedule: string }>>;
lastBackups: Record<string, string>;
limit: number;
offset: number;
view: View;
search: string | null;
getDatabaseUrl: (database: Models.Database, firstTableId?: string | null) => string | null;
} = $props();
let showCreate = false;
let showCreate = $state(false);
const clearSearchHref = page.url.pathname;
async function onCreated(e: CustomEvent<Models.Database>) {
showCreate = false;
const href = getDatabaseUrl(e.detail, null);
if (href) await goto(href);
}
function dbHref(id: string, tableId?: string | null) {
const t = tableId ? `/table-${tableId}` : '';
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${id}${t}`;
}
function getPolicyDescription(cron: string): string {
const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' ');
if (dayOfMonth !== '*') return 'Monthly';
@@ -1,6 +1,15 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
import type { Models } from '@appwrite.io/console';
const createMessageUrl = (message: Models.Message) => `${base}/project-${page.params.region}-${page.params.project}/messaging/message-${message.$id}`;
</script>
<View messages={data.messages} limit={data.limit} offset={data.offset} view={data.view} search={data.search} query={data.query} pageNum={data.page} />
<Container>
<View messages={data.messages} limit={data.limit} offset={data.offset} search={data.search} {createMessageUrl} />
</Container>
@@ -1,12 +1,11 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
import MessageStatusPill from './messageStatusPill.svelte';
@@ -25,21 +24,28 @@
import { onDestroy, onMount } from 'svelte';
import { stopPolling, pollMessagesStatus } from './helper';
export let messages: { total: number; messages: any[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
export let query: string | null;
export let pageNum: number;
export let clearSearchHref: string | null = null;
export let getMessageUrl: (m: any) => string = (m) => `${base}/project-${region}-${project}/messaging/message-${m.$id}`;
let {
messages,
limit,
offset,
search,
createMessageUrl
}: {
messages: { total: number; messages: Models.Message[] };
limit: number;
offset: number;
search: string | null;
createMessageUrl: (message: Models.Message) => string;
} = $props();
let selected: string[] = [];
let showDelete = false;
let deleting = false;
let showFailed = false;
let errors: string[] = [];
let selected = $state<string[]>([]);
let showDelete = $state(false);
let deleting = $state(false);
let showFailed = $state(false);
let errors = $state<string[]>([]);
const region = page.params.region;
const project = page.params.project;
const columns = writable<Column[]>([
{ id: '$id', title: 'Message ID', type: 'string', width: 200 },
@@ -50,9 +56,6 @@
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', width: { min: 120 } }
]);
const region = page.params.region;
const project = page.params.project;
async function handleDelete() {
showDelete = false;
const promises = selected.map((id) => sdk.forProject(region, project).messaging.delete({ messageId: id }));
@@ -77,19 +80,17 @@
onDestroy(stopPolling);
</script>
<Container>
<ResponsiveContainerHeader
{columns}
bind:view
hideView
hasFilters
hasSearch
analyticsSource="messaging_messages"
searchPlaceholder="Search by description, type, status, or ID">
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</ResponsiveContainerHeader>
<ResponsiveContainerHeader
{columns}
hideView
hasFilters
hasSearch
analyticsSource="messaging_messages"
searchPlaceholder="Search by description, type, status, or ID">
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</ResponsiveContainerHeader>
{#if messages.total}
<Table.Root columns={$columns} allowSelection={$canWriteMessages} let:root bind:selectedRows={selected}>
@@ -99,7 +100,7 @@
{/each}
</svelte:fragment>
{#each messages.messages as message (message.$id)}
<Table.Row.Link {root} id={message.$id} href={getMessageUrl(message)}>
<Table.Row.Link {root} id={message.$id} href={createMessageUrl(message)}>
{#each $columns as column (column.id)}
<Table.Cell column={column.id} {root}>
{#if column.id === '$id'}
@@ -156,7 +157,7 @@
</div>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>Documentation</Button>
<Button secondary href={clearSearchHref ?? `${base}/project-${region}-${project}/messaging`}>Clear search</Button>
<Button secondary href={page.url.pathname}>Clear search</Button>
</div>
</EmptySearch>
{:else}
@@ -171,7 +172,6 @@
</svelte:fragment>
</Empty>
{/if}
</Container>
<FailedModal bind:show={showFailed} {errors} />
<Confirm title="Delete messages" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
@@ -3,15 +3,26 @@
</script>
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
import { writable } from 'svelte/store';
export let data;
const createBucketUrl = (bucket: Models.Bucket) => {
return `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucket.$id}`;
};
</script>
<View
buckets={data.buckets}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
/>
<Container>
<View
buckets={data.buckets}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
{createBucketUrl}
/>
</Container>
@@ -1,6 +1,22 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Container } from '$lib/layout';
import View from './view.svelte';
export let data;
const createFileUrl = (file: Models.File) => {
return `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/file-${file.$id}`;
};
</script>
<View files={data.files} limit={data.limit} offset={data.offset} search={data.search} />
<Container>
<View
files={data.files}
limit={data.limit}
offset={data.offset}
search={data.search}
{createFileUrl}
/>
</Container>
@@ -1,6 +1,5 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Avatar, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components';
@@ -8,7 +7,6 @@
import { Badge } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
import { uploader } from '$lib/stores/uploader';
@@ -19,15 +17,25 @@
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { IconDotsHorizontal, IconPencil, IconPlus, IconTrash } from '@appwrite.io/pink-icons-svelte';
export let files: { total: number; files: Models.File[] };
export let limit: number;
export let offset: number;
export let search: string | null;
let {
files,
limit,
offset,
search,
createFileUrl
}: {
files: { total: number; files: Models.File[] };
limit: number;
offset: number;
search: string | null;
createFileUrl?: (file: Models.File) => string;
} = $props();
let showDelete = false;
let selectedFile: Models.File = null;
let showDelete = $state(false);
let selectedFile = $state<Models.File | null>(null);
const bucketId = page.params.bucket;
const clearSearchHref = page.url.pathname;
function getPreview(fileId: string) {
return (
@@ -56,113 +64,154 @@
}
}
let isUploading = false;
const beforeunload = (event: BeforeUnloadEvent) => {
const message = 'An upload is in progress. Are you sure you want to leave?';
if (isUploading) {
function beforeunload(event: BeforeUnloadEvent) {
if (uploader.uploading) {
event.preventDefault();
event.returnValue = message;
return message;
event.returnValue = '';
}
};
}
onMount(() => {
return uploader.subscribe(() => {
isUploading = $uploader.files.some((file) => file.status !== 'success' && file.progress < 100 && file.status !== 'failed');
});
});
onMount(() => uploader.setBucket(bucketId));
</script>
<svelte:window on:beforeunload={beforeunload} />
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search files" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`} event="create_file" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create file
</Button>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search files" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button secondary href="create" event="create_file" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Upload
</Button>
</Layout.Stack>
</Layout.Stack>
{#if files.total}
<Table.Root let:root columns={[{ id: 'filename' }, { id: 'type', width: { min: 140 } }, { id: 'size', width: { min: 100 } }, { id: 'created', width: { min: 120 } }, { id: 'actions', width: 40 }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="filename" {root}>Filename</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="size" {root}>Size</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each files.files as file (file.$id)}
{#if $uploader.files.find(({ $id }) => $id === file.$id)}
<Table.Row.Base {root}>
<Table.Cell column="filename" {root}>
<Layout.Stack direction="row" alignItems="center">
<span class="avatar is-size-small is-color-empty"></span>
<span class="text u-trim">{file.name}</span>
<div><Badge variant="secondary" type="warning" content="Pending" /></div>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>{calculateSize(file.sizeOriginal)}</Table.Cell>
<Table.Cell column="created" {root}><DualTimeView time={file.$createdAt} /></Table.Cell>
<Table.Cell column="actions" {root}>
<div class="u-flex u-main-center">
<button class="button is-only-icon is-text" aria-label="Delete item" on:click|preventDefault={() => deleteFile(file)}>
<span class="icon-trash" aria-hidden="true"></span>
</button>
</div>
</Table.Cell>
</Table.Row.Base>
{:else}
{@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/file-${file.$id}`}
{#if files.total}
<Table.Root columns={7} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Name</Table.Header.Cell>
<Table.Header.Cell {root}>Type</Table.Header.Cell>
<Table.Header.Cell {root}>Size</Table.Header.Cell>
<Table.Header.Cell {root}>Created</Table.Header.Cell>
<Table.Header.Cell {root}>Updated</Table.Header.Cell>
<Table.Header.Cell {root}></Table.Header.Cell>
</svelte:fragment>
{#each files.files as file}
{#if uploader.uploading && file.$id === uploader.file?.$id}
<Table.Row.Base {root}>
<Table.Cell column="filename" {root}>
<div class="u-flex u-gap-12 u-cross-center">
<Avatar size="s" src={getPreview(file.$id)} alt={file.name} />
<span class="text u-trim">{file.name}</span>
</div>
</Table.Cell>
<Table.Cell column="mimeType" {root}>
<Badge size="s" variant="secondary">{file.mimeType}</Badge>
</Table.Cell>
<Table.Cell column="sizeOriginal" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="$createdAt" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
<Table.Cell column="$updatedAt" {root}>
<DualTimeView time={file.$updatedAt} />
</Table.Cell>
<Table.Cell {root}>
<!-- uploading state -->
</Table.Cell>
</Table.Row.Base>
{:else}
{@const href = createFileUrl ? createFileUrl(file) : null}
{#if href}
<Table.Row.Link {href} {root}>
<Table.Cell column="filename" {root}>
<div class="u-flex u-gap-12 u-cross-center">
<Avatar size="xs" src={getPreview(file.$id)} alt={file.name} />
<Avatar size="s" src={getPreview(file.$id)} alt={file.name} />
<span class="text u-trim">{file.name}</span>
</div>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>{calculateSize(file.sizeOriginal)}</Table.Cell>
<Table.Cell column="created" {root}><DualTimeView time={file.$createdAt} /></Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button text icon ariaLabel="more options" on:click={(e) => { e.stopPropagation(); e.preventDefault(); toggle(); }}>
<Table.Cell column="mimeType" {root}>
<Badge size="s" variant="secondary">{file.mimeType}</Badge>
</Table.Cell>
<Table.Cell column="sizeOriginal" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="$createdAt" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
<Table.Cell column="$updatedAt" {root}>
<DualTimeView time={file.$updatedAt} />
</Table.Cell>
<Table.Cell {root}>
<Popover>
<Button icon size="s" variant="ghost">
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Anchor {href} leadingIcon={IconPencil}>Update</ActionMenu.Item.Anchor>
<ActionMenu.Item.Button leadingIcon={IconTrash} on:click={(e) => { e.stopPropagation(); e.preventDefault(); selectedFile = file; showDelete = true; }}>Delete</ActionMenu.Item.Button>
</ActionMenu.Root>
<svelte:fragment slot="content" let:close>
<ActionMenu.Root>
<ActionMenu.Item.Button icon={IconPencil} on:click={() => { close(); goto(href); }}>
Edit
</ActionMenu.Item.Button>
<ActionMenu.Item.Button icon={IconTrash} on:click={() => { close(); selectedFile = file; showDelete = true; }}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Table.Cell>
</Table.Row.Link>
{:else}
<Table.Row.Base {root}>
<Table.Cell column="filename" {root}>
<div class="u-flex u-gap-12 u-cross-center">
<Avatar size="s" src={getPreview(file.$id)} alt={file.name} />
<span class="text u-trim">{file.name}</span>
</div>
</Table.Cell>
<Table.Cell column="mimeType" {root}>
<Badge size="s" variant="secondary">{file.mimeType}</Badge>
</Table.Cell>
<Table.Cell column="sizeOriginal" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="$createdAt" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
<Table.Cell column="$updatedAt" {root}>
<DualTimeView time={file.$updatedAt} />
</Table.Cell>
<Table.Cell {root}>
<Popover>
<Button icon size="s" variant="ghost">
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<svelte:fragment slot="content" let:close>
<ActionMenu.Root>
<ActionMenu.Item.Button icon={IconTrash} on:click={() => { close(); selectedFile = file; showDelete = true; }}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</svelte:fragment>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/if}
{/each}
</Table.Root>
{/if}
{/each}
</Table.Root>
<PaginationWithLimit name="Files" {limit} {offset} total={files.total} />
{:else if search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{search}'</b>
<p>There are no files that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/storage/upload-download" text>Documentation</Button>
<Button secondary href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}`}>Clear Search</Button>
</div>
</EmptySearch>
{:else}
<Empty single target="file" href="https://appwrite.io/docs/products/storage/upload-download" on:click={() => goto(`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`)} />
{/if}
</Container>
<PaginationWithLimit name="Files" {limit} {offset} total={files.total} />
{:else if search}
<EmptySearch target="files" hidePagination>
<Button size="s" secondary href={clearSearchHref}>Clear Search</Button>
</EmptySearch>
{:else}
<Empty single target="file" href="https://appwrite.io/docs/products/storage/upload-download" on:click={() => goto("create")} />
{/if}
{#if selectedFile}
<DeleteFile file={selectedFile} bind:showDelete on:deleted={fileDeleted} />
{/if}
{/if}
@@ -1,6 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, Id, CardContainer, GridItem1 } from '$lib/components';
import { canWriteBuckets } from '$lib/stores/roles';
@@ -14,19 +13,27 @@
import type { Models } from '@appwrite.io/console';
import { showCreateBucket } from './+page.svelte';
export let buckets: { total: number; buckets: Models.Bucket[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
let {
buckets,
limit,
offset,
view,
search,
createBucketUrl
}: {
buckets: { total: number; buckets: Models.Bucket[] };
limit: number;
offset: number;
view: 'grid' | 'table';
search: string | null;
createBucketUrl: (bucket: Models.Bucket) => string;
} = $props();
function bucketHref(id: string) {
return `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${id}`;
}
const clearSearchHref = page.url.pathname;
async function bucketCreated(event: CustomEvent<Models.Bucket>) {
showCreateBucket.set(false);
await goto(bucketHref(event.detail.$id));
await goto(createBucketUrl(event.detail));
}
</script>
@@ -49,7 +56,7 @@
{#if view === 'grid'}
<CardContainer disableEmpty={!$canWriteBuckets} total={buckets.total} {offset} event="bucket" service="buckets" on:click={() => ($showCreateBucket = true)}>
{#each buckets.buckets as bucket}
<GridItem1 href={bucketHref(bucket.$id)}>
<GridItem1 href={createBucketUrl(bucket)}>
<svelte:fragment slot="title">{bucket.name}</svelte:fragment>
<svelte:fragment slot="status">
{#if !bucket.enabled}
@@ -77,7 +84,7 @@
{/each}
</svelte:fragment>
{#each buckets.buckets as bucket (bucket.$id)}
<PinkTable.Row.Link {root} href={bucketHref(bucket.$id)}>
<PinkTable.Row.Link {root} href={createBucketUrl(bucket)}>
{#each $columns as column}
<PinkTable.Cell column={column.id} {root}>
{#if column.id === '$id'}
@@ -1,15 +1,20 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/auth/view.svelte';
export let data;
let { data } = $props();
const createUserUrl = (user: Models.User<{}>) => `${base}/embed/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`;
</script>
<View
users={data.users}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
<Layout.Stack>
<View
users={data.users}
limit={data.limit}
offset={data.offset}
search={data.search}
{createUserUrl}
/>
</Layout.Stack>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { Layout } from '@appwrite.io/pink-svelte';
import UpdatePasswordDictionary from '$routes/(console)/project-[region]-[project]/auth/security/updatePasswordDictionary.svelte';
import UpdatePasswordHistory from '$routes/(console)/project-[region]-[project]/auth/security/updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from '$routes/(console)/project-[region]-[project]/auth/security/updatePersonalDataCheck.svelte';
@@ -11,7 +11,7 @@
import UpdateSessionInvalidation from '$routes/(console)/project-[region]-[project]/auth/security/updateSessionInvalidation.svelte';
</script>
<Container>
<Layout.Stack>
<UpdateUsersLimit />
<UpdateSessionLength />
<UpdateSessionsLimit />
@@ -21,5 +21,5 @@
<UpdateSessionAlerts />
<UpdateSessionInvalidation />
<UpdateMembershipPrivacy />
</Container>
</Layout.Stack>
@@ -1,5 +1,8 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/auth/settings/view.svelte';
</script>
<View />
<Layout.Stack>
<View />
</Layout.Stack>
@@ -1,12 +1,20 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/auth/teams/view.svelte';
export let data;
let { data } = $props();
const createTeamUrl = (team: Models.Team<Record<string, unknown>>) => `${base}/embed/project-${page.params.region}-${page.params.project}/auth/teams/team-${team.$id}`;
</script>
<View
teams={data.teams}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
<Layout.Stack>
<View
teams={data.teams}
limit={data.limit}
offset={data.offset}
search={data.search}
{createTeamUrl}
/>
</Layout.Stack>
@@ -1,6 +1,9 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/auth/templates/view.svelte';
export let data;
let { data } = $props();
</script>
<View project={data.project} />
<Layout.Stack>
<View project={data.project} />
</Layout.Stack>
@@ -1,5 +1,8 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/auth/user-[user]/view.svelte';
</script>
<View />
<Layout.Stack>
<View />
</Layout.Stack>
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,27 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/databases/view.svelte';
let { data } = $props();
const getDatabaseUrl = (database: Models.Database, firstTableId?: string | null) => {
const tableSegment = firstTableId ? `/table-${firstTableId}` : '';
return `${base}/embed/project-${page.params.region}-${page.params.project}/databases/database-${database.$id}${tableSegment}`;
};
</script>
<Layout.Stack>
<View
databases={data?.databases || { total: 0, databases: [] }}
tables={data?.tables || {}}
policies={data?.policies || {}}
lastBackups={data?.lastBackups || {}}
limit={data?.limit || 25}
offset={data?.offset || 0}
view={data?.view || 'grid'}
search={data?.search || null}
{getDatabaseUrl}
/>
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/databases/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url });
};
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,22 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/databases/database-[database]/view.svelte';
let { data } = $props();
const createTableUrl = (table) => {
return `${base}/embed/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/table-${table.$id}`;
};
</script>
<Layout.Stack>
<View
tables={data.tables}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
{createTableUrl}
/>
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url, params }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url, params });
};
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,9 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import TablePage from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte';
let { data } = $props();
</script>
<Layout.Stack>
<TablePage {data} />
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url, params }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url, params });
};
@@ -1,21 +1,20 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/messaging/view.svelte';
export let data;
let { data } = $props();
const clearSearchHref = `${base}/embed/project-${page.params.region}-${page.params.project}/messaging`;
const getMessageUrl = (m: any) => `${base}/embed/project-${page.params.region}-${page.params.project}/messaging/message-${m.$id}`;
const createMessageUrl = (message) => `${base}/embed/project-${page.params.region}-${page.params.project}/messaging/message-${message.$id}`;
</script>
<View
messages={data.messages}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
query={data.query}
pageNum={data.page}
{clearSearchHref}
{getMessageUrl}
/>
<Layout.Stack>
<View
messages={data.messages}
limit={data.limit}
offset={data.offset}
search={data.search}
{createMessageUrl}
/>
</Layout.Stack>
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,24 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/storage/view.svelte';
let { data } = $props();
const createBucketUrl = (bucket: Models.Bucket) => {
return `${base}/embed/project-${page.params.region}-${page.params.project}/storage/bucket-${bucket.$id}`;
};
</script>
<Layout.Stack>
<View
buckets={data?.buckets || { total: 0, buckets: [] }}
limit={data?.limit || 25}
offset={data?.offset || 0}
view={data?.view || 'grid'}
search={data?.search || null}
{createBucketUrl}
/>
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url, params }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/storage/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url, params });
};
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,23 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
import View from '$routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/view.svelte';
let { data } = $props();
const createFileUrl = (file: Models.File) => {
return `${base}/embed/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/file-${file.$id}`;
};
</script>
<Layout.Stack>
<View
files={data?.files || { total: 0, files: [] }}
limit={data?.limit || 25}
offset={data?.offset || 0}
search={data?.search || null}
{createFileUrl}
/>
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url, params }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url, params });
};
@@ -0,0 +1,6 @@
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ parent }) => {
const { project } = await parent();
return { project };
};
@@ -0,0 +1,10 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
import FilePage from '$routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.svelte';
let { data } = $props();
</script>
<Layout.Stack>
<FilePage {data} />
</Layout.Stack>
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent, url, params }) => {
const { project } = await parent();
// Import the console page loader to reuse the same logic
const consoleLoader = await import('$routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/+page.ts');
// Call the console loader with the same parameters
return await consoleLoader.load({ parent, url, params });
};
+6 -1
View File
@@ -4,7 +4,12 @@ body {
width: 100%;
}
[data-loading='true'] .page-loader {
[data-embed] {
background: none !important;
background-color: none !important;
}
[data-loading='true']:not([data-embed]) .page-loader {
pointer-events: initial;
visibility: visible;
opacity: 1;