mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/console-roles-dl' into console-roles-dl
This commit is contained in:
@@ -153,3 +153,6 @@ dist
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
# IDE specifics
|
||||
.idea
|
||||
Generated
+1350
-224
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+4115
-5023
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'
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@
|
||||
showEdit = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Edit
|
||||
Update
|
||||
</DropListItem>
|
||||
{#if !isRelationship(attribute)}
|
||||
<DropListItem
|
||||
|
||||
+5
-4
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+13
-6
@@ -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
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+5
-3
@@ -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>
|
||||
|
||||
+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"
|
||||
|
||||
+5
-3
@@ -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>
|
||||
-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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
+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 |
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user