Merge remote-tracking branch 'origin/console-roles-dl' into console-roles-dl

This commit is contained in:
ernstmul
2024-09-24 14:01:27 +02:00
59 changed files with 6049 additions and 5442 deletions
+3
View File
@@ -153,3 +153,6 @@ dist
# Sentry Config File
.sentryclirc
# IDE specifics
.idea
+1350 -224
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -19,7 +19,7 @@
"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",
@@ -73,6 +73,11 @@
"vite": "^5.4.0",
"vitest": "^1.6.0"
},
"pnpm": {
"overrides": {
"rollup": "^4.22.4"
}
},
"type": "module",
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
}
+4115 -5023
View File
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@
let budgetEnabled = false;
$: currentPlan = $plansInfo.get(billingPlan);
$: extraSeatsCost = (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
$: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
$: grossCost = currentPlan.price + extraSeatsCost;
$: estimatedTotal =
couponData?.status === 'active'
+2
View File
@@ -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 />
+1 -1
View File
@@ -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';
+20 -3
View File
@@ -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>
+5
View File
@@ -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) {
+23 -23
View File
@@ -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>
+17
View File
@@ -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';
+15
View File
@@ -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');
+17
View File
@@ -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);
};
+2 -9
View File
@@ -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>
+3
View File
@@ -179,6 +179,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;
+3
View File
@@ -335,6 +335,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`;
@@ -372,6 +374,7 @@ export async function paymentExpired(org: Organization) {
]
});
}
sessionStorage.setItem('expiredPaymentNotification', 'true');
}
export function checkForMarkedForDeletion(org: Organization) {
+18
View File
@@ -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.',
@@ -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>
+1 -1
View File
@@ -85,7 +85,7 @@
{
label: 'Create new organization',
callback: () => {
newOrgModal.set(true);
isCloud ? goto(`${base}/create-organization`) : newOrgModal.set(true);
},
keys: ['c', 'o'],
group: 'organizations'
+5 -6
View File
@@ -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"
@@ -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}
@@ -93,7 +93,7 @@
<InputSelect
popover={[
'<span class="u-bold">Roles</span>',
'Owner, Developer, Editor, Analyst, Billing.',
'Owner, Developer <span class="inline-tag u-normal u-x-small">Default</span>, Editor, Analyst, Billing.',
'<a class="link" target="_blank" rel="noopener noreferrer" href="https://appwrite.io/docs/roles">Learn more</a> about roles.'
]}
id="role"
@@ -111,7 +111,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
@@ -121,7 +121,7 @@
$organization.billingStartDate
)}. ${$daysLeftInTrial} days remaining.`
}}>
<Pill>TRIAL</Pill>
<Pill class="eyebrow-heading-3">TRIAL</Pill>
</div>
{/if}
</span>
@@ -22,8 +22,9 @@
import { base } from '$app/paths';
import { isOwner } from '$lib/stores/roles';
import Edit from './edit.svelte';
import { getRoleLabel } from '$lib/stores/billing';
import { getRoleLabel, upgradeURL } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
import { isCloud } from '$lib/system';
export let data: PageData;
@@ -84,37 +85,63 @@
</button>
<svelte:fragment slot="list">
<div class="u-flex u-flex-vertical u-gap-8 u-break-word">
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag">Pro plan</span>
{#if isCloud}
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag u-normal u-x-small"
>Pro plan</span>
{/if}
</p>
{#if $organization?.billingPlan != BillingPlan.FREE}
<p>
Owner, Developer <span
class="inline-tag u-normal u-x-small"
>Default</span
>, Editor, Analyst, Billing.
</p>
<p class="">
<a
class="link"
href="http://appwrite.io/docs/roles"
target="_blank"
rel="noopener noreferrer">Learn more</a> about roles.
</p>
{:else}
<p>
Owner, <span class="u-color-text-disabled"
>Developer, Editor, Analyst.</span>
</p>
<p class="u-flex u-main-end u-cross-center u-gap-12">
<a
class="button is-text"
href="http://appwrite.io/docs/roles"
target="_blank"
rel="noopener noreferrer">Learn more</a>
<a class="button is-secondary" href={$upgradeURL}
>Upgrade plan</a>
</p>
{/if}
</p>
{#if $organization?.billingPlan != BillingPlan.FREE}
<p>
Owner, Developer <span class="inline-tag">Default</span
>, Editor, Analyst, Billing.
</p>
<p>
<a
class="link"
href="http://appwrite.io/docs/roles"
target="_blank"
rel="noopener noreferrer">Learn more</a> about roles.
</p>
{:else}
<p>
Owner, <span class="u-color-text-disabled"
>Developer, Editor, Analyst.</span>
<span class="u-bold">Roles</span>
<span class="inline-tag u-normal u-x-small"
>Appwrite Cloud</span>
</p>
<p>
Upgrade to Cloud to assign new roles to members or ask
us about our entriprise self hosted offering.
</p>
<p class="u-flex u-main-end u-cross-center u-gap-12">
<a
class="link"
class="button is-text"
href="http://appwrite.io/docs/roles"
target="_blank"
rel="noopener noreferrer">Learn more</a>
<a class="button is-secondary" href="upgrade"
>Upgrade plan</a>
<a
class="button is-secondary"
href="https://cloud.appwrite.io"
>Upgrade to Cloud</a>
</p>
{/if}
</div>
@@ -56,7 +56,7 @@
showEdit = false;
addNotification({
type: 'success',
message: `Membership updated`
message: `Role updated`
});
trackEvent(Submit.MembershipUpdate);
dispatch('updated', membership);
@@ -78,7 +78,17 @@
<Modal title="Edit role" {error} size="big" bind:show={showEdit} onSubmit={submit}>
<FormList>
<InputSelect id="role" label="Role" required options={roles} bind:value={role} />
<InputSelect
popover={[
'<span class="u-bold">Roles</span>',
'Owner, Developer <span class="inline-tag u-normal u-x-small">Default</span>, Editor, Analyst, Billing.',
'<a class="link" target="_blank" rel="noopener noreferrer" href="https://appwrite.io/docs/roles">Learn more</a> about roles.'
]}
id="role"
label="Role"
required
options={roles}
bind:value={role} />
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showEdit = false)}>Cancel</Button>
@@ -282,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}
@@ -289,7 +315,7 @@
maxValue={max.toString()}
progressValue={bytesToSize(current, 'GB')}
progressMax={max}
minimum={1} />
progressBarData={progressBarStorageDate} />
{#if project?.length > 0}
<ProjectBreakdown projects={project} metric="storage" {data} />
{/if}
@@ -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
@@ -162,7 +162,7 @@
showEdit = true;
showDropdown[index] = false;
}}>
Edit
Update
</DropListItem>
{#if !isRelationship(attribute)}
<DropListItem
@@ -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,
originalKey,
data.required,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -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,
originalKey,
data.required,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -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(
@@ -51,9 +52,16 @@
}
}
$: if (showEdit) {
currentAttr ??= { ...selectedAttribute };
error = null;
$: onShow(showEdit);
function onShow(show: boolean) {
if (show) {
currentAttr ??= { ...selectedAttribute };
originalKey = currentAttr.key;
error = null;
} else {
currentAttr = null;
}
}
</script>
@@ -76,8 +84,7 @@
label="Attribute Key"
placeholder="Enter Key"
bind:value={selectedAttribute.key}
autofocus
readonly />
autofocus />
{/if}
{#if option}
<svelte:component
@@ -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,
originalKey,
data.required,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -22,15 +22,17 @@
export async function updateEnum(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeEnum>
data: Partial<Models.AttributeEnum>,
originalKey?: string
) {
await sdk.forProject.databases.updateEnumAttribute(
databaseId,
collectionId,
data.key,
originalKey,
data.elements,
data.required,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -23,16 +23,18 @@
export async function updateFloat(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeFloat>
data: Partial<Models.AttributeFloat>,
originalKey?: string
) {
await sdk.forProject.databases.updateFloatAttribute(
databaseId,
collectionId,
data.key,
originalKey,
data.required,
data.min,
data.max,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -23,16 +23,18 @@
export async function updateInteger(
databaseId: string,
collectionId: string,
data: Partial<Models.AttributeInteger>
data: Partial<Models.AttributeInteger>,
originalKey?: string
) {
await sdk.forProject.databases.updateIntegerAttribute(
databaseId,
collectionId,
data.key,
originalKey,
data.required,
data.min,
data.max,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -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,
originalKey,
data.required,
data.default
data.default,
data.key !== originalKey ? data.key : undefined
);
}
</script>
@@ -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">
@@ -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;
};
@@ -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"
@@ -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,
originalKey,
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>
@@ -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`
@@ -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">
@@ -147,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>
@@ -172,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"
@@ -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}
@@ -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">
+1 -1
View File
@@ -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

+1 -1
View File
@@ -7,7 +7,7 @@ test('upgrade - free tier', async ({ page }) => {
await registerUserStep(page);
await createFreeProject(page);
await test.step('upgrade project', async () => {
await page.getByRole('link', { name: 'upgrade' }).click();
await page.getByRole('link', { name: 'Upgrade', exact: true }).click();
await page.waitForURL('./organization-**/change-plan');
await page.locator('input[value="tier-1"]').click();
await page.getByRole('button', { name: 'add' }).first().click();
+2 -1
View File
@@ -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,