mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'mgn-dv' of https://github.com/appwrite/console into mgn-dv
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
|
||||
Generated
+9
-9
@@ -27,8 +27,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)
|
||||
'@popperjs/core':
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
@@ -282,8 +282,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@e5a77b9b25e2cb5e55587e93725d7a06df0b3d94':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@e5a77b9b25e2cb5e55587e93725d7a06df0b3d94}
|
||||
version: 1.0.0-next.7
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -297,8 +297,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4}
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9}
|
||||
version: 1.0.0-next.85
|
||||
peerDependencies:
|
||||
react-dom: ^18.0.0
|
||||
@@ -3836,7 +3836,7 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@e5a77b9b25e2cb5e55587e93725d7a06df0b3d94(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
@@ -3849,9 +3849,9 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@de27ca4(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@e5a77b9(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@de27ca407599e51b378814b421fb63625cb98c10(svelte@5.25.3)
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@e5a77b9b25e2cb5e55587e93725d7a06df0b3d94(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
|
||||
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
|
||||
|
||||
@@ -152,6 +152,7 @@ export enum Click {
|
||||
DatabaseIndexDelete = 'click_index_delete',
|
||||
DatabaseCollectionDelete = 'click_collection_delete',
|
||||
DatabaseDatabaseDelete = 'click_database_delete',
|
||||
DatabaseImportCsv = 'click_database_import_csv',
|
||||
DomainCreateClick = 'click_domain_create',
|
||||
DomainDeleteClick = 'click_domain_delete',
|
||||
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
|
||||
@@ -161,6 +162,7 @@ export enum Click {
|
||||
FunctionsDeploymentDeleteClick = 'click_deployment_delete',
|
||||
FunctionsDeploymentCancelClick = 'click_deployment_cancel',
|
||||
KeyCreateClick = 'click_key_create',
|
||||
DevKeyCreateClick = 'click_dev_key_create',
|
||||
MenuDropDownClick = 'click_menu_dropdown',
|
||||
MenuOverviewClick = 'click_menu_overview',
|
||||
ModalCloseClick = 'click_close_modal',
|
||||
@@ -270,6 +272,7 @@ export enum Submit {
|
||||
DatabaseCreate = 'submit_database_create',
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
DatabaseImportCsv = 'submit_database_import_csv',
|
||||
AttributeCreate = 'submit_attribute_create',
|
||||
AttributeUpdate = 'submit_attribute_update',
|
||||
AttributeDelete = 'submit_attribute_delete',
|
||||
@@ -311,12 +314,18 @@ export enum Submit {
|
||||
VariableDelete = 'submit_variable_delete',
|
||||
VariableUpdate = 'submit_variable_update',
|
||||
VariableEditor = 'submit_variable_editor',
|
||||
|
||||
KeyCreate = 'submit_key_create',
|
||||
KeyDelete = 'submit_key_delete',
|
||||
KeyUpdateName = 'submit_key_update_name',
|
||||
KeyUpdateScopes = 'submit_key_update_scopes',
|
||||
KeyUpdateExpire = 'submit_key_update_expire',
|
||||
|
||||
DevKeyCreate = 'submit_dev_key_create',
|
||||
DevKeyDelete = 'submit_dev_key_delete',
|
||||
DevKeyUpdateName = 'submit_dev_key_update_name',
|
||||
DevKeyUpdateExpire = 'submit_dev_key_update_expire',
|
||||
|
||||
PlatformCreate = 'submit_platform_create',
|
||||
PlatformDelete = 'submit_platform_delete',
|
||||
PlatformUpdate = 'submit_platform_update',
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { type Models, type Payload, Query } from '@appwrite.io/console';
|
||||
|
||||
type ImportItem = {
|
||||
status: string;
|
||||
collection?: string;
|
||||
};
|
||||
|
||||
type ImportItemsMap = Map<string, ImportItem>;
|
||||
|
||||
/**
|
||||
* Keeps a track of the active and ongoing csv migrations.
|
||||
*
|
||||
* The structure is as follows -
|
||||
* `{ migrationId: { status: status, collection: collection } }`
|
||||
*/
|
||||
const importItems: Writable<ImportItemsMap> = writable(new Map());
|
||||
|
||||
async function showCompletionNotification(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
importData: Payload
|
||||
) {
|
||||
const projectId = page.params.project;
|
||||
await invalidate(Dependencies.DOCUMENTS);
|
||||
const url = `${base}/project-${projectId}/databases/database-${databaseId}/collection-${collectionId}`;
|
||||
|
||||
// extract clean message from nested backend error.
|
||||
const match = importData.errors.join('').match(/message: '(.*)' Message:/i);
|
||||
const errorMessage = match?.[1];
|
||||
|
||||
const type = importData.status === 'completed' ? 'success' : 'error';
|
||||
const message =
|
||||
importData.status === 'completed'
|
||||
? 'CSV import finished successfully.'
|
||||
: `${errorMessage}`;
|
||||
|
||||
addNotification({
|
||||
type,
|
||||
message,
|
||||
isHtml: true,
|
||||
buttons:
|
||||
collectionId === page.params.collection || type === 'error'
|
||||
? undefined
|
||||
: [
|
||||
{
|
||||
name: 'View documents',
|
||||
method: () => goto(url)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async function updateOrAddItem(importData: Payload | Models.Migration) {
|
||||
if (importData.source.toLowerCase() !== 'csv') return;
|
||||
|
||||
const status = importData.status;
|
||||
const resourceId = importData.resourceId ?? '';
|
||||
const [databaseId, collectionId] = resourceId.split(':') ?? [];
|
||||
|
||||
const current = $importItems.get(importData.$id);
|
||||
let collectionName = current?.collection ?? null;
|
||||
|
||||
if (!collectionName && collectionId) {
|
||||
try {
|
||||
const collection = await sdk.forProject.databases.getCollection(
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
collectionName = collection.name;
|
||||
} catch {
|
||||
collectionName = null;
|
||||
}
|
||||
}
|
||||
|
||||
importItems.update((items) => {
|
||||
const existing = items.get(importData.$id);
|
||||
|
||||
const isDone = (s: string) => s === 'completed' || s === 'failed';
|
||||
const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s);
|
||||
|
||||
const shouldSkip =
|
||||
(existing && isDone(existing.status) && isInProgress(status)) ||
|
||||
existing?.status === status;
|
||||
|
||||
if (shouldSkip) return items;
|
||||
|
||||
const next = new Map(items);
|
||||
next.set(importData.$id, { status, collection: collectionName ?? undefined });
|
||||
return next;
|
||||
});
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
await showCompletionNotification(databaseId, collectionId, importData);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
importItems.update((items) => {
|
||||
items.clear();
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
function graphSize(status: string): number {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 10;
|
||||
case 'processing':
|
||||
return 30;
|
||||
case 'uploading':
|
||||
return 60;
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
return 100;
|
||||
default:
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
function text(status: string, collectionName = '') {
|
||||
const name = collectionName ? `<b>${collectionName}</b>` : '';
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
return `Import to ${name} ${status}`;
|
||||
case 'processing':
|
||||
return `Importing CSV file${name ? ` to ${name}` : ''}`;
|
||||
default:
|
||||
return 'Preparing CSV for import...';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sdk.forProject.migrations
|
||||
.list([Query.equal('source', 'CSV'), Query.equal('status', ['pending', 'processing'])])
|
||||
.then((migrations) => {
|
||||
migrations.migrations.forEach(updateOrAddItem);
|
||||
});
|
||||
|
||||
return sdk.forConsole.client.subscribe('console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
if (response.events.includes('migrations.*')) {
|
||||
updateOrAddItem(response.payload as Payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$: isOpen = true;
|
||||
$: showCsvImportBox = $importItems.size > 0;
|
||||
</script>
|
||||
|
||||
{#if showCsvImportBox}
|
||||
<Layout.Stack direction="column" gap="l" alignItems="flex-end">
|
||||
<section class="upload-box">
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<Typography.Text variant="m-500">
|
||||
Importing documents ({$importItems.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
class:is-open={isOpen}
|
||||
aria-label="toggle upload box"
|
||||
on:click={() => (isOpen = !isOpen)}>
|
||||
<span class="icon-cheveron-up" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
aria-label="close backup restore box"
|
||||
on:click={clear}>
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#each [...$importItems.entries()] as [key, value] (key)}
|
||||
<div class="upload-box-content" class:is-open={isOpen}>
|
||||
<ul class="upload-box-list">
|
||||
<li class="upload-box-item">
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<Typography.Text>
|
||||
{@html text(value.status, value.collection)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={value.status === 'failed'}
|
||||
style="--graph-size:{graphSize(value.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.upload-box-content {
|
||||
min-width: 400px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.upload-box-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 4px;
|
||||
|
||||
&::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-neutral-invert);
|
||||
}
|
||||
|
||||
&.is-danger::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-error);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Id } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID, Query, Permission, Role } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { EmptySearch, Id } from '.';
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Button, InputSelect } from '$lib/elements/forms';
|
||||
import DualTimeView from './dualTimeView.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
|
||||
import { ID, Query, Permission, Role } from '@appwrite.io/console';
|
||||
import {
|
||||
Layout,
|
||||
Typography,
|
||||
Modal,
|
||||
ActionMenu,
|
||||
Table,
|
||||
Spinner,
|
||||
ToggleButton,
|
||||
Selector,
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Card
|
||||
Layout,
|
||||
Modal,
|
||||
Selector,
|
||||
Spinner,
|
||||
Table,
|
||||
ToggleButton,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { IconCheck, IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
|
||||
import { showCreateBucket } from '$routes/(console)/project-[project]/storage/+page.svelte';
|
||||
|
||||
export let show: boolean;
|
||||
export let mimeTypeQuery: string = 'image/';
|
||||
export let allowedExtension: string = '*';
|
||||
export let selectedBucket: string = null;
|
||||
export let selectedFile: string = null;
|
||||
export let onSelect: (e: Models.File) => void;
|
||||
export let gridImageDimensions: { imageHeight?: number; imageWidth?: number } = {
|
||||
imageHeight: 148
|
||||
};
|
||||
|
||||
let search = writable('');
|
||||
let searchEnabled = false;
|
||||
@@ -120,14 +129,27 @@
|
||||
return response;
|
||||
}
|
||||
|
||||
function truncatedFilename(file: Models.File, max: number = 15): string {
|
||||
const length = file.name.length;
|
||||
return length > 15 ? `${file.name.substring(0, max)}...` : file.name;
|
||||
}
|
||||
|
||||
function getProperQuery(): string[] {
|
||||
let query = [Query.orderDesc('$createdAt')];
|
||||
|
||||
if (allowedExtension === '*') {
|
||||
query.push(Query.startsWith('mimeType', mimeTypeQuery));
|
||||
} else {
|
||||
query.push(Query.endsWith('name', `.${allowedExtension}`));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
$: files =
|
||||
currentBucket &&
|
||||
sdk.forProject.storage
|
||||
.listFiles(
|
||||
currentBucket.$id,
|
||||
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
|
||||
$search || undefined
|
||||
)
|
||||
.listFiles(currentBucket.$id, getProperQuery(), $search || undefined)
|
||||
.then((response) => {
|
||||
if ($search === '') {
|
||||
searchEnabled = response.total > 0;
|
||||
@@ -139,236 +161,462 @@
|
||||
$: if ($search) {
|
||||
resetFile();
|
||||
}
|
||||
|
||||
$: extension = allowedExtension === '*' ? mimeTypeQuery : `.${allowedExtension}`;
|
||||
|
||||
$: console.log(`Allowed extensions: ${allowedExtension}, MimeType: ${mimeTypeQuery}`);
|
||||
</script>
|
||||
|
||||
<svelte:document on:visibilitychange={handleVisibilityChange} />
|
||||
|
||||
<Form {onSubmit}>
|
||||
<Form {onSubmit} isModal class="file-picker-modal-form">
|
||||
<Modal bind:open={show} title="Select file" size="l">
|
||||
<Layout.Stack direction="row" height="50vh">
|
||||
<Layout.Stack direction={$isSmallViewport ? 'column' : 'row'} height="50vh" gap="none">
|
||||
<!-- min-width to avoid a layout-shift -->
|
||||
<aside>
|
||||
<Typography.Caption variant="500">Buckets</Typography.Caption>
|
||||
{#if !$isSmallViewport}
|
||||
<Typography.Caption variant="500">Buckets</Typography.Caption>
|
||||
{/if}
|
||||
|
||||
{#await buckets}
|
||||
<div class="u-flex u-main-center">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
{#if $isSmallViewport}
|
||||
<!-- disabled state -->
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
disabled
|
||||
id="bucket"
|
||||
options={[]}
|
||||
label="Bucket"
|
||||
placeholder="Loading buckets..." />
|
||||
</div>
|
||||
{/if}
|
||||
{:then response}
|
||||
<ActionMenu.Root>
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected = bucket.$id === selectedBucket}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => selectBucket(bucket)}
|
||||
leadingIcon={isSelected ? IconCheck : undefined}>
|
||||
{bucket.name}
|
||||
</ActionMenu.Item.Button>
|
||||
{:else}
|
||||
<ActionMenu.Item.Button>No buckets found</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
{#if $isSmallViewport}
|
||||
<div style:padding-block-start="1rem">
|
||||
<InputSelect
|
||||
required
|
||||
id="bucket"
|
||||
label="Bucket"
|
||||
bind:value={selectedBucket}
|
||||
placeholder="Select bucket"
|
||||
on:change={(event) => {
|
||||
const bucketId = event.detail;
|
||||
const bucket = response.buckets.find(
|
||||
(bucket) => bucket.$id === bucketId
|
||||
);
|
||||
selectBucket(bucket);
|
||||
}}
|
||||
options={response.buckets.map((bucket) => {
|
||||
return {
|
||||
value: bucket.$id,
|
||||
label: `${bucket.name}`
|
||||
};
|
||||
})} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="action-menu-holder">
|
||||
<ActionMenu.Root width="180px">
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected = bucket.$id === selectedBucket}
|
||||
<div class="action-button" class:active-item={isSelected}>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => selectBucket(bucket)}>
|
||||
{bucket.name}
|
||||
</ActionMenu.Item.Button>
|
||||
</div>
|
||||
{:else}
|
||||
<ActionMenu.Item.Button
|
||||
>No buckets found</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</aside>
|
||||
|
||||
<Layout.Stack>
|
||||
{#await buckets then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Typography.Title>{currentBucket?.name}</Typography.Title>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled} />
|
||||
<ToggleButton
|
||||
bind:active={view}
|
||||
buttons={[
|
||||
{
|
||||
id: 'list',
|
||||
label: 'list view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewList
|
||||
},
|
||||
{
|
||||
id: 'grid',
|
||||
label: 'grid view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewGrid
|
||||
}
|
||||
]} />
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="u-hide"
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<div style:padding-inline-start="1rem" style:opacity={$isSmallViewport ? 0 : 1}>
|
||||
<Divider vertical />
|
||||
</div>
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<Layout.Stack
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
height="100%">
|
||||
<Spinner size="l" />
|
||||
<span>Loading files...</span>
|
||||
</Layout.Stack>
|
||||
{:then response}
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
<Layout.Grid
|
||||
columnsXXS={1}
|
||||
columnsXS={2}
|
||||
columnsS={3}
|
||||
columns={4}>
|
||||
{#each response?.files as file}
|
||||
<Card.Selector
|
||||
group="files"
|
||||
name="files"
|
||||
title="files"
|
||||
value={file.$id}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)}
|
||||
on:click={() => selectFile(file)} />
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
<div class="files-section">
|
||||
<Layout.Stack gap="l">
|
||||
{#if $isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#await buckets then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<Layout.Stack>
|
||||
{#if !$isSmallViewport}
|
||||
{#key currentBucket?.$id}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Typography.Title size="s"
|
||||
>{currentBucket?.name}</Typography.Title>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
</Layout.Stack>
|
||||
{/key}
|
||||
{/if}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled} />
|
||||
<ToggleButton
|
||||
bind:active={view}
|
||||
buttons={[
|
||||
{
|
||||
id: 'list',
|
||||
label: 'list view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewList
|
||||
},
|
||||
{
|
||||
id: 'grid',
|
||||
label: 'grid view',
|
||||
disabled: !searchEnabled,
|
||||
icon: IconViewGrid
|
||||
}
|
||||
]} />
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
type="file"
|
||||
tabindex="-1"
|
||||
class="u-hide"
|
||||
accept={extension}
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"
|
||||
></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'filename', width: { min: 140 } },
|
||||
{ id: 'id', width: { min: 100 } },
|
||||
{ id: 'type', width: { min: 100 } },
|
||||
{ id: 'size', width: { min: 100 } },
|
||||
{ id: 'created', width: { min: 120 } }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="filename" {root}>
|
||||
Filename
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="id" {root}>
|
||||
ID
|
||||
</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>
|
||||
</svelte:fragment>
|
||||
{#each response?.files as file}
|
||||
<Table.Row.Button
|
||||
{root}
|
||||
on:click={() => selectFile(file)}>
|
||||
<Table.Cell column="filename" {root}>
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<Selector.Radio
|
||||
name="file"
|
||||
group="file"
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<Layout.Stack
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
gap="xl"
|
||||
height="100%">
|
||||
<Spinner size="l" />
|
||||
<span>Loading files...</span>
|
||||
</Layout.Stack>
|
||||
{:then response}
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
{#if $isSmallViewport}
|
||||
<Layout.Stack gap="l">
|
||||
{#each response?.files as file}
|
||||
<Card.Selector
|
||||
radius="s"
|
||||
name="files"
|
||||
padding="xxs"
|
||||
imageHeight={32}
|
||||
imageWidth={32}
|
||||
imageRadius="xs"
|
||||
bind:group={selectedFile}
|
||||
title={truncatedFilename(file, 14)}
|
||||
value={file.$id}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)}
|
||||
on:click={() => selectFile(file)} />
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Layout.Grid
|
||||
columnsXXS={1}
|
||||
columnsXS={2}
|
||||
columnsS={3}
|
||||
columns={4}>
|
||||
{#each response?.files as file}
|
||||
<div class="image-selector">
|
||||
<Card.Selector
|
||||
radius="s"
|
||||
name="files"
|
||||
padding="xxs"
|
||||
imageWidth={gridImageDimensions.imageWidth}
|
||||
imageHeight={gridImageDimensions.imageHeight}
|
||||
imageRadius="xs"
|
||||
bind:group={selectedFile}
|
||||
title={truncatedFilename(
|
||||
file,
|
||||
14
|
||||
)}
|
||||
value={file.$id}
|
||||
checked={file.$id ===
|
||||
selectedFile} />
|
||||
<img
|
||||
style:border-radius="var(--border-radius-xsmall)"
|
||||
width="28"
|
||||
height="28"
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
file.$id,
|
||||
360
|
||||
)}
|
||||
alt={file.name} />
|
||||
<Typography.Text truncate>
|
||||
{file.name}
|
||||
</Typography.Text>
|
||||
on:click={() =>
|
||||
selectFile(file)} />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="id" {root}>
|
||||
<Id value={file.$id}>{file.$id}</Id>
|
||||
</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.Row.Button>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'filename', width: { min: 225 } },
|
||||
{ id: 'id', width: { min: 200 } },
|
||||
{ id: 'type', width: { min: 100 } },
|
||||
{ id: 'size', width: { min: 120 } },
|
||||
{ id: 'created', width: { min: 140 } }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="filename" {root}>
|
||||
Filename
|
||||
</Table.Header.Cell>
|
||||
<Table.Header.Cell column="id" {root}>
|
||||
ID
|
||||
</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>
|
||||
</svelte:fragment>
|
||||
{#each response?.files as file (file.$id)}
|
||||
<Table.Row.Button
|
||||
{root}
|
||||
on:click={() => selectFile(file)}>
|
||||
<Table.Cell column="filename" {root}>
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
bind:group={selectedFile} />
|
||||
|
||||
<div class="preview-block">
|
||||
<img
|
||||
alt={file.name}
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
disabled={file.name.length <
|
||||
15}
|
||||
maxWidth="fit-content">
|
||||
<Typography.Text truncate>
|
||||
{truncatedFilename(
|
||||
file
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
<span slot="tooltip">
|
||||
{file.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="id" {root}>
|
||||
<Id value={file.$id}>{file.$id}</Id>
|
||||
</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.Row.Button>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<EmptySearch
|
||||
hidePages
|
||||
hidePagination
|
||||
bind:search={$search}
|
||||
target="files">
|
||||
<Button secondary on:click={() => ($search = '')}>
|
||||
Clear search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
title="No files found within this bucket."
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
size="s"
|
||||
event="empty_documentation"
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
ariaLabel="create document"
|
||||
>Documentation</Button>
|
||||
<Button
|
||||
secondary
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}
|
||||
>Upload file
|
||||
</Button>
|
||||
</slot>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<Empty
|
||||
type="secondary"
|
||||
title={`Sorry we couldn't find "${$search}"`}
|
||||
description="There are no files that match your search.">
|
||||
<Button
|
||||
secondary
|
||||
slot="actions"
|
||||
on:click={() => ($search = '')}
|
||||
>Clear search</Button>
|
||||
</Empty>
|
||||
{:else}
|
||||
<Empty title="No files found within this bucket.">
|
||||
<Button
|
||||
secondary
|
||||
slot="actions"
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}
|
||||
>Upload file</Button>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/await}
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<Empty
|
||||
title="No buckets found"
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
size="s"
|
||||
event="empty_documentation"
|
||||
href="https://appwrite.io/docs/products/storage/buckets"
|
||||
ariaLabel="create document">Documentation</Button>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
await goto(
|
||||
`${base}/project-${page.params.project}/storage`
|
||||
);
|
||||
$showCreateBucket = true;
|
||||
}}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</slot>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty title="No buckets found">
|
||||
<Button
|
||||
slot="actions"
|
||||
secondary
|
||||
external
|
||||
href={`${base}/project-${page.params.project}/storage`}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
{/await}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button text on:click={closeModal}>Cancel</Button>
|
||||
<Button submit disabled={selectedBucket === null || selectedFile === null}
|
||||
>Select</Button>
|
||||
>Select
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Form>
|
||||
|
||||
<style>
|
||||
:global(.file-picker-modal-form dialog .content) {
|
||||
overflow: hidden;
|
||||
padding: unset !important;
|
||||
|
||||
/* multiple scroll bars from `.content` and `.files-section` look very odd */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
aside {
|
||||
min-width: 200px;
|
||||
padding: var(--space-7);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-block: unset;
|
||||
padding-inline: var(--space-7);
|
||||
}
|
||||
}
|
||||
|
||||
.files-section {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: var(--space-8);
|
||||
background: var(--bgcolor-neutral-default);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-menu-holder :global(div:first-of-type) {
|
||||
padding-inline: unset;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
margin-block: 0.125rem;
|
||||
|
||||
& :global(button) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.active-item {
|
||||
border-radius: var(--border-radius-s);
|
||||
background-color: var(--bgcolor-neutral-secondary, #f4f4f7);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
border: var(--border-width-S, 1px) solid var(--border-neutral-strong, #d8d8db);
|
||||
|
||||
& img {
|
||||
border-radius: 50%;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.image-selector :global(img) {
|
||||
border: 1px solid var(--border-neutral);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -83,4 +83,3 @@ export { default as Confirm } from './confirm.svelte';
|
||||
export { default as UsageCard } from './usageCard.svelte';
|
||||
export { default as ViewToggle } from './viewToggle.svelte';
|
||||
export { default as Terminal } from './studio/terminal.svelte';
|
||||
export { default as ExpirationInput } from './expirationInput.svelte';
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<style>
|
||||
.subMenu {
|
||||
min-width: 244px;
|
||||
margin-inline: -4px;
|
||||
margin-block: -4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,6 +48,8 @@ export enum Dependencies {
|
||||
PLATFORMS = 'dependency:platforms',
|
||||
KEY = 'dependency:key',
|
||||
KEYS = 'dependency:keys',
|
||||
DEV_KEY = 'dependency:dev_key',
|
||||
DEV_KEYS = 'dependency:dev_keys',
|
||||
DOMAINS = 'dependency:domains',
|
||||
DOMAIN = 'dependency:domains',
|
||||
WEBHOOK = 'dependency:webhook',
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<Label {optionalText} {tooltip} hide={!label}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<!-- TODO: make un-clicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script context="module" lang="ts">
|
||||
import { ProjectUsageRange, type Models } from '@appwrite.io/console';
|
||||
export type UsagePeriods = '24h' | '30d' | '90d';
|
||||
|
||||
export function periodToDates(period: UsagePeriods): {
|
||||
@@ -68,14 +69,14 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { Card } from '$lib/components';
|
||||
import { ProjectUsageRange, type Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/state';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { Card } from '$lib/components';
|
||||
import { InputSelect } from '$lib/elements/forms';
|
||||
import { formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
type MetricMetadata = {
|
||||
title: string;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
canWriteSites
|
||||
} from '$lib/stores/roles';
|
||||
import { consoleProfile } from '$lib/system';
|
||||
import CsvImportBox from '$lib/components/csvImportBox.svelte';
|
||||
|
||||
onMount(() => {
|
||||
return sdk.forConsole.client.subscribe(['project', 'console'], (response) => {
|
||||
@@ -119,6 +120,7 @@
|
||||
<UploadBox />
|
||||
<MigrationBox />
|
||||
<BackupRestoreBox />
|
||||
<CsvImportBox />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
+77
-11
@@ -13,14 +13,20 @@
|
||||
import CreateAttributeDropdown from './attributes/createAttributeDropdown.svelte';
|
||||
import type { Option } from './attributes/store';
|
||||
import CreateAttribute from './createAttribute.svelte';
|
||||
import { collection, columns } from './store';
|
||||
import { collection, columns, isCsvImportInProgress } from './store';
|
||||
import Table from './table.svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showImportCSV = false;
|
||||
let showCreateAttribute = false;
|
||||
let selectedAttribute: Option['name'] = null;
|
||||
|
||||
@@ -38,18 +44,65 @@
|
||||
);
|
||||
$: hasAttributes = !!$collection.attributes.length;
|
||||
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
|
||||
|
||||
async function onSelect(file: Models.File) {
|
||||
$isCsvImportInProgress = true;
|
||||
|
||||
try {
|
||||
await sdk.forProject.migrations.createCsvMigration(
|
||||
file.bucketId,
|
||||
file.$id,
|
||||
`${page.params.database}:${page.params.collection}`
|
||||
);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Documents import from csv has started'
|
||||
});
|
||||
|
||||
trackEvent(Submit.DatabaseImportCsv);
|
||||
} catch (e) {
|
||||
trackError(e, Submit.DatabaseImportCsv);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
} finally {
|
||||
$isCsvImportInProgress = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key page.params.collection}
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Filters
|
||||
query={data.query}
|
||||
{columns}
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
analyticsSource="database_documents" />
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector view={data.view} {columns} hideView />
|
||||
<Layout.Stack direction="column" gap="xl">
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Filters
|
||||
query={data.query}
|
||||
{columns}
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
analyticsSource="database_documents" />
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector view={data.view} {columns} hideView />
|
||||
<Button
|
||||
secondary
|
||||
event={Click.DatabaseImportCsv}
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
href={`${base}/project-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
|
||||
event="create_document">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create document
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{#if $isSmallViewport}
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
href={`${base}/project-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/create`}
|
||||
@@ -57,7 +110,7 @@
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create document
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if hasAttributes && hasValidAttributes}
|
||||
@@ -149,3 +202,16 @@
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute} />
|
||||
{/if}
|
||||
|
||||
{#if showImportCSV}
|
||||
<!-- CSVs can be text/plain or text/csv sometimes! -->
|
||||
<FilePicker
|
||||
{onSelect}
|
||||
mimeTypeQuery="text/"
|
||||
allowedExtension="csv"
|
||||
bind:show={showImportCSV}
|
||||
gridImageDimensions={{
|
||||
imageHeight: 32,
|
||||
imageWidth: 32
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
+46
-36
@@ -19,7 +19,7 @@
|
||||
import { isRelationship } from '../document-[document]/attributes/store';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import CreateIndex from '../indexes/createIndex.svelte';
|
||||
import { attributes, type Attributes } from '../store';
|
||||
import { attributes, type Attributes, isCsvImportInProgress } from '../store';
|
||||
import CreateAttributeDropdown from './createAttributeDropdown.svelte';
|
||||
import Delete from './deleteAttribute.svelte';
|
||||
import Edit from './edit.svelte';
|
||||
@@ -32,10 +32,12 @@
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconSwitchHorizontal,
|
||||
IconTrash
|
||||
IconTrash,
|
||||
IconViewList
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import CsvDisabled from '../csvDisabled.svelte';
|
||||
|
||||
const projectId = page.params.project;
|
||||
const databaseId = page.params.database;
|
||||
@@ -54,7 +56,7 @@
|
||||
ip: IconLocationMarker,
|
||||
url: IconLink,
|
||||
email: IconLink,
|
||||
enum: IconSwitchHorizontal
|
||||
enum: IconViewList
|
||||
};
|
||||
|
||||
function getAttributeStatusBadge(status: string): ComponentProps<Badge>['type'] {
|
||||
@@ -155,45 +157,53 @@
|
||||
: '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Button text icon ariaLabel="more options" on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPencil}
|
||||
on:click={() => {
|
||||
selectedAttribute = attribute;
|
||||
showEdit = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Update
|
||||
</ActionMenu.Item.Button>
|
||||
{#if !isRelationship(attribute)}
|
||||
{#if $isCsvImportInProgress}
|
||||
<CsvDisabled>
|
||||
<Button disabled text icon ariaLabel="more options">
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
</CsvDisabled>
|
||||
{:else}
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Button text icon ariaLabel="more options" on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPlus}
|
||||
leadingIcon={IconPencil}
|
||||
on:click={() => {
|
||||
selectedAttribute = attribute;
|
||||
showCreateIndex = true;
|
||||
showEdit = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Create index
|
||||
Update
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{#if attribute.status !== 'processing'}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => {
|
||||
selectedAttribute = attribute;
|
||||
showDelete = true;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.DatabaseAttributeDelete);
|
||||
}}>
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
{#if !isRelationship(attribute)}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPlus}
|
||||
on:click={() => {
|
||||
selectedAttribute = attribute;
|
||||
showCreateIndex = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Create index
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{#if attribute.status !== 'processing'}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => {
|
||||
selectedAttribute = attribute;
|
||||
showDelete = true;
|
||||
showDropdown[index] = false;
|
||||
trackEvent(Click.DatabaseAttributeDelete);
|
||||
}}>
|
||||
Delete
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
|
||||
+28
-17
@@ -3,28 +3,39 @@
|
||||
import { ActionMenu, Icon, Popover } from '@appwrite.io/pink-svelte';
|
||||
import { attributeOptions, type Option } from './store';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import CsvDisabled from '../csvDisabled.svelte';
|
||||
import { isCsvImportInProgress } from '../store';
|
||||
|
||||
export let selectedOption: Option['name'] = null;
|
||||
export let showCreate = false;
|
||||
</script>
|
||||
|
||||
<Popover let:toggle padding="none" placement="bottom-start">
|
||||
<slot {toggle}>
|
||||
<Button on:click={toggle} event="create_attribute">
|
||||
{#if $isCsvImportInProgress}
|
||||
<CsvDisabled>
|
||||
<Button size="s" disabled>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create attribute
|
||||
</Button>
|
||||
</slot>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
{#each attributeOptions as attribute}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={attribute.icon}
|
||||
on:click={() => {
|
||||
selectedOption = attribute.name;
|
||||
showCreate = true;
|
||||
}}>
|
||||
{attribute.name}
|
||||
</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</CsvDisabled>
|
||||
{:else}
|
||||
<Popover let:toggle padding="none" placement="bottom-start">
|
||||
<slot {toggle}>
|
||||
<Button on:click={toggle} event="create_attribute">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create attribute
|
||||
</Button>
|
||||
</slot>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
{#each attributeOptions as attribute}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={attribute.icon}
|
||||
on:click={() => {
|
||||
selectedOption = attribute.name;
|
||||
showCreate = true;
|
||||
}}>
|
||||
{attribute.name}
|
||||
</ActionMenu.Item.Button>
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { isCsvImportInProgress } from './store';
|
||||
import { Tooltip } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
|
||||
<Tooltip disabled={!$isCsvImportInProgress} maxWidth="12rem">
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div slot="tooltip">This action is disabled during import.</div>
|
||||
</Tooltip>
|
||||
+2
@@ -26,3 +26,5 @@ export const attributes = derived(
|
||||
export const indexes = derived(page, ($page) => $page.data.collection.indexes as Models.Index[]);
|
||||
|
||||
export const columns = writable<Column[]>([]);
|
||||
|
||||
export const isCsvImportInProgress = writable(false);
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@
|
||||
href={`${base}/project-${project}/databases/database-${databaseId}`}
|
||||
class="database-name u-flex u-cross-center body-text-2 u-gap-8 is-not-mobile is-selected">
|
||||
<Icon icon={IconDatabase} size="s" color="--fgcolor-neutral-weak" />
|
||||
{data.database.name}
|
||||
{data.database?.name}
|
||||
</a>
|
||||
<div class="collection-content">
|
||||
{#if data?.allCollections?.total}
|
||||
|
||||
+3
-16
@@ -3,7 +3,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert, Id, Modal } from '$lib/components';
|
||||
import { Alert, Confirm, Id } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button as ConsoleButton, InputChoice } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -112,7 +112,6 @@
|
||||
|
||||
let selectedRows: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
@@ -276,11 +275,7 @@
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
title="Delete Documents"
|
||||
bind:show={showDelete}
|
||||
onSubmit={handleDelete}
|
||||
dismissible={!deleting}>
|
||||
<Confirm title="Delete Documents" bind:open={showDelete} onSubmit={handleDelete}>
|
||||
<div>
|
||||
Are you sure you want to delete <b>{selectedRows.length}</b>
|
||||
{selectedRows.length > 1 ? 'documents' : 'document'}?
|
||||
@@ -332,12 +327,4 @@
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<ConsoleButton text on:click={() => (showDelete = false)} disabled={deleting}
|
||||
>Cancel</ConsoleButton>
|
||||
<ConsoleButton secondary submit disabled={deleting || (relAttributes?.length && !checked)}>
|
||||
Delete
|
||||
</ConsoleButton>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</Confirm>
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<CardGrid hideFooter={message.status !== 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Typography.Title size="s">Message</Typography.Title>
|
||||
<div class="u-flex u-margin-block-start-24 u-width-full-line">
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { InputText } from '$lib/elements/forms/index.js';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Fieldset, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import ExpirationInput from './expirationInput.svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onboarding } from '../../store';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { writable } from 'svelte/store';
|
||||
import Scopes from '../keys/scopes.svelte';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
let showExitModal = false;
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let scopes = [];
|
||||
let name = '',
|
||||
expire = '';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
const { $id } = await sdk.forConsole.projects.createKey(
|
||||
projectId,
|
||||
name,
|
||||
scopes,
|
||||
expire || undefined
|
||||
);
|
||||
|
||||
if ($onboarding) {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
}
|
||||
|
||||
trackEvent(Submit.KeyCreate);
|
||||
await goto(`${base}/project-${projectId}/overview/keys/${$id}`);
|
||||
addNotification({
|
||||
message: `API key has been created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyCreate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Wizard
|
||||
title="Create API key"
|
||||
href={`${base}/project-${projectId}/overview/keys/`}
|
||||
bind:showExitModal
|
||||
column
|
||||
columnSize="s"
|
||||
confirmExit>
|
||||
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Fieldset legend="Configuration">
|
||||
<Layout.Stack>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter key name"
|
||||
required
|
||||
bind:value={name} />
|
||||
|
||||
<ExpirationInput bind:value={expire} keyType="api" />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Scopes">
|
||||
<Layout.Stack gap="xl">
|
||||
<Typography.Text>
|
||||
Choose which permission scopes to grant your application. It is best
|
||||
practice to allow only the permissions you need to meet your project goals.
|
||||
</Typography.Text>
|
||||
<Scopes bind:scopes />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
export let showDelete = false;
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
export let key: Models.DevKey | Models.Key;
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'Dev';
|
||||
const slug = isApiKey ? 'keys' : 'dev-keys';
|
||||
const event = isApiKey ? Submit.KeyDelete : Submit.DevKeyDelete;
|
||||
const dependency = isApiKey ? Dependencies.KEYS : Dependencies.DEV_KEYS;
|
||||
|
||||
let error: string;
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
isApiKey
|
||||
? await sdk.forConsole.projects.deleteKey(projectId, key.$id)
|
||||
: await sdk.forConsole.projects.deleteDevKey(projectId, key.$id);
|
||||
|
||||
await invalidate(dependency);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${key.name} has been deleted`
|
||||
});
|
||||
trackEvent(event);
|
||||
await goto(`${base}/project-${projectId}/overview/${slug}`);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, event);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm onSubmit={handleDelete} title="Delete {label} key" bind:open={showDelete} bind:error>
|
||||
Are you sure you want to delete this key?
|
||||
</Confirm>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
export let showDelete = false;
|
||||
export let keyIds: string[] = [];
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
|
||||
let error: string;
|
||||
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'dev';
|
||||
const projectId = page.params.project;
|
||||
|
||||
async function handleDelete() {
|
||||
const slug = isApiKey ? 'keys' : 'dev-keys';
|
||||
const event = isApiKey ? Submit.KeyDelete : Submit.DevKeyDelete;
|
||||
const dependency = isApiKey ? Dependencies.KEYS : Dependencies.DEV_KEYS;
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
keyIds.map((key) =>
|
||||
isApiKey
|
||||
? sdk.forConsole.projects.deleteKey(projectId, key)
|
||||
: sdk.forConsole.projects.deleteDevKey(projectId, key)
|
||||
)
|
||||
);
|
||||
|
||||
await invalidate(dependency);
|
||||
showDelete = false;
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${keyIds.length} ${label} key${keyIds.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
|
||||
trackEvent(event);
|
||||
await goto(`${base}/project-${projectId}/overview/${slug}`);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, event);
|
||||
} finally {
|
||||
keyIds = [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm onSubmit={handleDelete} title={`Delete ${label} key`} bind:open={showDelete} bind:error>
|
||||
<p>
|
||||
Are you sure you want to delete <b>{keyIds.length}</b>
|
||||
{label} key{keyIds.length > 1 ? 's' : ''}?
|
||||
</p>
|
||||
</Confirm>
|
||||
+36
-12
@@ -19,7 +19,7 @@
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
const options = [
|
||||
const defaultOptions = [
|
||||
{
|
||||
label: 'Never',
|
||||
value: null
|
||||
@@ -46,13 +46,30 @@
|
||||
}
|
||||
];
|
||||
|
||||
const limitedOptions = [
|
||||
{
|
||||
label: '1 Day',
|
||||
value: incrementToday(1, 'day')
|
||||
},
|
||||
{
|
||||
label: '7 Days',
|
||||
value: incrementToday(7, 'day')
|
||||
},
|
||||
{
|
||||
label: '30 days',
|
||||
value: incrementToday(30, 'day')
|
||||
}
|
||||
];
|
||||
|
||||
export let value: string | null = null;
|
||||
export let dateSelectorLabel: string | undefined = undefined;
|
||||
export let selectorLabel: string | undefined = 'Expiration Date';
|
||||
export let resourceType: string | 'key' | 'token' | undefined = 'key';
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
|
||||
const options = keyType === 'api' ? defaultOptions : limitedOptions;
|
||||
|
||||
function initExpirationSelect() {
|
||||
if (value === null || !isValidDate(value)) return null;
|
||||
if (value === null || !isValidDate(value)) {
|
||||
return options[0]?.value ?? null;
|
||||
}
|
||||
|
||||
let result = 'custom';
|
||||
for (const option of options) {
|
||||
@@ -68,20 +85,27 @@
|
||||
}
|
||||
let expirationSelect = initExpirationSelect();
|
||||
let expirationCustom: string | null = value ?? null;
|
||||
$: helper =
|
||||
expirationSelect !== 'custom' && expirationSelect !== null
|
||||
? `Your ${resourceType} will expire in ${toLocaleDate(value)}`
|
||||
: null;
|
||||
|
||||
$: {
|
||||
if (!isSameDay(new Date(expirationSelect), new Date(value))) {
|
||||
value = expirationSelect === 'custom' ? expirationCustom : expirationSelect;
|
||||
}
|
||||
}
|
||||
|
||||
$: helper =
|
||||
expirationSelect !== 'custom' && expirationSelect !== null
|
||||
? `Your key will expire in ${toLocaleDate(value)}`
|
||||
: undefined;
|
||||
</script>
|
||||
|
||||
<InputSelect bind:value={expirationSelect} {options} id="preset" label={selectorLabel} {helper}>
|
||||
</InputSelect>
|
||||
<InputSelect
|
||||
required
|
||||
{helper}
|
||||
{options}
|
||||
id="preset"
|
||||
label="Expiration date"
|
||||
bind:value={expirationSelect} />
|
||||
|
||||
{#if expirationSelect === 'custom'}
|
||||
<InputDateTime required id="expire" label={dateSelectorLabel} bind:value={expirationCustom} />
|
||||
<InputDateTime required id="expire" label="" bind:value={expirationCustom} />
|
||||
{/if}
|
||||
@@ -0,0 +1,205 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Box, CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project } from '../../store';
|
||||
import Delete from './delete.svelte';
|
||||
import UpdateExpirationDate from './updateExpirationDate.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import Scopes from '../keys/scopes.svelte';
|
||||
import { InteractiveText, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let key: Models.DevKey | Models.Key;
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
|
||||
let name = null;
|
||||
let scopes = null;
|
||||
|
||||
let showDelete = false;
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'Dev';
|
||||
const event = isApiKey ? Submit.KeyUpdateName : Submit.DevKeyUpdateName;
|
||||
const dependency = isApiKey ? Dependencies.KEY : Dependencies.DEV_KEY;
|
||||
|
||||
onMount(() => {
|
||||
name ??= key.name;
|
||||
if (isApiKey) {
|
||||
scopes ??= (key as Models.Key).scopes;
|
||||
}
|
||||
});
|
||||
|
||||
function asApiKey(key: Models.Key | Models.DevKey) {
|
||||
return key as Models.Key;
|
||||
}
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
isApiKey
|
||||
? await sdk.forConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
key.$id,
|
||||
name,
|
||||
scopes,
|
||||
key.expire
|
||||
)
|
||||
: await sdk.forConsole.projects.updateDevKey(
|
||||
$project.$id,
|
||||
key.$id,
|
||||
name,
|
||||
key.expire
|
||||
);
|
||||
await invalidate(dependency);
|
||||
trackEvent(event);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${label} key name has been updated`
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, event);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateScopes() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
key.$id,
|
||||
key.name,
|
||||
scopes,
|
||||
key.expire
|
||||
);
|
||||
await invalidate(Dependencies.KEY);
|
||||
trackEvent(Submit.KeyUpdateScopes, {
|
||||
scopes
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API key scopes have been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyUpdateScopes);
|
||||
}
|
||||
}
|
||||
|
||||
let tiles: Record<string, string>;
|
||||
$: tiles = {
|
||||
Name: key.name,
|
||||
'Last accessed': key.accessedAt ? toLocaleDate(key.accessedAt) : 'never',
|
||||
'Expiration date': key.expire ? toLocaleDateTime(key.expire) : 'never',
|
||||
Secret: key.secret
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{label} key - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
{@const accessedAt = key.accessedAt ? toLocaleDate(key.accessedAt) : 'never'}
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Key details</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.GridFraction start={1} end={2} gap="xxxl" rowGap="xl">
|
||||
{#each Object.entries(tiles) as [label, value]}
|
||||
<Layout.Stack gap="xxxs">
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
|
||||
{label}
|
||||
</Typography.Text>
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
|
||||
{#if label === 'Secret'}
|
||||
<InteractiveText
|
||||
variant="secret-code"
|
||||
isVisible={false}
|
||||
text={value} />
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
</Layout.GridFraction>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Name</svelte:fragment>
|
||||
Choose any name that will help you distinguish between {label} keys.
|
||||
<svelte:fragment slot="aside">
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="Enter name" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={name === key.name} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
{#if isApiKey}
|
||||
<Form onSubmit={updateScopes}>
|
||||
{@const apiKey = asApiKey(key)}
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Scopes</svelte:fragment>
|
||||
You can choose which permission scope to grant your application. It is a best practice
|
||||
to allow only the permissions you need to meet your project goals.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if scopes !== null}
|
||||
<Scopes bind:scopes />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
submit
|
||||
disabled={scopes &&
|
||||
apiKey?.scopes &&
|
||||
!symmetricDifference(scopes, apiKey.scopes).length}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
{/if}
|
||||
|
||||
<UpdateExpirationDate {keyType} {key} />
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Delete {label} key</svelte:fragment>
|
||||
The Dev key will be permanently deleted. This action is irreversible.
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<div class="u-flex u-gap-16">
|
||||
<div class="u-cross-child-center u-line-height-1-5">
|
||||
<h6 class="u-bold" data-private>{key.name}</h6>
|
||||
<p>Last accessed: {accessedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Container>
|
||||
|
||||
<Delete {keyType} {key} bind:showDelete />
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Empty } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { canWriteKeys } from '$lib/stores/roles';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { diffDays, toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { devKeyColumns, keyColumns, showDevKeysCreateModal } from '../store';
|
||||
import { Badge, FloatingActionBar, Layout, Table } from '@appwrite.io/pink-svelte';
|
||||
import DeleteBatch from './deleteBatch.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
export let keys: Models.KeyList | Models.DevKeyList;
|
||||
|
||||
let showDeleteModal = false;
|
||||
let selectedRows: string[] = [];
|
||||
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'dev';
|
||||
const slug = isApiKey ? 'keys' : 'dev-keys';
|
||||
|
||||
function asApiKey(key: Models.Key | Models.DevKey) {
|
||||
return key as Models.Key;
|
||||
}
|
||||
|
||||
function getExpiryDetails(key: Models.Key | Models.DevKey): {
|
||||
status: 'success' | 'warning' | 'error' | null;
|
||||
message: string | null;
|
||||
} {
|
||||
const isExpired = key.expire !== null && new Date(key.expire) < new Date();
|
||||
const isExpiring = key.expire && diffDays(new Date(), new Date(key.expire)) < 14;
|
||||
|
||||
return {
|
||||
status: isExpired ? 'error' : isExpiring ? 'warning' : null,
|
||||
message: isExpired ? 'Expired' : isExpiring ? 'Expires soon' : null
|
||||
};
|
||||
}
|
||||
|
||||
function getKeys(): Models.Key[] | Models.DevKey[] {
|
||||
if (isApiKey) return keys['keys'] as Models.Key[];
|
||||
else return keys['devKeys'] as Models.DevKey[];
|
||||
}
|
||||
|
||||
function getDescription(): string {
|
||||
if (isApiKey)
|
||||
return 'Use API keys to authenticate your app’s requests in production, granting secure access to live data and services.';
|
||||
else
|
||||
return 'Dev keys allow bypassing rate limits and accessing more detailed error messages while in development.';
|
||||
}
|
||||
|
||||
const columns = isApiKey ? $keyColumns : $devKeyColumns;
|
||||
</script>
|
||||
|
||||
{#if keys.total}
|
||||
<Table.Root let:root {columns} bind:selectedRows allowSelection={$canWriteKeys}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each columns as column}
|
||||
<Table.Header.Cell column={column.id} {root}>{column.title}</Table.Header.Cell>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
{#each getKeys() as key (key.$id)}
|
||||
<Table.Row.Link id={key.$id} href={`${slug}/${key.$id}`} {root}>
|
||||
<Table.Cell {root}>
|
||||
{key.name}
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{key.accessedAt ? toLocaleDate(key.accessedAt) : 'never'}
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{@const expiration = getExpiryDetails(key)}
|
||||
<Layout.Stack gap="s" direction="row">
|
||||
{key.expire ? toLocaleDateTime(key.expire) : 'never'}
|
||||
|
||||
{#if expiration.status}
|
||||
<Badge
|
||||
size="s"
|
||||
variant="secondary"
|
||||
type={expiration.status}
|
||||
content={expiration.message} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
{#if isApiKey}
|
||||
<Table.Cell {root}>
|
||||
{asApiKey(key).scopes.length} Scopes
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteKeys}
|
||||
href="https://appwrite.io/docs/advanced/platform/{slug}"
|
||||
target="{label} key"
|
||||
description={getDescription()}
|
||||
on:click={() => {
|
||||
if (isApiKey) {
|
||||
goto(`${base}/project-${page.params.project}/overview/${slug}/create`);
|
||||
} else {
|
||||
$showDevKeysCreateModal = true;
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
{#if selectedRows.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedRows.length.toString()} />
|
||||
<span>
|
||||
{capitalize(label)}
|
||||
{selectedRows.length > 1 ? 'keys' : 'key'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedRows = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDeleteModal = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<DeleteBatch {keyType} bind:keyIds={selectedRows} bind:showDelete={showDeleteModal} />
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { diffDays, isSameDay } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
import ExpirationInput from './expirationInput.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let keyType: 'api' | 'dev' = 'api';
|
||||
export let key: Models.DevKey | Models.Key;
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'Dev';
|
||||
const dependency = isApiKey ? Dependencies.KEY : Dependencies.DEV_KEY;
|
||||
const event = isApiKey ? Submit.KeyUpdateExpire : Submit.DevKeyUpdateExpire;
|
||||
|
||||
let expiration = key.expire;
|
||||
|
||||
async function updateExpire() {
|
||||
try {
|
||||
isApiKey
|
||||
? await sdk.forConsole.projects.updateKey(
|
||||
projectId,
|
||||
key.$id,
|
||||
key.name,
|
||||
(key as Models.Key).scopes,
|
||||
expiration
|
||||
)
|
||||
: await sdk.forConsole.projects.updateDevKey(
|
||||
projectId,
|
||||
key.$id,
|
||||
key.name,
|
||||
expiration
|
||||
);
|
||||
|
||||
await invalidate(dependency);
|
||||
trackEvent(event);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${label} key expiration has been updated`
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, event);
|
||||
}
|
||||
}
|
||||
|
||||
let alertsDismissed = false;
|
||||
$: isExpiring =
|
||||
!alertsDismissed && key.expire && diffDays(new Date(), new Date(key.expire)) < 14;
|
||||
$: isExpired = !alertsDismissed && key.expire !== null && new Date(key.expire) < new Date();
|
||||
|
||||
// for comparison's sake.
|
||||
$: disableButton =
|
||||
// expiration becomes `null` in `ExpirationInput`.
|
||||
typeof key?.expire === 'undefined' && expiration === null
|
||||
? true
|
||||
: isSameDay(new Date(expiration), new Date(key?.expire));
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateExpire}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Expiration date</svelte:fragment>
|
||||
Set a date after which your {label} key will expire.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if isExpired}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
on:dismiss={() => (alertsDismissed = true)}
|
||||
title="Your {label} key has expired">
|
||||
For security reasons, we recommend you delete your expired key and create a new
|
||||
one.
|
||||
</Alert.Inline>
|
||||
{:else if isExpiring}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
on:dismiss={() => (alertsDismissed = true)}
|
||||
title="Your {label} key is about to expire">
|
||||
Update the expiration date to keep the key active
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<ExpirationInput bind:value={expiration} {keyType} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={disableButton} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -52,6 +52,10 @@
|
||||
return usage.load(dates.start, dates.end, dates.period);
|
||||
}
|
||||
|
||||
function isTabSelected(key: string) {
|
||||
return page.url.pathname === `${path}/${key}`;
|
||||
}
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Add platform',
|
||||
@@ -67,11 +71,21 @@
|
||||
label: 'Create API Key',
|
||||
icon: IconPlus,
|
||||
callback() {
|
||||
goto(`${base}/project-[project]/overview/keys/create`);
|
||||
goto(`${base}/project-${projectId}/overview/keys/create`);
|
||||
},
|
||||
keys: ['c', 'k'],
|
||||
group: 'integrations',
|
||||
disabled: !$canWriteProjects
|
||||
},
|
||||
{
|
||||
label: 'Create Dev Key',
|
||||
icon: IconPlus,
|
||||
callback() {
|
||||
goto(`${base}/project-${projectId}/overview/dev-keys/create`);
|
||||
},
|
||||
keys: ['c', 'd', 'k'],
|
||||
group: 'integrations',
|
||||
disabled: !$canWriteProjects
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -205,28 +219,31 @@
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<div class="u-margin-block-start-64">
|
||||
<Layout.Stack gap="xl">
|
||||
<Typography.Title>Integrations</Typography.Title>
|
||||
<Layout.Stack gap="xl" direction="row" justifyContent="space-between">
|
||||
<Tabs>
|
||||
<Tab
|
||||
noscroll
|
||||
href={`${path}/platforms`}
|
||||
selected={page.url.pathname === `${path}/platforms`}
|
||||
event="platforms">Platforms</Tab>
|
||||
<Tab
|
||||
noscroll
|
||||
href={`${path}/keys`}
|
||||
selected={page.url.pathname === `${path}/keys`}
|
||||
event="keys">API keys</Tab>
|
||||
</Tabs>
|
||||
{#if $action}
|
||||
<svelte:component this={$action} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<slot />
|
||||
<Layout.Stack gap="xl">
|
||||
<Typography.Title>Integrations</Typography.Title>
|
||||
<Layout.Stack gap="xl" direction="row" justifyContent="space-between">
|
||||
<Tabs>
|
||||
<Tab
|
||||
noscroll
|
||||
event="platforms"
|
||||
href={`${path}/platforms`}
|
||||
selected={isTabSelected('platforms')}>Platforms</Tab>
|
||||
<Tab
|
||||
noscroll
|
||||
event="keys"
|
||||
href={`${path}/keys`}
|
||||
selected={isTabSelected('keys')}>API keys</Tab>
|
||||
<Tab
|
||||
noscroll
|
||||
event="keys"
|
||||
href={`${path}/dev-keys`}
|
||||
selected={isTabSelected('dev-keys')}>Dev keys</Tab>
|
||||
</Tabs>
|
||||
{#if $action}
|
||||
<svelte:component this={$action} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Action from './action.svelte';
|
||||
import { setOverviewAction } from '../context';
|
||||
import Table from '../(components)/table.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
setOverviewAction(Action);
|
||||
</script>
|
||||
|
||||
<Table keys={data.devKeys} keyType="dev" />
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { selectedTab } from '../store';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
selectedTab.set('dev-keys');
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.DEV_KEYS);
|
||||
return {
|
||||
devKeys: await sdk.forConsole.projects.listDevKeys(params.project)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import Header from './header.svelte';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.DEV_KEY);
|
||||
|
||||
const key = await sdk.forConsole.projects.getDevKey(params.project, params.key);
|
||||
if (key.expire) {
|
||||
key.expire = new Date(key.expire).toISOString().slice(0, 23);
|
||||
} else {
|
||||
key.expire = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
key
|
||||
};
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { devKey } from './store';
|
||||
import KeyDetails from '../../(components)/keyDetails.svelte';
|
||||
</script>
|
||||
|
||||
<KeyDetails key={$devKey} keyType="dev" />
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Breadcrumbs } from '$lib/layout';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { project } from '../../../store';
|
||||
import { devKey } from './store';
|
||||
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
href: `${base}/organization-${$organization?.$id}`,
|
||||
title: $organization?.name
|
||||
},
|
||||
{
|
||||
href: `${base}/project-${$project?.$id}`,
|
||||
title: $project?.name
|
||||
},
|
||||
{
|
||||
href: `${base}/project-${$project?.$id}/overview/dev-keys`,
|
||||
title: 'Dev keys'
|
||||
},
|
||||
{
|
||||
href: `${base}/project-${$project?.$id}/overview/dev-keys/${$devKey?.$id}`,
|
||||
title: $devKey?.name
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Id } from '$lib/components';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { devKey } from './store';
|
||||
|
||||
const projectId = page.params.project;
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle href={`${base}/project-${projectId}/overview/dev-keys`}>
|
||||
{$devKey?.name}
|
||||
</CoverTitle>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const devKey = derived(page, ($page) => $page.data.key as Models.DevKey);
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { canWriteKeys } from '$lib/stores/roles';
|
||||
import { Icon, Modal, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Form, InputText } from '$lib/elements/forms/index.js';
|
||||
import ExpirationInput from '../(components)/expirationInput.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { base } from '$app/paths';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { page } from '$app/state';
|
||||
import { showDevKeysCreateModal } from '$routes/(console)/project-[project]/overview/store';
|
||||
|
||||
let isSubmitting = false;
|
||||
let name = '',
|
||||
expire = '';
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
isSubmitting = true;
|
||||
const { $id } = await sdk.forConsole.projects.createDevKey(
|
||||
projectId,
|
||||
name,
|
||||
expire || undefined
|
||||
);
|
||||
|
||||
$showDevKeysCreateModal = false;
|
||||
trackEvent(Submit.DevKeyCreate);
|
||||
await goto(`${base}/project-${projectId}/overview/dev-keys/${$id}`);
|
||||
addNotification({
|
||||
message: `Dev key has been created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.DevKeyCreate);
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $canWriteKeys}
|
||||
<Button on:click={() => ($showDevKeysCreateModal = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create dev key
|
||||
</Button>
|
||||
|
||||
<Form onSubmit={create} isModal>
|
||||
<Modal title="Create dev key" bind:open={$showDevKeysCreateModal}>
|
||||
<span slot="description">
|
||||
Test your app without rate limits and more detailed error messages.
|
||||
</span>
|
||||
|
||||
<Layout.Stack>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter key name"
|
||||
required
|
||||
bind:value={name} />
|
||||
|
||||
<ExpirationInput bind:value={expire} keyType="dev" />
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" slot="footer">
|
||||
<Button fullWidthMobile secondary on:click={() => ($showDevKeysCreateModal = false)}
|
||||
>Cancel</Button>
|
||||
<Button fullWidthMobile submit disabled={isSubmitting}>Create</Button>
|
||||
</Layout.Stack>
|
||||
</Modal>
|
||||
</Form>
|
||||
{/if}
|
||||
@@ -1,50 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Empty } from '$lib/components';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { canWriteKeys } from '$lib/stores/roles';
|
||||
import type { PageData } from './$types';
|
||||
import Action from './action.svelte';
|
||||
import { setOverviewAction } from '../context';
|
||||
import { Table } from '@appwrite.io/pink-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import Table from '../(components)/table.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
setOverviewAction(Action);
|
||||
</script>
|
||||
|
||||
{#if data.keys.total}
|
||||
<Table.Root columns={4} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Last accessed</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Expiration date</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Scopes</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each data.keys.keys as key}
|
||||
<Table.Row.Link href={`keys/${key.$id}`} {root}>
|
||||
<Table.Cell {root}>
|
||||
{key.name}
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{key.accessedAt ? toLocaleDate(key.accessedAt) : 'never'}
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{key.expire ? toLocaleDateTime(key.expire) : 'never'}
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{key.scopes.length} Scopes
|
||||
</Table.Cell>
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteKeys}
|
||||
href="https://appwrite.io/docs/advanced/platform/api-keys"
|
||||
target="API key"
|
||||
on:click={() => goto(`${base}/project-${page.params.project}/overview/keys/create`)} />
|
||||
{/if}
|
||||
<Table keys={data.keys} />
|
||||
|
||||
+2
-161
@@ -1,165 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Box, CardGrid, Secret } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project } from '../../../store';
|
||||
import Scopes from '../scopes.svelte';
|
||||
import Delete from './delete.svelte';
|
||||
import { key } from './store';
|
||||
import UpdateExpirationDate from './updateExpirationDate.svelte';
|
||||
|
||||
let showDelete = false;
|
||||
let name: string = null;
|
||||
let secret: string = null;
|
||||
let scopes: string[] = null;
|
||||
|
||||
onMount(() => {
|
||||
name ??= $key.name;
|
||||
secret ??= $key.secret;
|
||||
scopes ??= $key.scopes;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
$key.$id,
|
||||
name,
|
||||
$key.scopes,
|
||||
$key.expire
|
||||
);
|
||||
await invalidate(Dependencies.KEY);
|
||||
trackEvent(Submit.KeyUpdateName);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API key name has been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyUpdateName);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateScopes() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
$key.$id,
|
||||
$key.name,
|
||||
scopes,
|
||||
$key.expire
|
||||
);
|
||||
await invalidate(Dependencies.KEY);
|
||||
trackEvent(Submit.KeyUpdateScopes, {
|
||||
scopes
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API key scopes have been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyUpdateScopes);
|
||||
}
|
||||
}
|
||||
import KeyDetails from '../../(components)/keyDetails.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>API key - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
{@const accessedAt = $key.accessedAt ? toLocaleDate($key.accessedAt) : 'never'}
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">{$key.name}</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<p>
|
||||
Last accessed: {accessedAt}<br />
|
||||
Scopes granted: {$key.scopes.length}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">API key secret</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<Secret copyEvent="key" bind:value={secret} />
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Name</svelte:fragment>
|
||||
Choose any name that will help you distinguish between API keys.
|
||||
<svelte:fragment slot="aside">
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="Enter name" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={name === $key.name} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
<Form onSubmit={updateScopes}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Scopes</svelte:fragment>
|
||||
You can choose which permission scope to grant your application. It is a best practice to
|
||||
allow only the permissions you need to meet your project goals.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if scopes !== null}
|
||||
<Scopes bind:scopes />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
submit
|
||||
disabled={scopes &&
|
||||
$key?.scopes &&
|
||||
!symmetricDifference(scopes, $key.scopes).length}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<UpdateExpirationDate />
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Delete API key</svelte:fragment>
|
||||
The API key will be permanently deleted. This action is irreversible.
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<div class="u-flex u-gap-16">
|
||||
<div class="u-cross-child-center u-line-height-1-5">
|
||||
<h6 class="u-bold" data-private>{$key.name}</h6>
|
||||
<p>Last accessed: {accessedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Container>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
<KeyDetails key={$key} keyType="api" />
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../../store';
|
||||
import { key } from './store';
|
||||
|
||||
export let showDelete = false;
|
||||
let error: string;
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await sdk.forConsole.projects.deleteKey($project.$id, $key.$id);
|
||||
await invalidate(Dependencies.KEYS);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$key.name} has been deleted`
|
||||
});
|
||||
trackEvent(Submit.KeyDelete);
|
||||
await goto(`${base}/project-${$project.$id}/overview/keys`);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.KeyDelete);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm onSubmit={handleDelete} title="Delete API key" bind:open={showDelete} bind:error>
|
||||
Are you sure you want to delete this execution?
|
||||
</Confirm>
|
||||
@@ -13,6 +13,5 @@
|
||||
<CoverTitle href={`${base}/project-${projectId}/overview/keys`}>
|
||||
{$key?.name}
|
||||
</CoverTitle>
|
||||
<Id value={$key?.secret}>API key secret</Id>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const key = derived(page, ($page) => $page.data.key as Models.Key);
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { diffDays } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../../store';
|
||||
import { ExpirationInput } from '$lib/components';
|
||||
import { key } from './store';
|
||||
|
||||
let expiration = $key.expire;
|
||||
|
||||
async function updateExpire() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateKey(
|
||||
$project.$id,
|
||||
$key.$id,
|
||||
$key.name,
|
||||
$key.scopes,
|
||||
expiration
|
||||
);
|
||||
await invalidate(Dependencies.KEY);
|
||||
trackEvent(Submit.KeyUpdateExpire);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'API key expiration has been updated'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyUpdateExpire);
|
||||
}
|
||||
}
|
||||
|
||||
let alertsDismissed = false;
|
||||
$: isExpiring =
|
||||
!alertsDismissed && $key.expire && diffDays(new Date(), new Date($key.expire)) < 14;
|
||||
$: isExpired = !alertsDismissed && $key.expire !== null && new Date($key.expire) < new Date();
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateExpire}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Expiration date</svelte:fragment>
|
||||
Set a date after which your API key will expire.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if isExpired}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
on:dismiss={() => (alertsDismissed = true)}
|
||||
title="Your API key has expired">
|
||||
For security reasons, we recommend you delete your expired key and create a new
|
||||
one.
|
||||
</Alert.Inline>
|
||||
{:else if isExpiring}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
on:dismiss={() => (alertsDismissed = true)}
|
||||
title="Your API key is about to expire">
|
||||
Update the expiration date to keep the key active
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<ExpirationInput bind:value={expiration} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={expiration === $key.expire} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
@@ -1,98 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { InputText } from '$lib/elements/forms/index.js';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Fieldset, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { ExpirationInput } from '$lib/components';
|
||||
import Scopes from '../scopes.svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onboarding } from '$routes/(console)/project-[project]/store';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let showExitModal = false;
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let name = '',
|
||||
expire = '';
|
||||
let scopes = [];
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
const { $id } = await sdk.forConsole.projects.createKey(
|
||||
page.params.project,
|
||||
name,
|
||||
scopes,
|
||||
expire || undefined
|
||||
);
|
||||
|
||||
if ($onboarding) {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
}
|
||||
trackEvent(Submit.KeyCreate, {
|
||||
scopes: scopes
|
||||
});
|
||||
goto(`${base}/project-${page.params.project}/overview/keys/${$id}`);
|
||||
addNotification({
|
||||
message: `API key has been created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.KeyCreate);
|
||||
}
|
||||
}
|
||||
import CreateKey from '../../(components)/create.svelte';
|
||||
</script>
|
||||
|
||||
<Wizard
|
||||
title="Create API key"
|
||||
href={`${base}/project-${page.params.project}/overview/keys/`}
|
||||
bind:showExitModal
|
||||
column
|
||||
confirmExit>
|
||||
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Fieldset legend="Configuration">
|
||||
<Layout.Stack>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="API key name"
|
||||
required
|
||||
bind:value={name} />
|
||||
<ExpirationInput bind:value={expire} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Scopes">
|
||||
<Layout.Stack gap="xl">
|
||||
<Typography.Text>
|
||||
Choose which permission scopes to grant your application. It is best
|
||||
practice to allow only the permissions you need to meet your project goals.
|
||||
</Typography.Text>
|
||||
<Scopes bind:scopes />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
<CreateKey />
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
Other = 'Other'
|
||||
}
|
||||
|
||||
const categories = [
|
||||
Category.Auth,
|
||||
Category.Database,
|
||||
Category.Functions,
|
||||
Category.Storage,
|
||||
Category.Messaging,
|
||||
Category.Sites,
|
||||
Category.Other
|
||||
];
|
||||
|
||||
let mounted = false;
|
||||
|
||||
onMount(() => {
|
||||
@@ -77,8 +87,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: console.log(scopes);
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
@@ -92,14 +100,16 @@
|
||||
<Layout.Stack gap="none">
|
||||
<Divider />
|
||||
|
||||
{#each [Category.Auth, Category.Database, Category.Functions, Category.Storage, Category.Messaging, Category.Sites, Category.Other] as category}
|
||||
{#each categories as category, index}
|
||||
{@const checked = categoryState(category, scopes)}
|
||||
{@const isLastItem = index === categories.length - 1}
|
||||
{@const scopesLength = allScopes.filter(
|
||||
(n) => n.category === category && scopes.includes(n.scope)
|
||||
).length}
|
||||
<Accordion
|
||||
selectable
|
||||
title={category}
|
||||
hideDivider={isLastItem}
|
||||
badge={`${scopesLength} ${scopesLength === 1 ? 'Scope' : 'Scopes'}`}
|
||||
{checked}
|
||||
on:change={(event) => onCategoryChange(event, category)}>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { get, readable, writable, type Writable } from 'svelte/store';
|
||||
import type { Models, ProjectUsageRange } from '@appwrite.io/console';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
|
||||
export const usage = cachedStore<
|
||||
Models.UsageProject,
|
||||
@@ -17,4 +18,17 @@ export const usage = cachedStore<
|
||||
};
|
||||
});
|
||||
|
||||
export const selectedTab: Writable<'platforms' | 'keys'> = writable('platforms');
|
||||
export const selectedTab: Writable<'platforms' | 'keys' | 'dev-keys'> = writable('platforms');
|
||||
|
||||
export const showDevKeysCreateModal: Writable<boolean> = writable(false);
|
||||
|
||||
export const devKeyColumns = readable<Column[]>([
|
||||
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
|
||||
{ id: 'last_accessed', title: 'Last accessed', type: 'datetime', width: { min: 120 } },
|
||||
{ id: 'expiration', title: 'Expiration date', type: 'datetime', width: { min: 120 } }
|
||||
]);
|
||||
|
||||
export const keyColumns = readable<Column[]>([
|
||||
...get(devKeyColumns),
|
||||
{ id: 'scopes', title: 'Scopes', type: 'string', width: { min: 120 } }
|
||||
]);
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
try {
|
||||
await sdk.forProject.storage.deleteFile(file.bucketId, file.$id);
|
||||
await invalidate(Dependencies.FILES);
|
||||
uploader.removeFile(file);
|
||||
await uploader.removeFile(file);
|
||||
trackEvent(Submit.FileDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
@@ -218,11 +218,11 @@
|
||||
<Empty
|
||||
single
|
||||
target="file"
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
on:click={() =>
|
||||
on:click={() => {
|
||||
goto(
|
||||
`${base}/project-${page.params.project}/storage/bucket-${page.params.bucket}/create`
|
||||
)} />
|
||||
);
|
||||
}} />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
|
||||
+3
-2
@@ -15,19 +15,20 @@
|
||||
try {
|
||||
const fileName = $file.name;
|
||||
await sdk.forProject.storage.deleteFile($file.bucketId, $file.$id);
|
||||
showDelete = false;
|
||||
await goto(`${base}/project-${page.params.project}/storage/bucket-${$file.bucketId}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${fileName} has been deleted`
|
||||
});
|
||||
trackEvent(Submit.FileDelete);
|
||||
await goto(`${base}/project-${page.params.project}/storage/bucket-${$file.bucketId}`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FileDelete);
|
||||
} finally {
|
||||
showDelete = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -186,11 +186,11 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="links">
|
||||
<li class="inline-links-item">
|
||||
<a href={`${base}/recover`}><span class="text">Forgot Password?</span></a>
|
||||
<a href={`${base}/recover`}><span class="text">Forgot password?</span></a>
|
||||
</li>
|
||||
<li class="inline-links-item">
|
||||
<a href={`${base}/register${page?.url?.search ?? ''}`}>
|
||||
<span class="text">Sign Up</span>
|
||||
<span class="text">Sign up</span>
|
||||
</a>
|
||||
</li>
|
||||
</svelte:fragment>
|
||||
|
||||
Reference in New Issue
Block a user