mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into bun
This commit is contained in:
@@ -268,7 +268,7 @@ export enum Submit {
|
||||
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
|
||||
AuthMembershipPrivacyUpdate = 'submit_auth_membership_privacy_update',
|
||||
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
|
||||
AuthInvalidateSesssion = 'submit_auth_invalidate_session',
|
||||
AuthInvalidateSession = 'submit_auth_invalidate_session',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
SessionDelete = 'submit_session_delete',
|
||||
@@ -277,6 +277,7 @@ export enum Submit {
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
DatabaseImportCsv = 'submit_database_import_csv',
|
||||
DatabaseBackupDelete = 'submit_database_backup_delete',
|
||||
|
||||
ColumnCreate = 'submit_column_create',
|
||||
ColumnUpdate = 'submit_column_update',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import PaginationWithLimit from './paginationWithLimit.svelte';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { GridItem1, CardContainer, Modal } from '$lib/components';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
@@ -45,9 +46,19 @@
|
||||
projectsToArchive: Models.Project[];
|
||||
organization: Organization;
|
||||
currentPlan: Plan;
|
||||
archivedTotalOverall: number;
|
||||
archivedOffset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
let { projectsToArchive, organization, currentPlan }: Props = $props();
|
||||
let {
|
||||
projectsToArchive,
|
||||
organization,
|
||||
currentPlan,
|
||||
archivedTotalOverall,
|
||||
archivedOffset,
|
||||
limit
|
||||
}: Props = $props();
|
||||
|
||||
// Check if current plan order is less than Pro (order < 1 means FREE plan)
|
||||
let isPlanBelowPro = $derived(currentPlan?.order < 1);
|
||||
@@ -187,7 +198,7 @@
|
||||
}
|
||||
|
||||
import { formatName as formatNameHelper } from '$lib/helpers/string';
|
||||
function formatName(name: string, limit: number = 19) {
|
||||
function formatName(name: string, limit: number = 16) {
|
||||
return formatNameHelper(name, limit, $isSmallViewport);
|
||||
}
|
||||
</script>
|
||||
@@ -196,7 +207,7 @@
|
||||
<div class="archive-projects-margin-top">
|
||||
<Accordion
|
||||
title={isPlanBelowPro ? 'Archived projects' : 'Pending archive'}
|
||||
badge={`${projectsToArchive.length}`}>
|
||||
badge={`${archivedTotalOverall}`}>
|
||||
<Typography.Text tag="p" size="s">
|
||||
{#if isPlanBelowPro}
|
||||
These projects are archived and require a plan upgrade to restore access.
|
||||
@@ -206,7 +217,7 @@
|
||||
</Typography.Text>
|
||||
|
||||
<div class="archive-projects-margin">
|
||||
<CardContainer disableEmpty={true} total={projectsToArchive.length}>
|
||||
<CardContainer disableEmpty={true} total={archivedTotalOverall}>
|
||||
{#each projectsToArchive as project}
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
@@ -266,7 +277,7 @@
|
||||
</Badge>
|
||||
{/each}
|
||||
|
||||
{#if platforms.length > 3}
|
||||
{#if platforms.length > 2}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
content={`+${platforms.length - 2}`}
|
||||
@@ -282,6 +293,15 @@
|
||||
</GridItem1>
|
||||
{/each}
|
||||
</CardContainer>
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Archived Projects"
|
||||
{limit}
|
||||
offset={archivedOffset}
|
||||
total={archivedTotalOverall}
|
||||
pageParam="archivedPage"
|
||||
removeOnFirstPage
|
||||
class="pagination-container" />
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
@@ -355,4 +375,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
:global(.pagination-container) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, paymentMissingMandate } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmSetup } from '$lib/stores/stripe';
|
||||
|
||||
async function verifyPaymentMethod() {
|
||||
const method = await sdk.forConsole.billing.setupPaymentMandate(
|
||||
$organization.$id,
|
||||
$paymentMissingMandate.$id
|
||||
);
|
||||
await confirmSetup(method.clientSecret, method.providerMethodId);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="info">
|
||||
The payment method for {$organization.name} needs to be verified.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button secondary on:click={verifyPaymentMethod}>Verify payment method</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -35,16 +35,22 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<Table.Root
|
||||
columns={[
|
||||
{ id: 'type', width: { min: 150 } },
|
||||
{ id: 'name', width: { min: 80 } },
|
||||
{ id: 'value', width: { min: 100 } }
|
||||
]}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell column="name" {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="value" {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>CNAME</Table.Cell>
|
||||
<Table.Cell {root}>{subdomain}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<Table.Cell column="type" {root}>CNAME</Table.Cell>
|
||||
<Table.Cell column="name" {root}>{subdomain}</Table.Cell>
|
||||
<Table.Cell column="value" {root}>
|
||||
<InteractiveText
|
||||
variant="copy"
|
||||
isVisible
|
||||
|
||||
@@ -101,29 +101,35 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<Table.Root
|
||||
columns={[
|
||||
{ id: 'type', width: { min: 150 } },
|
||||
{ id: 'name', width: { min: 80 } },
|
||||
{ id: 'value', width: { min: 100 } }
|
||||
]}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell column="name" {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="value" {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>{variant.toUpperCase()}</Table.Cell>
|
||||
<Table.Cell {root}>{subdomain || '@'}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<Table.Cell column="type" {root}>{variant.toUpperCase()}</Table.Cell>
|
||||
<Table.Cell column="name" {root}>{subdomain || '@'}</Table.Cell>
|
||||
<Table.Cell column="value" {root}>
|
||||
<InteractiveText variant="copy" isVisible text={setTarget()} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{#if $regionalConsoleVariables._APP_DOMAIN_TARGET_CAA}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Table.Cell column="type" {root}>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<span>CAA</span>
|
||||
<Badge variant="secondary" size="xs" content="Recommended" />
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>@</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<Table.Cell column="name" {root}>@</Table.Cell>
|
||||
<Table.Cell column="value" {root}>
|
||||
<InteractiveText variant="copy" isVisible text={caaText} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
export let sum: number;
|
||||
export let limit: number;
|
||||
export let name: string;
|
||||
export let pageParam: string = 'page';
|
||||
export let removeOnFirstPage: boolean = false;
|
||||
|
||||
const options = [
|
||||
{ label: '6', value: 6 },
|
||||
@@ -23,10 +25,17 @@
|
||||
url.searchParams.set('limit', limit.toString());
|
||||
await preferences.setLimit(limit);
|
||||
|
||||
if (url.searchParams.has('page')) {
|
||||
const page = Number(url.searchParams.get('page'));
|
||||
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
if (url.searchParams.has(pageParam)) {
|
||||
const page = Number(url.searchParams.get(pageParam));
|
||||
const prev =
|
||||
Number.isFinite(previousLimit) && previousLimit > 0 ? previousLimit : limit;
|
||||
const newPage = Math.floor(((page - 1) * prev) / limit) + 1;
|
||||
const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
|
||||
if (removeOnFirstPage && safePage === 1) {
|
||||
url.searchParams.delete(pageParam);
|
||||
} else {
|
||||
url.searchParams.set(pageParam, safePage.toString());
|
||||
}
|
||||
}
|
||||
|
||||
await goto(url.toString());
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script lang="ts" module>
|
||||
export type DeleteOperationState = Error | void;
|
||||
export type DeleteOperationState = {
|
||||
error?: Error;
|
||||
deleted: string[];
|
||||
} | void;
|
||||
|
||||
export type DeleteOperation = (
|
||||
deleteFn: (id: string) => Promise<unknown>,
|
||||
batchSize?: number
|
||||
) => Promise<Exclude<DeleteOperationState, void>>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -40,7 +48,14 @@
|
||||
children: Snippet<[root: TableRootProps]>;
|
||||
deleteContent?: Snippet<[count: number]>;
|
||||
deleteContentNotice?: Snippet;
|
||||
onDelete?: (selectedRows: string[]) => Promise<DeleteOperationState> | DeleteOperationState;
|
||||
onDelete?: (
|
||||
batchDelete: DeleteOperation,
|
||||
/**
|
||||
* this is useful when you have a custom deletion logic
|
||||
* and the default `batchDelete` helper doesn't fit the use-case!
|
||||
*/
|
||||
selectedRows: string[]
|
||||
) => Promise<DeleteOperationState> | DeleteOperationState;
|
||||
onCancel?: () => Promise<void> | void;
|
||||
} = $props();
|
||||
|
||||
@@ -49,10 +64,8 @@
|
||||
let onDeleteError: string | null = $state(null);
|
||||
let showConfirmDeletion: boolean = $state(false);
|
||||
|
||||
function notifySuccess() {
|
||||
function notifySuccess(count: number) {
|
||||
if (!showSuccessNotification) return;
|
||||
|
||||
const count = selectedRows.length;
|
||||
if (count === 0) return;
|
||||
|
||||
const label = `${resource}${count > 1 ? 's' : ''}`;
|
||||
@@ -71,6 +84,75 @@
|
||||
|
||||
return `${resource}s`;
|
||||
}
|
||||
|
||||
async function batchDelete(
|
||||
ids: string[],
|
||||
deleteFn: (id: string) => Promise<unknown>,
|
||||
batchSize: number | undefined = undefined
|
||||
): Promise<Exclude<DeleteOperationState, void>> {
|
||||
const deleted: string[] = [];
|
||||
let firstError: Error | undefined;
|
||||
|
||||
// prevent infinite loop
|
||||
if (batchSize !== undefined) {
|
||||
batchSize = Math.max(1, Math.floor(Math.abs(batchSize)));
|
||||
}
|
||||
|
||||
async function processBatch(batch: string[]) {
|
||||
// build promises
|
||||
const results = await Promise.allSettled(batch.map((id) => deleteFn(id)));
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
// success, log it!
|
||||
deleted.push(batch[index]);
|
||||
} else if (!firstError) {
|
||||
// error
|
||||
firstError =
|
||||
result.reason instanceof Error
|
||||
? result.reason
|
||||
: new Error(String(result.reason));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// batch when needed.
|
||||
// example: >= 100 items to delete!
|
||||
if (batchSize && batchSize < ids.length) {
|
||||
for (let i = 0; i < ids.length; i += batchSize) {
|
||||
const batch = ids.slice(i, i + batchSize);
|
||||
await processBatch(batch);
|
||||
}
|
||||
} else {
|
||||
await processBatch(ids);
|
||||
}
|
||||
|
||||
return {
|
||||
deleted,
|
||||
error: firstError
|
||||
};
|
||||
}
|
||||
|
||||
async function consumeDeleteOperation() {
|
||||
const state = await onDelete?.((deleteFn, batchSize) => {
|
||||
return batchDelete(selectedRows, deleteFn, batchSize);
|
||||
}, selectedRows);
|
||||
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deletedCount = state.deleted.length;
|
||||
selectedRows = selectedRows.filter((id) => !state.deleted.includes(id));
|
||||
|
||||
if (state.error) {
|
||||
onDeleteError = `Some ${getPluralResource()} were not deleted. Error: ${state.error.message}`;
|
||||
return false;
|
||||
}
|
||||
|
||||
notifySuccess(deletedCount);
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key computeKey}
|
||||
@@ -104,13 +186,7 @@
|
||||
if (confirmDeletion) {
|
||||
showConfirmDeletion = true;
|
||||
} else {
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (state instanceof Error) {
|
||||
// user should handle error on their own!
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
}
|
||||
await consumeDeleteOperation();
|
||||
}
|
||||
}}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
@@ -129,16 +205,13 @@
|
||||
disableModal = true;
|
||||
onDeleteError = null;
|
||||
|
||||
const state = await onDelete?.(selectedRows);
|
||||
if (state instanceof Error) {
|
||||
disableModal = false;
|
||||
onDeleteError = state.message || `Failed to delete ${resource}s`;
|
||||
} else {
|
||||
notifySuccess();
|
||||
selectedRows = [];
|
||||
disableModal = false;
|
||||
const allDeleted = await consumeDeleteOperation();
|
||||
|
||||
if (allDeleted) {
|
||||
showConfirmDeletion = false;
|
||||
}
|
||||
|
||||
disableModal = false;
|
||||
}}>
|
||||
<Typography.Text>
|
||||
{@const selectionCount = selectedRows.length}
|
||||
|
||||
@@ -6,15 +6,21 @@
|
||||
export let limit: number;
|
||||
export let offset: number;
|
||||
export let useCreateLink = true;
|
||||
export let pageParam: string = 'page';
|
||||
export let removeOnFirstPage: boolean = false;
|
||||
|
||||
$: currentPage = Math.floor(offset / limit + 1);
|
||||
|
||||
function getLink(page: number): string {
|
||||
const url = new URL(pageStore.url);
|
||||
if (page === 1) {
|
||||
url.searchParams.delete('page');
|
||||
if (removeOnFirstPage) {
|
||||
url.searchParams.delete(pageParam);
|
||||
} else {
|
||||
url.searchParams.set(pageParam, '1');
|
||||
}
|
||||
} else {
|
||||
url.searchParams.set('page', page.toString());
|
||||
url.searchParams.set(pageParam, page.toString());
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
offset,
|
||||
total,
|
||||
name,
|
||||
useCreateLink = true
|
||||
useCreateLink = true,
|
||||
pageParam = 'page',
|
||||
removeOnFirstPage = false,
|
||||
...restProps
|
||||
}: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
name: string;
|
||||
useCreateLink?: boolean;
|
||||
pageParam?: string;
|
||||
removeOnFirstPage?: boolean;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
||||
const showLimit = $derived(!!useCreateLink);
|
||||
@@ -22,10 +28,17 @@
|
||||
const alignItems = $derived(showLimit ? 'center' : 'flex-end');
|
||||
</script>
|
||||
|
||||
<Layout.Stack wrap="wrap" {direction} {alignItems} justifyContent="space-between">
|
||||
<Layout.Stack wrap="wrap" {direction} {alignItems} justifyContent="space-between" {...restProps}>
|
||||
{#if showLimit}
|
||||
<Limit {limit} sum={total} {name} />
|
||||
<Limit {limit} sum={total} {name} {pageParam} {removeOnFirstPage} />
|
||||
{/if}
|
||||
|
||||
<Pagination on:page {limit} {offset} sum={total} {useCreateLink} />
|
||||
<Pagination
|
||||
on:page
|
||||
{limit}
|
||||
{offset}
|
||||
sum={total}
|
||||
{useCreateLink}
|
||||
{pageParam}
|
||||
{removeOnFirstPage} />
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
name = 'items',
|
||||
gap = 's',
|
||||
offset = $bindable(0),
|
||||
children
|
||||
children,
|
||||
...restProps
|
||||
}: {
|
||||
items: T[];
|
||||
limit?: number;
|
||||
@@ -26,6 +27,7 @@
|
||||
| undefined;
|
||||
offset?: number;
|
||||
children: Snippet<[T[], number]>;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
||||
let total = $derived(items.length);
|
||||
@@ -33,7 +35,7 @@
|
||||
let paginatedItems = $derived(items.slice(offset, offset + limit));
|
||||
</script>
|
||||
|
||||
<Layout.Stack {gap}>
|
||||
<Layout.Stack {gap} {...restProps}>
|
||||
{@render children(paginatedItems, limit)}
|
||||
|
||||
{#if !hideFooter}
|
||||
|
||||
@@ -206,6 +206,18 @@ export function timeFromNow(datetime: string): string {
|
||||
return dayjs().to(dayjs(datetime));
|
||||
}
|
||||
|
||||
export function timeFromNowShort(datetime: string): string {
|
||||
if (!datetime || !isValidDate(datetime)) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
const timeStr = dayjs().to(dayjs(datetime));
|
||||
return timeStr
|
||||
.replace('second', 'sec') // seconds > secs
|
||||
.replace('minute', 'min') // minutes > mins
|
||||
.replace('hour', 'hr'); // hours > hrs
|
||||
}
|
||||
|
||||
export function hoursToDays(hours: number) {
|
||||
if (hours > 24) {
|
||||
return `${Math.floor(hours / 24)} days`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { AvatarInitials, PaginationWithLimit, Trim } from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Layout, Table, Card, Empty } from '@appwrite.io/pink-svelte';
|
||||
import { Layout, Table, Card, Empty, InteractiveText } from '@appwrite.io/pink-svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import type { PinkColumn } from '$lib/helpers/types';
|
||||
|
||||
@@ -14,12 +14,32 @@
|
||||
export let databasesScreen = false;
|
||||
export let useCreateLinkForPagination = true;
|
||||
|
||||
function getColumnWidth(columnId: string): Pick<PinkColumn, 'width'> {
|
||||
const widthConfig: Record<
|
||||
string,
|
||||
{ insideSheet: number | { min: number }; default: number | { min: number } }
|
||||
> = {
|
||||
user: { insideSheet: 140, default: { min: 100 } },
|
||||
event: { insideSheet: 125, default: { min: 160 } },
|
||||
location: { insideSheet: 100, default: { min: 120 } },
|
||||
ip: { insideSheet: { min: 150 }, default: { min: 250 } },
|
||||
date: { insideSheet: { min: 200 }, default: { min: 180 } }
|
||||
};
|
||||
|
||||
const config = widthConfig[columnId];
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { width: insideSideSheet ? config.insideSheet : config.default };
|
||||
}
|
||||
|
||||
const columns: PinkColumn[] = [
|
||||
{ id: 'user', ...(insideSideSheet ? { width: 140 } : {}) },
|
||||
{ id: 'event', ...(insideSideSheet ? { width: 125 } : {}) },
|
||||
{ id: 'location', ...(insideSideSheet ? { width: 100 } : {}) },
|
||||
{ id: 'ip', ...(insideSideSheet ? { width: { min: 150 } } : {}) },
|
||||
{ id: 'date', ...(insideSideSheet ? { width: { min: 200 } } : {}) }
|
||||
{ id: 'user', ...getColumnWidth('user') },
|
||||
{ id: 'event', ...getColumnWidth('event') },
|
||||
{ id: 'location', ...getColumnWidth('location') },
|
||||
{ id: 'ip', ...getColumnWidth('ip') },
|
||||
{ id: 'date', ...getColumnWidth('date') }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -29,55 +49,57 @@
|
||||
expanded={databasesScreen && !insideSideSheet}
|
||||
slotSpacing={databasesScreen && !insideSideSheet}>
|
||||
{#if logs.total}
|
||||
<div>
|
||||
<Table.Root {columns} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="user" {root}>User</Table.Header.Cell>
|
||||
<Table.Header.Cell column="event" {root}>Event</Table.Header.Cell>
|
||||
<Table.Header.Cell column="location" {root}>Location</Table.Header.Cell>
|
||||
<Table.Header.Cell column="ip" {root}>IP</Table.Header.Cell>
|
||||
<Table.Header.Cell column="date" {root}>Date</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each logs.logs as log}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="user" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{#if log.userEmail}
|
||||
{#if log.userName}
|
||||
<AvatarInitials size="xs" name={log.userName} />
|
||||
<Trim>{log.userName}</Trim>
|
||||
{:else}
|
||||
<AvatarInitials size="xs" name={log.userEmail} />
|
||||
<Trim>{log.userEmail}</Trim>
|
||||
{/if}
|
||||
<Table.Root {columns} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="user" {root}>User</Table.Header.Cell>
|
||||
<Table.Header.Cell column="event" {root}>Event</Table.Header.Cell>
|
||||
<Table.Header.Cell column="location" {root}>Location</Table.Header.Cell>
|
||||
<Table.Header.Cell column="ip" {root}>IP</Table.Header.Cell>
|
||||
<Table.Header.Cell column="date" {root}>Date</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each logs.logs as log}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="user" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
{#if log.userEmail}
|
||||
{#if log.userName}
|
||||
<AvatarInitials size="xs" name={log.userName} />
|
||||
<Trim>{log.userName}</Trim>
|
||||
{:else}
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-anonymous" aria-hidden="true"></span>
|
||||
</div>
|
||||
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
|
||||
<AvatarInitials size="xs" name={log.userEmail} />
|
||||
<Trim>{log.userEmail}</Trim>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="event" {root}>
|
||||
{log.event}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="location" {root}>
|
||||
{#if log.countryCode !== '--'}
|
||||
{log.countryName}
|
||||
{:else}
|
||||
Unknown
|
||||
<div class="avatar is-size-small">
|
||||
<span class="icon-anonymous" aria-hidden="true"></span>
|
||||
</div>
|
||||
<span class="text u-trim">{log.userName ?? 'Anonymous'}</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="ip" {root}>
|
||||
{log.ip}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="date" {root}>
|
||||
{toLocaleDateTime(log.time)}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell column="event" {root}>
|
||||
{log.event}
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell column="location" {root}>
|
||||
{#if log.countryCode !== '--'}
|
||||
{log.countryName}
|
||||
{:else}
|
||||
Unknown
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell column="ip" {root}>
|
||||
<InteractiveText variant="copy" text={log.ip} isVisible />
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell column="date" {root}>
|
||||
<DualTimeView time={log.time} showDatetime />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
<PaginationWithLimit
|
||||
{limit}
|
||||
|
||||
@@ -1300,26 +1300,6 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async setupPaymentMandate(
|
||||
organizationId: string,
|
||||
paymentMethodId: string
|
||||
): Promise<PaymentMethodData> {
|
||||
const path = `/account/payment-methods/${paymentMethodId}/setup`;
|
||||
const params = {
|
||||
organizationId,
|
||||
paymentMethodId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'patch',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async listAddresses(queries: string[] = []): Promise<AddressesList> {
|
||||
const path = `/account/billing-addresses`;
|
||||
const params = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.
|
||||
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
|
||||
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
|
||||
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
|
||||
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
|
||||
|
||||
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
Invoice,
|
||||
InvoiceList,
|
||||
PaymentList,
|
||||
PaymentMethodData,
|
||||
Plan,
|
||||
PlansMap
|
||||
} from '$lib/sdk/billing';
|
||||
@@ -535,24 +534,6 @@ export function checkForMarkedForDeletion(org: Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
export const paymentMissingMandate = writable<PaymentMethodData>(null);
|
||||
|
||||
export async function checkForMandate(org: Organization) {
|
||||
const paymentId = org.paymentMethodId ?? org.backupPaymentMethodId;
|
||||
if (!paymentId) return;
|
||||
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentId);
|
||||
if (paymentMethod?.mandateId === null && paymentMethod?.country.toLowerCase() === 'in') {
|
||||
headerAlert.add({
|
||||
id: 'paymentMandate',
|
||||
component: PaymentMandate,
|
||||
show: true,
|
||||
importance: 8
|
||||
});
|
||||
activeHeaderAlert.set(headerAlert.get());
|
||||
paymentMissingMandate.set(paymentMethod);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForMissingPaymentMethod() {
|
||||
const orgs = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE),
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import {
|
||||
calculateTrialDay,
|
||||
checkForEnterpriseTrial,
|
||||
checkForMandate,
|
||||
checkForMarkedForDeletion,
|
||||
checkForMissingPaymentMethod,
|
||||
checkForNewDevUpgradePro,
|
||||
@@ -308,7 +307,6 @@
|
||||
if (org?.billingPlan !== BillingPlan.FREE) {
|
||||
await paymentExpired(org);
|
||||
await checkPaymentAuthorizationRequired(org);
|
||||
await checkForMandate(org);
|
||||
|
||||
if ($plansInfo.get(org.billingPlan)?.trialDays) {
|
||||
calculateTrialDay(org);
|
||||
@@ -334,7 +332,6 @@
|
||||
<CommandCenter />
|
||||
<Shell
|
||||
showSideNavigation={page.url.pathname !== '/' &&
|
||||
!page.url.pathname.includes(base + '/account') &&
|
||||
!page.url.pathname.includes(base + '/card') &&
|
||||
!page.url.pathname.includes(base + '/onboarding')}
|
||||
showHeader={!page.url.pathname.includes(base + '/onboarding/create-project')}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { identities } from './store';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { oAuthProviders } from '$lib/stores/oauth-providers';
|
||||
@@ -82,7 +81,13 @@
|
||||
<DualTimeView time={identity.$createdAt} />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="expiryDate" {root}>
|
||||
{toLocaleDateTime(identity.providerAccessTokenExpiry)}
|
||||
{#if identity.providerAccessTokenExpiry}
|
||||
<DualTimeView
|
||||
time={identity.providerAccessTokenExpiry}
|
||||
showDatetime />
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Button text on:click={() => deleteIdentity(identity.$id)}>
|
||||
|
||||
@@ -9,13 +9,27 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
export let selectedPaymentMethod: PaymentMethodData;
|
||||
export let isLinked = false;
|
||||
let {
|
||||
show = $bindable(false),
|
||||
isLinked = false,
|
||||
selectedPaymentMethod
|
||||
}: {
|
||||
show: boolean;
|
||||
isLinked?: boolean;
|
||||
selectedPaymentMethod: PaymentMethodData;
|
||||
} = $props();
|
||||
|
||||
let year: number | null = $state(null);
|
||||
let month: string | null = $state(null);
|
||||
let error: string | null = $state(null);
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
let error: string;
|
||||
let month: string;
|
||||
let year: number;
|
||||
const months = Array.from({ length: 12 }, (_, i) => {
|
||||
const value = String(i + 1).padStart(2, '0');
|
||||
return { value, label: value };
|
||||
});
|
||||
|
||||
const options = $derived(createMonthOptions(year));
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
@@ -24,9 +38,9 @@
|
||||
month,
|
||||
year?.toString()
|
||||
);
|
||||
trackEvent(Submit.PaymentMethodUpdate);
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
show = false;
|
||||
trackEvent(Submit.PaymentMethodUpdate);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Your payment method has been updated'
|
||||
@@ -38,20 +52,6 @@
|
||||
}
|
||||
|
||||
function createMonthOptions(year: number) {
|
||||
const months = [
|
||||
{ value: '01', label: '01' },
|
||||
{ value: '02', label: '02' },
|
||||
{ value: '03', label: '03' },
|
||||
{ value: '04', label: '04' },
|
||||
{ value: '05', label: '05' },
|
||||
{ value: '06', label: '06' },
|
||||
{ value: '07', label: '07' },
|
||||
{ value: '08', label: '08' },
|
||||
{ value: '09', label: '09' },
|
||||
{ value: '10', label: '10' },
|
||||
{ value: '11', label: '11' },
|
||||
{ value: '12', label: '12' }
|
||||
];
|
||||
if (!year) return months;
|
||||
if (year === currentYear) {
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
@@ -60,8 +60,6 @@
|
||||
return months;
|
||||
}
|
||||
}
|
||||
|
||||
$: options = createMonthOptions(year);
|
||||
</script>
|
||||
|
||||
<Modal bind:error onSubmit={handleSubmit} bind:show title="Update payment method">
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
import type { PageData } from './$types';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { onMount } from 'svelte';
|
||||
import { Badge, Layout, Table, Typography, Icon } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Layout,
|
||||
Table,
|
||||
Typography,
|
||||
Icon,
|
||||
InteractiveText
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconGlobeAlt } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -92,9 +99,9 @@
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'client' },
|
||||
{ id: 'location', width: 200 },
|
||||
{ id: 'ip', width: 200 },
|
||||
{ id: 'client', width: { min: 450 } },
|
||||
{ id: 'location', width: { min: 200 } },
|
||||
{ id: 'ip', width: { min: 330 } },
|
||||
{ id: 'actions', width: 100 }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
@@ -146,7 +153,7 @@
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="ip" {root}>
|
||||
{session.ip}
|
||||
<InteractiveText variant="copy" text={session.ip} isVisible />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Button size="xs" secondary on:click={() => logout(session)}>Sign out</Button>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
depends(Dependencies.MEMBERS);
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
const requestedOrg = await checkPlatformAndRedirect(params, organizations, prefs);
|
||||
await checkPlatformAndRedirect(params, organizations, prefs);
|
||||
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
@@ -46,13 +46,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
|
||||
}
|
||||
|
||||
// fetch org only if we haven't already fetched it for platform check
|
||||
const orgPromise: Promise<Organization> = requestedOrg
|
||||
? Promise.resolve(requestedOrg)
|
||||
: (sdk.forConsole.teams.get({ teamId: params.organization }) as Promise<Organization>);
|
||||
|
||||
const [org, members, countryList, locale] = await Promise.all([
|
||||
orgPromise,
|
||||
sdk.forConsole.teams.get({ teamId: params.organization }) as Promise<Organization>,
|
||||
sdk.forConsole.teams.listMemberships({ teamId: params.organization }),
|
||||
sdk.forConsole.locale.listCountries(),
|
||||
sdk.forConsole.locale.get(),
|
||||
@@ -96,7 +91,7 @@ async function checkPlatformAndRedirect(
|
||||
params: { organization: string },
|
||||
organizations: { teams: Array<{ $id: string; platform?: string }> },
|
||||
prefs: Record<string, string>
|
||||
): Promise<Organization | null> {
|
||||
) {
|
||||
// check if preloaded
|
||||
let requestedOrg = organizations.teams.find((team) => team.$id === params.organization) as
|
||||
| Organization
|
||||
@@ -154,8 +149,4 @@ async function checkPlatformAndRedirect(
|
||||
redirect(303, resolve('/(console)'));
|
||||
}
|
||||
}
|
||||
|
||||
// send the org,
|
||||
// if already in the full list so we don't have to make another API request.
|
||||
return requestedOrg;
|
||||
}
|
||||
|
||||
@@ -71,12 +71,6 @@
|
||||
return getServiceLimit('projects', null, data.currentPlan);
|
||||
});
|
||||
|
||||
const projectsToArchive = $derived.by(() => {
|
||||
return isCloud
|
||||
? data.projects.projects.filter((project) => project.status === 'archived')
|
||||
: [];
|
||||
});
|
||||
|
||||
function filterPlatforms(platforms: { name: string; icon: string }[]) {
|
||||
return platforms.filter(
|
||||
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
|
||||
@@ -136,6 +130,19 @@
|
||||
return project.status === 'archived';
|
||||
}
|
||||
|
||||
const projectsToArchive = $derived(
|
||||
(data.archivedProjectsPage ?? data.projects.projects).filter(
|
||||
(project) => project.status === 'archived'
|
||||
)
|
||||
);
|
||||
|
||||
const activeTotalOverall = $derived(
|
||||
data?.activeTotalOverall ??
|
||||
data?.organization?.projects?.length ??
|
||||
data?.projects?.total ??
|
||||
0
|
||||
);
|
||||
|
||||
function clearSearch() {
|
||||
searchQuery?.clearInput();
|
||||
}
|
||||
@@ -238,7 +245,7 @@
|
||||
{#if data.projects.total > 0}
|
||||
<CardContainer
|
||||
disableEmpty={!$canWriteProjects}
|
||||
total={data.projects.total}
|
||||
total={activeTotalOverall}
|
||||
offset={data.offset}
|
||||
on:click={handleCreateProject}>
|
||||
{#each data.projects.projects as project}
|
||||
@@ -323,13 +330,16 @@
|
||||
name="Projects"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.projects.total} />
|
||||
total={activeTotalOverall} />
|
||||
|
||||
<!-- Archived Projects Section -->
|
||||
<ArchiveProject
|
||||
{projectsToArchive}
|
||||
organization={data.organization}
|
||||
currentPlan={$currentPlan} />
|
||||
currentPlan={$currentPlan}
|
||||
archivedTotalOverall={data.archivedTotalOverall}
|
||||
archivedOffset={data.archivedOffset}
|
||||
limit={data.limit} />
|
||||
</Container>
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
|
||||
|
||||
@@ -5,7 +5,6 @@ import { CARD_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
|
||||
const { scopes } = await parent();
|
||||
@@ -20,26 +19,69 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
|
||||
const offset = pageToOffset(page, limit);
|
||||
const search = getSearch(url);
|
||||
|
||||
const projects = await sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.offset(offset),
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc(''),
|
||||
Query.select(['$id', 'name', 'platforms', 'region', ...(isCloud ? ['status'] : [])])
|
||||
],
|
||||
search: search || undefined
|
||||
});
|
||||
const archivedPageRaw = parseInt(url.searchParams.get('archivedPage') || '1', 10);
|
||||
const archivedPage =
|
||||
Number.isFinite(archivedPageRaw) && archivedPageRaw > 0 ? archivedPageRaw : 1;
|
||||
const archivedOffset = pageToOffset(archivedPage, limit);
|
||||
const [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([
|
||||
sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.offset(offset),
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.or([Query.equal('status', 'active'), Query.isNull('status')]),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc('')
|
||||
],
|
||||
search: search || undefined
|
||||
}),
|
||||
sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.offset(archivedOffset),
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.equal('status', 'archived'),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc('')
|
||||
],
|
||||
search: search || undefined
|
||||
}),
|
||||
sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.or([Query.equal('status', 'active'), Query.isNull('status')])
|
||||
],
|
||||
search: search || undefined
|
||||
}),
|
||||
sdk.forConsole.projects.list({
|
||||
queries: [
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.equal('status', 'archived')
|
||||
],
|
||||
search: search || undefined
|
||||
})
|
||||
]);
|
||||
|
||||
// set `default` if no region!
|
||||
for (const project of projects.projects) {
|
||||
for (const project of activeProjects.projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
for (const project of archivedProjects.projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
projects,
|
||||
projects: {
|
||||
...activeProjects,
|
||||
projects: activeProjects.projects,
|
||||
total: activeTotal.total
|
||||
},
|
||||
activeProjectsPage: activeProjects.projects,
|
||||
archivedProjectsPage: archivedProjects.projects,
|
||||
activeTotalOverall: activeTotal.total,
|
||||
archivedTotalOverall: archivedTotal.total,
|
||||
archivedOffset,
|
||||
archivedPage,
|
||||
search
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import PaymentHistory from './paymentHistory.svelte';
|
||||
import TaxId from './taxId.svelte';
|
||||
import { failedInvoice, tierToPlan, upgradeURL, useNewPricingModal } from '$lib/stores/billing';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
@@ -21,22 +20,15 @@
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { base } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// Reactive statement to update organization when data changes
|
||||
$: organization = data.organization;
|
||||
|
||||
// why are these reactive?
|
||||
$: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === organization?.paymentMethodId
|
||||
);
|
||||
|
||||
$: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === organization?.backupPaymentMethodId
|
||||
);
|
||||
$: baseUrl = resolve('/(console)/organization-[organization]/billing', {
|
||||
organization: organization.$id
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (page.url.searchParams.has('type')) {
|
||||
@@ -58,7 +50,7 @@
|
||||
organization.$id,
|
||||
invoice.clientSecret,
|
||||
organization.paymentMethodId,
|
||||
`${base}/organization-${organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}`
|
||||
`${baseUrl}?type=validate-invoice&invoice=${invoice.$id}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +107,7 @@
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if defaultPaymentMethod?.failed && !backupPaymentMethod}
|
||||
{#if data.primaryPaymentMethod?.failed && !data.backupPaymentMethod}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
title={`The default payment method for ${organization.name} has expired`}>
|
||||
@@ -146,7 +138,13 @@
|
||||
currentInvoice={data?.billingInvoice} />
|
||||
{/if}
|
||||
<PaymentHistory />
|
||||
<PaymentMethods organization={data?.organization} methods={data?.paymentMethods} />
|
||||
|
||||
<PaymentMethods
|
||||
methods={data?.paymentMethods}
|
||||
organization={data?.organization}
|
||||
backupMethod={data.backupPaymentMethod}
|
||||
primaryMethod={data.primaryPaymentMethod} />
|
||||
|
||||
<BillingAddress
|
||||
organization={data?.organization}
|
||||
billingAddress={data?.billingAddress}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { BillingPlan, DEFAULT_BILLING_PROJECTS_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import type { Address, PaymentList } from '$lib/sdk/billing';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { base } from '$app/paths';
|
||||
import { type PaymentMethodData } from '$lib/sdk/billing';
|
||||
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
|
||||
@@ -21,7 +22,7 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
depends(Dependencies.CREDIT);
|
||||
depends(Dependencies.INVOICES);
|
||||
depends(Dependencies.ADDRESS);
|
||||
//aggregation reloads on page param changes
|
||||
// aggregation reloads on page param changes
|
||||
depends(Dependencies.BILLING_AGGREGATION);
|
||||
|
||||
const billingAddressId = (organization as Organization)?.billingAddressId;
|
||||
@@ -82,6 +83,7 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
|
||||
// make number
|
||||
const credits = availableCredit ? availableCredit.available : null;
|
||||
const { backup, primary } = getOrganizationPaymentMethods(organization, paymentMethods);
|
||||
|
||||
return {
|
||||
paymentMethods,
|
||||
@@ -98,6 +100,32 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
offset: pageToOffset(
|
||||
getPage(url) || 1,
|
||||
getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT)
|
||||
)
|
||||
),
|
||||
|
||||
backupPaymentMethod: backup,
|
||||
primaryPaymentMethod: primary
|
||||
};
|
||||
};
|
||||
|
||||
function getOrganizationPaymentMethods(
|
||||
organization: Organization,
|
||||
paymentMethods: PaymentList
|
||||
): {
|
||||
backup: PaymentMethodData | null;
|
||||
primary: PaymentMethodData | null;
|
||||
} {
|
||||
let backup: PaymentMethodData | null = null;
|
||||
let primary: PaymentMethodData | null = null;
|
||||
|
||||
for (const paymentMethod of paymentMethods.paymentMethods) {
|
||||
if (paymentMethod.$id === organization.paymentMethodId) {
|
||||
primary = paymentMethod;
|
||||
} else if (paymentMethod.$id === organization.backupPaymentMethodId) {
|
||||
backup = paymentMethod;
|
||||
}
|
||||
|
||||
if (primary && backup) break;
|
||||
}
|
||||
|
||||
return { primary, backup };
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
message: `The payment method has been removed from ${$organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationPaymentDelete);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
showDelete = false;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@@ -34,7 +34,7 @@
|
||||
showDelete = false;
|
||||
}
|
||||
}
|
||||
async function removeBackuptMethod() {
|
||||
async function removeBackupMethod() {
|
||||
if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return;
|
||||
showDelete = false;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
message: `The payment method has been removed from ${$organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationBackupPaymentDelete);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
showDelete = false;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@@ -65,10 +65,10 @@
|
||||
</Confirm>
|
||||
{:else}
|
||||
<Confirm
|
||||
onSubmit={isBackup ? removeBackuptMethod : removeDefaultMethod}
|
||||
title="Remove payment method"
|
||||
bind:error
|
||||
bind:open={showDelete}
|
||||
bind:error>
|
||||
title="Remove payment method"
|
||||
onSubmit={isBackup ? removeBackupMethod : removeDefaultMethod}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to remove the payment method from <b>{$organization?.name}</b>?
|
||||
</Typography.Text>
|
||||
|
||||
@@ -34,16 +34,17 @@
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let organization: Organization;
|
||||
export let methods: PaymentList;
|
||||
export let organization: Organization;
|
||||
|
||||
export let backupMethod: PaymentMethodData;
|
||||
export let primaryMethod: PaymentMethodData;
|
||||
|
||||
let showPayment = false;
|
||||
let showEdit = false;
|
||||
let showDelete = false;
|
||||
let showReplace = false;
|
||||
let isSelectedBackup = false;
|
||||
let backupPaymentMethod: PaymentMethodData;
|
||||
let defaultPaymentMethod: PaymentMethodData;
|
||||
|
||||
async function addPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
@@ -56,7 +57,7 @@
|
||||
message: `A new payment method has been added to ${organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationPaymentUpdate);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -76,7 +77,7 @@
|
||||
type: 'success',
|
||||
message: `A new payment method has been added to ${organization.name}`
|
||||
});
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -86,27 +87,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (organization?.backupPaymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getOrganizationPaymentMethod(organization.$id, organization.backupPaymentMethodId)
|
||||
.then((res) => (backupPaymentMethod = res));
|
||||
}
|
||||
|
||||
$: if (organization?.paymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getOrganizationPaymentMethod(organization.$id, organization.paymentMethodId)
|
||||
.then((res) => (defaultPaymentMethod = res));
|
||||
}
|
||||
|
||||
$: if (!showReplace) {
|
||||
isSelectedBackup = false;
|
||||
}
|
||||
|
||||
$: hasPaymentError =
|
||||
defaultPaymentMethod?.lastError ||
|
||||
defaultPaymentMethod?.expired ||
|
||||
backupPaymentMethod?.lastError ||
|
||||
backupPaymentMethod?.expired;
|
||||
primaryMethod?.lastError ||
|
||||
primaryMethod?.expired ||
|
||||
backupMethod?.lastError ||
|
||||
backupMethod?.expired;
|
||||
</script>
|
||||
|
||||
<CardGrid overflow={false}>
|
||||
@@ -132,14 +121,14 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<Table.Row.Base {root}>
|
||||
<CreditCardInfo {root} paymentMethod={defaultPaymentMethod} />
|
||||
<CreditCardInfo {root} paymentMethod={primaryMethod} />
|
||||
<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">
|
||||
{#if defaultPaymentMethod?.userId === $user?.$id}
|
||||
{#if primaryMethod?.userId === $user?.$id}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPencil}
|
||||
on:click={() => {
|
||||
@@ -171,14 +160,14 @@
|
||||
</Table.Row.Base>
|
||||
{#if organization?.backupPaymentMethodId}
|
||||
<Table.Row.Base {root}>
|
||||
<CreditCardInfo {root} isBackup paymentMethod={backupPaymentMethod} />
|
||||
<CreditCardInfo {root} isBackup paymentMethod={backupMethod} />
|
||||
<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">
|
||||
{#if backupPaymentMethod?.userId === $user?.$id}
|
||||
{#if backupMethod?.userId === $user?.$id}
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPencil}
|
||||
on:click={() => {
|
||||
@@ -318,8 +307,8 @@
|
||||
{/if}
|
||||
{#if showEdit && isCloud && hasStripePublicKey}
|
||||
<EditPaymentModal
|
||||
selectedPaymentMethod={isSelectedBackup ? backupPaymentMethod : defaultPaymentMethod}
|
||||
bind:show={showEdit} />
|
||||
bind:show={showEdit}
|
||||
selectedPaymentMethod={isSelectedBackup ? backupMethod : primaryMethod} />
|
||||
{/if}
|
||||
{#if isCloud && hasStripePublicKey}
|
||||
<ReplaceCard {organization} {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
|
||||
@@ -13,17 +13,29 @@
|
||||
import { PaymentBoxes } from '$lib/components/billing';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
|
||||
export let organization: Organization;
|
||||
export let show = false;
|
||||
export let isBackup = false;
|
||||
export let methods: PaymentList;
|
||||
let {
|
||||
show = $bindable(false),
|
||||
isBackup = false,
|
||||
methods,
|
||||
organization
|
||||
}: {
|
||||
show?: boolean;
|
||||
isBackup?: boolean;
|
||||
methods: PaymentList;
|
||||
organization: Organization;
|
||||
} = $props();
|
||||
|
||||
let name: string;
|
||||
let error: string = null;
|
||||
let selectedPaymentMethodId: string;
|
||||
let showState: boolean = false;
|
||||
let state: string = '';
|
||||
let paymentMethod: PaymentMethod | null = null;
|
||||
let name: string | null = $state(null);
|
||||
let error: string | null = $state(null);
|
||||
let showState: boolean = $state(false);
|
||||
let countryState: string | null = $state(null);
|
||||
let paymentMethod: PaymentMethod | null = $state(null);
|
||||
let selectedPaymentMethodId: string | null = $state(null);
|
||||
|
||||
const filteredMethods = $derived(methods?.paymentMethods.filter((method) => !!method?.last4));
|
||||
const submitEvent = $derived(
|
||||
isBackup ? Submit.OrganizationBackupPaymentUpdate : Submit.OrganizationPaymentUpdate
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (!organization.paymentMethodId && !organization.backupPaymentMethodId) {
|
||||
@@ -45,12 +57,12 @@
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (selectedPaymentMethodId === '$new') {
|
||||
if (showState && !state) {
|
||||
if (showState && !countryState) {
|
||||
throw Error('Please select a state');
|
||||
}
|
||||
let method: PaymentMethodData;
|
||||
if (showState) {
|
||||
method = await setPaymentMethod(paymentMethod.id, name, state);
|
||||
method = await setPaymentMethod(paymentMethod.id, name, countryState);
|
||||
} else {
|
||||
const card = await submitStripeCard(name, organization.$id);
|
||||
if (card && Object.hasOwn(card, 'id')) {
|
||||
@@ -69,21 +81,17 @@
|
||||
? await addBackupPaymentMethod(selectedPaymentMethodId)
|
||||
: await addPaymentMethod(selectedPaymentMethodId);
|
||||
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Your ${isBackup ? 'backup' : 'default'} payment method has been updated`
|
||||
});
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
trackEvent(
|
||||
isBackup ? Submit.OrganizationBackupPaymentDelete : Submit.OrganizationPaymentDelete
|
||||
);
|
||||
trackEvent(submitEvent);
|
||||
show = false;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(
|
||||
e,
|
||||
isBackup ? Submit.OrganizationBackupPaymentDelete : Submit.OrganizationPaymentDelete
|
||||
);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
trackError(err, submitEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +101,8 @@
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,23 +112,21 @@
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);
|
||||
</script>
|
||||
|
||||
<FakeModal bind:show bind:error onSubmit={handleSubmit} size="big" title="Replace payment method">
|
||||
<p class="text">Replace the existing payment method for your organization.</p>
|
||||
|
||||
<PaymentBoxes
|
||||
methods={filteredMethods}
|
||||
bind:name
|
||||
bind:paymentMethod
|
||||
bind:showState
|
||||
bind:state
|
||||
bind:paymentMethod
|
||||
methods={filteredMethods}
|
||||
bind:state={countryState}
|
||||
bind:group={selectedPaymentMethodId}
|
||||
defaultMethod={organization?.paymentMethodId}
|
||||
backupMethod={organization?.backupPaymentMethodId}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {
|
||||
AvatarInitials,
|
||||
Copy,
|
||||
type DeleteOperation,
|
||||
type DeleteOperationState,
|
||||
Empty,
|
||||
EmptySearch,
|
||||
@@ -60,20 +61,22 @@
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((userId) => {
|
||||
return sdk.forProject(page.params.region, page.params.project).users.delete({ userId });
|
||||
});
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((userId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).users.delete({ userId })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.UserDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.UserDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.UserDelete);
|
||||
} else {
|
||||
trackEvent(Submit.UserDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.USERS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import type { PageProps } from './$types';
|
||||
import UpdateMockNumbers from './updateMockNumbers.svelte';
|
||||
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
|
||||
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
|
||||
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
|
||||
import UpdateSessionAlerts from './updateSessionAlerts.svelte';
|
||||
import UpdateSessionLength from './updateSessionLength.svelte';
|
||||
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
|
||||
import UpdateMembershipPrivacy from './updateMembershipPrivacy.svelte';
|
||||
import UpdateUsersLimit from './updateUsersLimit.svelte';
|
||||
import UpdateSessionInvalidation from './updateSessionInvalidation.svelte';
|
||||
import UpdateSessionLength from './updateSessionLength.svelte';
|
||||
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
|
||||
import PasswordPolicies from './passwordPolicies.svelte';
|
||||
import SessionSecurity from './sessionSecurity.svelte';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<UpdateUsersLimit />
|
||||
<UpdateSessionLength />
|
||||
<UpdateSessionsLimit />
|
||||
<UpdatePasswordHistory />
|
||||
<UpdatePasswordDictionary />
|
||||
<UpdatePersonalDataCheck />
|
||||
<UpdateSessionAlerts />
|
||||
<UpdateSessionInvalidation />
|
||||
<PasswordPolicies project={data.project} />
|
||||
<SessionSecurity project={data.project} />
|
||||
<UpdateMockNumbers />
|
||||
<UpdateMembershipPrivacy />
|
||||
</Container>
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography, Link, Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
project
|
||||
}: {
|
||||
project: Models.Project;
|
||||
} = $props();
|
||||
|
||||
let lastValidLimit = $state(5);
|
||||
let passwordHistory = $state(5);
|
||||
let passwordDictionary = $state(false);
|
||||
let passwordHistoryEnabled = $state(false);
|
||||
let authPersonalDataCheck = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
// update initial states here in onMount.
|
||||
const historyValue = project.authPasswordHistory;
|
||||
if (historyValue && historyValue > 0) {
|
||||
passwordHistory = historyValue;
|
||||
lastValidLimit = historyValue;
|
||||
}
|
||||
|
||||
passwordHistoryEnabled = (historyValue ?? 0) !== 0;
|
||||
passwordDictionary = project.authPasswordDictionary ?? false;
|
||||
authPersonalDataCheck = project.authPersonalDataCheck ?? false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// restore last valid limit when enabling
|
||||
if (passwordHistoryEnabled && passwordHistory < 1) {
|
||||
passwordHistory = lastValidLimit;
|
||||
}
|
||||
});
|
||||
|
||||
const hasChanges = $derived.by(() => {
|
||||
const dictChanged = passwordDictionary !== (project.authPasswordDictionary ?? false);
|
||||
const dataCheckChanged = authPersonalDataCheck !== (project.authPersonalDataCheck ?? false);
|
||||
const historyChanged =
|
||||
passwordHistoryEnabled !== ((project.authPasswordHistory ?? 0) !== 0);
|
||||
const limitChanged =
|
||||
passwordHistoryEnabled &&
|
||||
Number(passwordHistory) !== (project.authPasswordHistory ?? 0);
|
||||
|
||||
return historyChanged || dictChanged || dataCheckChanged || limitChanged;
|
||||
});
|
||||
|
||||
async function updatePasswordPolicies() {
|
||||
try {
|
||||
const projectSdk = sdk.forConsole.projects;
|
||||
|
||||
await projectSdk.updateAuthPasswordHistory({
|
||||
projectId: project.$id,
|
||||
limit: passwordHistoryEnabled ? passwordHistory : 0
|
||||
});
|
||||
|
||||
await projectSdk.updateAuthPasswordDictionary({
|
||||
projectId: project.$id,
|
||||
enabled: passwordDictionary
|
||||
});
|
||||
|
||||
await projectSdk.updatePersonalDataCheck({
|
||||
projectId: project.$id,
|
||||
enabled: authPersonalDataCheck
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated password policies.'
|
||||
});
|
||||
trackEvent(Submit.AuthPasswordHistoryUpdate);
|
||||
trackEvent(Submit.AuthPasswordDictionaryUpdate);
|
||||
trackEvent(Submit.AuthPersonalDataCheckUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthPasswordHistoryUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePasswordPolicies}>
|
||||
<CardGrid gap="xxl">
|
||||
<svelte:fragment slot="title">Password policies</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={passwordHistoryEnabled}
|
||||
id="passwordHistoryEnabled"
|
||||
label="Password history">
|
||||
<svelte:fragment slot="description">
|
||||
<Layout.Stack gap="m">
|
||||
<Typography.Text>
|
||||
Enabling this option prevents users from reusing recent passwords by
|
||||
comparing the new password with their password history.
|
||||
</Typography.Text>
|
||||
{#if passwordHistoryEnabled}
|
||||
<InputNumber
|
||||
required
|
||||
max={20}
|
||||
min={1}
|
||||
autofocus
|
||||
label="Limit"
|
||||
id="password-history"
|
||||
bind:value={passwordHistory}
|
||||
helper="Maximum 20 passwords." />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
|
||||
<InputSwitch
|
||||
bind:value={passwordDictionary}
|
||||
id="passwordDictionary"
|
||||
label="Password dictionary">
|
||||
<svelte:fragment slot="description">
|
||||
<Typography.Text>
|
||||
Enabling this option prevents users from setting insecure passwords by
|
||||
comparing the user's password with the <Link.Anchor
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
href="https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10k-most-common.txt"
|
||||
>10k most commonly used passwords.</Link.Anchor>
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
|
||||
<InputSwitch
|
||||
bind:value={authPersonalDataCheck}
|
||||
id="personalDataCheck"
|
||||
label="Disallow personal data">
|
||||
<svelte:fragment slot="description">
|
||||
<Typography.Text>
|
||||
Do not allow passwords that contain any part of the user's personal data.
|
||||
This includes the user's <Typography.Code>name</Typography.Code>, <Typography.Code
|
||||
>email</Typography.Code
|
||||
>, or <Typography.Code>phone</Typography.Code>.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={!hasChanges} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { project }: { project: Models.Project } = $props();
|
||||
|
||||
let authSessionAlerts = $state(false);
|
||||
let sessionInvalidation = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
authSessionAlerts = project?.authSessionAlerts ?? false;
|
||||
sessionInvalidation = project?.authInvalidateSessions ?? false;
|
||||
});
|
||||
|
||||
const hasChanges = $derived.by(() => {
|
||||
const alertsChanged = authSessionAlerts !== (project?.authSessionAlerts ?? false);
|
||||
const invalidationChanged =
|
||||
sessionInvalidation !== (project?.authInvalidateSessions ?? false);
|
||||
return alertsChanged || invalidationChanged;
|
||||
});
|
||||
|
||||
async function updateSessionSecurity() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateSessionAlerts({
|
||||
projectId: project.$id,
|
||||
alerts: authSessionAlerts
|
||||
});
|
||||
await sdk.forConsole.projects.updateSessionInvalidation({
|
||||
projectId: project.$id,
|
||||
enabled: sessionInvalidation
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated session security settings.'
|
||||
});
|
||||
trackEvent(Submit.AuthSessionAlertsUpdate);
|
||||
trackEvent(Submit.AuthInvalidateSession);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthSessionAlertsUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSessionSecurity}>
|
||||
<CardGrid gap="xxl">
|
||||
<svelte:fragment slot="title">Session security</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={authSessionAlerts}
|
||||
id="authSessionAlerts"
|
||||
label="Session alerts">
|
||||
<svelte:fragment slot="description">
|
||||
<Typography.Text>
|
||||
Enabling this option will send an email to the users when a new session is
|
||||
created.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
|
||||
<InputSwitch
|
||||
bind:value={sessionInvalidation}
|
||||
id="invalidateSessions"
|
||||
label="Invalidate sessions">
|
||||
<svelte:fragment slot="description">
|
||||
<Typography.Text>
|
||||
Enabling this option will clear all existing sessions when the user changes
|
||||
their password.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={!hasChanges} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography, Link } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../store';
|
||||
|
||||
let passwordDictionary = $project?.authPasswordDictionary ?? false;
|
||||
|
||||
async function updatePasswordDictionary() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateAuthPasswordDictionary({
|
||||
projectId: $project.$id,
|
||||
enabled: passwordDictionary
|
||||
});
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated password dictionary check.'
|
||||
});
|
||||
trackEvent(Submit.AuthPasswordDictionaryUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthPasswordDictionaryUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePasswordDictionary}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Password dictionary</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={passwordDictionary}
|
||||
id="passwordDictionary"
|
||||
label="Password dictionary" />
|
||||
<Typography.Text>
|
||||
Enabling this option prevent users from setting insecure passwords by comparing the
|
||||
user's password with the <Link.Anchor
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
href="https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10k-most-common.txt"
|
||||
>10k most commonly used passwords.</Link.Anchor>
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={passwordDictionary === $project?.authPasswordDictionary} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../store';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
let passwordHistory = $project?.authPasswordHistory < 1 ? 5 : $project?.authPasswordHistory;
|
||||
let passwordHistoryEnabled = ($project?.authPasswordHistory ?? 0) !== 0;
|
||||
let initialPasswordHistoryEnabled = passwordHistoryEnabled;
|
||||
|
||||
async function updatePasswordHistoryLimit() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateAuthPasswordHistory({
|
||||
projectId: $project.$id,
|
||||
limit: passwordHistoryEnabled ? passwordHistory : 0
|
||||
});
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
initialPasswordHistoryEnabled = passwordHistoryEnabled;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated password history limit.'
|
||||
});
|
||||
trackEvent(Submit.AuthPasswordHistoryUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthPasswordHistoryUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
let maxPasswordInputField: InputNumber | null = null;
|
||||
|
||||
$: if (passwordHistoryEnabled && maxPasswordInputField) {
|
||||
tick().then(() => {
|
||||
maxPasswordInputField.addInputFocus();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePasswordHistoryLimit}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Password history</svelte:fragment>
|
||||
Set the maximum number of passwords saved per user.
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={passwordHistoryEnabled}
|
||||
id="passwordHistoryEnabled"
|
||||
label="Password history" />
|
||||
<Typography.Text>
|
||||
Enabling this option prevents users from reusing recent passwords by comparing the
|
||||
new password with their password history.
|
||||
</Typography.Text>
|
||||
<InputNumber
|
||||
required
|
||||
max={20}
|
||||
min={1}
|
||||
id="password-history"
|
||||
label="Limit"
|
||||
disabled={!passwordHistoryEnabled}
|
||||
bind:value={passwordHistory}
|
||||
helper="Maximum 20 passwords." />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={(passwordHistory === $project?.authPasswordHistory ||
|
||||
$project?.authPasswordHistory === 0) &&
|
||||
initialPasswordHistoryEnabled === passwordHistoryEnabled}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../store';
|
||||
|
||||
let authPersonalDataCheck = $project?.authPersonalDataCheck ?? false;
|
||||
|
||||
async function updatePersonalDataCheck() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updatePersonalDataCheck({
|
||||
projectId: $project.$id,
|
||||
enabled: authPersonalDataCheck
|
||||
});
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Toggled personal data checks for passwords'
|
||||
});
|
||||
trackEvent(Submit.AuthPersonalDataCheckUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthPersonalDataCheckUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePersonalDataCheck}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Personal data</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={authPersonalDataCheck}
|
||||
id="personalDataCheck"
|
||||
label="Disallow personal data" />
|
||||
<Typography.Text>
|
||||
Do not allow passwords that contain any part of the user's personal data. This
|
||||
includes the user's <Typography.Code>name</Typography.Code>, <Typography.Code
|
||||
>email</Typography.Code
|
||||
>, or <Typography.Code>phone</Typography.Code>.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={authPersonalDataCheck === $project?.authPersonalDataCheck} submit
|
||||
>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../store';
|
||||
|
||||
let authSessionAlerts = $project?.authSessionAlerts ?? false;
|
||||
|
||||
async function updateSessionAlerts() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateSessionAlerts({
|
||||
projectId: $project.$id,
|
||||
alerts: authSessionAlerts
|
||||
});
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated session alerts.'
|
||||
});
|
||||
trackEvent(Submit.AuthSessionAlertsUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthSessionAlertsUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSessionAlerts}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Session alerts</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={authSessionAlerts}
|
||||
id="authSessionAlerts"
|
||||
label="Session alerts" />
|
||||
<Typography.Text>
|
||||
Enabling this option will send an email to the users when a new session is created.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={authSessionAlerts === $project?.authSessionAlerts} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import { project } from '../../store';
|
||||
|
||||
let sessionInvalidation = $project?.authInvalidateSessions ?? false;
|
||||
|
||||
async function updateSessionInvalidation() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateSessionInvalidation({
|
||||
projectId: $project.$id,
|
||||
enabled: sessionInvalidation
|
||||
});
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated session invalidation check.'
|
||||
});
|
||||
trackEvent(Submit.AuthInvalidateSesssion);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthInvalidateSesssion);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSessionInvalidation}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Invalidate sessions</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<InputSwitch
|
||||
bind:value={sessionInvalidation}
|
||||
id="invalidateSessions"
|
||||
label="Invalidate sessions" />
|
||||
<Typography.Text>
|
||||
Enabling this option will clear all existing sessions when the user changes their
|
||||
password.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={sessionInvalidation === $project?.authInvalidateSessions} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -12,6 +12,7 @@
|
||||
SearchQuery,
|
||||
PaginationWithLimit,
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import Create from '../createTeam.svelte';
|
||||
@@ -44,20 +45,22 @@
|
||||
);
|
||||
};
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((teamId) => {
|
||||
return sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId });
|
||||
});
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((teamId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.TeamDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.TeamDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.TeamDelete);
|
||||
} else {
|
||||
trackEvent(Submit.TeamDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.TEAMS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+13
-10
@@ -6,6 +6,7 @@
|
||||
AvatarInitials,
|
||||
PaginationWithLimit,
|
||||
MultiSelectionTable,
|
||||
type DeleteOperation,
|
||||
type DeleteOperationState
|
||||
} from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
@@ -29,23 +30,25 @@
|
||||
let showDelete = $state(false);
|
||||
let selectedMembership: Models.Membership | null = $state(null);
|
||||
|
||||
async function handleBulkDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((membershipId) => {
|
||||
return sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
|
||||
async function handleBulkDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((membershipId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
|
||||
teamId: page.params.team,
|
||||
membershipId
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MembershipUpdate, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MembershipUpdate);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MembershipUpdate);
|
||||
} else {
|
||||
trackEvent(Submit.MembershipUpdate, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MEMBERSHIPS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+18
-11
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -21,22 +26,24 @@
|
||||
columns: Column[];
|
||||
} = $props();
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((id) => {
|
||||
return sdk
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((id) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.users.deleteIdentity({ identityId: id });
|
||||
});
|
||||
.users.deleteIdentity({ identityId: id })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.UserIdentityDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.UserIdentityDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.UserIdentityDelete);
|
||||
} else {
|
||||
trackEvent(Submit.UserIdentityDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.USER_IDENTITIES);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+10
-7
@@ -4,6 +4,7 @@
|
||||
import {
|
||||
AvatarInitials,
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
@@ -23,14 +24,14 @@
|
||||
let showDelete = $state(false);
|
||||
let selectedMembership: Models.Membership | null = $state(null);
|
||||
|
||||
async function handleBulkDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
async function handleBulkDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
// Precompute a lookup map from membershipId to teamId for efficient access
|
||||
const membershipIdToTeamId: Record<string, string> = {};
|
||||
for (const membership of data.memberships.memberships) {
|
||||
membershipIdToTeamId[membership.$id] = membership.teamId;
|
||||
}
|
||||
|
||||
const promises = selectedRows.map((membershipId) =>
|
||||
const result = await batchDelete((membershipId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
|
||||
teamId: membershipIdToTeamId[membershipId] || '',
|
||||
membershipId
|
||||
@@ -38,14 +39,16 @@
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MembershipUpdate, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MembershipUpdate);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MembershipUpdate);
|
||||
} else {
|
||||
trackEvent(Submit.MembershipUpdate, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MEMBERSHIPS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+12
-5
@@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { EmptySearch, Trim } from '$lib/components';
|
||||
import { Badge, Layout, Table, Typography, Icon } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Layout,
|
||||
Table,
|
||||
Typography,
|
||||
Icon,
|
||||
InteractiveText
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconGlobeAlt } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
@@ -36,9 +43,9 @@
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'client' },
|
||||
{ id: 'location', width: 200 },
|
||||
{ id: 'ip', width: 200 },
|
||||
{ id: 'client', width: { min: 450 } },
|
||||
{ id: 'location', width: { min: 200 } },
|
||||
{ id: 'ip', width: { min: 200 } },
|
||||
{ id: 'actions', width: 100 }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
@@ -84,7 +91,7 @@
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="ip" {root}>
|
||||
{session.ip}
|
||||
<InteractiveText variant="copy" text={session.ip} isVisible />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="actions" {root}>
|
||||
<Button
|
||||
|
||||
+18
-11
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
@@ -19,22 +24,24 @@
|
||||
data: PageData;
|
||||
} = $props();
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((id) => {
|
||||
return sdk
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((id) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.users.deleteTarget({ userId: page.params.user, targetId: id });
|
||||
});
|
||||
.users.deleteTarget({ userId: page.params.user, targetId: id })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.UserTargetDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.UserTargetDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.UserTargetDelete);
|
||||
} else {
|
||||
trackEvent(Submit.UserTargetDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.USER_TARGETS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
import Table from './table.svelte';
|
||||
import { registerCommands } from '$lib/commandCenter';
|
||||
import { canWriteDatabases } from '$lib/stores/roles';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import EmptySearch from '$lib/components/emptySearch.svelte';
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -30,6 +32,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
$: isLimited = isServiceLimited('databases', $organization?.billingPlan, data.databases.total);
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Create database',
|
||||
@@ -37,7 +41,7 @@
|
||||
showCreate = true;
|
||||
},
|
||||
keys: ['c'],
|
||||
disabled: showCreate || !$canWriteDatabases,
|
||||
disabled: showCreate || !$canWriteDatabases || isLimited,
|
||||
icon: IconPlus,
|
||||
group: 'databases',
|
||||
rank: 10
|
||||
@@ -52,10 +56,20 @@
|
||||
bind:view={data.view}
|
||||
searchPlaceholder="Search by name or ID">
|
||||
{#if $canWriteDatabases}
|
||||
<Button event="create_database" on:click={() => (showCreate = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create database
|
||||
</Button>
|
||||
<Tooltip disabled={!isLimited}>
|
||||
<div>
|
||||
<Button
|
||||
disabled={isLimited}
|
||||
event="create_database"
|
||||
on:click={() => (showCreate = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create database
|
||||
</Button>
|
||||
</div>
|
||||
<svelte:fragment slot="tooltip">
|
||||
You have reached the maximum number of databases for your plan.
|
||||
</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</ResponsiveContainerHeader>
|
||||
|
||||
|
||||
+32
-26
@@ -3,6 +3,7 @@
|
||||
Card,
|
||||
Confirm,
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Modal,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
@@ -18,7 +19,7 @@
|
||||
import { columns } from './store';
|
||||
import { database } from '../store';
|
||||
import type { BackupArchive, BackupPolicy } from '$lib/sdk/backups';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { LabelCard } from '$lib/components/index.js';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
@@ -110,39 +111,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBackups(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((archiveId) => {
|
||||
return sdk
|
||||
async function deleteSingleBackup(archiveId: string) {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.backups.deleteArchive(archiveId);
|
||||
});
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Backup deleted'
|
||||
});
|
||||
|
||||
showDelete = false;
|
||||
selectedBackup = null;
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBackups(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((archiveId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).backups.deleteArchive(archiveId)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
|
||||
if (selectedBackup) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: '1 backup deleted'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedBackup) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.DatabaseBackupDelete);
|
||||
} else {
|
||||
return error;
|
||||
trackEvent(Submit.DatabaseBackupDelete);
|
||||
}
|
||||
} finally {
|
||||
if (selectedBackup) {
|
||||
showDelete = false;
|
||||
selectedBackup = null;
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.BACKUPS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function restoreBackup() {
|
||||
@@ -299,7 +305,7 @@
|
||||
bind:open={showDelete}
|
||||
onSubmit={async () => {
|
||||
if (!selectedBackup) return;
|
||||
await deleteBackups([selectedBackup.$id]);
|
||||
await deleteSingleBackup(selectedBackup.$id);
|
||||
}}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete the <b>{getCleanBackupName(selectedBackup)}</b> backup?
|
||||
|
||||
+3
-3
@@ -86,7 +86,7 @@
|
||||
label="Min"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.min}
|
||||
step={0.1}
|
||||
step="any"
|
||||
{disabled}
|
||||
required={editing} />
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
label="Max"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.max}
|
||||
step={0.1}
|
||||
step="any"
|
||||
{disabled}
|
||||
required={editing} />
|
||||
</Layout.Stack>
|
||||
@@ -109,7 +109,7 @@
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array}
|
||||
step={0.1} />
|
||||
step="any" />
|
||||
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
|
||||
+1
-1
@@ -31,5 +31,5 @@
|
||||
min={column.min}
|
||||
max={column.max}
|
||||
required={column.required}
|
||||
step={column.type === 'double' ? 0.1 : 1}
|
||||
step={column.type === 'double' ? 'any' : 1}
|
||||
leadingIcon={!limited ? IconHashtag : undefined} />
|
||||
|
||||
+8
-2
@@ -630,7 +630,6 @@
|
||||
permissions: row.$permissions
|
||||
});
|
||||
|
||||
invalidate(Dependencies.ROW);
|
||||
trackEvent(Submit.RowUpdate);
|
||||
addNotification({
|
||||
message: 'Row has been updated',
|
||||
@@ -1105,7 +1104,14 @@
|
||||
<EditRowCell
|
||||
{row}
|
||||
column={rowColumn}
|
||||
onRowStructureUpdate={updateRowContents}
|
||||
onRowStructureUpdate={async (row) => {
|
||||
const success = await updateRowContents(row);
|
||||
if (success) {
|
||||
// database update succeeded!
|
||||
paginatedRows.update(index, row);
|
||||
}
|
||||
return success;
|
||||
}}
|
||||
noInlineEdit={isRelatedToMany && hasItems}
|
||||
onChange={(row) => paginatedRows.update(index, row)}
|
||||
onRevert={(row) => paginatedRows.update(index, row)}
|
||||
|
||||
+3
-1
@@ -193,7 +193,9 @@ export const expandTabs = writable(null);
|
||||
export const spreadsheetRenderKey = writable('initial');
|
||||
|
||||
export const paginatedRowsLoading = writable(false);
|
||||
export const paginatedRows = createSparsePagedDataStore<Models.DefaultRow>(SPREADSHEET_PAGE_LIMIT);
|
||||
export const paginatedRows = createSparsePagedDataStore<Models.DefaultRow | Models.Row>(
|
||||
SPREADSHEET_PAGE_LIMIT
|
||||
);
|
||||
|
||||
export const PROHIBITED_ROW_KEYS = [
|
||||
'$id',
|
||||
|
||||
+16
-8
@@ -3,7 +3,12 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Id, MultiSelectionTable, type DeleteOperationState } from '$lib/components';
|
||||
import {
|
||||
Id,
|
||||
MultiSelectionTable,
|
||||
type DeleteOperation,
|
||||
type DeleteOperationState
|
||||
} from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { canWriteTables } from '$lib/stores/roles';
|
||||
@@ -20,23 +25,26 @@
|
||||
data: PageData;
|
||||
} = $props();
|
||||
|
||||
async function onDelete(selectedTables: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedTables.map((tableId) =>
|
||||
async function onDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((tableId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).tablesDB.deleteTable({
|
||||
databaseId: page.params.database,
|
||||
tableId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.TableDelete);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.TableDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.TableDelete);
|
||||
} else {
|
||||
trackEvent(Submit.TableDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.TABLES);
|
||||
subNavigation.update();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getTableHref(table: Models.Table) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Id } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { columns } from './store';
|
||||
import { IconExclamation } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Layout, Tooltip, Table, Icon } from '@appwrite.io/pink-svelte';
|
||||
@@ -72,8 +72,10 @@
|
||||
{`Last backup: ${lastBackup}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{:else if column.type === 'datetime'}
|
||||
<DualTimeView time={database[column.id]} showDatetime />
|
||||
{:else}
|
||||
{toLocaleDateTime(database[column.id])}
|
||||
{database[column.id]}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export const load: PageLoad = async ({ depends, params, url, route, parent }) =>
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc(''),
|
||||
Query.orderDesc('$updatedAt'),
|
||||
...parsedQueries.values()
|
||||
],
|
||||
search: search || undefined
|
||||
|
||||
+7
@@ -15,5 +15,12 @@ export const columns = writable<Column[]>([
|
||||
title: 'Target',
|
||||
type: 'string',
|
||||
width: { min: 120, max: 400 }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'updated',
|
||||
title: '',
|
||||
type: 'string',
|
||||
width: { min: 160, max: 180 }
|
||||
}
|
||||
]);
|
||||
|
||||
+32
@@ -25,6 +25,7 @@
|
||||
import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte';
|
||||
import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte';
|
||||
import { timeFromNowShort } from '$lib/helpers/date';
|
||||
|
||||
let {
|
||||
proxyRules,
|
||||
@@ -46,6 +47,28 @@
|
||||
? 'Deployed from ' + proxy.deploymentVcsProviderBranch
|
||||
: 'Active deployment';
|
||||
};
|
||||
|
||||
function updatedLabel(proxyRule: Models.ProxyRule): string {
|
||||
if (proxyRule.status === 'verified') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const timeStr = timeFromNowShort(proxyRule.$updatedAt);
|
||||
if (timeStr === 'n/a') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix =
|
||||
proxyRule.status === 'created'
|
||||
? 'Checked'
|
||||
: proxyRule.status === 'verifying'
|
||||
? 'Updated'
|
||||
: proxyRule.status === 'unverified'
|
||||
? 'Failed'
|
||||
: '';
|
||||
|
||||
return prefix + ' ' + timeStr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
|
||||
@@ -117,6 +140,15 @@
|
||||
</Layout.Stack>
|
||||
{:else if column.id === 'target'}
|
||||
{proxyTarget(proxyRule)}
|
||||
{:else if column.id === 'updated' && proxyRule.status !== 'verified'}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Typography.Text
|
||||
variant="m-400"
|
||||
color="--fgcolor-neutral-tertiary"
|
||||
style="font-size: 0.875rem;">
|
||||
{updatedLabel(proxyRule)}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
+15
-8
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
@@ -30,8 +35,8 @@
|
||||
let open = $state(false);
|
||||
let selectedLogId: string | null = $state(null);
|
||||
|
||||
async function deleteExecutions(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((executionId) =>
|
||||
async function deleteExecutions(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((executionId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).functions.deleteExecution({
|
||||
functionId: page.params.function,
|
||||
executionId
|
||||
@@ -39,14 +44,16 @@
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.ExecutionDelete);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.ExecutionDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.ExecutionDelete);
|
||||
} else {
|
||||
trackEvent(Submit.ExecutionDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.EXECUTIONS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+16
-8
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
@@ -52,22 +57,25 @@
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
async function deleteDeployments(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((deploymentId) =>
|
||||
async function deleteDeployments(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((deploymentId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).functions.deleteDeployment({
|
||||
functionId: page.params.function,
|
||||
deploymentId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.DeploymentDelete);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.DeploymentDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.DeploymentDelete);
|
||||
} else {
|
||||
trackEvent(Submit.DeploymentDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Empty,
|
||||
EmptyFilter,
|
||||
EmptySearch,
|
||||
@@ -99,22 +100,24 @@
|
||||
}
|
||||
]);
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((id) =>
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((id) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.delete({ messageId: id })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MessagingMessageDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MessagingMessageDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MessagingMessageDelete);
|
||||
} else {
|
||||
trackEvent(Submit.MessagingMessageDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGES);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import Provider from '../provider.svelte';
|
||||
@@ -20,22 +25,24 @@
|
||||
data: PageData;
|
||||
} = $props();
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((id) => {
|
||||
return sdk
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((id) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.deleteProvider({ providerId: id });
|
||||
});
|
||||
.messaging.deleteProvider({ providerId: id })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MessagingProviderDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MessagingProviderDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MessagingProviderDelete);
|
||||
} else {
|
||||
trackEvent(Submit.MessagingProviderDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MESSAGING_PROVIDERS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -20,22 +25,24 @@
|
||||
columns: Column[];
|
||||
} = $props();
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((id) => {
|
||||
return sdk
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((id) =>
|
||||
sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.deleteTopic({ topicId: id });
|
||||
});
|
||||
.messaging.deleteTopic({ topicId: id })
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MessagingTopicDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MessagingTopicDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MessagingTopicDelete);
|
||||
} else {
|
||||
trackEvent(Submit.MessagingTopicDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MESSAGING_TOPICS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+16
-11
@@ -2,7 +2,12 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageData } from './$types';
|
||||
import ProviderType from '../../providerType.svelte';
|
||||
@@ -32,8 +37,8 @@
|
||||
return record;
|
||||
});
|
||||
|
||||
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
async function deleteSubscriber(subscriberId: string) {
|
||||
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete(async (subscriberId) => {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.deleteSubscriber({
|
||||
@@ -44,19 +49,19 @@
|
||||
const { target } = subscribers[subscriberId];
|
||||
const { [target.$id]: _, ...rest } = $targetsById;
|
||||
$targetsById = rest;
|
||||
}
|
||||
|
||||
const promises = selectedRows.map((id) => deleteSubscriber(id));
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MessagingTopicSubscriberDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.MessagingTopicSubscriberDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.MessagingTopicSubscriberDelete);
|
||||
} else {
|
||||
trackEvent(Submit.MessagingTopicSubscriberDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
allowSelection={$canWriteKeys}
|
||||
showSuccessNotification={false}
|
||||
resource={`${capitalize(label)} key`}
|
||||
onDelete={(selectedRows) => {
|
||||
onDelete={(_, selectedRows) => {
|
||||
showDeleteModal = true;
|
||||
selectedKeys = selectedRows;
|
||||
}}>
|
||||
|
||||
@@ -81,7 +81,11 @@
|
||||
import { page } from '$app/state';
|
||||
import type { PageProps } from './$types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { type DeleteOperationState, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError } from '$lib/actions/analytics';
|
||||
import { invalidate } from '$app/navigation';
|
||||
@@ -129,24 +133,26 @@
|
||||
}
|
||||
|
||||
async function handlePlatformDelete(
|
||||
selectedPlatforms: string[]
|
||||
batchDelete: DeleteOperation
|
||||
): Promise<DeleteOperationState> {
|
||||
const promises = selectedPlatforms.map((platformId) => {
|
||||
return sdk.forConsole.projects.deletePlatform({
|
||||
const result = await batchDelete((platformId) =>
|
||||
sdk.forConsole.projects.deletePlatform({
|
||||
projectId: page.params.project,
|
||||
platformId
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.PlatformDelete, { total: selectedPlatforms.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.PlatformDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.PlatformDelete);
|
||||
} else {
|
||||
trackEvent(Submit.PlatformDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setOverviewAction(Action);
|
||||
|
||||
@@ -21,7 +21,11 @@ export const load: PageLoad = async ({ depends, url, route, params, parent }) =>
|
||||
const { organization } = await parent();
|
||||
|
||||
const rules = await sdk.forProject(params.region, params.project).proxy.listRules({
|
||||
queries: [Query.equal('type', RuleType.API), Query.equal('trigger', RuleTrigger.MANUAL)],
|
||||
queries: [
|
||||
Query.equal('type', RuleType.API),
|
||||
Query.equal('trigger', RuleTrigger.MANUAL),
|
||||
Query.orderDesc('$updatedAt')
|
||||
],
|
||||
search: search || undefined
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { regionalProtocol } from '../../store';
|
||||
import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte';
|
||||
import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte';
|
||||
import { timeFromNowShort } from '$lib/helpers/date';
|
||||
|
||||
let {
|
||||
domains,
|
||||
@@ -45,8 +46,36 @@
|
||||
type: 'string',
|
||||
format: 'string',
|
||||
width: { min: 300, max: 550 }
|
||||
},
|
||||
{
|
||||
id: 'updated',
|
||||
title: '',
|
||||
type: 'string',
|
||||
width: { min: 160, max: 180 }
|
||||
}
|
||||
];
|
||||
|
||||
function updatedLabel(proxyRule: Models.ProxyRule): string {
|
||||
if (proxyRule.status === 'verified') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const timeStr = timeFromNowShort(proxyRule.$updatedAt);
|
||||
if (timeStr === 'n/a') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix =
|
||||
proxyRule.status === 'created'
|
||||
? 'Checked'
|
||||
: proxyRule.status === 'verifying'
|
||||
? 'Updated'
|
||||
: proxyRule.status === 'unverified'
|
||||
? 'Failed'
|
||||
: '';
|
||||
|
||||
return prefix + ' ' + timeStr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root columns={[...columns, { id: 'actions', width: 40 }]} let:root>
|
||||
@@ -116,6 +145,15 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{:else if column.id === 'updated' && proxyRule.status !== 'verified'}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Typography.Text
|
||||
variant="m-400"
|
||||
color="--fgcolor-neutral-tertiary"
|
||||
style="font-size: 0.875rem;">
|
||||
{updatedLabel(proxyRule)}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Empty, Id } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import type { PageData } from './$types';
|
||||
@@ -58,7 +58,11 @@
|
||||
{:else if column.id === 'events'}
|
||||
{webhook.events.length}
|
||||
{:else if column.type === 'datetime'}
|
||||
{webhook[column.id] ? toLocaleDateTime(webhook[column.id]) : '-'}
|
||||
{#if webhook[column.id]}
|
||||
<DualTimeView time={webhook[column.id]} showDatetime />
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
{:else if column.id === 'enabled'}
|
||||
<Layout.Stack direction="row" gap="s" alignItems="normal">
|
||||
<Status
|
||||
|
||||
+16
-8
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
|
||||
@@ -37,25 +42,28 @@
|
||||
|
||||
let selectedDeployment: Models.Deployment | null = $state(null);
|
||||
|
||||
async function deleteDeployments(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((deploymentId) =>
|
||||
async function deleteDeployments(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((deploymentId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).sites.deleteDeployment({
|
||||
siteId: page.params.site,
|
||||
deploymentId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.DeploymentDelete);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.DeploymentDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.DeploymentDelete);
|
||||
} else {
|
||||
trackEvent(Submit.DeploymentDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([
|
||||
invalidate(Dependencies.DEPLOYMENTS),
|
||||
invalidate(Dependencies.SITE)
|
||||
]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export const load = async ({ params, depends, url, route, parent }) => {
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc(''),
|
||||
Query.orderDesc('$updatedAt'),
|
||||
...parsedQueries.values()
|
||||
],
|
||||
search: search || undefined
|
||||
|
||||
@@ -15,5 +15,12 @@ export const columns = writable<Column[]>([
|
||||
title: 'Target',
|
||||
type: 'string',
|
||||
width: { min: 120, max: 400 }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'updated',
|
||||
title: '',
|
||||
type: 'string',
|
||||
width: { min: 160, max: 180 }
|
||||
}
|
||||
]);
|
||||
|
||||
+32
@@ -25,6 +25,7 @@
|
||||
import { regionalProtocol } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import DnsRecordsAction from '$lib/components/domains/dnsRecordsAction.svelte';
|
||||
import ViewLogsModal from '$lib/components/domains/viewLogsModal.svelte';
|
||||
import { timeFromNowShort } from '$lib/helpers/date';
|
||||
|
||||
let {
|
||||
proxyRules,
|
||||
@@ -46,6 +47,28 @@
|
||||
? 'Deployed from ' + proxy.deploymentVcsProviderBranch
|
||||
: 'Active deployment';
|
||||
};
|
||||
|
||||
function updatedLabel(proxyRule: Models.ProxyRule): string {
|
||||
if (proxyRule.status === 'verified') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const timeStr = timeFromNowShort(proxyRule.$updatedAt);
|
||||
if (timeStr === 'n/a') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix =
|
||||
proxyRule.status === 'created'
|
||||
? 'Checked'
|
||||
: proxyRule.status === 'verifying'
|
||||
? 'Updated'
|
||||
: proxyRule.status === 'unverified'
|
||||
? 'Failed'
|
||||
: '';
|
||||
|
||||
return prefix + ' ' + timeStr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root columns={[...$columns, { id: 'actions', width: 40 }]} let:root>
|
||||
@@ -117,6 +140,15 @@
|
||||
</Layout.Stack>
|
||||
{:else if column.id === 'target'}
|
||||
{proxyTarget(proxyRule)}
|
||||
{:else if column.id === 'updated' && proxyRule.status !== 'verified'}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Typography.Text
|
||||
variant="m-400"
|
||||
color="--fgcolor-neutral-tertiary"
|
||||
style="font-size: 0.875rem;">
|
||||
{updatedLabel(proxyRule)}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
+15
-8
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
|
||||
import {
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Id,
|
||||
MultiSelectionTable
|
||||
} from '$lib/components';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Badge, Table, Typography } from '@appwrite.io/pink-svelte';
|
||||
@@ -26,8 +31,8 @@
|
||||
let selectedLogId = $state<string | null>(null);
|
||||
const filteredColumns = $derived(columns.filter((c) => !c.exclude));
|
||||
|
||||
async function deleteLogs(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((logId) =>
|
||||
async function deleteLogs(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((logId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).sites.deleteLog({
|
||||
siteId: page.params.site,
|
||||
logId
|
||||
@@ -35,14 +40,16 @@
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.LogDelete);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.LogDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.LogDelete);
|
||||
} else {
|
||||
trackEvent(Submit.LogDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.EXECUTIONS);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { timeFromNow } from '$lib/helpers/date';
|
||||
import { Avatar, Icon, Layout, Popover, Table, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { columns } from './store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
@@ -80,7 +80,7 @@
|
||||
time={site?.latestDeploymentCreatedAt ?? site.$createdAt} />
|
||||
{/if}
|
||||
{:else if column.id === '$createdAt'}
|
||||
{toLocaleDateTime(site[column.id])}
|
||||
<DualTimeView time={site[column.id]} showDatetime />
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
+13
-10
@@ -6,6 +6,7 @@
|
||||
import {
|
||||
Avatar,
|
||||
type DeleteOperationState,
|
||||
type DeleteOperation,
|
||||
Empty,
|
||||
EmptySearch,
|
||||
MultiSelectionTable,
|
||||
@@ -65,23 +66,25 @@
|
||||
showDelete = true;
|
||||
}
|
||||
|
||||
async function handleBulkDelete(selectedRows: string[]): Promise<DeleteOperationState> {
|
||||
const promises = selectedRows.map((fileId) => {
|
||||
return sdk.forProject(page.params.region, page.params.project).storage.deleteFile({
|
||||
async function handleBulkDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
|
||||
const result = await batchDelete((fileId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).storage.deleteFile({
|
||||
bucketId: page.params.bucket,
|
||||
fileId
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.FileDelete, { total: selectedRows.length });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.FileDelete);
|
||||
return error;
|
||||
if (result.error) {
|
||||
trackError(result.error, Submit.FileDelete);
|
||||
} else {
|
||||
trackEvent(Submit.FileDelete, { total: result.deleted.length });
|
||||
}
|
||||
} finally {
|
||||
await invalidate(Dependencies.FILES);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const beforeunload = (event: BeforeUnloadEvent) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Id } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { Table } from '@appwrite.io/pink-svelte';
|
||||
@@ -30,8 +30,10 @@
|
||||
{/key}
|
||||
{:else if column.id === 'name'}
|
||||
{bucket.name}
|
||||
{:else if column.type === 'datetime'}
|
||||
<DualTimeView time={bucket[column.id]} showDatetime />
|
||||
{:else}
|
||||
{toLocaleDateTime(bucket[column.id])}
|
||||
{bucket[column.id]}
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
|
||||
@@ -94,6 +94,15 @@
|
||||
<svelte:fragment>
|
||||
<Form onSubmit={login}>
|
||||
<Layout.Stack>
|
||||
{#if isCloud}
|
||||
<div style:margin-bottom="var(--gap-s, 8px)">
|
||||
<Button secondary fullWidth on:click={onGithubLogin} {disabled}>
|
||||
<span class="icon-github" aria-hidden="true"></span>
|
||||
<span class="text">Sign in with GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
{/if}
|
||||
<InputEmail
|
||||
id="email"
|
||||
label="Email"
|
||||
@@ -108,13 +117,6 @@
|
||||
required={true}
|
||||
bind:value={pass} />
|
||||
<Button fullWidth submit {disabled}>Sign in</Button>
|
||||
{#if isCloud}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<Button secondary fullWidth on:click={onGithubLogin} {disabled}>
|
||||
<span class="icon-github" aria-hidden="true"></span>
|
||||
<span class="text">Sign in with GitHub</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -106,9 +106,17 @@
|
||||
}
|
||||
|
||||
function onGithubLogin() {
|
||||
let successUrl = window.location.origin;
|
||||
|
||||
if (page.url.searchParams.has('code')) {
|
||||
successUrl += `?code=${page.url.searchParams.get('code')}`;
|
||||
} else if (page.url.searchParams.has('campaign')) {
|
||||
successUrl += `?campaign=${page.url.searchParams.get('campaign')}`;
|
||||
}
|
||||
|
||||
sdk.forConsole.account.createOAuth2Session({
|
||||
provider: OAuthProvider.Github,
|
||||
success: window.location.origin,
|
||||
success: successUrl,
|
||||
failure: window.location.origin,
|
||||
scopes: ['read:user', 'user:email']
|
||||
});
|
||||
@@ -124,6 +132,16 @@
|
||||
<svelte:fragment>
|
||||
<Form onSubmit={register}>
|
||||
<Layout.Stack>
|
||||
{#if isCloud}
|
||||
<div style:margin-bottom="var(--gap-s, 8px)">
|
||||
<Button secondary fullWidth on:click={onGithubLogin} {disabled}>
|
||||
<span class="icon-github" aria-hidden="true"></span>
|
||||
<span class="text">Sign up with GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
{/if}
|
||||
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
@@ -159,18 +177,6 @@
|
||||
>.</InputChoice>
|
||||
|
||||
<Button fullWidth submit disabled={disabled || !terms}>Sign up</Button>
|
||||
|
||||
{#if isCloud}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
on:click={onGithubLogin}
|
||||
disabled={disabled || !terms}>
|
||||
<span class="icon-github" aria-hidden="true"></span>
|
||||
<span class="text">Sign up with GitHub</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
</svelte:fragment>
|
||||
|
||||
Reference in New Issue
Block a user