Merge branch 'main' of https://github.com/appwrite/console into feat-campaign-server-side

This commit is contained in:
Torsten Dittmann
2024-11-06 13:09:46 +01:00
249 changed files with 12580 additions and 15665 deletions
+1
View File
@@ -1,6 +1,7 @@
.DS_Store
.envrc
.vscode
.zed
node_modules
/build
/.svelte-kit
-1
View File
@@ -7,7 +7,6 @@ map $sent_http_content_type $expires {
server {
listen 80;
listen [::]:80;
server_name localhost;
# serve compressed file if filename.gz exists
-10486
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -19,11 +19,11 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "^1.2.0",
"@appwrite.io/console": "1.4.1",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.31.0",
"@sentry/sveltekit": "^8.36.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.14",
@@ -32,7 +32,7 @@
"deep-equal": "^2.2.3",
"echarts": "^5.5.1",
"envfile": "^7.1.0",
"nanoid": "^5.0.7",
"nanoid": "^5.0.8",
"plausible-tracker": "^0.3.9",
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
@@ -42,35 +42,35 @@
"devDependencies": {
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.47.2",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.5.28",
"@playwright/test": "^1.48.2",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.7.4",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/svelte": "^5.2.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.4",
"@types/prismjs": "^1.26.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/ui": "^1.6.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.0",
"eslint-plugin-svelte": "^2.46.0",
"jsdom": "^22.1.0",
"kleur": "^4.1.5",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.79.3",
"prettier-plugin-svelte": "^3.2.7",
"sass": "^1.80.6",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vitest": "^1.6.0"
},
"type": "module",
+2 -2
View File
@@ -4,7 +4,7 @@ const config: PlaywrightTestConfig = {
timeout: 120000,
reportSlowTests: null,
reporter: [['html', { open: 'never' }]],
retries: 1,
retries: 3,
use: {
baseURL: 'http://localhost:4173/console/',
trace: 'on-first-retry'
@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = {
webServer: {
timeout: 120000,
env: {
PUBLIC_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
PUBLIC_APPWRITE_ENDPOINT: 'https://dlbilling.appwrite.org/v1',
PUBLIC_CONSOLE_MODE: 'cloud',
PUBLIC_STRIPE_KEY:
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
+5144 -3977
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -184,6 +184,7 @@ export enum Submit {
ProjectUpdateSMTP = 'submit_project_update_smtp',
MemberCreate = 'submit_member_create',
MemberDelete = 'submit_member_delete',
MembershipUpdate = 'submit_membership_update',
MembershipUpdateStatus = 'submit_membership_update_status',
ProviderUpdate = 'submit_provider_update',
TeamCreate = 'submit_team_create',
@@ -320,5 +321,7 @@ export enum Submit {
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
ApplyQuickFilter = 'submit_apply_quick_filter'
ApplyQuickFilter = 'submit_apply_quick_filter',
RequestBAA = 'submit_request_baa',
RequestSoc2 = 'submit_request_soc2'
}
+2
View File
@@ -5,10 +5,12 @@
export let series: BarSeriesOption[];
export let options: EChartsOption = null;
export let formatted: 'days' | 'hours' = 'days';
</script>
<Base
{options}
{formatted}
series={series.map((s) => {
s.type = 'bar';
s.stack = 'total';
+3 -1
View File
@@ -2,7 +2,7 @@
import { registerTheme, use, init } from 'echarts/core';
import { onDestroy, onMount } from 'svelte';
import { app } from '$lib/stores/app';
import { defaultConfig } from './config';
import { dailyFormat, hourlyFormat, defaultConfig } from './config';
import base from './base.json';
import light from './light.json';
import dark from './dark.json';
@@ -16,12 +16,14 @@
export let options: EChartsOption;
export let series: (BarSeriesOption | LineSeriesOption)[];
export let formatted: 'days' | 'hours' = 'days';
let chart: ECharts;
let container: HTMLDivElement;
$: option = {
...defaultConfig,
...(formatted === 'days' ? dailyFormat : hourlyFormat),
...options,
series
};
+29 -3
View File
@@ -1,3 +1,4 @@
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import type { EChartsOption } from 'echarts';
export enum Colors {
@@ -10,9 +11,6 @@ export enum Colors {
export const defaultConfig: EChartsOption = {
color: Object.values(Colors),
animation: false,
tooltip: {
trigger: 'axis'
},
grid: {
containLabel: true,
left: 0,
@@ -35,3 +33,31 @@ export const defaultConfig: EChartsOption = {
}
}
};
export const dailyFormat: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
label: {
formatter: function (params) {
const date = new Date(params.value);
return toLocaleDate(date.toString());
}
}
}
}
};
export const hourlyFormat: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
label: {
formatter: function (params) {
const date = new Date(params.value);
return toLocaleDateTime(date.toString());
}
}
}
}
};
+2
View File
@@ -6,10 +6,12 @@
export let series: LineSeriesOption[];
export let options: EChartsOption = null;
export let formatted: 'days' | 'hours' = 'days';
</script>
<Base
{options}
{formatted}
series={series.map((s) => {
s.type = 'line';
s.stack = 'total';
@@ -0,0 +1,46 @@
<script lang="ts">
import { page } from '$app/stores';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { HeaderAlert } from '$lib/layout';
import { isCloud } from '$lib/system';
import { upgradeURL } from '$lib/stores/billing';
import { hideNotification } from '$lib/helpers/notifications';
import { backupsBannerId, showPolicyAlert } from '$lib/stores/database';
function handleClose() {
showPolicyAlert.set(false);
hideNotification(backupsBannerId);
}
</script>
{#if $showPolicyAlert && isCloud && $organization?.$id && $page.url.pathname.match(/\/databases\/database-[^/]+$/)}
{@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
{@const subtitle = isFreePlan
? 'Upgrade your plan to ensure your data stays safe and backed up'
: 'Protect your data by quickly adding a backup policy'}
{@const ctaText = isFreePlan ? 'Upgrade plan' : 'Create policy'}
{@const ctaURL = isFreePlan ? $upgradeURL : `${$page.url.pathname}/backups`}
<HeaderAlert type="warning" title="Your database has no backup policy">
<svelte:fragment>{subtitle}</svelte:fragment>
<svelte:fragment slot="buttons">
<div class="u-flex u-gap-16">
<Button
href={ctaURL}
secondary
fullWidthMobile
event={isFreePlan ? 'backup_banner_upgrade' : 'backup_banner_add'}>
<span class="text">{ctaText}</span>
</Button>
<Button text on:click={handleClose} event="backup_banner_close">
<span class="icon-x" aria-hidden="true"></span>
</Button>
</div>
</svelte:fragment>
</HeaderAlert>
{/if}
+257
View File
@@ -0,0 +1,257 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
import { type Payload, Query } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { isCloud, isSelfHosted } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups';
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
import { base } from '$app/paths';
import { getProjectId } from '$lib/helpers/project';
import { toLocaleDate } from '$lib/helpers/date';
const backupRestoreItems: {
archives: Map<string, BackupArchive>;
restorations: Map<string, BackupRestoration>;
} = {
archives: new Map(),
restorations: new Map()
};
const openStates = {
archives: true,
restorations: true
};
$: showBackupRestoreBox =
backupRestoreItems.archives.size > 0 || backupRestoreItems.restorations.size > 0;
let lastDatabaseRestorationId = null;
function showRestoreNotification(newDatabaseId: string, newDatabaseName: string) {
if (newDatabaseId && newDatabaseName && lastDatabaseRestorationId !== newDatabaseId) {
const project = $page.params.project;
lastDatabaseRestorationId = newDatabaseId;
addNotification({
type: 'success',
isHtml: true,
message: `Restoration complete. <b>${newDatabaseName}</b> has been created.`,
buttons: [
{
name: 'View restored data',
method: () => {
goto(`${base}/project-${project}/databases/database-${newDatabaseId}`);
}
}
]
});
}
}
async function fetchBackupRestores() {
try {
const query = [
Query.equal('status', 'pending'),
Query.equal('status', 'uploading'),
Query.equal('status', 'processing')
];
const [archivesResponse, restorationsResponse] = await Promise.all([
sdk.forProject.backups.listArchives([
...query,
// only manual backups
Query.isNull('policyId')
]),
sdk.forProject.backups.listRestorations(query)
]);
// this is a one time op.
backupRestoreItems.archives = new Map(
archivesResponse.archives.map((item) => [item.$id, item])
);
backupRestoreItems.restorations = new Map(
restorationsResponse.restorations.map((item) => [item.$id, item])
);
} catch (e) {
// ignore?
}
}
// fresh fetch.
fetchBackupRestores();
function updateOrAddItem(payload: Payload) {
const { $id, status, $collection, policyId } = payload;
if ($collection === 'archives' && policyId !== null) {
return;
}
if ($collection in backupRestoreItems) {
const collectionMap = backupRestoreItems[$collection];
if (collectionMap.has($id)) {
collectionMap.get($id).status = status;
if (status === 'completed') {
invalidate(Dependencies.BACKUPS);
if ($collection === 'restorations') {
const { newId, newName } =
collectionMap.get($id).options?.['databases']?.['database'][0] || {};
showRestoreNotification(newId, newName);
}
}
} else if (status === 'pending' || status === 'processing' || status === 'uploading') {
collectionMap.set($id, payload);
}
backupRestoreItems[$collection] = collectionMap;
}
}
function graphSize(status: string): number {
switch (status) {
case 'pending':
return 10;
case 'processing':
return 30;
case 'uploading':
return 60;
case 'completed':
case 'failed':
return 100;
default:
return 0;
}
}
function text(status: string, key: string) {
const service = key === 'archives' ? 'backup' : 'restore';
if (status === 'completed') {
return `Database ${service} complete`;
} else if (status === 'failed') {
return `Database ${service} failed`;
} else {
return 'Preparing database...';
}
}
function handleClose(which: string) {
backupRestoreItems[which] = new Map();
if (which === 'restorations') lastDatabaseRestorationId = null;
}
function backupName(item: BackupArchive | BackupRestoration, key: string) {
const attribute = key === 'archives' ? '$createdAt' : 'startedAt';
return toLocaleDate(item[attribute]);
}
onMount(() => {
// fast path: don't subscribe if org is on a free plan or is self-hosted.
if (isSelfHosted || (isCloud && $organization.billingPlan === BillingPlan.FREE)) return;
sdk.forConsole.client.subscribe('console', (response) => {
// nice!
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
if (
response.events.includes('archives.*') ||
response.events.includes('restorations.*')
) {
updateOrAddItem(response.payload);
}
});
});
</script>
{#if showBackupRestoreBox}
<div class="box-holder u-flex u-flex-vertical u-gap-16" style="align-items: end">
{#each Object.keys(backupRestoreItems) as key}
{@const isBackup = key === 'archives'}
{@const items = backupRestoreItems[key]}
{@const titleText = isBackup ? 'Backup status' : 'Restoration status'}
{#if items.size > 0}
<section class="upload-box">
<header class="upload-box-header">
<h4 class="upload-box-title">
<span class="text">{titleText} ({items.size})</span>
</h4>
<button
class="upload-box-button"
class:is-open={openStates[key]}
aria-label="toggle upload box"
on:click={() => {
openStates[key] = !openStates[key];
}}>
<span class="icon-cheveron-up" aria-hidden="true" />
</button>
<button
class="upload-box-button"
aria-label="close backup restore box"
on:click={() => handleClose(key)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="upload-box-content" class:is-open={openStates[key]}>
<ul class="upload-box-list">
{#each [...items.values()] as item (item.$id)}
<li class="upload-box-item">
<section class="progress-bar u-width-full-line">
<div
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span class="body-text-2">
{text(item.status, key)}
</span>
<span class="backup-name">
{backupName(item, key)}
</span>
</div>
<div
class="progress-bar-container"
class:is-danger={item.status === 'failed'}
style="--graph-size:{graphSize(item.status)}%" />
</section>
</li>
{/each}
</ul>
</div>
</section>
{/if}
{/each}
</div>
{/if}
<style>
.upload-box-title {
font-size: 11px;
}
.upload-box-content {
min-width: 400px;
max-width: 100vw;
}
.upload-box-button {
display: flex;
align-items: center;
justify-content: center;
}
.backup-name {
font-size: 12px;
font-weight: 400;
line-height: 130%;
font-style: normal;
letter-spacing: -0.12px;
color: var(--mid-neutrals-50, #818186);
font-family: var(--font-family-sansSerif, Inter);
}
</style>
@@ -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'
@@ -52,17 +52,28 @@
{/if}
<div class="u-sep-block-start" />
<span class="u-flex u-main-space-between">
<p class="text">Estimated total</p>
<p class="text">
Upcoming charge<br /><span class="u-color-text-gray"
>Due on {!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span>
</p>
<p class="text">
{formatCurrency(estimatedTotal)}
</p>
</span>
<p class="text u-margin-block-start-16">
Your payment method will be charged this amount plus usage fees every 30 days {!currentPlan.trialDays
? `starting ${toLocaleDate(billingPayDate.toString())}`
: ` after your trial period ends on ${toLocaleDate(trialEndDate.toString())}`}.
You'll pay <span class="u-bold">{formatCurrency(estimatedTotal)}</span> now, with our first
billing cycle starting on
<span class="u-bold"
>{!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span
>. Once your credits run out, you'll be charged
<span class="u-bold">{formatCurrency(currentPlan.price)}</span> plus usage fees every 30 days.
</p>
<FormList class="u-margin-block-start-24">
<InputChoice
type="switchbox"
@@ -8,6 +8,7 @@
export let billingPlan: Tier;
export let anyOrgFree = false;
export let isNewOrg = false;
export let selfService = true;
let classes: string = '';
export { classes as class };
@@ -22,7 +23,7 @@
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree}
disabled={anyOrgFree || !selfService}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
tooltipText="You are limited to 1 Free organization per account."
@@ -49,9 +50,16 @@
</li>
<li>
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.PRO} padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<LabelCard
name="plan"
disabled={!selfService}
bind:group={billingPlan}
value={BillingPlan.PRO}
padding={1.5}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={disabled}>
<h4 class="body-text-2 u-bold">
{tierPro.name}
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
+79 -40
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import { isCloud } from '$lib/system';
import { Button } from '$lib/elements/forms/index';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { app } from '$lib/stores/app';
@@ -13,28 +12,51 @@
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import { upgradeURL } from '$lib/stores/billing';
import { addBottomModalAlerts } from '$routes/(console)/project-[project]/bottomAlerts';
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
import { project } from '$routes/(console)/project-[project]/store';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
let currentIndex = 0;
let openModalOnMobile = false;
$: filteredModalAlerts = $bottomModalAlerts
.sort((a, b) => b.importance - a.importance)
.filter((alert) => alert.show && shouldShowNotification(alert.id));
function getPageScope(pathname: string) {
const isProjectPage = pathname.includes('project-[project]');
const isOrganizationPage = pathname.includes('organization-[organization]');
return { isProjectPage, isOrganizationPage };
}
function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) {
const { isProjectPage, isOrganizationPage } = getPageScope(pathname);
return alerts
.sort((a, b) => b.importance - a.importance)
.filter((alert) => {
return (
alert.show &&
shouldShowNotification(alert.id) &&
// if no scope > show in projects & org pages.
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
// project scope, show only in project pages
(isProjectPage && alert.scope === 'project') ||
// organization scope, show only in organization pages
(isOrganizationPage && alert.scope === 'organization'))
);
});
}
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, $page.route.id);
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
function handleClose() {
const modalAlert = currentModalAlert;
dismissBottomModalAlert(modalAlert.id);
hideNotification(modalAlert.id);
if (modalAlert.closed) modalAlert.closed();
if (currentIndex === filteredModalAlerts.length - 1 && filteredModalAlerts.length > 1) {
currentIndex = currentIndex - 1;
} else {
currentIndex = currentIndex % filteredModalAlerts.length;
}
filteredModalAlerts.forEach((alert) => {
const modalAlert = alert;
dismissBottomModalAlert(modalAlert.id);
hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 });
if (modalAlert.closed) modalAlert.closed();
});
}
function showNext() {
@@ -139,23 +161,34 @@
<Button
secondary
class="button"
href={shouldShowUpgrade ? $upgradeURL : currentModalAlert.cta.link}
external={!isCloud}
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => handleClose()}>
{shouldShowUpgrade ? 'Upgrade plan' : currentModalAlert.cta.text}
on:click={() => {
trackEvent('click_promo', {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
}}>
{currentModalAlert.cta.text}
</Button>
{#if currentModalAlert.learnMore && currentModalAlert.learnMore.link}
{#if currentModalAlert.learnMore}
<Button
text
class="button"
external
fullWidthMobile
href={currentModalAlert.learnMore.link}>
{currentModalAlert.learnMore.text
? currentModalAlert.learnMore.text
: 'Learn More'}
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
})}>
{currentModalAlert.learnMore.text}
</Button>
{/if}
</div>
@@ -241,25 +274,36 @@
class="button"
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link}
external={!isCloud}
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => handleClose()}>
on:click={() => {
openModalOnMobile = false;
trackEvent('click_promo', {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
}}>
{shouldShowUpgrade
? 'Upgrade plan'
: currentModalAlert.cta.text}
</Button>
{#if currentModalAlert.learnMore && currentModalAlert.learnMore.link}
{#if currentModalAlert.learnMore}
<Button
text
class="button"
external
fullWidthMobile
href={currentModalAlert.learnMore.link}>
{currentModalAlert.learnMore.text
? currentModalAlert.learnMore.text
: 'Learn More'}
on:click={() => (openModalOnMobile = false)}
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
})}>
{currentModalAlert.learnMore.text}
</Button>
{/if}
</div>
@@ -274,14 +318,14 @@
on:click={() => (openModalOnMobile = true)}>
<div class="u-flex-vertical u-gap-4">
<div class="u-flex u-cross-center u-main-space-between">
<h3 class="body-text-2 u-bold">Early access</h3>
<h3 class="body-text-2 u-bold">New features available</h3>
<button on:click={hideAllModalAlerts}>
<span class="icon-x" />
</button>
</div>
<span class="u-width-fit-content">
Opt-in to experiments, new features, and more.
Explore new features to enhance your projects and improve security.
</span>
</div>
</button>
@@ -302,11 +346,6 @@
max-width: 289px;
}
.showcase-image {
height: 132px;
min-width: 273px;
}
.feature-count-tag {
font-size: 12px;
font-weight: 400;
@@ -378,7 +417,7 @@
}
.alert-container {
max-width: 289px;
max-width: 90vw;
}
}
</style>
+2 -1
View File
@@ -7,6 +7,7 @@
import { isCloud } from '$lib/system';
import CardPlanLimit from './cardPlanLimit.svelte';
export let showEmpty = true;
export let offset = 0;
export let total = 0;
export let event: string = null;
@@ -27,7 +28,7 @@
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
{#if isCloud && serviceId && total >= planLimit}
<CardPlanLimit {service} />
{:else}
{:else if showEmpty}
<Empty on:click target={event}>
<slot name="empty" />
</Empty>
@@ -57,6 +57,9 @@
<style lang="scss">
// TODO: remove once pink is updated
.collapsible-item {
.collapsible-wrapper {
padding-left: 0;
}
.collapsible-wrapper.is-disabled {
cursor: not-allowed;
+1 -1
View File
@@ -8,7 +8,7 @@
</script>
<div class:box={isBox}>
<div class="u-flex u-main-space-between u-cross-start">
<div class="u-flex u-main-space-between u-cross-start" style="padding-block: 0.5rem;">
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
<span class="u-flex u-cross-center u-gap-8">
<p class="text u-bold">
+4 -1
View File
@@ -42,7 +42,7 @@
{
name: 'offset',
options: {
offset: [noArrow ? 0 : -arrowSize, noArrow ? 0 : arrowSize / 1.5]
offset: [noArrow ? 0 : arrowSize * 1.75, noArrow ? 0 : arrowSize / 1.5]
}
},
{
@@ -135,10 +135,12 @@
<style global lang="scss">
.drop-arrow.is-popover {
--drop-arrow-pop-over-bg-color: var(--color-neutral-90);
body.theme-light & {
--drop-arrow-pop-over-bg-color: var(--color-neutral-0);
}
}
.drop-arrow,
.drop-arrow::before {
position: absolute;
@@ -148,6 +150,7 @@
--drop-arrow-border: 1px solid hsl(var(--color-neutral-85));
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-105)));
body.theme-light & {
--drop-arrow-border: 1px solid hsl(var(--color-neutral-10));
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-0)));
+31 -11
View File
@@ -5,14 +5,29 @@
import EmptyDark from '$lib/images/empty-dark.svg';
import { Heading } from '.';
import { trackEvent } from '$lib/actions/analytics';
import { createEventDispatcher } from 'svelte';
export let single = false;
export let isCard = true;
export let noMedia = false;
export let target: string = null;
export let href: string = null;
export let marginTop = false;
export let allowCreate = true;
const dispatch = createEventDispatcher();
function onClick(event) {
if (!allowCreate) {
return;
}
dispatch('click', event);
}
function track() {
if (!allowCreate) {
return;
}
if (target) {
trackEvent(`click_create_${target}`, {
from: 'empty'
@@ -22,20 +37,22 @@
</script>
{#if single}
<article class="card u-grid u-cross-center u-width-full-line common-section">
<article class:card={isCard} class="u-grid u-cross-center u-width-full-line common-section">
<div
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden u-padding-block-8">
{#if !noMedia}
<button
type="button"
on:click|preventDefault
on:click={track}
on:click={onClick}
aria-label="create {target}">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" height="242" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" height="242" />
{/if}
<slot name="empty-media">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" height="242" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" height="242" />
{/if}
</slot>
</button>
{/if}
<slot>
@@ -54,9 +71,11 @@
text
event="empty_documentation"
ariaLabel="create {target}">Documentation</Button>
<Button secondary on:click on:click={track}>
Create {target}
</Button>
{#if allowCreate}
<Button secondary on:click on:click={track}>
Create {target}
</Button>
{/if}
</div>
</slot>
</div>
@@ -67,7 +86,8 @@
on:click={track}
aria-label="create"
type="button"
class="card u-grid u-cross-center u-width-full-line dashed"
class:card={isCard}
class="u-grid u-cross-center u-width-full-line dashed"
class:common-section={marginTop}>
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
+1 -1
View File
@@ -79,7 +79,7 @@
: selected.service?.actions;
// We need to check if the selected action name is still
// on the available list, and if so, change tje action to the
// on the available list, and if so, change the action to the
// appropriate one, as it may contain different attributes
const availableAction = availableActions.find(
(a) => a.name === selected.action?.name
+8 -2
View File
@@ -6,7 +6,10 @@
feedbackOptions,
feedbackData
} from '$lib/stores/feedback';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/stores';
$: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type);
@@ -15,8 +18,11 @@
await feedback.submitFeedback(
`feedback-${$feedback.type}`,
$feedbackData.message,
$feedbackData.name,
$feedbackData.email
$user.name,
$user.email,
$organization.billingPlan,
$page.url.href,
$feedbackData.value
);
addNotification({
type: 'success',
@@ -1,15 +1,9 @@
<script lang="ts">
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
import { FormList, InputTextarea } from '$lib/elements/forms';
import { feedbackData } from '$lib/stores/feedback';
</script>
<FormList>
<InputText label="Name" id="name" bind:value={$feedbackData.name} placeholder="Enter name" />
<InputEmail
label="Email"
id="email"
bind:value={$feedbackData.email}
placeholder="Enter email" />
<InputTextarea
id="feedback"
placeholder="Your message here"
+1 -11
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
import { FormList, InputTextarea } from '$lib/elements/forms';
import { feedbackData } from '$lib/stores/feedback';
import Evaluation from './evaluation.svelte';
</script>
@@ -16,15 +16,5 @@
required
bind:value={$feedbackData.message}
showLabel={false} />
<InputText
label="Name"
id="name"
bind:value={$feedbackData.name}
placeholder="Enter name" />
<InputEmail
label="Email"
id="email"
bind:value={$feedbackData.email}
placeholder="Enter email" />
{/if}
</FormList>
+2
View File
@@ -8,7 +8,9 @@
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
<Heading tag="h2" size="7"><slot name="title" /></Heading>
<div class="u-padding-block-start-4"><slot name="subtitle" /></div>
</div>
<div class="grid-item-1-start-end">
<slot name="status" />
</div>
+1
View File
@@ -12,6 +12,7 @@ export { default as Tiles } from './tiles.svelte';
export { default as Copy } from './copy.svelte';
export { default as CopyInput } from './copyInput.svelte';
export { default as UploadBox } from './uploadBox.svelte';
export { default as BackupRestoreBox } from './backupRestoreBox.svelte';
export { default as List } from './list.svelte';
export { default as ListItem } from './listItem.svelte';
export { default as Empty } from './empty.svelte';
+11 -2
View File
@@ -66,7 +66,7 @@
</script>
{#if $showMigrationBox && migration}
<section class="upload-box is-float">
<section class="upload-box">
<header class="upload-box-header">
<h4 class="upload-box-title">
<span class="text">Importing Data</span>
@@ -93,9 +93,18 @@
{/if}
<style>
.upload-box-title {
font-size: 11px;
}
.upload-box-content {
padding: 1.5rem;
min-width: 400px;
max-width: 100vw;
}
.upload-box-button {
display: flex;
align-items: center;
justify-content: center;
}
</style>
+1 -1
View File
@@ -70,7 +70,7 @@
{/if}
</div>
{#if description.length > 0}
<p class="u-margin-block-start-4">
<p class="modal-description u-margin-block-start-4">
<slot name="description">
{description}
</slot>
+1 -1
View File
@@ -34,7 +34,7 @@
<Modal
title="Label"
description="Labels allow you to grant access to users with the specfied label."
description="Labels allow you to grant access to users with the specified label."
bind:show
on:close={reset}
onSubmit={create}>
+2 -2
View File
@@ -14,7 +14,7 @@
</script>
<section class="progress-bar">
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined && maxUnit !== undefined}
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined}
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
@@ -26,7 +26,7 @@
<p class="body-text-2">
{maxValue}
{maxUnit}
{maxUnit ? maxUnit : ''}
</p>
</div>
{/if}
@@ -64,6 +64,7 @@
display: flex;
flex-direction: row;
gap: 2px;
margin-top: 1rem;
}
&__content {
+21
View File
@@ -0,0 +1,21 @@
<div class="u-flex-vertical u-gap-16">
<slot />
</div>
<style>
div {
color: hsl(var(--color-neutral-50));
line-height: 1.25rem;
&:first-child {
color: hsl(var(--color-neutral-70));
}
}
:global(.theme-dark) div {
color: hsl(var(--color-neutral-20));
&:first-child {
color: hsl(var(--color-neutral-10));
}
}
</style>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import Base from './base.svelte';
</script>
<Base>
<div class="u-flex-vertical u-gap-8">
<p>
By default, all members are assigned a <span class="u-bold">Developer</span> role. You can
change this at any time from your organization settings.
</p>
<p>
<a
class="link"
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
</p>
</div>
</Base>
+17
View File
@@ -0,0 +1,17 @@
<script>
import Base from './base.svelte';
</script>
<Base>
<div class="u-flex-vertical u-gap-8">
<p class="u-bold">Roles</p>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
<p>
<a
class="link"
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
</p>
</div>
</Base>
+62
View File
@@ -0,0 +1,62 @@
<script>
import Base from './base.svelte';
import { upgradeURL } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import Button from '$lib/elements/forms/button.svelte';
</script>
<Base>
{#if isCloud}
{#if $organization?.billingPlan !== BillingPlan.FREE}
<div class="u-flex-vertical u-gap-8">
<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>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
</div>
<p>
<Button link external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button> about roles.
</p>
{:else}
<div class="u-flex-vertical u-gap-8">
<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>
<p>
Upgrade to Pro to assign new roles to members such as Owner, Developer, Editor
or Analyst.
</p>
</div>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade plan</Button>
</p>
{/if}
{:else}
<div class="u-flex-vertical u-gap-8">
<p>
<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>
</div>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
</p>
{/if}
</Base>
+22 -1
View File
@@ -13,7 +13,7 @@
</script>
{#if $uploader?.isOpen}
<section class="upload-box is-float">
<section class="upload-box">
<header class="upload-box-header">
<h4 class="upload-box-title">
<span class="text">Uploading files</span>
@@ -101,3 +101,24 @@
</div>
</section>
{/if}
<style>
.upload-box-title {
font-size: 11px;
}
.upload-box-button {
display: flex;
align-items: center;
justify-content: center;
}
.upload-box-content {
min-width: 400px;
max-width: 100vw;
}
.file-name {
max-width: 24ch;
}
</style>
+63 -1
View File
@@ -50,6 +50,7 @@ export enum Dependencies {
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
BACKUPS = 'dependency:backups',
RUNTIMES = 'dependency:runtimes',
CONSOLE_VARIABLES = 'dependency:console_variables',
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
@@ -61,6 +62,65 @@ export enum Dependencies {
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
}
export const defaultScopes: string[] = [
'global',
'public',
'home',
'console',
'graphql',
'sessions.write',
'account',
'teams.read',
'teams.write',
'documents.read',
'documents.write',
'files.read',
'files.write',
'projects.read',
'projects.write',
'locale.read',
'avatars.read',
'execution.read',
'execution.write',
'targets.read',
'targets.write',
'subscribers.write',
'subscribers.read',
'assistant.read',
'users.read',
'users.write',
'databases.read',
'databases.write',
'collections.read',
'collections.write',
'buckets.read',
'buckets.write',
'functions.read',
'functions.write',
'platforms.read',
'platforms.write',
'keys.read',
'keys.write',
'webhooks.read',
'webhooks.write',
'rules.read',
'rules.write',
'migrations.read',
'migrations.write',
'vcs.read',
'vcs.write',
'providers.read',
'providers.write',
'messages.read',
'messages.write',
'topics.read',
'topics.write',
'billing.read',
'billing.write'
];
export const defaultRoles: string[] = ['owner'];
export const scopes: {
scope: string;
description: string;
@@ -408,7 +468,9 @@ export const eventServices: Array<EventService> = [
export enum BillingPlan {
FREE = 'tier-0',
PRO = 'tier-1',
SCALE = 'tier-2'
SCALE = 'tier-2',
GITHUB_EDUCATION = 'auto-1',
CUSTOM = 'cont-1'
}
export const feedbackDowngradeOptions = [
+29 -1
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { FormItem, FormItemPart, Helper, Label } from '.';
import type { FormItemTag } from './formItem.svelte';
import { Drop } from '$lib/components';
import type { SvelteComponent } from 'svelte';
export let id: string;
export let label: string | undefined = undefined;
@@ -19,10 +21,14 @@
}[];
export let isMultiple = false;
export let fullWidth = false;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let element: HTMLSelectElement;
let error: string;
let show: boolean = false;
const handleInvalid = (event: Event) => {
event.preventDefault();
@@ -54,7 +60,29 @@
<svelte:component this={wrapper} {fullWidth} tag={wrapperTag}>
{#if label}
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
@@ -68,7 +68,7 @@
<span
class:icon-cheveron-up={show}
class:icon-cheveron-down={!show}
class="u-position-absolute u-inset-block-start-4 u-inset-inline-end-12"
class="chevron-icon u-position-absolute u-inset-inline-end-12"
aria-hidden="true"></span>
</button>
@@ -86,3 +86,15 @@
{/each}
</svelte:fragment>
</DropList>
<style>
@media (max-width: 768px) {
.chevron-icon {
inset-block-start: 0.25rem !important;
}
.tags-input {
padding-right: 2rem;
}
}
</style>
+28 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { last } from '$lib/helpers/array';
import { onMount } from 'svelte';
import { onMount, SvelteComponent } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let id: string;
export let label: string;
@@ -15,11 +16,14 @@
export let tooltip: string = null;
export let validityRegex: RegExp = null;
export let validityMessage: string = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let value = '';
let element: HTMLInputElement;
let hiddenEl: HTMLInputElement;
let error: string;
let show: boolean = false;
onMount(() => {
if (element && autofocus) {
@@ -90,7 +94,29 @@
{required}
on:invalid={handleInvalid} />
<Label {required} {tooltip} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper">
+6
View File
@@ -21,3 +21,9 @@
data-private>
<slot />
</svelte:element>
<style>
:global(.table .button.is-text) {
--p-text-color-default: initial !important;
}
</style>
+167
View File
@@ -0,0 +1,167 @@
export type UserBackupPolicy = {
id?: string;
label: string;
retained: number;
default: boolean;
description: string;
schedule?: string;
checked?: boolean;
selectedTime?: string;
plainTextFrequency?: string;
weeklySelectedDays?: string[];
monthlyBackupFrequency?: string;
};
export const cronExpression = (policy: UserBackupPolicy) => {
const now = new Date();
if (policy.plainTextFrequency === 'hourly') {
const utcMinute = now.getUTCMinutes();
if (!policy.default) {
policy.schedule = `${utcMinute} * * * *`;
}
return;
}
let cronExpression = '';
if (policy.default) {
now.setHours(0, 0, 0);
const utcHour = now.getUTCHours();
cronExpression = policy.schedule.replace('{time}', `0 ${utcHour}`);
} else {
const [localHour, localMinute] = policy.selectedTime.split(':');
now.setHours(parseInt(localHour), parseInt(localMinute), 0);
const utcHour = now.getUTCHours();
const utcMinute = now.getUTCMinutes();
if (policy.plainTextFrequency === 'daily') {
cronExpression = `${utcMinute} ${utcHour} * * *`;
} else if (policy.plainTextFrequency === 'weekly') {
const selectedDays = policy.weeklySelectedDays
?.map(
(dayLabel) =>
backupFrequencies.weekly.find((option) => option.label === dayLabel)?.index
)
.filter((index) => index !== undefined)
.join(',');
// Default to Monday (1)
cronExpression = `${utcMinute} ${utcHour} * * ${selectedDays || '1'}`;
} else if (policy.plainTextFrequency === 'monthly') {
cronExpression = `${utcMinute} ${utcHour} 28 * *`;
}
}
policy.schedule = cronExpression;
};
const generateHourlyOptions = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (_, i) => ({
value: `${i + start}`,
label: `${i + start}`
}));
const generateWeeklyOptions = () =>
['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(
(day, index) => ({
label: day,
value: day,
index: index,
checked: false
})
);
export const backupFrequencies = {
monthly: [
{ value: 'first', label: 'First day of month', day: '1' },
{ value: 'middle', label: 'Middle of month (15th)', day: '15' },
{ value: 'end', label: 'End of month (28th)', day: '28' }
],
weekly: generateWeeklyOptions(),
hourly: generateHourlyOptions(1, 12)
};
export const backupPolicyDescription = (
frequency: string,
time: string | null = null,
retained: number | null = null,
monthlyBackupFrequency: string,
weeklySelectedDays: string[] | null = null
) => {
let retainedText = '';
const timeFormatted = time ?? '';
if (retained !== null) {
if (retained === 365 * 100) {
retainedText = 'forever';
} else if (retained >= 365) {
const years = Math.floor(retained / 365);
retainedText = `${years} year${years > 1 ? 's' : ''}`;
} else if (retained >= 30) {
const months = Math.floor(retained / 30);
retainedText = `${months} month${months > 1 ? 's' : ''}`;
} else if (retained >= 7) {
const weeks = Math.floor(retained / 7);
retainedText = `${weeks} week${weeks > 1 ? 's' : ''}`;
} else {
retainedText = `${retained} day${retained > 1 ? 's' : ''}`;
}
}
switch (frequency) {
case 'hourly':
return retained !== null
? `Runs every hour and is retained for ${retainedText}.`
: 'A backup will run every hour.';
case 'daily':
return retained !== null
? `Runs every day and is retained for ${retainedText}.`
: `A backup will run daily at ${timeFormatted}.`;
case 'weekly': {
const daysArray = weeklySelectedDays.length ? weeklySelectedDays : ['Monday'];
const dayString =
daysArray.length > 1
? daysArray.slice(0, -1).join(', ') + ' and ' + daysArray.slice(-1)
: daysArray[0];
return retained !== null
? `Runs every ${dayString} and is retained for ${retainedText}.`
: `A backup will run weekly on ${dayString} at ${timeFormatted}.`;
}
case 'monthly': {
const monthDay =
backupFrequencies[frequency]
.find((option) => option.value === monthlyBackupFrequency)
?.label.toLowerCase() || '28th';
let actualDay: string;
switch (monthlyBackupFrequency) {
case 'first':
actualDay = '1st';
break;
case 'middle':
actualDay = '15th';
break;
case 'end':
default:
actualDay = '28th';
break;
}
return retained !== null
? `Runs every ${actualDay} of the month and is retained for ${retainedText}.`
: `A backup will run every month on the ${monthDay} at ${timeFormatted}.`;
}
default:
return 'A backup schedule is not set.';
}
};
+2 -2
View File
@@ -51,7 +51,7 @@ export const toLocaleDateTime = (datetime: string | number) => {
};
/**
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
* Returns a date string using the local timezone in ISO format (yyyy-mm-dd)
*
* @param datetime date string or milliseconds since the epoch
*/
@@ -67,7 +67,7 @@ export const toLocaleDateISO = (datetime: string | number) => {
};
/**
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
* Returns a time string using the local timezone in ISO format (hh:mm:ss)
*
* @param datetime date string or milliseconds since the epoch
*/
+7 -1
View File
@@ -36,7 +36,13 @@ export function getQuery(url: URL): string | undefined {
return url.searchParams.get('query') ?? undefined;
}
export type TabElement = { href: string; title: string; event: string; hasChildren?: boolean };
export type TabElement = {
href: string;
title: string;
event: string;
hasChildren?: boolean;
disabled?: boolean;
};
export function isTabSelected(
tab: TabElement,
+8 -2
View File
@@ -14,11 +14,17 @@ export type NotificationCoolOffOptions = {
exponentialBackoffFactor?: number;
};
const userPreferences = () => get(user).prefs;
const userPreferences = () => get(user)?.prefs;
const notificationPrefs = (): Record<string, NotificationPrefItem> => {
const prefs = userPreferences();
return prefs.notificationPrefs ? prefs.notificationPrefs : {};
if (prefs === null) {
return {};
}
// for some reason, the prefs become array as default or on all clear. let's reset.
return Array.isArray(prefs.notificationPrefs) ? {} : prefs.notificationPrefs || {};
};
function updateNotificationPrefs(parsedPrefs: Record<string, NotificationPrefItem>) {
+5
View File
@@ -14,6 +14,7 @@ export function calculateSize(bytes: number, decimals = 1, base: 1000 | 1024 = 1
}
export function sizeToBytes(value: number, unit: Size, base = 1000) {
if (typeof value !== 'number') return 0;
const index = sizes.indexOf(unit);
return value * Math.pow(base, index);
}
@@ -23,6 +24,10 @@ export function bytesToSize(value: number, unit: Size, base = 1000) {
return value / Math.pow(base, index);
}
export function mbSecondsToGBHours(value: number, base: 1000 | 1024 = 1000) {
return value / base / (60 * 60);
}
export function humanFileSize(
bytes: number,
useBits = false
+1 -1
View File
@@ -70,7 +70,7 @@ export function createTimeUnitPair(initialValue = 0) {
{ name: 'Minutes', value: 60 },
{ name: 'Seconds', value: 1 }
];
return { ...createValueUnitPair(initialValue, units), units };
return { ...createValueUnitPair(initialValue || 0, units), units };
}
export function createByteUnitPair(initialValue = 0, base: 1000 | 1024 = 1000) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,6 @@
import ArtworkDark from './artwork-dark.svg';
import ArtworkDarkMobile from './artwork-dark-mobile.svg';
import ArtworkLight from './artwork-light.svg';
import ArtworkLightMobile from './artwork-light-mobile.svg';
export { ArtworkDark, ArtworkDarkMobile, ArtworkLight, ArtworkLightMobile };
+4
View File
@@ -0,0 +1,4 @@
<svg width="99" height="24" viewBox="0 0 99 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.372 0C5.52847 0 0 5.49518 0 12.2975C0 17.7393 3.54252 22.3542 8.45373 23.9814C9.07099 24.0881 9.31252 23.7146 9.31252 23.3945C9.31252 23.1011 9.28568 22.1408 9.28568 21.1004C6.17256 21.6606 5.36745 20.3535 5.12591 19.66C4.99173 19.3132 4.37447 18.2195 3.86456 17.926C3.43517 17.686 2.81791 17.1258 3.83772 17.0991C4.80387 17.0724 5.50163 17.9794 5.74317 18.3528C6.87033 20.2201 8.64159 19.6866 9.33936 19.3665C9.44671 18.5662 9.76875 18.0327 10.1176 17.7126C7.3534 17.3925 4.48182 16.3522 4.48182 11.6306C4.48182 10.2968 4.96489 9.17642 5.74317 8.3228C5.60898 8.00269 5.17959 6.74894 5.87735 5.06837C5.87735 5.06837 6.92401 4.74827 9.28568 6.32213C10.2787 6.05537 11.3253 5.89532 12.372 5.89532C13.4186 5.89532 14.4653 6.0287 15.4582 6.32213C17.8199 4.72159 18.8666 5.06837 18.8666 5.06837C19.5375 6.74894 19.1081 8.02937 19.0008 8.3228C19.779 9.17642 20.2621 10.2701 20.2621 11.6306C20.2621 16.3522 17.3637 17.3925 14.5995 17.7126C15.0557 18.0861 15.4314 18.833 15.4314 19.9801C15.4314 21.634 15.4046 22.9411 15.4046 23.3679C15.4046 23.688 15.6461 24.0881 16.2634 23.9547C21.1477 22.3275 24.6903 17.7126 24.6903 12.2708C24.7708 5.49518 19.2155 0 12.372 0Z" fill="white"/>
<path d="M59.4712 17.8993H59.4443C59.4712 17.8993 59.4712 17.8993 59.4712 17.8993ZM59.4712 17.8993C59.337 17.8993 58.9881 17.9793 58.6124 17.9793C57.4315 17.9793 57.029 17.4458 57.029 16.7523V12.084H59.4175C59.5517 12.084 59.659 11.9773 59.659 11.7906V9.25641C59.659 9.12303 59.5517 9.01633 59.4175 9.01633H57.029V5.89528C57.029 5.78858 56.9485 5.70855 56.8143 5.70855H53.5938C53.4596 5.70855 53.3791 5.78858 53.3791 5.89528V9.12303C53.3791 9.12303 51.742 9.52317 51.6347 9.54984C51.5273 9.57652 51.4468 9.68322 51.4468 9.78993V11.8173C51.4468 11.9773 51.5542 12.1107 51.6884 12.1107H53.3523V16.9924C53.3523 20.6202 55.9018 20.9937 57.6462 20.9937C58.4513 20.9937 59.3906 20.7536 59.5517 20.6736C59.6322 20.6469 59.6859 20.5402 59.6859 20.4335V18.1928C59.7127 18.0327 59.6053 17.926 59.4712 17.8993ZM94.95 14.6449C94.95 11.9507 93.8497 11.6039 92.6957 11.7106C91.81 11.7639 91.0854 12.2174 91.0854 12.2174V17.4458C91.0854 17.4458 91.81 17.9527 92.9104 17.9793C94.4669 18.0327 94.95 17.4725 94.95 14.6449ZM98.5998 14.4048C98.5998 19.4999 96.9359 20.967 94.0375 20.967C91.5953 20.967 90.2535 19.7399 90.2535 19.7399C90.2535 19.7399 90.1998 20.4335 90.1193 20.5135C90.0656 20.5936 90.0119 20.6202 89.9046 20.6202H87.6771C87.5161 20.6202 87.3819 20.5135 87.3819 20.3802L87.4087 3.84126C87.4087 3.70788 87.5161 3.60118 87.6503 3.60118H90.8439C90.9781 3.60118 91.0854 3.70788 91.0854 3.84126L91.0586 9.41647C91.0586 9.41647 92.2931 8.6162 94.0912 8.6162H94.118C95.943 8.6162 98.5998 9.28309 98.5998 14.4048ZM85.5301 9.01633H82.3902C82.2291 9.01633 82.1486 9.12303 82.1486 9.30976V17.3925C82.1486 17.3925 81.3167 17.9793 80.1895 17.9793C79.0623 17.9793 78.7403 17.4725 78.7403 16.3521V9.28309C78.7403 9.14971 78.6329 9.04301 78.4988 9.04301H75.3051C75.1709 9.04301 75.0636 9.14971 75.0636 9.28309V16.8856C75.0636 20.1668 76.9154 20.967 79.4381 20.967C81.5314 20.967 83.2221 19.82 83.2221 19.82C83.2221 19.82 83.3026 20.4068 83.3295 20.4869C83.3563 20.5669 83.4636 20.6202 83.571 20.6202H85.5838C85.7448 20.6202 85.8253 20.5135 85.8253 20.3802L85.8522 9.25641C85.8253 9.14971 85.6911 9.01633 85.5301 9.01633ZM50.0513 9.01633H46.8577C46.7235 9.01633 46.6161 9.14971 46.6161 9.30976V20.2201C46.6161 20.5135 46.804 20.6202 47.0724 20.6202H49.9439C50.2391 20.6202 50.3197 20.4869 50.3197 20.2201V9.25641C50.3197 9.12303 50.1855 9.01633 50.0513 9.01633ZM48.4947 3.97463C47.3407 3.97463 46.4283 4.88161 46.4283 6.02866C46.4283 7.17571 47.3407 8.08268 48.4947 8.08268C49.6219 8.08268 50.5344 7.17571 50.5344 6.02866C50.5344 4.88161 49.6219 3.97463 48.4947 3.97463ZM73.185 3.60118H70.0182C69.884 3.60118 69.7767 3.70788 69.7767 3.84126V9.9233H64.8118V3.86793C64.8118 3.73455 64.7044 3.62785 64.5702 3.62785H61.3766C61.2424 3.62785 61.1351 3.73455 61.1351 3.86793V20.4068C61.1351 20.5402 61.2692 20.6469 61.3766 20.6469H64.5702C64.7044 20.6469 64.8118 20.5402 64.8118 20.4068V13.3111H69.7767L69.7498 20.3802C69.7498 20.5135 69.8572 20.6202 69.9913 20.6202H73.185C73.3192 20.6202 73.4265 20.5135 73.4265 20.3802V3.84126C73.4265 3.73455 73.3192 3.60118 73.185 3.60118ZM44.9254 10.937V19.4732C44.9254 19.5265 44.8985 19.6332 44.8449 19.6599C44.8449 19.6599 42.9663 20.9937 39.88 20.9937C36.1496 20.9937 31.7483 19.82 31.7483 12.1907C31.7483 4.53482 35.6129 2.96096 39.3701 2.98764C42.6442 2.98764 43.9592 3.70788 44.1471 3.84126C44.2008 3.92128 44.2276 3.97463 44.2276 4.05466L43.6103 6.69555C43.6103 6.82893 43.4762 6.98898 43.3151 6.93563C42.7784 6.77558 41.9733 6.45547 40.0678 6.45547C37.8672 6.45547 35.5055 7.06901 35.5055 12.004C35.5055 16.939 37.7598 17.4992 39.3701 17.4992C40.7388 17.4992 41.2487 17.3391 41.2487 17.3391V13.9247H39.048C38.887 13.9247 38.7528 13.8179 38.7528 13.6846V10.937C38.7528 10.8036 38.8602 10.6969 39.048 10.6969H44.657C44.818 10.6702 44.9254 10.8036 44.9254 10.937Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="99" height="24" viewBox="0 0 99 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.372 0C5.52847 0 0 5.49518 0 12.2975C0 17.7393 3.54251 22.3542 8.45373 23.9814C9.07098 24.0881 9.31252 23.7146 9.31252 23.3945C9.31252 23.1011 9.28568 22.1408 9.28568 21.1004C6.17256 21.6606 5.36745 20.3535 5.12591 19.66C4.99172 19.3132 4.37447 18.2195 3.86456 17.926C3.43517 17.686 2.81791 17.1258 3.83772 17.0991C4.80386 17.0724 5.50163 17.9794 5.74317 18.3528C6.87033 20.2201 8.64159 19.6866 9.33936 19.3665C9.4467 18.5662 9.76875 18.0327 10.1176 17.7126C7.3534 17.3925 4.48182 16.3522 4.48182 11.6306C4.48182 10.2968 4.96489 9.17642 5.74317 8.3228C5.60898 8.00269 5.17959 6.74894 5.87735 5.06837C5.87735 5.06837 6.92401 4.74827 9.28568 6.32213C10.2787 6.05537 11.3253 5.89532 12.372 5.89532C13.4186 5.89532 14.4653 6.0287 15.4582 6.32213C17.8199 4.72159 18.8666 5.06837 18.8666 5.06837C19.5375 6.74894 19.1081 8.02937 19.0008 8.3228C19.779 9.17642 20.2621 10.2701 20.2621 11.6306C20.2621 16.3522 17.3637 17.3925 14.5995 17.7126C15.0557 18.0861 15.4314 18.833 15.4314 19.9801C15.4314 21.634 15.4046 22.9411 15.4046 23.3679C15.4046 23.688 15.6461 24.0881 16.2634 23.9547C21.1477 22.3275 24.6903 17.7126 24.6903 12.2708C24.7708 5.49518 19.2155 0 12.372 0Z" fill="#1B1F24"/>
<path d="M59.4711 17.8993H59.4443C59.4711 17.8993 59.4711 17.8993 59.4711 17.8993ZM59.4711 17.8993C59.337 17.8993 58.9881 17.9793 58.6124 17.9793C57.4315 17.9793 57.029 17.4458 57.029 16.7523V12.084H59.4175C59.5517 12.084 59.659 11.9773 59.659 11.7906V9.25641C59.659 9.12303 59.5517 9.01633 59.4175 9.01633H57.029V5.89528C57.029 5.78858 56.9484 5.70855 56.8143 5.70855H53.5938C53.4596 5.70855 53.3791 5.78858 53.3791 5.89528V9.12303C53.3791 9.12303 51.742 9.52317 51.6347 9.54984C51.5273 9.57652 51.4468 9.68322 51.4468 9.78993V11.8173C51.4468 11.9773 51.5542 12.1107 51.6884 12.1107H53.3523V16.9923C53.3523 20.6202 55.9018 20.9937 57.6462 20.9937C58.4513 20.9937 59.3906 20.7536 59.5517 20.6736C59.6322 20.6469 59.6858 20.5402 59.6858 20.4335V18.1928C59.7127 18.0327 59.6053 17.926 59.4711 17.8993ZM94.95 14.6449C94.95 11.9507 93.8496 11.6039 92.6956 11.7106C91.81 11.7639 91.0854 12.2174 91.0854 12.2174V17.4458C91.0854 17.4458 91.81 17.9527 92.9103 17.9793C94.4669 18.0327 94.95 17.4725 94.95 14.6449ZM98.5998 14.4048C98.5998 19.4999 96.9359 20.967 94.0375 20.967C91.5953 20.967 90.2534 19.7399 90.2534 19.7399C90.2534 19.7399 90.1998 20.4335 90.1193 20.5135C90.0656 20.5936 90.0119 20.6202 89.9046 20.6202H87.6771C87.5161 20.6202 87.3819 20.5135 87.3819 20.3802L87.4087 3.84126C87.4087 3.70788 87.5161 3.60118 87.6502 3.60118H90.8439C90.9781 3.60118 91.0854 3.70788 91.0854 3.84126L91.0586 9.41647C91.0586 9.41647 92.2931 8.6162 94.0912 8.6162H94.118C95.9429 8.6162 98.5998 9.28309 98.5998 14.4048ZM85.5301 9.01633H82.3901C82.2291 9.01633 82.1486 9.12303 82.1486 9.30976V17.3925C82.1486 17.3925 81.3167 17.9793 80.1895 17.9793C79.0623 17.9793 78.7403 17.4725 78.7403 16.3521V9.28309C78.7403 9.14971 78.6329 9.04301 78.4987 9.04301H75.3051C75.1709 9.04301 75.0636 9.14971 75.0636 9.28309V16.8856C75.0636 20.1667 76.9153 20.967 79.438 20.967C81.5314 20.967 83.2221 19.82 83.2221 19.82C83.2221 19.82 83.3026 20.4068 83.3294 20.4869C83.3563 20.5669 83.4636 20.6202 83.571 20.6202H85.5838C85.7448 20.6202 85.8253 20.5135 85.8253 20.3802L85.8521 9.25641C85.8253 9.14971 85.6911 9.01633 85.5301 9.01633ZM50.0513 9.01633H46.8577C46.7235 9.01633 46.6161 9.14971 46.6161 9.30976V20.2201C46.6161 20.5135 46.804 20.6202 47.0723 20.6202H49.9439C50.2391 20.6202 50.3197 20.4869 50.3197 20.2201V9.25641C50.3197 9.12303 50.1855 9.01633 50.0513 9.01633ZM48.4947 3.97463C47.3407 3.97463 46.4283 4.88161 46.4283 6.02866C46.4283 7.17571 47.3407 8.08268 48.4947 8.08268C49.6219 8.08268 50.5344 7.17571 50.5344 6.02866C50.5344 4.88161 49.6219 3.97463 48.4947 3.97463ZM73.185 3.60118H70.0182C69.884 3.60118 69.7766 3.70788 69.7766 3.84126V9.9233H64.8118V3.86793C64.8118 3.73455 64.7044 3.62785 64.5702 3.62785H61.3766C61.2424 3.62785 61.1351 3.73455 61.1351 3.86793V20.4068C61.1351 20.5402 61.2692 20.6469 61.3766 20.6469H64.5702C64.7044 20.6469 64.8118 20.5402 64.8118 20.4068V13.3111H69.7766L69.7498 20.3802C69.7498 20.5135 69.8572 20.6202 69.9913 20.6202H73.185C73.3192 20.6202 73.4265 20.5135 73.4265 20.3802V3.84126C73.4265 3.73455 73.3192 3.60118 73.185 3.60118ZM44.9254 10.937V19.4732C44.9254 19.5265 44.8985 19.6332 44.8449 19.6599C44.8449 19.6599 42.9663 20.9937 39.88 20.9937C36.1496 20.9937 31.7483 19.82 31.7483 12.1907C31.7483 4.53482 35.6129 2.96096 39.3701 2.98764C42.6442 2.98764 43.9592 3.70788 44.1471 3.84126C44.2008 3.92128 44.2276 3.97463 44.2276 4.05466L43.6103 6.69555C43.6103 6.82893 43.4762 6.98898 43.3151 6.93563C42.7784 6.77558 41.9733 6.45547 40.0678 6.45547C37.8672 6.45547 35.5055 7.06901 35.5055 12.004C35.5055 16.939 37.7598 17.4992 39.3701 17.4992C40.7388 17.4992 41.2487 17.3391 41.2487 17.3391V13.9246H39.048C38.887 13.9246 38.7528 13.8179 38.7528 13.6846V10.937C38.7528 10.8036 38.8602 10.6969 39.048 10.6969H44.657C44.818 10.6702 44.9254 10.8036 44.9254 10.937Z" fill="#1B1F24"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

+4
View File
@@ -78,6 +78,10 @@
onMount(() => {
dispatch('data', { isButtonDisabled, limit, tier });
});
// on free plan, if the only db is deleted,
// `create database` button needs to be enabled again.
$: if (isLimited) dispatch('data', { isButtonDisabled, limit, tier });
</script>
<!-- Show only if on Cloud, alerts are enabled, and it isn't a project limited service -->
+12 -1
View File
@@ -7,7 +7,7 @@
<footer class="main-footer u-cross-center">
<div class="main-footer-start">
<ul class="inline-links is-no-padding-first-and-last u-x-small u-flex-wrap u-row-gap-16">
<ul class="inline-links is-no-padding-first-and-last u-x-small u-flex-wrap u-gap-16">
<li class="inline-links-item" style="line-height: 1.02;">
<div class="u-flex u-cross-center u-gap-8">
{#if isCloud}
@@ -79,7 +79,18 @@
margin-block-start: auto;
}
.main-footer-start .inline-links-item {
padding-inline-end: 1rem;
padding-inline-start: unset;
}
[class^='icon-']:not(.icon-cloud):not(:hover) {
color: hsl(var(--color-neutral-50));
}
@media (max-width: 1200px) {
.main-footer {
gap: 2rem;
}
}
</style>
+17 -1
View File
@@ -10,7 +10,13 @@
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
<div class="alert-grid">
<span class="icon-info" aria-hidden="true" />
<span
aria-hidden="true"
class:icon-check-circle={type === 'success'}
class:icon-exclamation={type === 'warning'}
class:icon-exclamation-circle={type === 'error'}
class:icon-info={type === 'info' || type === 'default'} />
<div class="alert-content">
{#if title || $$slots.title}
<h6 class="alert-title">
@@ -30,3 +36,13 @@
{/if}
</div>
</section>
<style>
.alert {
padding: 1rem 1rem 0.75rem 1rem;
}
.alert-content {
gap: 0.25rem;
}
</style>
+14 -2
View File
@@ -48,6 +48,10 @@
return queries;
}
function unescapeNewLines(logs: string) {
return logs.replace(/\\n/g, '\n');
}
beforeNavigate((n) => {
if (!$log.show) return;
if (n.type === 'popstate') {
@@ -343,7 +347,11 @@
{/if}
</Alert>
{/if}
<Code withCopy noMargin code={execution.logs} language="sh" />
<Code
withCopy
noMargin
code={unescapeNewLines(execution.logs)}
language="sh" />
{:else}
<Card isDashed isTile>
<p class="text u-text-center">No response was recorded.</p>
@@ -351,7 +359,11 @@
{/if}
{:else if selectedResponse === 'errors'}
{#if execution?.errors}
<Code withCopy noMargin code={execution.errors} language="sh" />
<Code
withCopy
noMargin
code={unescapeNewLines(execution.errors)}
language="sh" />
{:else}
<Card isDashed isTile>
<p class="text u-text-center">No response was recorded.</p>
+20 -17
View File
@@ -8,6 +8,7 @@
import { slide } from '$lib/helpers/transition';
import { upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { canSeeDatabases } from '$lib/stores/roles';
import { wizard } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
import Create from '$routes/(console)/feedbackWizard.svelte';
@@ -95,23 +96,25 @@
<span class="text">Auth</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/databases`
)}
on:click={() => trackEvent('click_menu_databases')}
href={`${projectPath}/databases`}
use:tooltip={{
content: 'Databases',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
{#if $canSeeDatabases}
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${projectPath}/databases`
)}
on:click={() => trackEvent('click_menu_databases')}
href={`${projectPath}/databases`}
use:tooltip={{
content: 'Databases',
placement: 'right',
disabled: !narrow
}}>
<span class="icon-database" aria-hidden="true" />
<span class="text">Databases</span>
</a>
</li>
{/if}
<li class="drop-list-item">
<a
class="drop-button"
+1
View File
@@ -101,6 +101,7 @@
<div class="u-margin-block-start-16" />
<div class="chart-container">
<BarChart
formatted={$page.params.period === '24h' ? 'hours' : 'days'}
series={[
{
name: countMetadata.legend,
+44 -36
View File
@@ -20,6 +20,7 @@
import Delete from './delete.svelte';
import Retry from './wizard/retry.svelte';
import { Pill } from '$lib/elements';
import { canWriteRules } from '$lib/stores/roles';
export let rules: Models.ProxyRuleList;
export let type: ResourceType;
@@ -55,9 +56,11 @@
<slot name="heading" />
</Heading>
<Button on:click={openWizard}>
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create domain</span>
</Button>
{#if $canWriteRules}
<Button on:click={openWizard}>
<span class="icon-plus" aria-hidden="true" /> <span class="text">Create domain</span>
</Button>
{/if}
</div>
{#if rules.total}
<TableScroll>
@@ -65,7 +68,9 @@
<TableCellHead width={150}>Name</TableCellHead>
<TableCellHead width={120}>Verification Status</TableCellHead>
<TableCellHead width={180}>Certificate Status</TableCellHead>
<TableCellHead width={40} />
{#if $canWriteRules}
<TableCellHead width={40} />
{/if}
</TableHeader>
<TableBody>
{#each rules.rules as domain, i}
@@ -134,40 +139,43 @@
</Pill>
{/if}
</TableCell>
<TableCell right>
<DropList
bind:show={showDomainsDropdown[i]}
placement="bottom-start"
noArrow>
<Button
text
round
ariaLabel="more options"
on:click={() => (showDomainsDropdown[i] = !showDomainsDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
{#if domain.status !== 'verified'}
{#if $canWriteRules}
<TableCell right>
<DropList
bind:show={showDomainsDropdown[i]}
placement="bottom-start"
noArrow>
<Button
text
round
ariaLabel="more options"
on:click={() =>
(showDomainsDropdown[i] = !showDomainsDropdown[i])}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
{#if domain.status !== 'verified'}
<DropListItem
icon="refresh"
on:click={() => openRetry(domain, i)}>
{domain.status === 'unverified'
? 'Retry generation'
: 'Retry verification'}
</DropListItem>
{/if}
<DropListItem
icon="refresh"
on:click={() => openRetry(domain, i)}>
{domain.status === 'unverified'
? 'Retry generation'
: 'Retry verification'}
icon="trash"
on:click={() => {
selectedDomain = domain;
showDelete = true;
showDomainsDropdown[i] = false;
}}>
Delete
</DropListItem>
{/if}
<DropListItem
icon="trash"
on:click={() => {
selectedDomain = domain;
showDelete = true;
showDomainsDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</TableCell>
</svelte:fragment>
</DropList>
</TableCell>
{/if}
</TableRow>
{/each}
</TableBody>
+332
View File
@@ -0,0 +1,332 @@
import { AppwriteException, Client, type Payload } from '@appwrite.io/console';
export type BackupPolicyList = {
total: number;
policies: BackupPolicy[];
};
export type BackupArchiveList = {
total: number;
archives: BackupArchive[];
};
export type BackupRestorationList = {
total: number;
restorations: BackupRestoration[];
};
export type BackupPolicy = {
$id: string;
name: string;
$createdAt: string;
$updatedAt: string;
services: string[];
resources: string[];
resourceId?: string;
resourceType?: string;
retention: number;
schedule: string;
enabled: boolean;
};
export type BackupArchive = {
$id: string;
$createdAt: string;
$updatedAt: string;
policyId: string;
size: number;
status: string;
startedAt: string;
migrationId: string;
services: string[];
resources: string[];
resourceId?: string;
resourceType?: string;
};
export type BackupRestoration = {
$id: string;
$createdAt: string;
$updatedAt: string;
archiveId: string;
policyId: string;
status: string;
startedAt: string;
migrationId: string;
services: string[];
resources: string[];
options: string;
};
export class Backups {
client: Client;
constructor(client: Client) {
this.client = client;
}
async createArchive(services: string[], resourceId?: string): Promise<BackupArchive> {
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
const apiPath = '/backups/archives';
const payload: Payload = {};
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof resourceId !== 'undefined') {
payload['resourceId'] = resourceId;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async deleteArchive(archiveId: string): Promise<object> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('delete', uri, apiHeaders, payload);
}
async listArchives(queries?: string[]): Promise<BackupArchiveList> {
const apiPath = '/backups/archives';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async getArchive(archiveId: string): Promise<BackupArchive> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async listPolicies(queries?: string[]): Promise<BackupPolicyList> {
const apiPath = '/backups/policies';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async createPolicy(
policyId: string,
services: string[],
retention: number,
schedule: string,
name?: string,
resourceId?: string,
enabled?: boolean
): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
if (typeof retention === 'undefined') {
throw new AppwriteException('Missing required parameter: "retention"');
}
if (typeof schedule === 'undefined') {
throw new AppwriteException('Missing required parameter: "schedule"');
}
const apiPath = '/backups/policies';
const payload: Payload = {};
if (typeof policyId !== 'undefined') {
payload['policyId'] = policyId;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof resourceId !== 'undefined') {
payload['resourceId'] = resourceId;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
}
if (typeof retention !== 'undefined') {
payload['retention'] = retention;
}
if (typeof schedule !== 'undefined') {
payload['schedule'] = schedule;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async getPolicy(policyId: string): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async updatePolicy(
policyId: string,
name?: string,
retention?: number,
schedule?: string,
enabled?: boolean
): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof retention !== 'undefined') {
payload['retention'] = retention;
}
if (typeof schedule !== 'undefined') {
payload['schedule'] = schedule;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('patch', uri, apiHeaders, payload);
}
async deletePolicy(policyId: string): Promise<object> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('delete', uri, apiHeaders, payload);
}
async createRestoration(
archiveId: string,
services: string[],
newResourceId?: string,
newResourceName?: string
): Promise<BackupRestoration> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
const apiPath = '/backups/restoration';
const payload: Payload = {};
if (typeof archiveId !== 'undefined') {
payload['archiveId'] = archiveId;
}
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof newResourceId !== 'undefined') {
payload['newResourceId'] = newResourceId;
}
if (typeof newResourceName !== 'undefined') {
payload['newResourceName'] = newResourceName;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async listRestorations(queries?: string[]): Promise<BackupRestorationList> {
const apiPath = '/backups/restorations';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async getRestoration(restorationId: string): Promise<BackupArchive> {
if (typeof restorationId === 'undefined') {
throw new AppwriteException('Missing required parameter: "restorationId"');
}
const apiPath = '/backups/restorations/{restorationId}'.replace(
'{restorationId}',
restorationId
);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
}
+36 -2
View File
@@ -10,6 +10,7 @@ export type PaymentMethodData = {
$updatedAt: string;
providerMethodId: string;
providerUserId: string;
userId: string;
expiryMonth: number;
expiryYear: number;
expired: boolean;
@@ -146,7 +147,7 @@ export type Aggregation = {
*/
amount: number;
/**
* Price for addional members
* Price for additional members
*/
additionalMembers: number;
/**
@@ -182,6 +183,9 @@ export type OrganizationUsage = {
filesStorageTotal: number;
buildsStorageTotal: number;
deploymentsStorageTotal: number;
executionsMBSecondsTotal: number;
buildsMBSecondsTotal: number;
backupsStorageTotal: number;
storageTotal: number;
users: Array<Models.Metric>;
usersTotal: number;
@@ -232,6 +236,7 @@ export type Address = {
city: string;
state?: string;
postalCode: string;
userId: string;
};
export type AddressesList = {
@@ -274,6 +279,8 @@ export type Plan = {
users: AdditionalResource;
};
trialDays: number;
isAvailable: boolean;
selfService: boolean;
};
export type PlansInfo = {
@@ -283,6 +290,11 @@ export type PlansInfo = {
export type PlansMap = Map<Tier, Plan>;
export type Roles = {
scopes: string[];
roles: string[];
};
export class Billing {
client: Client;
@@ -364,6 +376,14 @@ export class Billing {
);
}
async getRoles(organizationId: string): Promise<Roles> {
const path = `/organizations/${organizationId}/roles`;
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call('get', uri, {
'content-type': 'application/json'
});
}
async updatePlan(
organizationId: string,
billingPlan: string,
@@ -697,6 +717,20 @@ export class Billing {
});
}
async setMembership(
programId: string
): Promise<{ $createdAt: string } | { error: { code: number; message: string } }> {
const path = `/console/programs/${programId}/memberships`;
const uri = new URL(this.client.config.endpoint + path);
try {
return await this.client.call('POST', uri, {
'content-type': 'application/json'
});
} catch (e) {
return { error: { code: e.code, message: e.message } };
}
}
async getCoupon(couponId: string): Promise<Coupon> {
const path = `/console/coupons/${couponId}`;
const params = {
@@ -940,7 +974,7 @@ export class Billing {
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'post',
'patch',
uri,
{
'content-type': 'application/json'
+44 -1
View File
@@ -32,8 +32,32 @@ import { last } from '$lib/helpers/array';
import { sizeToBytes, type Size } from '$lib/helpers/sizeConvertion';
import { user } from './user';
import { browser } from '$app/environment';
import { canSeeBilling } from './roles';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1';
export const roles = [
{
label: 'Owner',
value: 'owner'
},
{
label: 'Developer',
value: 'developer'
},
{
label: 'Editor',
value: 'editor'
},
{
label: 'Analyst',
value: 'analyst'
},
{
label: 'Billing',
value: 'billing'
}
];
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
@@ -41,6 +65,10 @@ export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansM
export const daysLeftInTrial = writable<number>(0);
export const readOnly = writable<boolean>(false);
export function getRoleLabel(role: string) {
return roles.find((r) => r.value === role)?.label ?? role;
}
export function tierToPlan(tier: Tier) {
switch (tier) {
case BillingPlan.FREE:
@@ -49,6 +77,10 @@ export function tierToPlan(tier: Tier) {
return tierPro;
case BillingPlan.SCALE:
return tierScale;
case BillingPlan.GITHUB_EDUCATION:
return tierGitHubEducation;
case BillingPlan.CUSTOM:
return tierCustom;
default:
return tierFree;
}
@@ -116,6 +148,7 @@ export const failedInvoice = cachedStore<
return {
load: async (orgId) => {
if (!isCloud) set(null);
if (!get(canSeeBilling)) set(null);
const invoices = await sdk.forConsole.billing.listInvoices(orgId);
const failedInvoices = invoices.invoices.filter((i) => i.status === 'failed');
// const failedInvoices = invoices.invoices;
@@ -144,6 +177,11 @@ export const tierFree: TierData = {
description: 'For personal hobby projects of small scale and students.'
};
export const tierGitHubEducation: TierData = {
name: 'GitHub Education',
description: 'For members of GitHub student developers program.'
};
export const tierPro: TierData = {
name: 'Pro',
description: 'For pro developers and production projects that need the ability to scale.'
@@ -153,6 +191,11 @@ export const tierScale: TierData = {
description: 'For scaling teams and agencies that need dedicated support.'
};
export const tierCustom: TierData = {
name: 'Custom',
description: 'Team on a custom contract'
};
export const showUsageRatesModal = writable<boolean>(false);
export function checkForUsageFees(plan: Tier, id: PlanServices) {
+12 -3
View File
@@ -1,5 +1,13 @@
import { writable } from 'svelte/store';
import type { NotificationCoolOffOptions } from '$lib/helpers/notifications';
import type { Organization } from '$lib/stores/organization';
import type { Models } from '@appwrite.io/console';
type BottomModalAlertAction = {
text: string;
link: (ctx: { organization: Organization; project: Models.Project }) => string;
external?: boolean;
};
export type BottomModalAlertItem = {
id: string;
@@ -7,15 +15,15 @@ export type BottomModalAlertItem = {
message: string;
src: Record<'dark' | 'light', string>;
cta: Record<'text' | 'link', string>;
cta: BottomModalAlertAction;
learnMore?: BottomModalAlertAction;
plan: 'free' | 'pro' | 'scale' /*| 'enterprise'*/;
learnMore?: Partial<Record<'text' | 'link', string>>;
show?: boolean;
isHtml?: boolean;
importance?: number;
closed?: () => void;
scope?: 'organization' | 'project';
notificationHideOptions?: NotificationCoolOffOptions;
};
@@ -38,6 +46,7 @@ export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
};
bottomModalAlerts.update((all) => {
if (all.some((t) => t.id === notification.id)) return all;
return [...all, defaults as BottomModalAlertItem];
});
};
+42
View File
@@ -0,0 +1,42 @@
import { derived, writable } from 'svelte/store';
import { page } from '$app/stores';
import { type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { headerAlert } from '$lib/stores/headerAlert';
import BackupDatabase from '$lib/components/backupDatabaseAlert.svelte';
import { shouldShowNotification } from '$lib/helpers/notifications';
export const database = derived(page, ($page) => $page.data?.database as Models.Database);
export const backupsBannerId = 'banner:databaseBackups';
export const showPolicyAlert = writable<boolean>(false);
export async function checkForDatabaseBackupPolicies(database: Models.Database) {
// fast path: return if user dismissed the banner
if (!shouldShowNotification(backupsBannerId)) return;
let total = 0;
try {
const policies = await sdk.forProject.backups.listPolicies([
Query.limit(1),
Query.equal('resourceId', database.$id)
]);
total = policies.total;
} catch (e) {
// ignore, backups not allowed on free plan error.
}
showPolicyAlert.set(total <= 0);
if (!total) {
headerAlert.add({
id: backupsBannerId,
component: BackupDatabase,
show: true,
importance: 1
});
}
}
+27 -12
View File
@@ -1,6 +1,6 @@
import { browser } from '$app/environment';
import { VARS } from '$lib/system';
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
import FeedbackGeneral from '$lib/components/feedback/feedbackGeneral.svelte';
import FeedbackNps from '$lib/components/feedback/feedbackNPS.svelte';
@@ -11,12 +11,11 @@ export type Feedback = {
notification: boolean;
type: 'nps' | 'general';
show: boolean;
source: string;
};
export type FeedbackData = {
message: string;
name?: string;
email?: string;
value?: number;
};
@@ -47,8 +46,6 @@ export const selectedFeedback = writable<FeedbackOption>();
function createFeedbackDataStore() {
const { set, subscribe, update } = writable<FeedbackData>({
message: '',
name: '',
email: '',
value: null
});
return {
@@ -58,8 +55,6 @@ function createFeedbackDataStore() {
reset: () => {
update((feedbackData) => {
feedbackData.message = '';
feedbackData.name = '';
feedbackData.email = '';
feedbackData.value = null;
return feedbackData;
});
@@ -75,13 +70,15 @@ function createFeedbackStore() {
visualized: browser ? (parseInt(localStorage.getItem('feedbackVisualized')) ?? 0) : 0,
notification: false,
type: 'general',
show: false
show: false,
source: 'n/a'
});
return {
subscribe,
update,
toggleFeedback: () => {
toggleFeedback: (source: string = 'n/a') => {
update((feedback) => {
feedback.source = source;
feedback.show = !feedback.show;
return feedback;
});
@@ -110,11 +107,17 @@ function createFeedbackStore() {
return feedback;
});
},
// TODO: update growth server to accept `billingPlan`.
// TODO: update growth server to accept `source` key to know the feedback source area.
submitFeedback: async (
subject: string,
message: string,
firstname?: string,
name?: string,
email?: string,
// eslint-disable-next-line
// @ts-expect-error
billingPlan?: string,
currentPage?: string,
value?: number
) => {
if (!VARS.GROWTH_ENDPOINT) return;
@@ -127,14 +130,26 @@ function createFeedbackStore() {
subject,
message,
email,
firstname: firstname ? firstname : undefined,
customFields: value ? [{ id: '40655', value }] : undefined
// billingPlan,
firstname: name || 'Unknown',
customFields: [
{ id: '47364', value: currentPage },
...(value ? [{ id: '40655', value }] : [])
],
metaFields: {
source: get(feedback).source
}
})
});
// reset the state
get(feedback).source = 'n/a';
if (response.status >= 400) {
throw new Error('Failed to submit feedback');
}
}
};
}
export const feedback = createFeedbackStore();
+1 -1
View File
@@ -31,7 +31,7 @@ function createHeaderAlertStore() {
});
},
get: (): HeaderAlert => {
//return higest importance component
//return highest importance component
let component = {
id: '',
show: false,
+3 -3
View File
@@ -42,7 +42,7 @@ export const ResourcesFriendly = {
file: { singular: 'File', plural: 'Files' },
bucket: { singular: 'Bucket', plural: 'Buckets' },
function: { singular: 'Function', plural: 'Functions' },
'environment variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
'environment-variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
deployment: { singular: 'Deployment', plural: 'Deployments' },
database: { singular: 'Database', plural: 'Databases' },
collection: { singular: 'Collection', plural: 'Collections' },
@@ -102,7 +102,7 @@ export const migrationFormToResources = (
addResource('function');
}
if (formData.functions.env) {
addResource('environment variable');
addResource('environment-variable');
}
if (formData.functions.inactive) {
addResource('deployment');
@@ -156,7 +156,7 @@ export const resourcesToMigrationForm = (
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
}
if (resources.includes('environment variable') && isVersionAtLeast(version, '1.4.0')) {
if (resources.includes('environment-variable') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.env = true;
}
if (resources.includes('deployment') && isVersionAtLeast(version, '1.4.0')) {
+1
View File
@@ -41,5 +41,6 @@ export const organizationList = derived(
page,
($page) => $page.data.organizations as Models.TeamList<Record<string, unknown>>
);
export const organization = derived(page, ($page) => $page.data?.organization as Organization);
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
+48
View File
@@ -0,0 +1,48 @@
import { page } from '$app/stores';
import { derived } from 'svelte/store';
export const roles = derived(page, ($page) => $page.data?.roles ?? []);
export const scopes = derived(page, ($page) => $page.data?.scopes ?? []);
export const isDeveloper = derived(roles, ($roles) => $roles.includes('developer'));
export const isBilling = derived(roles, ($roles) => $roles.includes('billing'));
export const isOwner = derived(roles, ($roles) => $roles.includes('owner'));
export const canWriteDatabases = derived(scopes, ($scopes) => $scopes.includes('databases.write'));
export const canWriteProjects = derived(scopes, ($scopes) => $scopes.includes('projects.write'));
export const canWriteFunctions = derived(scopes, ($scopes) => $scopes.includes('functions.write'));
export const canWriteBuckets = derived(scopes, ($scopes) => $scopes.includes('buckets.write'));
export const canWriteProviders = derived(scopes, ($scopes) => $scopes.includes('providers.write'));
export const canWriteMessages = derived(scopes, ($scopes) => $scopes.includes('messages.write'));
export const canWriteWebhooks = derived(scopes, ($scopes) => $scopes.includes('webhooks.write'));
export const canWritePlatforms = derived(scopes, ($scopes) => $scopes.includes('platforms.write'));
export const canWriteTargets = derived(scopes, ($scopes) => $scopes.includes('targets.write'));
export const canWriteUsers = derived(scopes, ($scopes) => $scopes.includes('users.write'));
export const canWriteTeams = derived(scopes, ($scopes) => $scopes.includes('teams.write'));
export const canWriteCollections = derived(scopes, ($scopes) =>
$scopes.includes('collections.write')
);
export const canWriteDocuments = derived(scopes, ($scopes) => $scopes.includes('documents.write'));
export const canWriteExecutions = derived(scopes, ($scopes) =>
$scopes.includes('executions.write')
);
export const canWriteSubscribers = derived(scopes, ($scopes) =>
$scopes.includes('subscribers.write')
);
export const canWriteKeys = derived(scopes, ($scopes) => $scopes.includes('keys.write'));
export const canWriteRules = derived(scopes, ($scopes) => $scopes.includes('rules.write'));
export const canWriteMigrations = derived(scopes, ($scopes) =>
$scopes.includes('migrations.write')
);
export const canWriteVcs = derived(scopes, ($scopes) => $scopes.includes('vcs.write'));
export const canWriteTopics = derived(scopes, ($scopes) => $scopes.includes('topics.write'));
export const canSeeBilling = derived(scopes, ($scopes) => $scopes.includes('billing.read'));
export const canSeeProjects = derived(scopes, function ($scopes) {
return $scopes.includes('projects.read');
});
export const canSeeDatabases = derived(scopes, ($scopes) => $scopes.includes('databases.read'));
export const canSeeFunctions = derived(scopes, ($scopes) => $scopes.includes('functions.read'));
export const canSeeTeams = derived(scopes, ($scopes) => $scopes.includes('teams.read'));
export const canSeeBuckets = derived(scopes, ($scopes) => $scopes.includes('buckets.read'));
export const canSeeMessages = derived(scopes, ($scopes) => $scopes.includes('messages.read'));
+2
View File
@@ -22,6 +22,7 @@ import {
Vcs
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
import { Backups } from '../sdk/backups';
import { Sources } from '$lib/sdk/sources';
export function getApiEndpoint(): string {
@@ -41,6 +42,7 @@ const sdkForProject = {
client: clientProject,
account: new Account(clientProject),
avatars: new Avatars(clientProject),
backups: new Backups(clientProject),
databases: new Databases(clientProject),
functions: new Functions(clientProject),
health: new Health(clientProject),
+3 -3
View File
@@ -36,7 +36,7 @@ export async function initializeStripe() {
// Set up the options for the stripe elements
const options = {
clientSecret: clientSecret,
appearance: get(app).themeInUse === 'dark' ? apperanceDark : apperanceLight
appearance: get(app).themeInUse === 'dark' ? appearanceDark : appearanceLight
};
// Set up Elements and then create form
elements = get(stripe).elements(options);
@@ -184,7 +184,7 @@ export async function confirmSetup(
}
}
const apperanceLight = {
const appearanceLight = {
variables: {
colorPrimary: '#606a7b',
colorText: 'rgb(107, 107, 112)',
@@ -213,7 +213,7 @@ const apperanceLight = {
}
};
const apperanceDark = {
const appearanceDark = {
variables: {
colorPrimary: '#606a7b',
colorText: 'rgb(195, 195, 198)',
+13 -3
View File
@@ -1,6 +1,7 @@
import type { Models } from '@appwrite.io/console';
import { Client, type Models, Storage } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import { sdk } from './sdk';
import { getProjectId } from '$lib/helpers/project';
import { getApiEndpoint } from '$lib/stores/sdk';
type UploaderFile = {
$id: string;
@@ -17,6 +18,15 @@ export type Uploader = {
files: UploaderFile[];
};
const temporaryStorage = () => {
const clientProject = new Client()
.setEndpoint(getApiEndpoint())
.setMode('admin')
.setProject(getProjectId());
return new Storage(clientProject);
};
const createUploader = () => {
const { subscribe, set, update } = writable<Uploader>({
isOpen: false,
@@ -71,7 +81,7 @@ const createUploader = () => {
n.files.unshift(newFile);
return n;
});
const uploadedFile = await sdk.forProject.storage.createFile(
const uploadedFile = await temporaryStorage().createFile(
bucketId,
id ?? 'unique()',
file,
+2 -2
View File
@@ -159,7 +159,7 @@
</div>
<div class="body-text-2">
{runtimeDetail.name}
{#if runtimeDetail.name.toLowerCase() === 'go'}
{#if runtimeDetail.name.toLowerCase() === 'deno'}
<span class="inline-tag">New</span>
{/if}
</div>
@@ -192,7 +192,7 @@
<Button
text
class="u-margin-block-start-24 u-margin-inline-start-auto"
href={`${base}/project-${$page.params.project}/functions/templates?useCase=Starter`}>
href={`${base}/project-${$page.params.project}/functions/templates?useCase=starter`}>
All starter templates <span class="icon-cheveron-right" />
</Button>
<div class="u-sep-block-start common-section" />
@@ -36,7 +36,7 @@
/**
*
* @param field {string} The field to update.
* Representes the path to the field in the formData object e.g. 'users.root'
* Represents the path to the field in the formData object e.g. 'users.root'
*/
function handleInputChange(field: FormKeys) {
return (event: Event) => {
@@ -344,7 +344,7 @@
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources?.includes('environment variable')}
{#if resources?.includes('environment-variable')}
<li class="checkbox-field">
<input
type="checkbox"
@@ -0,0 +1,13 @@
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { organizations, account } = await parent();
const teamId = account.prefs.organization ?? organizations.teams[0]?.$id;
if (teamId) {
redirect(303, `${base}/organization-${teamId}/members`);
}
redirect(303, base);
};
+30 -13
View File
@@ -8,23 +8,24 @@
import Shell from '$lib/layout/shell.svelte';
import { app } from '$lib/stores/app';
import { log } from '$lib/stores/logs';
import { newOrgModal, organization } from '$lib/stores/organization';
import { newOrgModal, organization, type Organization } from '$lib/stores/organization';
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
import { wizard } from '$lib/stores/wizard';
import { afterUpdate, onMount } from 'svelte';
import { loading } from '$routes/store';
import { requestedMigration } from '../store';
import Create from './createOrganization.svelte';
import {
showUsageRatesModal,
checkForUsageLimit,
checkPaymentAuthorizationRequired,
calculateTrialDay,
paymentExpired,
checkForMarkedForDeletion,
checkForMandate,
checkForMarkedForDeletion,
checkForMissingPaymentMethod,
checkForNewDevUpgradePro,
plansInfo
checkForUsageLimit,
checkPaymentAuthorizationRequired,
paymentExpired,
plansInfo,
showUsageRatesModal
} from '$lib/stores/billing';
import { goto } from '$app/navigation';
import { CommandCenter, registerCommands, registerSearchers } from '$lib/commandCenter';
@@ -35,7 +36,7 @@
import { openMigrationWizard } from './(migration-wizard)';
import { project } from './project-[project]/store';
import { feedback } from '$lib/stores/feedback';
import { VARS, hasStripePublicKey, isCloud } from '$lib/system';
import { hasStripePublicKey, isCloud, VARS } from '$lib/system';
import { stripe } from '$lib/stores/stripe';
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
import { showSupportModal } from './wizard/support/store';
@@ -43,6 +44,8 @@
import { headerAlert } from '$lib/stores/headerAlert';
import { UsageRates } from '$lib/components/billing';
import { base } from '$app/paths';
import { canSeeProjects } from '$lib/stores/roles';
import { BottomModalAlert } from '$lib/components';
function kebabToSentenceCase(str: string) {
return str
@@ -66,9 +69,10 @@
keys: ['g', 'p'],
group: 'navigation',
disabled:
$page.url.pathname.includes('/console/organization-') &&
!$page.url.pathname.endsWith('/members') &&
!$page.url.pathname.endsWith('/settings'),
($page.url.pathname.includes('/console/organization-') &&
!$page.url.pathname.endsWith('/members') &&
!$page.url.pathname.endsWith('/settings')) ||
!$canSeeProjects,
rank: -1
},
{
@@ -265,9 +269,18 @@
}
}
organization.subscribe(async (org) => {
database.subscribe(async (database) => {
if (!database) return;
// the component checks `isCloud` internally.
await checkForDatabaseBackupPolicies(database);
});
let currentOrganizationId = null;
async function checkForUsageLimits(org: Organization) {
if (!org) return;
if (currentOrganizationId === org.$id) return;
if (isCloud) {
currentOrganizationId = org.$id;
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
await checkForNewDevUpgradePro(org);
@@ -283,7 +296,9 @@
}
$activeHeaderAlert = headerAlert.get();
}
});
}
$: checkForUsageLimits($organization);
$: if (!$log.show) {
$log.data = null;
@@ -335,3 +350,5 @@
{#if isCloud && $showUsageRatesModal}
<UsageRates bind:show={$showUsageRatesModal} org={$organization} />
{/if}
<BottomModalAlert />
+3 -1
View File
@@ -34,6 +34,8 @@ export const load: LayoutLoad = async ({ fetch, depends, parent }) => {
return {
consoleVariables: variables,
version: data?.version ?? null,
plansInfo
plansInfo,
roles: [],
scopes: []
};
};
@@ -17,7 +17,7 @@
import { Pill } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import type { Organization } from '$lib/stores/organization';
import { daysLeftInTrial, plansInfo } from '$lib/stores/billing';
import { daysLeftInTrial, plansInfo, tierToPlan } from '$lib/stores/billing';
import { tooltip } from '$lib/actions/tooltip';
import { toLocaleDate } from '$lib/helpers/date';
import { BillingPlan } from '$lib/constants';
@@ -71,14 +71,15 @@
</svelte:fragment>
<svelte:fragment slot="status">
{#if isCloudOrg(organization)}
{#if organization?.billingPlan === BillingPlan.FREE}
{#if organization?.billingPlan === BillingPlan.FREE || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div
class="u-flex u-cross-center"
use:tooltip={{
content:
'You are limited to 1 free organization per account'
}}>
<Pill class="eyebrow-heading-3">FREE</Pill>
<Pill class="eyebrow-heading-3"
>{tierToPlan(organization?.billingPlan)?.name}</Pill>
</div>
{/if}
{#if organization?.billingTrialStartDate && $daysLeftInTrial > 0 && organization.billingPlan !== BillingPlan.FREE && $plansInfo.get(organization.billingPlan)?.trialDays}
@@ -10,7 +10,6 @@ export const load: PageLoad = async ({ depends }) => {
sdk.forConsole.billing.listPaymentMethods(),
sdk.forConsole.billing.listAddresses()
]);
return {
paymentMethods,
addressList

Some files were not shown because too many files have changed in this diff Show More