mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into fix-upload-context-switches.
This commit is contained in:
@@ -41,6 +41,7 @@ jobs:
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
|
||||
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
|
||||
publish-cloud-stage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -150,3 +150,9 @@ dist
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
# IDE specifics
|
||||
.idea
|
||||
@@ -22,11 +22,14 @@ ARG PUBLIC_CONSOLE_MODE
|
||||
ARG PUBLIC_APPWRITE_ENDPOINT
|
||||
ARG PUBLIC_GROWTH_ENDPOINT
|
||||
ARG PUBLIC_STRIPE_KEY
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
|
||||
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
|
||||
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
|
||||
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
|
||||
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
|
||||
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
|
||||
ENV NODE_OPTIONS=--max_old_space_size=8192
|
||||
|
||||
RUN pnpm run sync && pnpm run build
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
|
||||
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
|
||||
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
|
||||
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
|
||||
Generated
+1350
-224
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -19,10 +19,11 @@
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "1.0.1",
|
||||
"@appwrite.io/console": "^1.2.0",
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.26.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"ai": "^2.2.37",
|
||||
"analytics": "^0.8.14",
|
||||
|
||||
Generated
+1290
-229
File diff suppressed because it is too large
Load Diff
+23
-10
@@ -1,14 +1,27 @@
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import { isCloud, isProd } from '$lib/system';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
Sentry.init({
|
||||
enabled: isCloud && isProd,
|
||||
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
|
||||
tracesSampleRate: 1,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 1,
|
||||
integrations: [Sentry.replayIntegration()]
|
||||
});
|
||||
|
||||
export const handleError: HandleClientError = Sentry.handleErrorWithSentry(
|
||||
async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
};
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { isCloud, isProd } from '$lib/system';
|
||||
|
||||
Sentry.init({
|
||||
enabled: isCloud && isProd,
|
||||
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
|
||||
tracesSampleRate: 1.0
|
||||
});
|
||||
|
||||
export const handle = sequence(sentryHandle());
|
||||
|
||||
export const handleError = handleErrorWithSentry();
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { plansInfo, tierFree, tierPro, type Tier } from '$lib/stores/billing';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
@@ -23,11 +23,11 @@
|
||||
on:click={() => (selectedTab = BillingPlan.PRO)}>
|
||||
{tierPro.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
<!-- <SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.SCALE}
|
||||
on:click={() => (selectedTab = BillingPlan.SCALE)}>
|
||||
{tierScale.name}
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabsItem> -->
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
export let size: Size;
|
||||
export let trimmed = true;
|
||||
export let trimmedSecondLine = false;
|
||||
export let id: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
@@ -15,6 +16,7 @@
|
||||
this={tag}
|
||||
class={`heading-level-${size} u-min-width-0 ${classes}`}
|
||||
class:u-trim-1={trimmed}
|
||||
class:u-trim-2={trimmedSecondLine}
|
||||
{style}
|
||||
{id}>
|
||||
<slot />
|
||||
|
||||
@@ -56,7 +56,7 @@ export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
|
||||
export { default as ClickableList } from './clickableList.svelte';
|
||||
export { default as ClickableListItem } from './clickableListItem.svelte';
|
||||
export { default as Id } from './id.svelte';
|
||||
export { default as ProgressBar } from './progressBar.svelte';
|
||||
export * from './progressbar';
|
||||
export { default as ProgressBarBig } from './progressBarBig.svelte';
|
||||
export { default as CreditCardInfo } from './creditCardInfo.svelte';
|
||||
export { default as CreditCardBrandImage } from './creditCardBrandImage.svelte';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
let inputDigitFields: InputDigits;
|
||||
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
@@ -10,6 +12,7 @@
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
inputDigitFields?.clearInputsAndRefocus();
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
@@ -24,6 +27,7 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
/** If true, the form will be submitted automatically when the code is entered. */
|
||||
@@ -40,8 +44,16 @@
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
disabled = false;
|
||||
try {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -72,7 +84,12 @@
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
|
||||
{/if}
|
||||
<InputDigits bind:value={code} required autofocus {autoSubmit} />
|
||||
<InputDigits
|
||||
bind:value={code}
|
||||
required
|
||||
autofocus
|
||||
{autoSubmit}
|
||||
bind:this={inputDigitFields} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Form } from '$lib/elements/forms';
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
@@ -19,6 +20,10 @@
|
||||
|
||||
let alert: HTMLElement;
|
||||
|
||||
beforeNavigate(() => {
|
||||
show = false;
|
||||
});
|
||||
|
||||
$: $disableCommands(show);
|
||||
|
||||
$: if (error) {
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<script lang="ts">
|
||||
export let currentValue: string;
|
||||
export let currentUnit: string;
|
||||
export let maxValue: string;
|
||||
export let maxUnit: string;
|
||||
import { ProgressBar, type ProgressbarData } from '$lib/components/progressbar';
|
||||
|
||||
export let currentValue: string | undefined = undefined;
|
||||
export let currentUnit: string | undefined = undefined;
|
||||
export let maxValue: string | undefined = undefined;
|
||||
export let maxUnit: string | undefined = undefined;
|
||||
export let progressValue: number;
|
||||
export let progressMax: number;
|
||||
export let showBar = true;
|
||||
export let minimum = 0;
|
||||
export let maximum = 100;
|
||||
export let progressBarData: Array<ProgressbarData> = [];
|
||||
|
||||
$: progress = Math.round((progressValue / progressMax) * 100);
|
||||
</script>
|
||||
|
||||
<section class="progress-bar">
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{currentValue}</span>
|
||||
<span class="body-text-1 u-bold">{currentUnit}</span>
|
||||
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined && maxUnit !== undefined}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{currentValue}</span>
|
||||
<span class="body-text-1 u-bold">{currentUnit}</span>
|
||||
</p>
|
||||
<p class="heading-level-4">{progress}%</p>
|
||||
</div>
|
||||
|
||||
<p class="body-text-2">
|
||||
{maxValue}
|
||||
{maxUnit}
|
||||
</p>
|
||||
<p class="heading-level-4">{progress}%</p>
|
||||
</div>
|
||||
<p class="body-text-2">
|
||||
{maxValue}
|
||||
{maxUnit}
|
||||
</p>
|
||||
</div>
|
||||
{#if showBar}
|
||||
<div
|
||||
class="progress-bar-container u-margin-block-start-16"
|
||||
class:is-warning={progress >= 75 && progress < 100}
|
||||
class:is-danger={progress >= 100}
|
||||
style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} />
|
||||
{/if}
|
||||
{#if showBar && progressBarData.length > 0}
|
||||
<ProgressBar maxSize={progressMax} data={progressBarData} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import type { ProgressbarData, ProgressbarProps } from '$lib/components';
|
||||
|
||||
type $$Props = ProgressbarProps;
|
||||
|
||||
/**
|
||||
* The max value of the progressbar
|
||||
*/
|
||||
export let maxSize: $$Props['maxSize'];
|
||||
|
||||
/**
|
||||
* The data for the progressbar
|
||||
*/
|
||||
export let data: $$Props['data'];
|
||||
|
||||
/**
|
||||
* The remaining value of the progressbar
|
||||
*/
|
||||
$: remainder = data.reduce((sum: number, item: ProgressbarData) => sum - item.size, maxSize);
|
||||
</script>
|
||||
|
||||
<section class="progressbar__container">
|
||||
{#each $$props.data as item}
|
||||
<div
|
||||
class="progressbar__content"
|
||||
style:background-color={item.color}
|
||||
style:width={`${(item.size / maxSize) * 100}%`}
|
||||
use:tooltip={{
|
||||
disabled: !item.tooltip,
|
||||
allowHTML: true,
|
||||
content: `<span class="u-bold">${item.tooltip.title}</span> ${item.tooltip.label}`
|
||||
}}>
|
||||
</div>
|
||||
{/each}
|
||||
{#if remainder > 0}
|
||||
<div class="progressbar__content" style:width={`${(remainder / maxSize) * 100}%`} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
:root {
|
||||
--progressbar-border-radius: 0.25rem;
|
||||
--progressbar-tooltip-label-color: #818186;
|
||||
--progressbar-tooltip-link-color: #6c6c71;
|
||||
}
|
||||
|
||||
:global(.theme-dark) {
|
||||
--progressbar-background-color: var(--neutral-800, #2d2d31);
|
||||
--progressbar-tooltip-background-color: var(--neutral-800, #2d2d31);
|
||||
--progressbar-tooltip-border-color: var(--neutral-80, #424248);
|
||||
}
|
||||
:global(.theme-light) {
|
||||
--progressbar-background-color: var(--neutral-40, #f4f4f7);
|
||||
--progressbar-tooltip-background-color: #ffffff;
|
||||
--progressbar-tooltip-border-color: #ededf0;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
&__container {
|
||||
height: 0.5rem;
|
||||
background-color: var(--progressbar-background-color);
|
||||
border-radius: var(--progressbar-border-radius);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
height: 100%;
|
||||
min-width: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 2.5rem;
|
||||
margin-top: -1.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: var(--progressbar-border-radius);
|
||||
border-bottom-left-radius: var(--progressbar-border-radius);
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: var(--progressbar-border-radius);
|
||||
border-bottom-right-radius: var(--progressbar-border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
export type ProgressbarData = {
|
||||
size: number;
|
||||
color: string;
|
||||
tooltip?: {
|
||||
title: string;
|
||||
label: string;
|
||||
// linkTitle?: string;
|
||||
// linkPath?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProgressbarProps = {
|
||||
maxSize: number;
|
||||
data: Array<ProgressbarData>;
|
||||
};
|
||||
|
||||
export { default as ProgressBar } from './ProgressBar.svelte';
|
||||
@@ -38,6 +38,21 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the input fields and moves the focus to the first input.
|
||||
* Usually used when resetting fields on auth fails, etc.
|
||||
*/
|
||||
export function clearInputsAndRefocus() {
|
||||
value = '';
|
||||
autoSubmitted = false;
|
||||
|
||||
if (element) {
|
||||
const inputs = element.querySelectorAll('input');
|
||||
inputs.forEach((input) => (input.value = ''));
|
||||
if (autofocus) inputs[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isFileOverSize(file: File) {
|
||||
if (maxSize && file.size > maxSize) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function dropHandler(ev: DragEvent) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
hovering = false;
|
||||
@@ -47,6 +54,10 @@
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
if (isFileOverSize(ev.dataTransfer.items[i].getAsFile())) {
|
||||
error = 'File size exceeds the limit';
|
||||
return;
|
||||
}
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(ev.dataTransfer.items[i].getAsFile());
|
||||
@@ -86,12 +97,18 @@
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return isFileExtensionAllowed(fileExtension);
|
||||
});
|
||||
const isOverSize = maxSize && Array.from(target.files).some((file) => isFileOverSize(file));
|
||||
|
||||
if (!isValidFiles) {
|
||||
error = 'Invalid file extension';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
if (isOverSize) {
|
||||
error = 'File size exceeds the limit';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(target.files);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll {classes}" class:u-margin-block-start-16={!noMargin} data-private>
|
||||
<div
|
||||
class="table-with-scroll {classes}"
|
||||
style:border-radius={noStyles ? '0' : ''}
|
||||
class:u-margin-block-start-16={!noMargin}
|
||||
data-private>
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<table
|
||||
class="table"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script>
|
||||
import { settings } from '$lib/components/consent.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { version } from '$routes/(console)/store';
|
||||
|
||||
@@ -44,14 +42,9 @@
|
||||
</li>
|
||||
{#if isCloud}
|
||||
<li class="inline-links-item">
|
||||
<span
|
||||
style:cursor="pointer"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click={() => settings.set(true)}>
|
||||
<a href="https://appwrite.io/cookies" target="_blank" rel="noreferrer">
|
||||
<span class="text">Cookies</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
@@ -178,6 +178,9 @@ export type OrganizationUsage = {
|
||||
bandwidth: Array<Models.Metric>;
|
||||
executions: Array<Models.Metric>;
|
||||
executionsTotal: number;
|
||||
filesStorageTotal: number;
|
||||
buildsStorageTotal: number;
|
||||
deploymentsStorageTotal: number;
|
||||
storageTotal: number;
|
||||
users: Array<Models.Metric>;
|
||||
usersTotal: number;
|
||||
|
||||
@@ -306,6 +306,8 @@ export async function paymentExpired(org: Organization) {
|
||||
org.paymentMethodId
|
||||
);
|
||||
if (!payment?.expiryYear) return;
|
||||
const sessionStorageNotification = sessionStorage.getItem('expiredPaymentNotification');
|
||||
if (sessionStorageNotification === 'true') return;
|
||||
const year = new Date().getFullYear();
|
||||
const month = new Date().getMonth();
|
||||
const expiredMessage = `The default payment method for <b>${org.name}</b> has expired`;
|
||||
@@ -343,6 +345,7 @@ export async function paymentExpired(org: Organization) {
|
||||
]
|
||||
});
|
||||
}
|
||||
sessionStorage.setItem('expiredPaymentNotification', 'true');
|
||||
}
|
||||
|
||||
export function checkForMarkedForDeletion(org: Organization) {
|
||||
|
||||
@@ -138,6 +138,24 @@ campaigns
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('NetNinja', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Net Ninja credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('CodeWithAntonio', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Code With Antonio credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('Hacktoberfest2024', {
|
||||
template: 'card',
|
||||
title: 'Claim your $60 Hacktobefest credits.',
|
||||
description:
|
||||
'Get $60 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('VueJS', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 VueJS credits.',
|
||||
|
||||
@@ -23,5 +23,7 @@ export const ENV = {
|
||||
export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED;
|
||||
export const isCloud = MODE === Mode.CLOUD;
|
||||
export const isSelfHosted = MODE !== Mode.CLOUD;
|
||||
export const isDev = ENV.DEV;
|
||||
export const isProd = ENV.PROD;
|
||||
export const hasStripePublicKey = !!VARS.PUBLIC_STRIPE_KEY;
|
||||
export const GRACE_PERIOD_OVERRIDE = false;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
const callbackState = {
|
||||
from: 'github',
|
||||
to: 'template',
|
||||
step: '4',
|
||||
step: '5',
|
||||
template: $template.id,
|
||||
templateConfig: JSON.stringify($templateConfig)
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
}} />
|
||||
</p>
|
||||
<p class="text u-margin-block-start-4">
|
||||
This could include unauthorized users and search engines.
|
||||
This could include unauthorized users and bots.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
{
|
||||
label: 'Create new organization',
|
||||
callback: () => {
|
||||
newOrgModal.set(true);
|
||||
isCloud ? goto(`${base}/create-organization`) : newOrgModal.set(true);
|
||||
},
|
||||
keys: ['c', 'o'],
|
||||
group: 'organizations'
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export let showDelete = false;
|
||||
let error: string;
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
@@ -19,12 +20,9 @@
|
||||
message: `Account was deleted `
|
||||
});
|
||||
trackEvent(Submit.AccountDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountDelete);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.AccountDelete);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -32,6 +30,7 @@
|
||||
<Modal
|
||||
title="Delete account"
|
||||
bind:show={showDelete}
|
||||
bind:error
|
||||
onSubmit={deleteAccount}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AuthenticatorType, type Models } from '@appwrite.io/console';
|
||||
import QrFrame from '$lib/images/qr2.svg';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let showSetup = false;
|
||||
|
||||
@@ -28,6 +29,10 @@
|
||||
await sdk.forConsole.account.updateMfaAuthenticator(AuthenticatorType.Totp, code);
|
||||
await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
|
||||
showSetup = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Authenticator app connected successfully'
|
||||
});
|
||||
trackEvent(Submit.AccountAuthenticatorUpdate);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
content:
|
||||
'You are limited to 1 free organization per account'
|
||||
}}>
|
||||
<Pill>FREE</Pill>
|
||||
<Pill class="eyebrow-heading-3">FREE</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
{#if organization?.billingTrialStartDate && $daysLeftInTrial > 0 && organization.billingPlan !== BillingPlan.FREE && $plansInfo.get(organization.billingPlan)?.trialDays}
|
||||
@@ -89,7 +89,7 @@
|
||||
organization.billingStartDate
|
||||
)}. ${$daysLeftInTrial} days remaining.`
|
||||
}}>
|
||||
<Pill>TRIAL</Pill>
|
||||
<Pill class="eyebrow-heading-3">TRIAL</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -109,13 +109,18 @@
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-device-mobile" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Authenticator app</span>
|
||||
<span
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
<span class="body-text-2 u-bold">Authenticator app</span>
|
||||
{#if $factors.totp}
|
||||
<Pill>connected</Pill>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="body-text-2"
|
||||
>Use an authentication app to generate two-factor authentication
|
||||
codes.</span>
|
||||
</div>
|
||||
@@ -141,7 +146,7 @@
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-mail" aria-hidden="true" />
|
||||
</div>
|
||||
@@ -185,7 +190,7 @@
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="u-flex u-gap-8 u-cross-baseline">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-lock-open" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
{
|
||||
value: BillingPlan.PRO,
|
||||
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
|
||||
},
|
||||
{
|
||||
value: BillingPlan.SCALE,
|
||||
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
|
||||
}
|
||||
// {
|
||||
// value: BillingPlan.SCALE,
|
||||
// label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
|
||||
// }
|
||||
]
|
||||
: [];
|
||||
|
||||
|
||||
@@ -149,9 +149,9 @@
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
message: `
|
||||
<b>${$organization.name}</b> has been changed to ${
|
||||
tierToPlan(billingPlan).name
|
||||
} plan.`
|
||||
<b>${$organization.name}</b> will change to ${
|
||||
tierToPlan(billingPlan).name
|
||||
} plan at the end of the current billing cycle.`
|
||||
});
|
||||
|
||||
trackEvent(Submit.OrganizationDowngrade, {
|
||||
@@ -162,7 +162,7 @@
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade);
|
||||
trackError(e, Submit.OrganizationDowngrade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,22 +213,12 @@
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
await goto(`${base}/organization-${org.$id}`);
|
||||
if (isUpgrade) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Your organization has been upgraded'
|
||||
});
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
message: `
|
||||
<b>${$organization.name}</b> will change to ${
|
||||
tierToPlan(billingPlan).name
|
||||
} plan at the end of the current billing cycle.`
|
||||
});
|
||||
}
|
||||
trackEvent(isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Your organization has been upgraded'
|
||||
});
|
||||
|
||||
trackEvent(Submit.OrganizationUpgrade, {
|
||||
plan: tierToPlan(billingPlan)?.name
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -236,7 +226,7 @@
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade);
|
||||
trackError(e, Submit.OrganizationUpgrade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +314,7 @@
|
||||
bind:value={feedbackDowngradeReason} />
|
||||
<InputTextarea
|
||||
id="comment"
|
||||
required
|
||||
label="If you need to elaborate, please do so here"
|
||||
placeholder="Enter feedback"
|
||||
bind:value={feedbackMessage} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, Modal } from '$lib/components';
|
||||
import { InputText, InputEmail, Button, FormList } from '$lib/elements/forms';
|
||||
@@ -17,7 +18,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const url = `${$page.url.origin}/invite`;
|
||||
const url = `${$page.url.origin}${base}/invite`;
|
||||
$: plan = $plansInfo?.get($organization?.billingPlan);
|
||||
|
||||
let email: string, name: string, error: string;
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
{$organization.name}
|
||||
</span>
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.FREE}
|
||||
<Pill>FREE</Pill>
|
||||
<Pill class="eyebrow-heading-3">FREE</Pill>
|
||||
{/if}
|
||||
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0 && $organization.billingPlan !== BillingPlan.FREE && $plansInfo.get($organization.billingPlan)?.trialDays}
|
||||
<div
|
||||
@@ -118,7 +118,7 @@
|
||||
$organization.billingStartDate
|
||||
)}. ${$daysLeftInTrial} days remaining.`
|
||||
}}>
|
||||
<Pill>TRIAL</Pill>
|
||||
<Pill class="eyebrow-heading-3">TRIAL</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Box, CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
@@ -43,7 +44,7 @@
|
||||
external
|
||||
class="u-margin-block-start-16"
|
||||
on:click={downloadPdf}
|
||||
href="/legal/dpa.pdf"
|
||||
href="{base}/legal/dpa.pdf"
|
||||
event="download_dpa">
|
||||
<span class="icon-download" aria-hidden="true" />
|
||||
<span class="text">Download</span>
|
||||
|
||||
@@ -149,7 +149,9 @@
|
||||
}
|
||||
}
|
||||
]} />
|
||||
<ProjectBreakdown projects={project} metric="bandwidth" {data} />
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="bandwidth" {data} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
@@ -198,7 +200,9 @@
|
||||
)
|
||||
}
|
||||
]} />
|
||||
<ProjectBreakdown projects={project} metric="users" {data} />
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="users" {data} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
@@ -248,7 +252,9 @@
|
||||
]
|
||||
}
|
||||
]} />
|
||||
<ProjectBreakdown projects={project} metric="executions" {data} />
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="executions" {data} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
@@ -276,6 +282,32 @@
|
||||
{@const current = data.organizationUsage.storageTotal}
|
||||
{@const currentHumanized = humanFileSize(current)}
|
||||
{@const max = getServiceLimit('storage', tier)}
|
||||
{@const progressBarStorageDate = [
|
||||
{
|
||||
size: bytesToSize(data.organizationUsage.filesStorageTotal, 'GB'),
|
||||
color: '#85DBD8',
|
||||
tooltip: {
|
||||
title: 'File storage',
|
||||
label: `${Math.round(bytesToSize(data.organizationUsage.filesStorageTotal, 'GB') * 100) / 100}GB`
|
||||
}
|
||||
},
|
||||
{
|
||||
size: bytesToSize(data.organizationUsage.deploymentsStorageTotal, 'GB'),
|
||||
color: '#7C67FE',
|
||||
tooltip: {
|
||||
title: 'Deployments storage',
|
||||
label: `${Math.round(bytesToSize(data.organizationUsage.deploymentsStorageTotal, 'GB') * 100) / 100}GB`
|
||||
}
|
||||
},
|
||||
{
|
||||
size: bytesToSize(data.organizationUsage.buildsStorageTotal, 'GB'),
|
||||
color: '#FE9567',
|
||||
tooltip: {
|
||||
title: 'Builds storage',
|
||||
label: `${Math.round(bytesToSize(data.organizationUsage.buildsStorageTotal, 'GB') * 100) / 100}GB`
|
||||
}
|
||||
}
|
||||
]}
|
||||
<ProgressBarBig
|
||||
currentUnit={currentHumanized.unit}
|
||||
currentValue={currentHumanized.value}
|
||||
@@ -283,8 +315,10 @@
|
||||
maxValue={max.toString()}
|
||||
progressValue={bytesToSize(current, 'GB')}
|
||||
progressMax={max}
|
||||
minimum={1} />
|
||||
<ProjectBreakdown projects={project} metric="storage" {data} />
|
||||
progressBarData={progressBarStorageDate} />
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="storage" {data} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
|
||||
@@ -19,6 +19,9 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
users: null,
|
||||
usersTotal: null,
|
||||
storageTotal: null,
|
||||
filesStorageTotal: null,
|
||||
buildsStorageTotal: null,
|
||||
deploymentsStorageTotal: null,
|
||||
executions: null,
|
||||
executionsTotal: null,
|
||||
projects: null
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Button, Form, FormItem, FormItemPart } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -32,7 +33,7 @@
|
||||
? 'Available on Appwrite Cloud'
|
||||
: 'Upgrade to add mock phone numbers';
|
||||
let emptyStateDescription: string = isSelfHosted
|
||||
? 'Sign up to Cloud to add mock phone numbers to your projects.'
|
||||
? 'Sign up for Cloud to add mock phone numbers to your projects.'
|
||||
: 'Upgrade to a Pro plan to add mock phone numbers to your project.';
|
||||
let cta: string = isSelfHosted ? 'Sign up' : 'Upgrade plan';
|
||||
|
||||
@@ -137,8 +138,8 @@
|
||||
class="u-margin-block-start-32"
|
||||
secondary
|
||||
fullWidth
|
||||
href="https://cloud.appwrite.io/register"
|
||||
external
|
||||
external={isSelfHosted}
|
||||
href={isCloud ? $upgradeURL : 'https://cloud.appwrite.io/register'}
|
||||
on:click={() => {
|
||||
trackEvent('click_cloud_signup', {
|
||||
from: 'button',
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
>here</a
|
||||
>.
|
||||
</p>
|
||||
<div class="u-margin-block-start-16 u-flex u-gap-8">
|
||||
<div class="u-margin-block-start-16 u-flex u-flex-wrap u-gap-8">
|
||||
<slot />
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
<Box radius="small" class="u-flex u-gap-16 u-cross-center">
|
||||
<div class="u-un-break-text">
|
||||
<div class="u-un-break-text is-not-mobile">
|
||||
<span class="icon-translate" />
|
||||
<span class="text">Template language</span>
|
||||
</div>
|
||||
|
||||
+88
-83
@@ -44,30 +44,11 @@
|
||||
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="heading-grid u-main-justify-between u-cross-center">
|
||||
<Heading tag="h2" size="5">Documents</Heading>
|
||||
<div class="u-flex u-main-end is-only-mobile">
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
on:click={openWizard}
|
||||
event="create_document">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create document</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Filters query={data.query} {columns} disabled={!(hasAttributes && hasValidAttributes)} />
|
||||
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
isCustomCollection
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<div class="is-not-mobile">
|
||||
{#key $page.params.collection}
|
||||
<Container>
|
||||
<div class="heading-grid u-main-justify-between u-cross-center">
|
||||
<Heading tag="h2" size="5">Documents</Heading>
|
||||
<div class="u-flex u-main-end is-only-mobile">
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
on:click={openWizard}
|
||||
@@ -76,76 +57,100 @@
|
||||
<span class="text">Create document</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Filters
|
||||
query={data.query}
|
||||
{columns}
|
||||
disabled={!(hasAttributes && hasValidAttributes)} />
|
||||
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
isCustomCollection
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<div class="is-not-mobile">
|
||||
<Button
|
||||
disabled={!(hasAttributes && hasValidAttributes)}
|
||||
on:click={openWizard}
|
||||
event="create_document">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create document</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasAttributes && hasValidAttributes}
|
||||
{#if data.documents.total}
|
||||
<Table {data} />
|
||||
{#if hasAttributes && hasValidAttributes}
|
||||
{#if data.documents.total}
|
||||
<Table {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Documents"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.documents.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
|
||||
<p>There are no documents that match your filters.</p>
|
||||
<PaginationWithLimit
|
||||
name="Documents"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.documents.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry, we couldn't find any documents.</b>
|
||||
<p>There are no documents that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex common-section u-main-center">
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
href="https://appwrite.io/docs/products/databases/documents"
|
||||
target="document"
|
||||
on:click={openWizard} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/databases/collections#attributes"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Clear filters
|
||||
Create attribute
|
||||
</Button>
|
||||
</div>
|
||||
</CreateAttributeDropdown>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
href="https://appwrite.io/docs/products/databases/documents"
|
||||
target="document"
|
||||
on:click={openWizard} />
|
||||
</Empty>
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/databases/collections#attributes"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</Container>
|
||||
</Container>
|
||||
{/key}
|
||||
|
||||
<CreateAttribute bind:showCreate={showCreateAttribute} bind:selectedOption={selectedAttribute} />
|
||||
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@
|
||||
showEdit = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Edit
|
||||
Update
|
||||
</DropListItem>
|
||||
{#if !isRelationship(attribute)}
|
||||
<DropListItem
|
||||
|
||||
+4
-3
@@ -20,15 +20,16 @@
|
||||
export async function updateBoolean(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
|
||||
data: Partial<Models.AttributeBoolean>
|
||||
data: Partial<Models.AttributeBoolean>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateBooleanAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+4
-2
@@ -21,14 +21,16 @@
|
||||
export async function updateDatetime(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeDatetime>
|
||||
data: Partial<Models.AttributeDatetime>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateDatetimeAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+6
-3
@@ -16,6 +16,7 @@
|
||||
|
||||
const databaseId = $page.params.database;
|
||||
const collectionId = $page.params.collection;
|
||||
let originalKey = '';
|
||||
|
||||
let error: string;
|
||||
let currentAttr: Attributes;
|
||||
@@ -32,7 +33,7 @@
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await option.update(databaseId, collectionId, selectedAttribute);
|
||||
await option.update(databaseId, collectionId, selectedAttribute, originalKey);
|
||||
await invalidate(Dependencies.COLLECTION);
|
||||
if (!$page.url.pathname.includes('attributes')) {
|
||||
await goto(
|
||||
@@ -53,7 +54,10 @@
|
||||
|
||||
$: if (showEdit) {
|
||||
currentAttr ??= { ...selectedAttribute };
|
||||
originalKey = currentAttr.key;
|
||||
error = null;
|
||||
} else {
|
||||
currentAttr = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -76,8 +80,7 @@
|
||||
label="Attribute Key"
|
||||
placeholder="Enter Key"
|
||||
bind:value={selectedAttribute.key}
|
||||
autofocus
|
||||
readonly />
|
||||
autofocus />
|
||||
{/if}
|
||||
{#if option}
|
||||
<svelte:component
|
||||
|
||||
+4
-2
@@ -22,14 +22,16 @@
|
||||
export async function updateEmail(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeEmail>
|
||||
data: Partial<Models.AttributeEmail>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateEmailAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+4
-2
@@ -22,7 +22,8 @@
|
||||
export async function updateEnum(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeEnum>
|
||||
data: Partial<Models.AttributeEnum>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateEnumAttribute(
|
||||
databaseId,
|
||||
@@ -30,7 +31,8 @@
|
||||
data.key,
|
||||
data.elements,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+4
-2
@@ -23,7 +23,8 @@
|
||||
export async function updateFloat(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeFloat>
|
||||
data: Partial<Models.AttributeFloat>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateFloatAttribute(
|
||||
databaseId,
|
||||
@@ -32,7 +33,8 @@
|
||||
data.required,
|
||||
data.min,
|
||||
data.max,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+4
-2
@@ -23,7 +23,8 @@
|
||||
export async function updateInteger(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeInteger>
|
||||
data: Partial<Models.AttributeInteger>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateIntegerAttribute(
|
||||
databaseId,
|
||||
@@ -32,7 +33,8 @@
|
||||
data.required,
|
||||
data.min,
|
||||
data.max,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+4
-2
@@ -20,14 +20,16 @@
|
||||
export async function updateIp(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeIp>
|
||||
data: Partial<Models.AttributeIp>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateIpAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
+6
-4
@@ -29,16 +29,19 @@
|
||||
export async function updateRelationship(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeRelationship>
|
||||
data: Partial<Models.AttributeRelationship>,
|
||||
originalKey?: string
|
||||
) {
|
||||
if (!isValueOfStringEnum(RelationMutate, data.onDelete)) {
|
||||
throw new Error(`Invalid on delete: ${data.onDelete}`);
|
||||
}
|
||||
|
||||
await sdk.forProject.databases.updateRelationshipAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.onDelete
|
||||
originalKey,
|
||||
data.onDelete,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -187,7 +190,6 @@
|
||||
placeholder="Enter Key"
|
||||
bind:value={data.key}
|
||||
autofocus
|
||||
readonly={editing}
|
||||
required />
|
||||
|
||||
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small u-cross-center">
|
||||
|
||||
+6
-1
@@ -32,7 +32,12 @@ export type Option = {
|
||||
key: string,
|
||||
data: Partial<Attributes>
|
||||
) => Promise<void>;
|
||||
update: (databaseId: string, collectionId: string, data: Partial<Attributes>) => Promise<void>;
|
||||
update: (
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Attributes>,
|
||||
originalKey: string
|
||||
) => Promise<void>;
|
||||
format?: 'email' | 'ip' | 'url' | 'enum';
|
||||
icon: string;
|
||||
};
|
||||
|
||||
+7
-10
@@ -21,14 +21,17 @@
|
||||
export async function updateString(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeString>
|
||||
data: Partial<Models.AttributeString>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateStringAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.size,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -69,13 +72,7 @@
|
||||
$: handleDefaultState($required || $array);
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
id="size"
|
||||
label="Size"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.size}
|
||||
required={!editing}
|
||||
readonly={editing} />
|
||||
<InputNumber id="size" label="Size" placeholder="Enter size" bind:value={data.size} />
|
||||
{#if data.size >= 50}
|
||||
<InputTextarea
|
||||
id="default"
|
||||
|
||||
+4
-2
@@ -21,14 +21,16 @@
|
||||
export async function updateUrl(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeUrl>
|
||||
data: Partial<Models.AttributeUrl>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateUrlAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
data.required,
|
||||
data.default
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script context="module" lang="ts">
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export async function submitString(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
key: string,
|
||||
data: Partial<Models.AttributeString>
|
||||
) {
|
||||
await sdk.forProject.databases.createStringAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
key,
|
||||
data.size,
|
||||
data.required,
|
||||
data.default,
|
||||
data.array
|
||||
);
|
||||
}
|
||||
export async function updateString(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
data: Partial<Models.AttributeString>,
|
||||
originalKey?: string
|
||||
) {
|
||||
await sdk.forProject.databases.updateStringAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.size,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import { createConservative } from '$lib/helpers/stores';
|
||||
|
||||
export let data: Partial<Models.AttributeString> = {
|
||||
required: false,
|
||||
size: 0,
|
||||
default: null,
|
||||
array: false
|
||||
};
|
||||
export let editing = false;
|
||||
|
||||
let savedDefault = data.default;
|
||||
|
||||
function handleDefaultState(hideDefault: boolean) {
|
||||
if (hideDefault) {
|
||||
savedDefault = data.default;
|
||||
data.default = null;
|
||||
} else {
|
||||
data.default = savedDefault;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
stores: { required, array },
|
||||
listen
|
||||
} = createConservative<Partial<Models.AttributeString>>({
|
||||
required: false,
|
||||
array: false,
|
||||
...data
|
||||
});
|
||||
$: listen(data);
|
||||
|
||||
$: handleDefaultState($required || $array);
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
id="size"
|
||||
label="Size"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.size}
|
||||
required={!editing}
|
||||
readonly={editing} />
|
||||
{#if data.size >= 50}
|
||||
<InputTextarea
|
||||
id="default"
|
||||
label="Default"
|
||||
placeholder="Enter string"
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array}
|
||||
maxlength={data.size}
|
||||
bind:value={data.default} />
|
||||
{:else}
|
||||
<InputText
|
||||
id="default"
|
||||
label="Default"
|
||||
placeholder="Enter string"
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array}
|
||||
maxlength={data.size}
|
||||
bind:value={data.default} />
|
||||
{/if}
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
<InputChoice id="array" label="Array" bind:value={data.array} disabled={data.required || editing}>
|
||||
Indicate whether this attribute should act as an array, with the default value set as an empty
|
||||
array.
|
||||
</InputChoice>
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Deployment size',
|
||||
title: 'Size',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
width: 140,
|
||||
|
||||
+8
-5
@@ -8,14 +8,17 @@
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
<Trim alternativeTrim>
|
||||
<a href={`http://${domain.rules[0].domain}`} target="_blank">
|
||||
<a
|
||||
href={`http://${domain.rules[0].domain}`}
|
||||
target="_blank"
|
||||
class="u-flex u-gap-4 u-cross-center">
|
||||
<Trim alternativeTrim>
|
||||
<span class="link">
|
||||
{domain.rules[0].domain}
|
||||
</span>
|
||||
</a>
|
||||
</Trim>
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
</Trim>
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
</a>
|
||||
{#if domain.rules.length > 1}
|
||||
<DropList bind:show={showDropdown} scrollable>
|
||||
<Pill button on:click={() => (showDropdown = !showDropdown)}>
|
||||
|
||||
-1
@@ -113,7 +113,6 @@
|
||||
`${base}/project-${$page.params.project}/functions/function-${func.$id}/executions`
|
||||
);
|
||||
invalidate(Dependencies.EXECUTIONS);
|
||||
close();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Function has been executed`
|
||||
|
||||
+12
-7
@@ -11,6 +11,7 @@
|
||||
|
||||
export let show = false;
|
||||
export let selectedDeployment: Models.Deployment = null;
|
||||
let error: string;
|
||||
|
||||
async function redeploy() {
|
||||
try {
|
||||
@@ -28,17 +29,21 @@
|
||||
invalidate(Dependencies.FUNCTION);
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionRedeploy);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
|
||||
trackError(e, Submit.FunctionRedeploy);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Redeploy function" size="big" bind:show onSubmit={redeploy} headerDivider={false}>
|
||||
<Modal
|
||||
title="Redeploy function"
|
||||
size="big"
|
||||
bind:show
|
||||
bind:error
|
||||
onSubmit={redeploy}
|
||||
headerDivider={false}>
|
||||
<p class="text">
|
||||
Are you sure you want to redeploy <b>{$func.name}</b>? Redeploying may affect your
|
||||
production code.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { CardGrid, Heading, Card } from '$lib/components';
|
||||
import { CardGrid, Heading, Card, ProgressBarBig } from '$lib/components';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -13,21 +13,25 @@
|
||||
import { showUsageRatesModal, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { bytesToSize, humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { total } from '$lib/layout/usage.svelte';
|
||||
import { BillingPlan } from '$lib/constants.js';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let data;
|
||||
|
||||
$: base = `${base}/project-${data.project.$id}`;
|
||||
$: baseRoute = `${base}/project-${data.project.$id}`;
|
||||
$: network = data.usage.network;
|
||||
$: users = data.usage.users;
|
||||
$: usersTotal = data.usage.usersTotal;
|
||||
$: executions = data.usage.executions;
|
||||
$: executionsTotal = data.usage.executionsTotal;
|
||||
$: storage = data.usage.filesStorageTotal;
|
||||
$: storage =
|
||||
data.usage.filesStorageTotal +
|
||||
data.usage.deploymentsStorageTotal +
|
||||
data.usage.buildsStorageTotal;
|
||||
|
||||
const tier = data?.currentInvoice?.plan ?? $organization?.billingPlan;
|
||||
const plan = tierToPlan(tier).name;
|
||||
@@ -237,7 +241,7 @@
|
||||
{formatNum(func.value)} executions
|
||||
</TableCell>
|
||||
<TableCellLink
|
||||
href={`${base}/functions/function-${func.resourceId}`}
|
||||
href={`${baseRoute}/functions/function-${func.resourceId}`}
|
||||
title="View function">
|
||||
View function
|
||||
</TableCellLink>
|
||||
@@ -270,6 +274,32 @@
|
||||
<svelte:fragment slot="aside">
|
||||
{#if storage}
|
||||
{@const humanized = humanFileSize(storage)}
|
||||
{@const progressBarStorageDate = [
|
||||
{
|
||||
size: bytesToSize(data.usage.filesStorageTotal, 'MB'),
|
||||
color: '#85DBD8',
|
||||
tooltip: {
|
||||
title: 'File storage',
|
||||
label: `${Math.round(bytesToSize(data.usage.filesStorageTotal, 'MB') * 100) / 100}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
size: bytesToSize(data.usage.deploymentsStorageTotal, 'MB'),
|
||||
color: '#7C67FE',
|
||||
tooltip: {
|
||||
title: 'Deployments storage',
|
||||
label: `${Math.round(bytesToSize(data.usage.deploymentsStorageTotal, 'MB') * 100) / 100}MB`
|
||||
}
|
||||
},
|
||||
{
|
||||
size: bytesToSize(data.usage.buildsStorageTotal, 'MB'),
|
||||
color: '#FE9567',
|
||||
tooltip: {
|
||||
title: 'Builds storage',
|
||||
label: `${Math.round(bytesToSize(data.usage.buildsStorageTotal, 'MB') * 100) / 100}MB`
|
||||
}
|
||||
}
|
||||
]}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
@@ -278,33 +308,10 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.usage.bucketsBreakdown.length > 0}
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={285}>Bucket</TableCellHead>
|
||||
<TableCellHead>Usage</TableCellHead>
|
||||
<TableCellHead width={140} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.usage.bucketsBreakdown.sort((a, b) => b.value - a.value) as bucket}
|
||||
{@const humanized = humanFileSize(bucket.value)}
|
||||
<TableRow>
|
||||
<TableCell title="Function">
|
||||
{bucket.name ?? bucket.resourceId}
|
||||
</TableCell>
|
||||
<TableCell title="Usage">
|
||||
{humanized.value}{humanized.unit}
|
||||
</TableCell>
|
||||
<TableCellLink
|
||||
href={`${base}/storage/bucket-${bucket.resourceId}`}
|
||||
title="View bucket">
|
||||
View bucket
|
||||
</TableCellLink>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
<ProgressBarBig
|
||||
progressValue={bytesToSize(storage, 'MB')}
|
||||
progressMax={bytesToSize(storage, 'MB')}
|
||||
progressBarData={progressBarStorageDate} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
import Create from './create-file/create.svelte';
|
||||
import DeleteFile from './deleteFile.svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -91,30 +90,6 @@
|
||||
$: maxFileSize = isCloud
|
||||
? humanFileSize(sizeToBytes(getServiceLimit('fileSize'), 'MB', 1000))
|
||||
: null;
|
||||
|
||||
let isUploading = false;
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (isUploading) {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'An upload is in progress. Are you sure you want to leave?';
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = uploader.subscribe(() => {
|
||||
isUploading = $uploader.files.some(
|
||||
(file) => (!file.completed || file.progress < 100) && !file.failed
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -172,14 +147,14 @@
|
||||
<TableCellHead onlyDesktop width={140}>Type</TableCellHead>
|
||||
<TableCellHead onlyDesktop width={100}>Size</TableCellHead>
|
||||
<TableCellHead onlyDesktop width={120}>Created</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.files.files as file, index}
|
||||
{#if file.chunksTotal / file.chunksUploaded !== 1}
|
||||
<TableRow>
|
||||
<TableCell title="Name">
|
||||
<div class="u-flex u-gap-12 u-main-space-between u-cross-center">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<span class="avatar is-size-small is-color-empty" />
|
||||
|
||||
<span class="text u-trim">{file.name}</span>
|
||||
@@ -197,12 +172,6 @@
|
||||
</TableCellText>
|
||||
<TableCell>
|
||||
<div class="u-flex u-main-center">
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="Delete item"
|
||||
on:click|preventDefault>
|
||||
<span class="icon-refresh" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="Delete item"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, url, route, parent }) => {
|
||||
const { organization } = await parent();
|
||||
@@ -13,20 +12,20 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
const search = getSearch(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
let organizationUsage: OrganizationUsage = null;
|
||||
if (isCloud && organization?.$id) {
|
||||
organizationUsage = await sdk.forConsole.billing.listUsage(organization.$id);
|
||||
}
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
files: await sdk.forProject.storage.listFiles(
|
||||
const [files, organizationUsage] = await Promise.all([
|
||||
sdk.forProject.storage.listFiles(
|
||||
params.bucket,
|
||||
[Query.limit(limit), Query.offset(offset), Query.orderDesc('')],
|
||||
search
|
||||
),
|
||||
isCloud && organization?.$id ? sdk.forConsole.billing.listUsage(organization.$id) : null
|
||||
]);
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
files,
|
||||
organizationUsage
|
||||
};
|
||||
};
|
||||
|
||||
+3
-1
@@ -47,7 +47,9 @@
|
||||
required
|
||||
bind:files={$createFile.files}
|
||||
allowedFileExtensions={$bucket.allowedFileExtensions}
|
||||
maxSize={$bucket.maximumFileSize} />
|
||||
maxSize={isCloud
|
||||
? $bucket.maximumFileSize
|
||||
: ($bucket.maximumFileSize ?? sizeToBytes(service, 'MB', 1000))} />
|
||||
</div>
|
||||
|
||||
{#if !showCustomId}
|
||||
|
||||
+12
-8
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, BoxAvatar, Heading, Alert, Id } from '$lib/components';
|
||||
import { CardGrid, BoxAvatar, Heading, Alert, CopyInput } from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { file } from './store';
|
||||
@@ -92,18 +92,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h2" size="7">{$file.name}</Heading>
|
||||
<Id value={getView($file.$id)}>File URL</Id>
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<Heading tag="h2" size="7" trimmed={false} trimmedSecondLine={true}
|
||||
>{$file.name}</Heading>
|
||||
<p>{$file.mimeType}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div>
|
||||
<p>MIME Type: {$file.mimeType}</p>
|
||||
<p>Size: {calculateSize($file.sizeOriginal)}</p>
|
||||
<p>Created: {toLocaleDate($file.$createdAt)}</p>
|
||||
<p>Last updated: {toLocaleDate($file.$updatedAt)}</p>
|
||||
<p><span class="u-bold">Size:</span> {calculateSize($file.sizeOriginal)}</p>
|
||||
<p><span class="u-bold">Created:</span> {toLocaleDate($file.$createdAt)}</p>
|
||||
<p>
|
||||
<span class="u-bold">Last updated:</span>
|
||||
{toLocaleDate($file.$updatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
<CopyInput label="File URL" showLabel={true} value={getView($file.$id)} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
body: JSON.stringify({
|
||||
subject: 'support',
|
||||
email: $user.email,
|
||||
firstName: $user?.name ?? '',
|
||||
firstName: $user?.name || 'Unknown',
|
||||
message: $supportData.message,
|
||||
tags: ['cloud'],
|
||||
customFields: [
|
||||
|
||||
@@ -51,17 +51,6 @@
|
||||
await goto(`${base}/apply-credit?campaign=${data.campaign}`);
|
||||
return;
|
||||
}
|
||||
if ($page.url.searchParams) {
|
||||
const redirect = $page.url.searchParams.get('redirect');
|
||||
$page.url.searchParams.delete('redirect');
|
||||
if (redirect) {
|
||||
await goto(`${redirect}${$page.url.search}`);
|
||||
} else {
|
||||
await goto(`${base}${$page.url.search ?? ''}`);
|
||||
}
|
||||
} else {
|
||||
await goto(base);
|
||||
}
|
||||
} catch (error) {
|
||||
disabled = false;
|
||||
addNotification({
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 416 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 416 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
+2
-1
@@ -6,7 +6,8 @@ beforeAll(() => {
|
||||
browser: true
|
||||
}));
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
goto: vi.fn(),
|
||||
beforeNavigate: vi.fn()
|
||||
}));
|
||||
vi.mock('$app/environment', () => ({
|
||||
dev: true,
|
||||
|
||||
+11
-3
@@ -1,16 +1,24 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { sentrySvelteKit } from '@sentry/sveltekit';
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
plugins: [
|
||||
sentrySvelteKit({
|
||||
adapter: 'auto',
|
||||
sourceMapsUploadOptions: {
|
||||
org: 'appwrite',
|
||||
project: 'console'
|
||||
}
|
||||
}),
|
||||
sveltekit()
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: ['echarts', 'prismjs']
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [
|
||||
'@analytics/google-analytics',
|
||||
'@sentry/browser',
|
||||
'@sentry-internal/tracing',
|
||||
'analytics',
|
||||
'dayjs',
|
||||
'echarts',
|
||||
|
||||
Reference in New Issue
Block a user