mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-add-view-certiicate-logs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { DropList, GridItem1, CardContainer } from '$lib/components';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { DropList, GridItem1, CardContainer, Modal } from '$lib/components';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
@@ -9,7 +10,8 @@
|
||||
Accordion,
|
||||
ActionMenu,
|
||||
Popover,
|
||||
Layout
|
||||
Layout,
|
||||
Divider
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconAndroid,
|
||||
@@ -21,7 +23,8 @@
|
||||
IconInfo,
|
||||
IconDotsHorizontal,
|
||||
IconInboxIn,
|
||||
IconSwitchHorizontal
|
||||
IconSwitchHorizontal,
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { getPlatformInfo } from '$lib/helpers/platform';
|
||||
import { Status, type Models } from '@appwrite.io/console';
|
||||
@@ -33,7 +36,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Modal } from '$lib/components';
|
||||
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
@@ -53,6 +56,17 @@
|
||||
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
|
||||
let showUnarchiveModal = $state(false);
|
||||
let projectToUnarchive = $state<Models.Project | null>(null);
|
||||
let showDeleteModal = $state(false);
|
||||
let projectToDelete = $state<Models.Project | null>(null);
|
||||
let deleteProjectName = $state<string | null>(null);
|
||||
let deleteError = $state<string | null>(null);
|
||||
|
||||
function resetDeleteState() {
|
||||
showDeleteModal = false;
|
||||
projectToDelete = null;
|
||||
deleteProjectName = null;
|
||||
deleteError = null;
|
||||
}
|
||||
|
||||
function filterPlatforms(platforms: { name: string; icon: string }[]) {
|
||||
return platforms.filter(
|
||||
@@ -103,6 +117,12 @@
|
||||
showUnarchiveModal = true;
|
||||
}
|
||||
|
||||
function handleDeleteProject(project: Models.Project) {
|
||||
projectToDelete = project;
|
||||
deleteProjectName = null;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
// Confirm unarchive action
|
||||
async function confirmUnarchive() {
|
||||
if (!projectToUnarchive) return;
|
||||
@@ -141,6 +161,29 @@
|
||||
projectToUnarchive = null;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!projectToDelete) return;
|
||||
|
||||
try {
|
||||
await sdk.forConsoleIn(projectToDelete.region).projects.delete({
|
||||
projectId: projectToDelete.$id
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
trackEvent(Submit.ProjectDelete);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${projectToDelete.name} has been deleted`
|
||||
});
|
||||
|
||||
resetDeleteState();
|
||||
} catch (error) {
|
||||
deleteError = error.message;
|
||||
trackError(error, Submit.ProjectDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function findRegion(project: Models.Project) {
|
||||
return $regionsStore.regions.find((region) => region.$id === project.region);
|
||||
}
|
||||
@@ -226,6 +269,14 @@
|
||||
leadingIcon={IconSwitchHorizontal}
|
||||
on:click={() => handleMigrateProject(project)}
|
||||
>Migrate project</ActionMenu.Item.Button>
|
||||
<div class="action-menu-divider">
|
||||
<Divider />
|
||||
</div>
|
||||
<ActionMenu.Item.Button
|
||||
status="danger"
|
||||
leadingIcon={IconTrash}
|
||||
on:click={() => handleDeleteProject(project)}
|
||||
>Delete project</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -275,10 +326,51 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<Modal
|
||||
size="s"
|
||||
bind:show={showDeleteModal}
|
||||
title="Delete project"
|
||||
onSubmit={confirmDelete}
|
||||
bind:error={deleteError}>
|
||||
<svelte:fragment slot="description">
|
||||
The archived project <strong>{projectToDelete?.name}</strong> will be deleted along with all
|
||||
of its metadata, stats, and other resources.
|
||||
<b>This action is irreversible.</b>
|
||||
</svelte:fragment>
|
||||
|
||||
<InputText
|
||||
label={`Enter "${projectToDelete?.name}" to continue`}
|
||||
placeholder="Enter name"
|
||||
id="delete-project-name"
|
||||
autofocus
|
||||
required
|
||||
bind:value={deleteProjectName} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
text
|
||||
on:click={() => {
|
||||
resetDeleteState();
|
||||
}}>Cancel</Button>
|
||||
<Button
|
||||
submissionLoader
|
||||
submit
|
||||
disabled={(deleteProjectName ?? '') !== projectToDelete?.name}>
|
||||
Delete
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.archive-projects-margin-top {
|
||||
margin-top: 36px;
|
||||
}
|
||||
.action-menu-divider {
|
||||
margin-inline: -1rem;
|
||||
padding-block-start: 0.25rem;
|
||||
padding-block-end: 0.25rem;
|
||||
}
|
||||
|
||||
.archive-projects-margin {
|
||||
margin-top: 16px;
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS changes may take time to
|
||||
propagate fully.
|
||||
Add the following nameservers on your DNS provider. Note that changes may take up to 48
|
||||
hours to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ function initQueries(initialValue = new Map<TagValue, string>()) {
|
||||
const currentLocation = window.location.pathname;
|
||||
|
||||
if (usableQueries.size) {
|
||||
const queryParam = mapToQueryParams(get(queries));
|
||||
const queryParam = mapToQueryParams(usableQueries);
|
||||
goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
|
||||
} else {
|
||||
goto(currentLocation, { noScroll: true });
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
const url = new URL(pageStore.url);
|
||||
const previousLimit = Number(url.searchParams.get('limit'));
|
||||
url.searchParams.set('limit', limit.toString());
|
||||
preferences.setLimit(limit);
|
||||
await preferences.setLimit(limit);
|
||||
|
||||
if (url.searchParams.has('page')) {
|
||||
const page = Number(url.searchParams.get('page'));
|
||||
|
||||
@@ -250,8 +250,6 @@
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{#if project && $isSmallViewport}
|
||||
<Divider />
|
||||
|
||||
<div class="mobile-tablet-settings">
|
||||
<Tooltip placement="right" disabled={state !== 'icons'}>
|
||||
<a
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 534 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 349 KiB |
@@ -1,34 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
// TODO: needs better props
|
||||
let {
|
||||
expanded = false,
|
||||
slotSpacing = false,
|
||||
overlapCover = false,
|
||||
paddingInlineEnd = true,
|
||||
paddingInlineEndDouble = false,
|
||||
insideSideSheet = false,
|
||||
databasesScreen = false,
|
||||
databasesMainScreen = false,
|
||||
expandHeightButton = false,
|
||||
size = null,
|
||||
children,
|
||||
...restProps
|
||||
}: {
|
||||
expanded?: boolean;
|
||||
slotSpacing?: boolean;
|
||||
overlapCover?: boolean;
|
||||
paddingInlineEnd?: boolean;
|
||||
paddingInlineEndDouble?: boolean;
|
||||
insideSideSheet?: boolean;
|
||||
databasesScreen?: boolean;
|
||||
databasesMainScreen?: boolean;
|
||||
expandHeightButton?: boolean;
|
||||
children?: Snippet;
|
||||
size?: 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | null;
|
||||
} & HTMLAttributes<HTMLDivElement> = $props();
|
||||
|
||||
export let expanded = false;
|
||||
export let slotSpacing = false;
|
||||
export let overlapCover = false;
|
||||
export let paddingInlineEnd = true;
|
||||
export let insideSideSheet = false;
|
||||
export let databasesScreen = false;
|
||||
export let expandHeightButton = false;
|
||||
export let size: 'small' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' = null;
|
||||
|
||||
$: style = size
|
||||
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
|
||||
: '';
|
||||
const style = $derived(
|
||||
size
|
||||
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
|
||||
: ''
|
||||
);
|
||||
</script>
|
||||
|
||||
<div style:container-type="inline-size" class:overlap-cover={overlapCover} {...$$restProps}>
|
||||
<div style:container-type="inline-size" class:overlap-cover={overlapCover} {...restProps}>
|
||||
<div
|
||||
{style}
|
||||
class:expanded
|
||||
class:slotSpacing
|
||||
class:databasesScreen
|
||||
class:insideSideSheet
|
||||
class:databasesScreen
|
||||
class:expandHeightButton
|
||||
class:databasesMainScreen
|
||||
class="console-container"
|
||||
class:paddingInlineEndDouble
|
||||
class:paddingInlineEnd={!paddingInlineEnd}>
|
||||
<Layout.Stack gap="l">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,6 +112,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.paddingInlineEndDouble {
|
||||
@media (min-width: 1024px) {
|
||||
padding-inline-end: calc(2 * 2.75rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.databasesScreen {
|
||||
@media (min-width: 1440px) {
|
||||
min-width: 1070px;
|
||||
@@ -102,6 +130,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.databasesMainScreen {
|
||||
@media (min-width: 1440px) {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 360px) {
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let expanded: boolean = false;
|
||||
export let animate: boolean = false;
|
||||
export let collapsed: boolean = false;
|
||||
export let databasesMainScreen: boolean = false;
|
||||
|
||||
let isAnimating = false;
|
||||
let animationTimeout: ReturnType<typeof setTimeout>;
|
||||
@@ -43,6 +44,7 @@
|
||||
class="cover-container"
|
||||
{style}
|
||||
class:expanded
|
||||
class:databasesMainScreen
|
||||
class:collapsed={animate && collapsed}
|
||||
class:animating={isAnimating}>
|
||||
<Layout.Stack direction="row" alignItems="baseline">
|
||||
@@ -119,6 +121,12 @@
|
||||
&.animating {
|
||||
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.databasesMainScreen {
|
||||
@media (min-width: 1440px) {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-slot {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { getSidebarState, isInDatabasesRoute, updateSidebarState } from '$lib/helpers/sidebar';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
|
||||
export let showHeader = true;
|
||||
export let showFooter = true;
|
||||
@@ -100,8 +101,15 @@
|
||||
*
|
||||
* This needs to be handled like this because
|
||||
* the setup around the sidebar is very tightly configured with 2 states sync.
|
||||
*
|
||||
* The sidebar is **always closed** on mobile and tablet devices!
|
||||
*/
|
||||
afterNavigate((navigation) => {
|
||||
if ($isTabletViewport) {
|
||||
state = 'closed';
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnteringDatabase = isInDatabasesRoute(navigation.to.route);
|
||||
const isLeavingDatabase =
|
||||
isInDatabasesRoute(navigation.from.route) && !isInDatabasesRoute(navigation.to.route);
|
||||
|
||||
@@ -3,8 +3,8 @@ import { isSameDay } from '$lib/helpers/date';
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
import TablesApiLight from '$lib/images/promos/tables-api-light.png';
|
||||
import TablesApiDark from '$lib/images/promos/tables-api-dark.png';
|
||||
import TimestampOverridesDark from '$lib/images/promos/timestamp-overrides-dark.png';
|
||||
import TimestampOverridesLight from '$lib/images/promos/timestamp-overrides-light.png';
|
||||
import TimeHelperQueriesDark from '$lib/images/promos/time-helper-queries-dark.png';
|
||||
import TimeHelperQueriesLight from '$lib/images/promos/time-helper-queries-light.png';
|
||||
import OptInRelationDark from '$lib/images/promos/opt-relation-dark.png';
|
||||
import OptInRelationLight from '$lib/images/promos/opt-relation-light.png';
|
||||
|
||||
@@ -32,21 +32,20 @@ if (isCloud) {
|
||||
show: true
|
||||
};
|
||||
|
||||
const timestampOverridesPromo: BottomModalAlertItem = {
|
||||
id: 'modal:timestamp_overrides_announcement',
|
||||
const timeHelperQueriesPromo: BottomModalAlertItem = {
|
||||
id: 'modal:time_helper_queries_announcement',
|
||||
src: {
|
||||
dark: TimestampOverridesDark,
|
||||
light: TimestampOverridesLight
|
||||
dark: TimeHelperQueriesDark,
|
||||
light: TimeHelperQueriesLight
|
||||
},
|
||||
title: 'Announcing Timestamp Overrides',
|
||||
message:
|
||||
'Move historical data into Appwrite without losing context or disrupting chronological accuracy.',
|
||||
title: 'Announcing Time helper queries',
|
||||
message: 'New before/after filters for simpler time-based queries.',
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
scope: 'project',
|
||||
cta: {
|
||||
text: 'Read announcement',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-timestamp-overrides',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-time-helper-queries',
|
||||
external: true,
|
||||
hideOnClick: true
|
||||
},
|
||||
@@ -73,7 +72,7 @@ if (isCloud) {
|
||||
},
|
||||
show: true
|
||||
};
|
||||
listOfPromotions.push(optInRelationPromo, tablesApiPromo, timestampOverridesPromo);
|
||||
listOfPromotions.push(timeHelperQueriesPromo, optInRelationPromo, tablesApiPromo);
|
||||
}
|
||||
|
||||
export function addBottomModalAlerts() {
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
$: projectCreationDisabled =
|
||||
(isCloud && getServiceLimit('projects') <= data.allProjectsCount) ||
|
||||
($readOnly && !GRACE_PERIOD_OVERRIDE) ||
|
||||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
|
||||
!$canWriteProjects;
|
||||
|
||||
$: $registerCommands([
|
||||
@@ -106,12 +106,14 @@
|
||||
function isSetToArchive(project: Models.Project): boolean {
|
||||
if (!isCloud) return false;
|
||||
if (!project || !project.$id) return false;
|
||||
return project.status !== 'active';
|
||||
return project.status === 'archived';
|
||||
}
|
||||
|
||||
$: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active');
|
||||
$: projectsToArchive = isCloud
|
||||
? data.projects.projects.filter((project) => project.status === 'archived')
|
||||
: [];
|
||||
|
||||
$: activeProjects = data.projects.projects.filter((project) => project.status === 'active');
|
||||
$: activeProjects = data.projects.projects.filter((project) => project.status !== 'archived');
|
||||
function clearSearch() {
|
||||
searchQuery?.clearInput();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { CardGrid, PaginationInline } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
@@ -28,6 +29,7 @@
|
||||
IconRefresh
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let limit = $state(5);
|
||||
let offset = $state(0);
|
||||
let isLoadingInvoices = $state(false);
|
||||
let invoiceList: InvoiceList = $state({
|
||||
@@ -35,16 +37,29 @@
|
||||
total: 0
|
||||
});
|
||||
|
||||
const limit = 5;
|
||||
const endpoint = getApiEndpoint();
|
||||
const hasPaymentError = $derived(invoiceList?.invoices.some((invoice) => invoice?.lastError));
|
||||
|
||||
async function request() {
|
||||
/**
|
||||
* Special case handling for the first page!
|
||||
*
|
||||
* As per Damodar - `there is some logic to **hide current cycle invoice** in the endpoint`.
|
||||
*
|
||||
* Due to this, the first page always loads `limit - 1` invoices which is inconsistent!
|
||||
* Therefore, we load `limit + 1` to counter that so the returned invoices are consistent.
|
||||
*/
|
||||
onMount(() => request(true));
|
||||
|
||||
async function request(patchQuery: boolean = false) {
|
||||
isLoadingInvoices = true;
|
||||
invoiceList = await sdk.forConsole.billing.listInvoices(page.params.organization, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc('$createdAt')
|
||||
Query.orderDesc('$createdAt'),
|
||||
|
||||
// first page extra must have an extra limit!
|
||||
Query.limit(patchQuery ? limit + 1 : limit),
|
||||
|
||||
// so an invoice isn't repeated on 2nd page!
|
||||
Query.offset(patchQuery ? offset : offset + 1)
|
||||
]);
|
||||
|
||||
isLoadingInvoices = false;
|
||||
@@ -62,17 +77,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (offset !== null) {
|
||||
request();
|
||||
}
|
||||
});
|
||||
|
||||
const columns = $derived([
|
||||
{ id: 'dueDate', width: { min: 120 } },
|
||||
{ id: 'status', width: { min: hasPaymentError ? 200 : 100 } },
|
||||
{ id: 'amount', width: { min: 120 } },
|
||||
{ id: 'action', width: 40 }
|
||||
{ id: 'actions', width: 40 }
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -86,11 +95,11 @@
|
||||
<Table.Header.Cell column="dueDate" {root}>Due date</Table.Header.Cell>
|
||||
<Table.Header.Cell column="status" {root}>Status</Table.Header.Cell>
|
||||
<Table.Header.Cell column="amount" {root}>Amount due</Table.Header.Cell>
|
||||
<Table.Header.Cell column="action" {root} />
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isLoadingInvoices}
|
||||
{#each Array.from({ length: 2 }).keys() as index (index)}
|
||||
{#each Array.from({ length: 5 }).keys() as index (index)}
|
||||
<Table.Row.Base {root}>
|
||||
{#each columns as column}
|
||||
<Table.Cell column={column.id} {root}>
|
||||
@@ -99,91 +108,95 @@
|
||||
{/each}
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#each invoiceList?.invoices as invoice}
|
||||
{@const status = invoice.status}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="dueDate" {root}>
|
||||
{toLocaleDate(invoice.dueAt)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{@const isDanger =
|
||||
status === 'overdue' ||
|
||||
status === 'failed' ||
|
||||
status === 'requires_authentication'}
|
||||
{@const isSuccess = status === 'paid' || status === 'succeeded'}
|
||||
{@const isWarning = status === 'pending'}
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
content={status === 'requires_authentication'
|
||||
? 'failed'
|
||||
: status}
|
||||
type={isDanger
|
||||
? 'error'
|
||||
: isWarning
|
||||
? 'warning'
|
||||
: isSuccess
|
||||
? 'success'
|
||||
: undefined} />
|
||||
{#if invoice?.lastError}
|
||||
<Popover let:toggle>
|
||||
<Link.Button on:click={toggle}>Details</Link.Button>
|
||||
<svelte:fragment slot="tooltip">
|
||||
The scheduled payment has failed.
|
||||
<Link.Button on:click={() => retryPayment(invoice)}
|
||||
>Try again
|
||||
</Link.Button>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="amount" {root}>
|
||||
{formatCurrency(invoice.grossAmount)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
<Popover let:toggle placement="bottom-start" padding="none">
|
||||
<Button text icon ariaLabel="more options" on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<!-- todo: add missing event -->
|
||||
<ActionMenu.Item.Anchor
|
||||
leadingIcon={IconExternalLink}
|
||||
external
|
||||
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
|
||||
View invoice
|
||||
</ActionMenu.Item.Anchor>
|
||||
<ActionMenu.Item.Anchor
|
||||
leadingIcon={IconDownload}
|
||||
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
|
||||
Download PDF
|
||||
</ActionMenu.Item.Anchor>
|
||||
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconRefresh}
|
||||
on:click={() => {
|
||||
retryPayment(invoice);
|
||||
trackEvent(`click_retry_payment`, {
|
||||
from: 'button',
|
||||
source: 'billing_invoice_menu'
|
||||
});
|
||||
}}>
|
||||
Retry payment
|
||||
</ActionMenu.Item.Button>
|
||||
{:else}
|
||||
{#each invoiceList?.invoices as invoice (invoice.$id)}
|
||||
{@const status = invoice.status}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="dueDate" {root}
|
||||
>{toLocaleDate(invoice.dueAt)}</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{@const isDanger =
|
||||
status === 'overdue' ||
|
||||
status === 'failed' ||
|
||||
status === 'requires_authentication'}
|
||||
{@const isSuccess = status === 'paid' || status === 'succeeded'}
|
||||
{@const isWarning = status === 'pending'}
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
content={status === 'requires_authentication'
|
||||
? 'failed'
|
||||
: status}
|
||||
type={isDanger
|
||||
? 'error'
|
||||
: isWarning
|
||||
? 'warning'
|
||||
: isSuccess
|
||||
? 'success'
|
||||
: undefined} />
|
||||
{#if invoice?.lastError}
|
||||
<Popover let:toggle>
|
||||
<Link.Button on:click={toggle}>Details</Link.Button>
|
||||
<svelte:fragment slot="tooltip">
|
||||
The scheduled payment has failed.
|
||||
<Link.Button on:click={() => retryPayment(invoice)}
|
||||
>Try again
|
||||
</Link.Button>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="amount" {root}>
|
||||
{formatCurrency(invoice.grossAmount)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Popover let:toggle placement="bottom-start" padding="none">
|
||||
<Button text icon ariaLabel="more options" on:click={toggle}>
|
||||
<Icon icon={IconDotsHorizontal} size="s" />
|
||||
</Button>
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
<!-- todo: add missing event -->
|
||||
<ActionMenu.Item.Anchor
|
||||
leadingIcon={IconExternalLink}
|
||||
external
|
||||
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
|
||||
View invoice
|
||||
</ActionMenu.Item.Anchor>
|
||||
<ActionMenu.Item.Anchor
|
||||
leadingIcon={IconDownload}
|
||||
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
|
||||
Download PDF
|
||||
</ActionMenu.Item.Anchor>
|
||||
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconRefresh}
|
||||
on:click={() => {
|
||||
retryPayment(invoice);
|
||||
trackEvent(`click_retry_payment`, {
|
||||
from: 'button',
|
||||
source: 'billing_invoice_menu'
|
||||
});
|
||||
}}>
|
||||
Retry payment
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Root>
|
||||
{#if invoiceList.total > limit}
|
||||
{#if invoiceList.total >= limit}
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<p class="text">Total results: {invoiceList.total}</p>
|
||||
<PaginationInline {limit} bind:offset total={invoiceList.total} hidePages />
|
||||
<PaginationInline
|
||||
{limit}
|
||||
hidePages
|
||||
bind:offset
|
||||
total={invoiceList.total}
|
||||
on:change={() => request()} />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS changes may take
|
||||
time to propagate fully.
|
||||
Add the following nameservers on your DNS provider. Note that changes may take up to
|
||||
48 hours to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
>{selectedDomain.domain}</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS changes may take time
|
||||
to propagate fully.
|
||||
Add the following nameservers on your DNS provider. Note that changes may take up to 48
|
||||
hours to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
const databaseId = page.params.database;
|
||||
</script>
|
||||
|
||||
<Container paddingInlineEnd={false}>
|
||||
<Container databasesMainScreen>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<SearchQuery placeholder="Search by name or ID" />
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container size="xxl">
|
||||
<Container size="xxl" databasesMainScreen>
|
||||
<div class="u-flex u-gap-32 u-flex-vertical-mobile">
|
||||
{#if !isDisabled}
|
||||
<div class="u-flex-vertical u-gap-16 policies-holder-card">
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
<Cover expanded>
|
||||
<Cover databasesMainScreen>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/databases`}>
|
||||
{$database.name}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
</script>
|
||||
|
||||
{#if $database}
|
||||
<Container>
|
||||
<Container databasesMainScreen>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">{$database.name}</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</Sidebar.Base>
|
||||
{:else}
|
||||
{:else if data?.database?.name}
|
||||
<Navbar.Base>
|
||||
<div slot="left">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
view,
|
||||
query,
|
||||
currentSort,
|
||||
parsedQueries,
|
||||
rows: await sdk.forProject(params.region, params.project).tablesDB.listRows({
|
||||
databaseId: params.database,
|
||||
tableId: params.table,
|
||||
|
||||
+8
-1
@@ -10,11 +10,13 @@
|
||||
row = $bindable(null),
|
||||
onChange = null,
|
||||
onRevert = null,
|
||||
noInlineEdit = false,
|
||||
openSideSheet = null,
|
||||
onRowStructureUpdate = null
|
||||
}: {
|
||||
row: Models.Row;
|
||||
column: Columns;
|
||||
noInlineEdit?: boolean;
|
||||
openSideSheet?: () => void;
|
||||
onChange?: (row: Models.DefaultRow) => void;
|
||||
onRevert?: (row: Models.DefaultRow) => void;
|
||||
@@ -27,6 +29,11 @@
|
||||
onMount(() => {
|
||||
original = structuredClone(row);
|
||||
|
||||
if (noInlineEdit) {
|
||||
openSideSheet?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const trigger = wrapperEl.querySelector('button.input') as HTMLButtonElement;
|
||||
if (trigger) {
|
||||
trigger.click();
|
||||
@@ -62,5 +69,5 @@
|
||||
fromSpreadsheet
|
||||
label={undefined}
|
||||
bind:formValues={row}
|
||||
on:click={openSideSheet} />
|
||||
on:click={() => openSideSheet?.()} />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
label = undefined;
|
||||
}
|
||||
|
||||
$: if (limited) {
|
||||
$: {
|
||||
column.min = isWithinSafeRange(column.min) ? column.min : Number.MIN_SAFE_INTEGER;
|
||||
column.max = isWithinSafeRange(column.max) ? column.max : Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
+6
-2
@@ -2,8 +2,12 @@ import { page } from '$app/state';
|
||||
import type { Columns } from '../store';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
|
||||
export function isRelationshipToMany(column: Models.ColumnRelationship) {
|
||||
if (!column) return false;
|
||||
export function isRelationshipToMany(col: Columns) {
|
||||
if (!col) return false;
|
||||
if (!isRelationship(col)) return false;
|
||||
|
||||
const column = col as Models.ColumnRelationship;
|
||||
|
||||
if (!column?.relationType) return false;
|
||||
if (column?.side === 'child') {
|
||||
return !['oneToOne', 'oneToMany'].includes(column?.relationType);
|
||||
|
||||
+46
-29
@@ -87,6 +87,7 @@
|
||||
import { hash } from '$lib/helpers/string';
|
||||
import { formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { chunks } from '$lib/helpers/array';
|
||||
import { mapToQueryParams } from '$lib/components/filters/store';
|
||||
|
||||
export let data: PageData;
|
||||
export let showRowCreateSheet: {
|
||||
@@ -308,26 +309,31 @@
|
||||
|
||||
async function sort(query: string | null) {
|
||||
$spreadsheetLoading = true;
|
||||
|
||||
const url = new URL(page.url);
|
||||
const parsedQueries = data.parsedQueries;
|
||||
|
||||
if (query === null) {
|
||||
if (parsedQueries.size > 0) {
|
||||
for (const [tagValue, queryString] of parsedQueries.entries()) {
|
||||
if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) {
|
||||
parsedQueries.delete(tagValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (query !== null) {
|
||||
const { attribute, method } = JSON.parse(query);
|
||||
const tagValue = {
|
||||
tag: `${attribute} ${method}`,
|
||||
value: attribute
|
||||
};
|
||||
|
||||
parsedQueries.set(tagValue, query);
|
||||
}
|
||||
|
||||
if (parsedQueries.size === 0) {
|
||||
url.searchParams.delete('query');
|
||||
} else {
|
||||
// compatible with `load` func!
|
||||
const { attribute, method } = JSON.parse(query);
|
||||
url.searchParams.set(
|
||||
'query',
|
||||
JSON.stringify([
|
||||
[
|
||||
{
|
||||
tag: `${attribute} ${method}`,
|
||||
value: attribute
|
||||
},
|
||||
query
|
||||
]
|
||||
])
|
||||
);
|
||||
url.searchParams.set('query', mapToQueryParams(parsedQueries));
|
||||
}
|
||||
|
||||
// save > navigate > restore!
|
||||
@@ -459,6 +465,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openSideSheetForRelationsToMany(tableId: string, rows: string | Models.Row[]) {
|
||||
$databaseRelatedRowSheetOptions.tableId = tableId;
|
||||
$databaseRelatedRowSheetOptions.rows = rows;
|
||||
$databaseRelatedRowSheetOptions.show = true;
|
||||
}
|
||||
|
||||
async function onSelectSheetOption(
|
||||
action: HeaderCellAction | RowCellAction,
|
||||
columnId: string,
|
||||
@@ -605,6 +617,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsedQueries = data.parsedQueries;
|
||||
const filterQueries = parsedQueries.size ? data.parsedQueries.values() : [];
|
||||
|
||||
$paginatedRowsLoading = true;
|
||||
const loadedRows = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
@@ -615,6 +630,7 @@
|
||||
getCorrectOrderQuery(),
|
||||
Query.limit(SPREADSHEET_PAGE_LIMIT),
|
||||
Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)),
|
||||
...filterQueries /* filter queries */,
|
||||
...buildWildcardColumnsQuery($table)
|
||||
]
|
||||
});
|
||||
@@ -885,18 +901,10 @@
|
||||
{/if}
|
||||
{:else}
|
||||
{@const itemsNum = row[columnId]?.length}
|
||||
<Button.Button
|
||||
variant="extra-compact"
|
||||
disabled={!itemsNum}
|
||||
badge={itemsNum ?? 0}
|
||||
on:click={() => {
|
||||
$databaseRelatedRowSheetOptions.show = true;
|
||||
$databaseRelatedRowSheetOptions.rows =
|
||||
row[columnId];
|
||||
$databaseRelatedRowSheetOptions.tableId = columnId;
|
||||
}}>
|
||||
Items
|
||||
</Button.Button>
|
||||
Items <Badge
|
||||
content={itemsNum}
|
||||
variant="secondary"
|
||||
size="s" />
|
||||
{/if}
|
||||
{:else}
|
||||
{@const value = row[columnId]}
|
||||
@@ -946,11 +954,20 @@
|
||||
{row}
|
||||
column={rowColumn}
|
||||
onRowStructureUpdate={updateRowContents}
|
||||
noInlineEdit={isRelationshipToMany(rowColumn)}
|
||||
onChange={(row) => paginatedRows.update(index, row)}
|
||||
onRevert={(row) => paginatedRows.update(index, row)}
|
||||
openSideSheet={() => {
|
||||
close(); /* closes the editor */
|
||||
onSelectSheetOption('update', null, 'row', row);
|
||||
|
||||
if (isRelationshipToMany(rowColumn)) {
|
||||
openSideSheetForRelationsToMany(
|
||||
columnId,
|
||||
row[columnId]
|
||||
);
|
||||
} else {
|
||||
onSelectSheetOption('update', null, 'row', row);
|
||||
}
|
||||
}} />
|
||||
</svelte:fragment>
|
||||
</Spreadsheet.Cell>
|
||||
|
||||
+12
-29
@@ -1,14 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Container, Usage, UsageMultiple } from '$lib/layout';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Container, UsageMultiple } from '$lib/layout';
|
||||
|
||||
export let data;
|
||||
|
||||
$: total = data.tablesTotal;
|
||||
$: count = data.tables;
|
||||
|
||||
$: reads = data.databaseReads;
|
||||
$: readsTotal = data.databaseReadsTotal;
|
||||
|
||||
@@ -16,26 +10,15 @@
|
||||
$: writesTotal = data.databaseWritesTotal;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Layout.Stack gap="l">
|
||||
<Usage
|
||||
path={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/usage`}
|
||||
{total}
|
||||
{count}
|
||||
countMetadata={{
|
||||
legend: 'Tables',
|
||||
title: 'Total tables'
|
||||
}} />
|
||||
|
||||
<UsageMultiple
|
||||
title="Reads and writes"
|
||||
showHeader={false}
|
||||
total={[readsTotal, writesTotal]}
|
||||
count={[reads, writes]}
|
||||
legendNumberFormat="abbreviate"
|
||||
legendData={[
|
||||
{ name: 'Reads', value: readsTotal },
|
||||
{ name: 'Writes', value: writesTotal }
|
||||
]} />
|
||||
</Layout.Stack>
|
||||
<Container databasesMainScreen>
|
||||
<UsageMultiple
|
||||
title="Reads and writes"
|
||||
showHeader={false}
|
||||
total={[readsTotal, writesTotal]}
|
||||
count={[reads, writes]}
|
||||
legendNumberFormat="abbreviate"
|
||||
legendData={[
|
||||
{ name: 'Reads', value: readsTotal },
|
||||
{ name: 'Writes', value: writesTotal }
|
||||
]} />
|
||||
</Container>
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@
|
||||
<Badge variant="secondary" type="warning" content="Pending verification" />
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS changes may
|
||||
take time to propagate fully.
|
||||
Add the following nameservers on your DNS provider. Note that changes may take
|
||||
up to 48 hours to propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user