mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of github.com:appwrite/console into fix-page-breaks-on-redirection
This commit is contained in:
@@ -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
|
||||
|
||||
+15
-15
@@ -23,12 +23,12 @@
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.26.0",
|
||||
"@sentry/sveltekit": "^8.31.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"ai": "^2.2.37",
|
||||
"analytics": "^0.8.14",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs": "^1.11.13",
|
||||
"deep-equal": "^2.2.3",
|
||||
"echarts": "^5.5.1",
|
||||
"envfile": "^7.1.0",
|
||||
@@ -42,12 +42,12 @@
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sveltejs/adapter-static": "^3.0.4",
|
||||
"@sveltejs/kit": "^2.5.22",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.5.28",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/svelte": "^5.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
@@ -55,22 +55,22 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"eslint-plugin-svelte": "^2.44.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.77.8",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.5",
|
||||
"sass": "^1.79.3",
|
||||
"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.6.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.7",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -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'
|
||||
|
||||
Generated
+910
-876
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms/index';
|
||||
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
|
||||
import { app } from '$lib/stores/app';
|
||||
import {
|
||||
type BottomModalAlertItem,
|
||||
bottomModalAlerts,
|
||||
dismissBottomModalAlert,
|
||||
hideAllModalAlerts
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
currentIndex = (currentIndex + 1) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showPrevious() {
|
||||
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showUpgrade() {
|
||||
const plan = currentModalAlert.plan;
|
||||
const organizationPlan = $organization.billingPlan;
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
return false;
|
||||
case 'pro':
|
||||
return organizationPlan === BillingPlan.FREE;
|
||||
case 'scale':
|
||||
return (
|
||||
organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
addBottomModalAlerts();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex === filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-4 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}
|
||||
external={!!currentModalAlert.cta.external}
|
||||
fullWidthMobile>
|
||||
{currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
href={currentModalAlert.learnMore.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}>
|
||||
{currentModalAlert.learnMore.text}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-alert-wrapper is-only-mobile" class:closed={!openModalOnMobile}>
|
||||
{#if openModalOnMobile}
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex ===
|
||||
filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-8 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}
|
||||
external={!!currentModalAlert.cta.external}
|
||||
fullWidthMobile
|
||||
on:click={() => (openModalOnMobile = false)}>
|
||||
{shouldShowUpgrade
|
||||
? 'Upgrade plan'
|
||||
: currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
on:click={() => (openModalOnMobile = false)}
|
||||
href={currentModalAlert.learnMore.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}>
|
||||
{currentModalAlert.learnMore.text}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
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">New features available</h3>
|
||||
<button on:click={hideAllModalAlerts}>
|
||||
<span class="icon-x" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
Explore new features to enhance your projects and improve security.
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.card {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.main-alert-wrapper {
|
||||
left: 1rem;
|
||||
z-index: 25;
|
||||
bottom: 1rem;
|
||||
position: fixed;
|
||||
max-width: 289px;
|
||||
}
|
||||
|
||||
.feature-count-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
width: fit-content;
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-inline-tag {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
padding: var(--space-2, 4px);
|
||||
border-radius: var(--border-radius-S, 8px);
|
||||
border: hsl(var(--color-neutral-10)) solid 1px;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .icon-inline-tag {
|
||||
background: #1d1d21;
|
||||
border: hsl(var(--color-neutral-80)) solid 1px;
|
||||
}
|
||||
|
||||
.u-gap-10 {
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.icon-cheveron-left,
|
||||
.icon-cheveron-right {
|
||||
opacity: 0.5;
|
||||
color: #97979b;
|
||||
width: var(--icon-size-M, 20px);
|
||||
height: var(--icon-size-M, 20px);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-alert-wrapper {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
max-width: 100vw;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.main-alert-wrapper.closed {
|
||||
backdrop-filter: unset;
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
padding: var(--space-5, 10px) var(--space-6, 12px);
|
||||
}
|
||||
|
||||
.main-alert-wrapper:has(.card.notification-card) {
|
||||
bottom: 0;
|
||||
top: unset;
|
||||
min-height: auto;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -5,14 +5,28 @@
|
||||
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 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'
|
||||
@@ -28,8 +42,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" />
|
||||
@@ -54,9 +68,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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -75,3 +75,4 @@ export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
let inputDigitFields: InputDigits;
|
||||
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
@@ -10,6 +12,7 @@
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
inputDigitFields?.clearInputsAndRefocus();
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
@@ -81,7 +84,12 @@
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
|
||||
{/if}
|
||||
<InputDigits bind:value={code} required autofocus {autoSubmit} />
|
||||
<InputDigits
|
||||
bind:value={code}
|
||||
required
|
||||
autofocus
|
||||
{autoSubmit}
|
||||
bind:this={inputDigitFields} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -61,6 +61,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;
|
||||
|
||||
@@ -38,6 +38,21 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the input fields and moves the focus to the first input.
|
||||
* Usually used when resetting fields on auth fails, etc.
|
||||
*/
|
||||
export function clearInputsAndRefocus() {
|
||||
value = '';
|
||||
autoSubmitted = false;
|
||||
|
||||
if (element) {
|
||||
const inputs = element.querySelectorAll('input');
|
||||
inputs.forEach((input) => (input.value = ''));
|
||||
if (autofocus) inputs[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
|
||||
@@ -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">
|
||||
<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}
|
||||
|
||||
|
||||
@@ -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">
|
||||
<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">
|
||||
|
||||
@@ -21,3 +21,9 @@
|
||||
data-private>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
:global(.table .button.is-text) {
|
||||
--p-text-color-default: initial !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
export type NotificationPrefItem = {
|
||||
expiry: number;
|
||||
hideCount: number;
|
||||
state: 'hidden' | 'shown' | undefined;
|
||||
};
|
||||
|
||||
export type NotificationCoolOffOptions = {
|
||||
coolOffPeriod?: number;
|
||||
exponentialBackoff?: boolean;
|
||||
exponentialBackoffFactor?: number;
|
||||
};
|
||||
|
||||
const userPreferences = () => get(user).prefs;
|
||||
|
||||
const notificationPrefs = (): Record<string, NotificationPrefItem> => {
|
||||
const prefs = userPreferences();
|
||||
return prefs.notificationPrefs ? prefs.notificationPrefs : {};
|
||||
};
|
||||
|
||||
function updateNotificationPrefs(parsedPrefs: Record<string, NotificationPrefItem>) {
|
||||
const currentPrefs = userPreferences();
|
||||
|
||||
const newPrefs = {
|
||||
...currentPrefs,
|
||||
notificationPrefs: parsedPrefs
|
||||
};
|
||||
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the notification banner by marking it as 'hidden' and setting an expiry time.
|
||||
* Supports normal cool-off periods or exponential backoff based on the options passed.
|
||||
*
|
||||
* @param {string} id - The ID of the notification.
|
||||
* @param {NotificationCoolOffOptions} [options] - Configuration for cool-off behavior.
|
||||
* @param {number} [options.coolOffPeriod=24] - Cool-off period in hours, defaults to 24 hours.
|
||||
* @param {boolean} [options.exponentialBackoff=false] - If true, the cool-off period doubles with each hide. Consider using a smaller `coolOffPeriod` when this option is enabled.
|
||||
* @param {number} [options.exponentialBackoffFactor=2] - The factor by which the cool-off period is multiplied with each hide. Default is 2.
|
||||
*/
|
||||
export function hideNotification(id: string, options: NotificationCoolOffOptions = {}) {
|
||||
const {
|
||||
coolOffPeriod = 24,
|
||||
exponentialBackoff = false,
|
||||
exponentialBackoffFactor = 2
|
||||
} = options;
|
||||
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
let expiryTime = Date.now() + coolOffPeriod * 3600000;
|
||||
|
||||
let hideCount = parsedBannerPrefs[id]?.hideCount || 0;
|
||||
|
||||
if (exponentialBackoff) {
|
||||
hideCount += 1;
|
||||
expiryTime =
|
||||
Date.now() + coolOffPeriod * 3600000 * exponentialBackoffFactor ** (hideCount - 1);
|
||||
}
|
||||
|
||||
parsedBannerPrefs[id] = {
|
||||
hideCount,
|
||||
state: 'hidden',
|
||||
expiry: expiryTime
|
||||
};
|
||||
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the notification preference for the given ID from the user's preferences.
|
||||
*
|
||||
* @param {string} id - The ID of the notification to remove from preferences.
|
||||
*/
|
||||
export function removeNotificationFromPrefs(id: string) {
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
if (!parsedBannerPrefs[id]) return;
|
||||
|
||||
delete parsedBannerPrefs[id];
|
||||
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the notification banner should be shown based on the expiry time.
|
||||
*
|
||||
* @param {string} id - The ID of the notification.
|
||||
* @returns {boolean} - Returns true if the banner should be shown, false otherwise.
|
||||
*/
|
||||
export function shouldShowNotification(id: string): boolean {
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
const notificationPref = parsedBannerPrefs[id];
|
||||
|
||||
if (!notificationPref) return true;
|
||||
|
||||
if (Date.now() >= notificationPref.expiry) {
|
||||
notificationPref.state = 'shown';
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -24,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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,6 +9,7 @@ export type PaymentMethodData = {
|
||||
$updatedAt: string;
|
||||
providerMethodId: string;
|
||||
providerUserId: string;
|
||||
userId: string;
|
||||
expiryMonth: number;
|
||||
expiryYear: number;
|
||||
expired: boolean;
|
||||
@@ -181,6 +182,8 @@ export type OrganizationUsage = {
|
||||
filesStorageTotal: number;
|
||||
buildsStorageTotal: number;
|
||||
deploymentsStorageTotal: number;
|
||||
executionsMBSecondsTotal: number;
|
||||
buildsMBSecondsTotal: number;
|
||||
storageTotal: number;
|
||||
users: Array<Models.Metric>;
|
||||
usersTotal: number;
|
||||
@@ -231,6 +234,7 @@ export type Address = {
|
||||
city: string;
|
||||
state?: string;
|
||||
postalCode: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type AddressesList = {
|
||||
@@ -282,6 +286,11 @@ export type PlansInfo = {
|
||||
|
||||
export type PlansMap = Map<Tier, Plan>;
|
||||
|
||||
export type Roles = {
|
||||
scopes: string[];
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export class Billing {
|
||||
client: Client;
|
||||
|
||||
@@ -363,6 +372,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,
|
||||
|
||||
@@ -32,15 +32,43 @@ 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 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);
|
||||
export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansMap);
|
||||
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:
|
||||
@@ -116,6 +144,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;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
src: Record<'dark' | 'light', string>;
|
||||
cta: BottomModalAlertAction;
|
||||
learnMore?: BottomModalAlertAction;
|
||||
plan: 'free' | 'pro' | 'scale' /*| 'enterprise'*/;
|
||||
show?: boolean;
|
||||
isHtml?: boolean;
|
||||
importance?: number;
|
||||
|
||||
closed?: () => void;
|
||||
scope?: 'organization' | 'project';
|
||||
notificationHideOptions?: NotificationCoolOffOptions;
|
||||
};
|
||||
|
||||
export const bottomModalAlerts = writable<BottomModalAlertItem[]>([]);
|
||||
|
||||
export const hideAllModalAlerts = () => {
|
||||
bottomModalAlerts.update((all) => all.map((t) => ({ ...t, show: false })));
|
||||
};
|
||||
|
||||
export const dismissBottomModalAlert = (id: string) => {
|
||||
bottomModalAlerts.update((all) => all.filter((t) => t.id !== id));
|
||||
};
|
||||
|
||||
export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
|
||||
const defaults: Partial<BottomModalAlertItem> = {
|
||||
show: true,
|
||||
importance: 5,
|
||||
isHtml: false,
|
||||
...notification
|
||||
};
|
||||
|
||||
bottomModalAlerts.update((all) => {
|
||||
if (all.some((t) => t.id === notification.id)) return all;
|
||||
return [...all, defaults as BottomModalAlertItem];
|
||||
});
|
||||
};
|
||||
@@ -138,6 +138,24 @@ campaigns
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('NetNinja', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Net Ninja credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('CodeWithAntonio', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Code With Antonio credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('Hacktoberfest2024', {
|
||||
template: 'card',
|
||||
title: 'Claim your $60 Hacktoberfest credits.',
|
||||
description:
|
||||
'Get $60 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('VueJS', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 VueJS credits.',
|
||||
|
||||
@@ -15,8 +15,6 @@ export type Feedback = {
|
||||
|
||||
export type FeedbackData = {
|
||||
message: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
value?: number;
|
||||
};
|
||||
|
||||
@@ -47,8 +45,6 @@ export const selectedFeedback = writable<FeedbackOption>();
|
||||
function createFeedbackDataStore() {
|
||||
const { set, subscribe, update } = writable<FeedbackData>({
|
||||
message: '',
|
||||
name: '',
|
||||
email: '',
|
||||
value: null
|
||||
});
|
||||
return {
|
||||
@@ -58,8 +54,6 @@ function createFeedbackDataStore() {
|
||||
reset: () => {
|
||||
update((feedbackData) => {
|
||||
feedbackData.message = '';
|
||||
feedbackData.name = '';
|
||||
feedbackData.email = '';
|
||||
feedbackData.value = null;
|
||||
return feedbackData;
|
||||
});
|
||||
@@ -110,11 +104,16 @@ function createFeedbackStore() {
|
||||
return feedback;
|
||||
});
|
||||
},
|
||||
// TODO: update growth server to accept `billingPlan` and other keys.
|
||||
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,8 +126,12 @@ function createFeedbackStore() {
|
||||
subject,
|
||||
message,
|
||||
email,
|
||||
firstname: firstname ? firstname : undefined,
|
||||
customFields: value ? [{ id: '40655', value }] : undefined
|
||||
// billingPlan,
|
||||
firstname: name || 'Unknown',
|
||||
customFields: [
|
||||
{ id: '47364', currentPage },
|
||||
...(value ? [{ id: '40655', value }] : [])
|
||||
]
|
||||
})
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
|
||||
@@ -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'));
|
||||
@@ -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,8 +2,14 @@ import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { browser } from '$app/environment';
|
||||
import type { NotificationPrefItem } from '$lib/helpers/notifications';
|
||||
|
||||
export type Account = Models.User<{ organization?: string } & Record<string, string>>;
|
||||
export type Account = Models.User<
|
||||
{
|
||||
organization?: string;
|
||||
notificationPrefs: Record<string, NotificationPrefItem>;
|
||||
} & Record<string, string>
|
||||
>;
|
||||
|
||||
export const user = derived(page, ($page) => {
|
||||
if (browser) sessionStorage.setItem('account', JSON.stringify($page.data.account));
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -15,16 +15,16 @@
|
||||
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 +35,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 +43,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 +68,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
|
||||
},
|
||||
{
|
||||
@@ -335,3 +338,5 @@
|
||||
{#if isCloud && $showUsageRatesModal}
|
||||
<UsageRates bind:show={$showUsageRatesModal} org={$organization} />
|
||||
{/if}
|
||||
|
||||
<BottomModalAlert />
|
||||
|
||||
@@ -34,6 +34,8 @@ export const load: LayoutLoad = async ({ fetch, depends, parent }) => {
|
||||
return {
|
||||
consoleVariables: variables,
|
||||
version: data?.version ?? null,
|
||||
plansInfo
|
||||
plansInfo,
|
||||
roles: [],
|
||||
scopes: []
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
content:
|
||||
'You are limited to 1 free organization per account'
|
||||
}}>
|
||||
<Pill>FREE</Pill>
|
||||
<Pill class="eyebrow-heading-3">FREE</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
{#if organization?.billingTrialStartDate && $daysLeftInTrial > 0 && organization.billingPlan !== BillingPlan.FREE && $plansInfo.get(organization.billingPlan)?.trialDays}
|
||||
@@ -89,7 +89,7 @@
|
||||
organization.billingStartDate
|
||||
)}. ${$daysLeftInTrial} days remaining.`
|
||||
}}>
|
||||
<Pill>TRIAL</Pill>
|
||||
<Pill class="eyebrow-heading-3">TRIAL</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedOrg?.billingPlan !== BillingPlan.FREE}
|
||||
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
@@ -287,7 +287,7 @@
|
||||
{/if}
|
||||
</section>
|
||||
{:else if selectedOrgId}
|
||||
<div class:u-margin-block-start={campaign?.template === 'card'}>
|
||||
<div class:u-margin-block-start-24={campaign?.template === 'card'}>
|
||||
<EstimatedTotalBox
|
||||
fixedCoupon={!!data?.couponData?.code}
|
||||
billingPlan={BillingPlan.PRO}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { base } from '$app/paths';
|
||||
import RolesDark from '$lib/images/roles-dark.png';
|
||||
import RolesLight from '$lib/images/roles-light.png';
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
|
||||
const listOfPromotions: BottomModalAlertItem[] = [
|
||||
{
|
||||
id: 'memberRoles',
|
||||
src: {
|
||||
dark: RolesDark,
|
||||
light: RolesLight
|
||||
},
|
||||
title: 'Roles are available now',
|
||||
message:
|
||||
'Enhance your workflow and security by assigning roles to members. <br/><b>Try for free until Jan 1st 2025 on paid plans.</b>',
|
||||
|
||||
isHtml: true,
|
||||
plan: 'pro',
|
||||
cta: {
|
||||
text: 'Try now',
|
||||
link: ({ organization }) => `${base}/organization-${organization.$id}/members`
|
||||
},
|
||||
learnMore: {
|
||||
text: 'Learn more',
|
||||
link: () => 'https://appwrite.io/docs/advanced/platform/roles'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function addBottomModalAlerts() {
|
||||
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import Default from '$lib/components/roles/default.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputTags, InputText, Label } from '$lib/elements/forms';
|
||||
import {
|
||||
@@ -101,17 +102,6 @@
|
||||
null
|
||||
);
|
||||
} else {
|
||||
// Create free organization if coming from onboarding
|
||||
if (previousPage.includes('/console/onboarding') && !anyOrgFree) {
|
||||
await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
name,
|
||||
@@ -136,7 +126,7 @@
|
||||
collaborators.forEach(async (collaborator) => {
|
||||
await sdk.forConsole.teams.createMembership(
|
||||
org.$id,
|
||||
['owner'],
|
||||
['developer'],
|
||||
collaborator,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -205,7 +195,7 @@
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
tooltip="Invited members will have access to all services and payment data within your organization"
|
||||
popover={Default}
|
||||
placeholder="Enter email address(es)"
|
||||
validityRegex={emailRegex}
|
||||
validityMessage="Invalid email address"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { requestedMigration } from '$routes/store';
|
||||
import { openMigrationWizard } from '../(migration-wizard)';
|
||||
import { base } from '$app/paths';
|
||||
import { isOwner } from '$lib/stores/roles';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -22,7 +23,7 @@
|
||||
goto(`${base}/organization-${data.organization.$id}/members`);
|
||||
},
|
||||
keys: ['g', 'm'],
|
||||
disabled: $page.url.pathname.endsWith('/members'),
|
||||
disabled: $page.url.pathname.endsWith('/members') || !$isOwner,
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
@@ -31,7 +32,7 @@
|
||||
goto(`${base}/organization-${data.organization.$id}/settings`);
|
||||
},
|
||||
keys: ['g', 's'],
|
||||
disabled: $page.url.pathname.endsWith('/settings'),
|
||||
disabled: $page.url.pathname.endsWith('/settings') || !$isOwner,
|
||||
group: 'navigation'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -11,26 +11,32 @@ import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte
|
||||
import { get } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
depends(Dependencies.MEMBERS);
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
if (isCloud) {
|
||||
await failedInvoice.load(params.organization);
|
||||
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: ProjectsAtRisk,
|
||||
id: 'projectsAtRisk',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
let roles = isCloud ? [] : defaultScopes;
|
||||
let scopes = isCloud ? [] : defaultRoles;
|
||||
|
||||
try {
|
||||
if (isCloud) {
|
||||
const res = await sdk.forConsole.billing.getRoles(params.organization);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
if (scopes.includes('billing.read')) {
|
||||
await failedInvoice.load(params.organization);
|
||||
if (get(failedInvoice)) {
|
||||
headerAlert.add({
|
||||
show: true,
|
||||
component: ProjectsAtRisk,
|
||||
id: 'projectsAtRisk',
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
if (prefs.organization !== params.organization) {
|
||||
const newPrefs = { ...prefs, organization: params.organization };
|
||||
@@ -47,7 +53,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
organization,
|
||||
members
|
||||
members,
|
||||
roles,
|
||||
scopes
|
||||
};
|
||||
} catch (e) {
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { canWriteProjects } from '$lib/stores/roles';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -79,6 +80,7 @@
|
||||
}
|
||||
|
||||
function handleCreateProject() {
|
||||
if (!$canWriteProjects) return;
|
||||
if (isCloud) wizard.start(Create);
|
||||
else showCreate = true;
|
||||
}
|
||||
@@ -89,7 +91,7 @@
|
||||
showCreate = true;
|
||||
},
|
||||
keys: ['c'],
|
||||
disabled: $readOnly && !GRACE_PERIOD_OVERRIDE,
|
||||
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteProjects,
|
||||
group: 'projects',
|
||||
icon: 'plus'
|
||||
}
|
||||
@@ -142,13 +144,15 @@
|
||||
<Heading tag="h2" size="5">Projects</Heading>
|
||||
|
||||
<DropList bind:show={showDropdown} placement="bottom-end">
|
||||
<Button
|
||||
on:click={handleCreateProject}
|
||||
event="create_project"
|
||||
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create project</span>
|
||||
</Button>
|
||||
{#if $canWriteProjects}
|
||||
<Button
|
||||
on:click={handleCreateProject}
|
||||
event="create_project"
|
||||
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create project</span>
|
||||
</Button>
|
||||
{/if}
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
|
||||
<DropListItem on:click={importProject}>
|
||||
@@ -162,6 +166,7 @@
|
||||
|
||||
{#if data.projects.total}
|
||||
<CardContainer
|
||||
showEmpty={$canWriteProjects}
|
||||
total={data.projects.total}
|
||||
offset={data.offset}
|
||||
on:click={handleCreateProject}>
|
||||
@@ -214,6 +219,7 @@
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteProjects}
|
||||
on:click={handleCreateProject}
|
||||
target="project"
|
||||
href="https://appwrite.io/docs/quick-starts"></Empty>
|
||||
|
||||
@@ -3,13 +3,17 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { CARD_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
|
||||
await parent();
|
||||
const { scopes } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
if (!scopes.includes('projects.read') && scopes.includes('billing.read')) {
|
||||
return redirect(301, `/console/organization-${params.organization}/billing`);
|
||||
}
|
||||
|
||||
return {
|
||||
offset,
|
||||
|
||||
@@ -2,10 +2,15 @@ import { Dependencies } from '$lib/constants';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends }) => {
|
||||
const { organization } = await parent();
|
||||
const { organization, scopes } = await parent();
|
||||
|
||||
if (!scopes.includes('billing.read')) {
|
||||
return redirect(301, `/console/organization-${organization.$id}`);
|
||||
}
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
depends(Dependencies.CREDIT);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import AddressModal from '$routes/(console)/account/payments/addressModal.svelte';
|
||||
import EditAddressModal from '$routes/(console)/account/payments/editAddressModal.svelte';
|
||||
import ReplaceAddress from './replaceAddress.svelte';
|
||||
@@ -81,14 +82,16 @@
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
showBillingAddressDropdown = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{#if billingAddress.userId === $user.$id}
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
showBillingAddressDropdown = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="switch-horizontal"
|
||||
on:click={() => {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import EditPaymentModal from '$routes/(console)/account/payments/editPaymentModal.svelte';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import PaymentModal from '$lib/components/billing/paymentModal.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
let showDropdown = false;
|
||||
let showDropdownBackup = false;
|
||||
@@ -112,15 +113,17 @@
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
isSelectedBackup = false;
|
||||
showEdit = true;
|
||||
showDropdown = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{#if defaultPaymentMethod.userId === $user.$id}
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
isSelectedBackup = false;
|
||||
showEdit = true;
|
||||
showDropdown = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="switch-horizontal"
|
||||
on:click={() => {
|
||||
@@ -208,15 +211,17 @@
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
isSelectedBackup = true;
|
||||
showDropdownBackup = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{#if backupPaymentMethod.userId === $user.$id}
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
isSelectedBackup = true;
|
||||
showDropdownBackup = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="switch-horizontal"
|
||||
on:click={() => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import PlanExcess from '$lib/components/billing/planExcess.svelte';
|
||||
import PlanSelection from '$lib/components/billing/planSelection.svelte';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import Default from '$lib/components/roles/default.svelte';
|
||||
import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants';
|
||||
import {
|
||||
Button,
|
||||
@@ -286,7 +287,7 @@
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
tooltip="Invited members will have access to all services and payment data within your organization"
|
||||
popover={Default}
|
||||
placeholder="Enter email address(es)"
|
||||
validityRegex={emailRegex}
|
||||
validityMessage="Invalid email address"
|
||||
|
||||
@@ -11,23 +11,26 @@
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { roles } from '$lib/stores/billing';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import Roles from '$lib/components/roles/roles.svelte';
|
||||
|
||||
export let showCreate = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const url = `${$page.url.origin}${base}/invite`;
|
||||
$: plan = $plansInfo?.get($organization?.billingPlan);
|
||||
|
||||
let email: string, name: string, error: string;
|
||||
let email: string,
|
||||
name: string,
|
||||
error: string,
|
||||
role: string = 'developer';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
const team = await sdk.forConsole.teams.createMembership(
|
||||
$organization.$id,
|
||||
['owner'],
|
||||
[role],
|
||||
email,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -61,9 +64,17 @@
|
||||
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
|
||||
{#if isCloud}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO}
|
||||
<Alert type="info">
|
||||
<!-- <Alert type="info">
|
||||
You can add unlimited organization members on the {plan.name} plan for
|
||||
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
|
||||
</Alert> -->
|
||||
<Alert type="info">
|
||||
New roles are free until 1st January 2025. <a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/advanced/platform/roles"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Learn more</a
|
||||
>.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -80,6 +91,7 @@
|
||||
label="Name (optional)"
|
||||
placeholder="Enter name"
|
||||
bind:value={name} />
|
||||
<InputSelect popover={Roles} id="role" label="Role" options={roles} bind:value={role} />
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
organization,
|
||||
organizationList
|
||||
} from '$lib/stores/organization';
|
||||
import {
|
||||
canSeeBilling,
|
||||
canSeeProjects,
|
||||
canSeeTeams,
|
||||
isBilling,
|
||||
isOwner
|
||||
} from '$lib/stores/roles';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
|
||||
let areMembersLimited: boolean;
|
||||
@@ -54,45 +61,41 @@
|
||||
$: avatars = $members.memberships?.map((m) => m.userName) ?? [];
|
||||
$: organizationId = $page.params.organization;
|
||||
$: path = `${base}/organization-${organizationId}`;
|
||||
$: permanentTabSettings = [
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings'
|
||||
}
|
||||
];
|
||||
$: permanentTabs = [
|
||||
$: tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Projects',
|
||||
event: 'projects',
|
||||
hasChildren: true
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeProjects
|
||||
},
|
||||
{
|
||||
href: `${path}/members`,
|
||||
title: 'Members',
|
||||
event: 'members',
|
||||
hasChildren: true
|
||||
hasChildren: true,
|
||||
disabled: !$canSeeTeams
|
||||
},
|
||||
{
|
||||
href: `${path}/usage`,
|
||||
event: 'usage',
|
||||
title: 'Usage',
|
||||
hasChildren: true,
|
||||
disabled: !(isCloud && ($isOwner || $isBilling))
|
||||
},
|
||||
{
|
||||
href: `${path}/billing`,
|
||||
event: 'billing',
|
||||
title: 'Billing',
|
||||
disabled: !(isCloud && $canSeeBilling)
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings',
|
||||
disabled: !$isOwner
|
||||
}
|
||||
];
|
||||
|
||||
$: tabs = isCloud
|
||||
? [
|
||||
...permanentTabs,
|
||||
{
|
||||
href: `${path}/usage`,
|
||||
event: 'usage',
|
||||
title: 'Usage',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/billing`,
|
||||
event: 'billing',
|
||||
title: 'Billing'
|
||||
},
|
||||
...permanentTabSettings
|
||||
]
|
||||
: [...permanentTabs, ...permanentTabSettings];
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id}
|
||||
@@ -108,7 +111,7 @@
|
||||
{$organization.name}
|
||||
</span>
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.FREE}
|
||||
<Pill>FREE</Pill>
|
||||
<Pill class="eyebrow-heading-3">FREE</Pill>
|
||||
{/if}
|
||||
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0 && $organization.billingPlan !== BillingPlan.FREE && $plansInfo.get($organization.billingPlan)?.trialDays}
|
||||
<div
|
||||
@@ -118,7 +121,7 @@
|
||||
$organization.billingStartDate
|
||||
)}. ${$daysLeftInTrial} days remaining.`
|
||||
}}>
|
||||
<Pill>TRIAL</Pill>
|
||||
<Pill class="eyebrow-heading-3">TRIAL</Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -160,16 +163,19 @@
|
||||
} plan`,
|
||||
disabled: !areMembersLimited
|
||||
}}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={areMembersLimited}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Invite</span>
|
||||
</Button>
|
||||
{#if $isOwner}
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={areMembersLimited}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Invite</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div></svelte:fragment>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<Tabs>
|
||||
{#each tabs as tab}
|
||||
<Tab
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { AvatarInitials, PaginationWithLimit } from '$lib/components';
|
||||
import { AvatarInitials, DropList, DropListItem, PaginationWithLimit } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -15,17 +14,25 @@
|
||||
} from '$lib/elements/table';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { members, newMemberModal, organization } from '$lib/stores/organization';
|
||||
import { newMemberModal, organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import Delete from '../deleteMember.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { isOwner } from '$lib/stores/roles';
|
||||
import Edit from './edit.svelte';
|
||||
import { getRoleLabel } from '$lib/stores/billing';
|
||||
import { Drop } from '$lib/components';
|
||||
import Upgrade from '$lib/components/roles/upgrade.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let selectedMember: Models.Membership;
|
||||
let showDelete = false;
|
||||
let showEdit = false;
|
||||
let showDropdown = [];
|
||||
let showPopover = false;
|
||||
const url = `${$page.url.origin}/${base}/`;
|
||||
const resend = async (member: Models.Membership) => {
|
||||
try {
|
||||
@@ -58,7 +65,7 @@
|
||||
<ContainerHeader
|
||||
title="Members"
|
||||
total={data.organizationMembers.total}
|
||||
buttonText="Invite"
|
||||
buttonText={$isOwner ? 'Invite' : ''}
|
||||
buttonMethod={() => newMemberModal.set(true)}
|
||||
showAlert={false} />
|
||||
|
||||
@@ -66,15 +73,43 @@
|
||||
<TableHeader>
|
||||
<TableCellHead width={160}>Name</TableCellHead>
|
||||
<TableCellHead width={120}>Email</TableCellHead>
|
||||
<div style:--p-col-width={120} class="table-thead-col" role="columnheader">
|
||||
<span class="u-flex u-cross-baseline">
|
||||
<span class="eyebrow-heading-3"> Role </span>
|
||||
<Drop isPopover bind:show={showPopover} display="inline-block">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (showPopover = !showPopover)}
|
||||
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-300 u-break-word"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={Upgrade} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<TableCellHead width={90}>2FA</TableCellHead>
|
||||
<TableCellHead width={60} />
|
||||
<TableCellHead width={30} />
|
||||
{#if $isOwner}
|
||||
<TableCellHead width={30} />
|
||||
{/if}
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
service="members"
|
||||
total={data.organizationMembers.total}
|
||||
event="members_list">
|
||||
{#each data.organizationMembers.memberships as member}
|
||||
{#each data.organizationMembers.memberships as member, index}
|
||||
<TableRow>
|
||||
<TableCell title="Name">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
@@ -88,6 +123,10 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText title="Email">{member.userEmail}</TableCellText>
|
||||
<TableCellText title="Role"
|
||||
>{member.roles
|
||||
.map((role) => getRoleLabel(role))
|
||||
.join(', ')}</TableCellText>
|
||||
<TableCellText title="2FA">
|
||||
<Pill success={member.mfa}>
|
||||
{#if member.mfa}
|
||||
@@ -100,26 +139,53 @@
|
||||
{/if}
|
||||
</Pill>
|
||||
</TableCellText>
|
||||
<TableCell>
|
||||
{#if member.invited && !member.joined}
|
||||
<Button
|
||||
secondary
|
||||
event="invite_resend"
|
||||
on:click={() => resend(member)}>Resend</Button>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCell right>
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="Delete item"
|
||||
disabled={$members.total === 1}
|
||||
on:click={() => {
|
||||
selectedMember = member;
|
||||
showDelete = true;
|
||||
}}>
|
||||
<span class="icon-trash" aria-hidden="true" />
|
||||
</button>
|
||||
</TableCell>
|
||||
{#if $isOwner}
|
||||
<TableCell showOverflow right>
|
||||
<DropList
|
||||
bind:show={showDropdown[index]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<button
|
||||
class="button is-only-icon is-text"
|
||||
aria-label="More options"
|
||||
on:click|preventDefault={() => {
|
||||
showDropdown[index] = !showDropdown[index];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
selectedMember = member;
|
||||
showEdit = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Edit role
|
||||
</DropListItem>
|
||||
{#if member.invited && !member.joined}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
resend(member);
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Resend
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={() => {
|
||||
selectedMember = member;
|
||||
showDelete = true;
|
||||
showDropdown[index] = false;
|
||||
}}>
|
||||
Remove
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
@@ -134,3 +200,4 @@
|
||||
</Container>
|
||||
|
||||
<Delete {selectedMember} bind:showDelete />
|
||||
<Edit {selectedMember} bind:showEdit />
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import Roles from '$lib/components/roles/roles.svelte';
|
||||
|
||||
export let showEdit = false;
|
||||
export let selectedMember: Models.Membership;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let error: string;
|
||||
let role = selectedMember?.roles?.[0];
|
||||
|
||||
const roles = [
|
||||
{
|
||||
label: 'Owner',
|
||||
value: 'owner'
|
||||
},
|
||||
{
|
||||
label: 'Developer',
|
||||
value: 'developer'
|
||||
},
|
||||
{
|
||||
label: 'Editor',
|
||||
value: 'editor'
|
||||
},
|
||||
{
|
||||
label: 'Analyst',
|
||||
value: 'analyst'
|
||||
},
|
||||
{
|
||||
label: 'Billing',
|
||||
value: 'billing'
|
||||
}
|
||||
];
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
const membership = await sdk.forConsole.teams.updateMembership(
|
||||
$organization.$id,
|
||||
selectedMember.$id,
|
||||
[role]
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.MEMBERS);
|
||||
|
||||
showEdit = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Role has been updated`
|
||||
});
|
||||
trackEvent(Submit.MembershipUpdate);
|
||||
dispatch('updated', membership);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.MembershipUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!showEdit) {
|
||||
error = null;
|
||||
role = null;
|
||||
}
|
||||
|
||||
$: if (showEdit && !role) {
|
||||
role = selectedMember.roles?.[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Edit role" {error} size="big" bind:show={showEdit} onSubmit={submit}>
|
||||
<FormList>
|
||||
<InputSelect
|
||||
popover={Roles}
|
||||
id="role"
|
||||
label="Role"
|
||||
required
|
||||
options={roles}
|
||||
bind:value={role} />
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showEdit = false)}>Cancel</Button>
|
||||
<Button submit submissionLoader>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -9,7 +9,7 @@
|
||||
} from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { bytesToSize, humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { bytesToSize, humanFileSize, mbSecondsToGBHours } from '$lib/helpers/sizeConvertion';
|
||||
import { BarChart } from '$lib/charts';
|
||||
import ProjectBreakdown from './ProjectBreakdown.svelte';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
@@ -119,36 +119,40 @@
|
||||
<ProgressBarBig
|
||||
currentUnit={currentHumanized.unit}
|
||||
currentValue={currentHumanized.value}
|
||||
maxUnit="GB"
|
||||
maxValue={max.toString()}
|
||||
maxValue={`of ${max.toString()} GB used`}
|
||||
progressValue={bytesToSize(current, 'GB')}
|
||||
progressMax={max}
|
||||
showBar={false} />
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: (value) =>
|
||||
value
|
||||
? `${humanFileSize(+value).value} ${
|
||||
humanFileSize(+value).unit
|
||||
}`
|
||||
: '0'
|
||||
<div style:margin-top="-1.5em">
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: (value) =>
|
||||
value
|
||||
? `${humanFileSize(+value).value} ${
|
||||
humanFileSize(+value).unit
|
||||
}`
|
||||
: '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Bandwidth',
|
||||
data: [
|
||||
...data.organizationUsage.bandwidth.map((e) => [e.date, e.value])
|
||||
],
|
||||
tooltip: {
|
||||
valueFormatter: (value) =>
|
||||
`${humanFileSize(+value).value} ${humanFileSize(+value).unit}`
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Bandwidth',
|
||||
data: [
|
||||
...data.organizationUsage.bandwidth.map((e) => [
|
||||
e.date,
|
||||
e.value
|
||||
])
|
||||
],
|
||||
tooltip: {
|
||||
valueFormatter: (value) =>
|
||||
`${humanFileSize(+value).value} ${humanFileSize(+value).unit}`
|
||||
}
|
||||
}
|
||||
}
|
||||
]} />
|
||||
]} />
|
||||
</div>
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="bandwidth" {data} />
|
||||
{/if}
|
||||
@@ -178,28 +182,30 @@
|
||||
<ProgressBarBig
|
||||
currentUnit="Users"
|
||||
currentValue={formatNum(current)}
|
||||
maxUnit="Users"
|
||||
maxValue={formatNum(max)}
|
||||
maxUnit="users"
|
||||
maxValue={`out of ${formatNum(max)}`}
|
||||
progressValue={current}
|
||||
progressMax={max}
|
||||
showBar={false} />
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
<div style:margin-top="-1.5em">
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Users',
|
||||
data: accumulateFromEndingTotal(
|
||||
data.organizationUsage.users,
|
||||
data.organizationUsage.usersTotal
|
||||
)
|
||||
}
|
||||
]} />
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Users',
|
||||
data: accumulateFromEndingTotal(
|
||||
data.organizationUsage.users,
|
||||
data.organizationUsage.usersTotal
|
||||
)
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="users" {data} />
|
||||
{/if}
|
||||
@@ -231,27 +237,31 @@
|
||||
<ProgressBarBig
|
||||
currentUnit="Executions"
|
||||
currentValue={formatNum(current)}
|
||||
maxUnit="Executions"
|
||||
maxValue={formatNum(max)}
|
||||
maxValue={`of ${formatNum(max)} Executions used`}
|
||||
progressValue={current}
|
||||
progressMax={max}
|
||||
showBar={false} />
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
<div style:margin-top="-1.5em">
|
||||
<BarChart
|
||||
options={{
|
||||
yAxis: {
|
||||
axisLabel: {
|
||||
formatter: formatNum
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Executions',
|
||||
data: [
|
||||
...data.organizationUsage.executions.map((e) => [e.date, e.value])
|
||||
]
|
||||
}
|
||||
]} />
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
name: 'Executions',
|
||||
data: [
|
||||
...data.organizationUsage.executions.map((e) => [
|
||||
e.date,
|
||||
e.value
|
||||
])
|
||||
]
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
{#if project?.length > 0}
|
||||
<ProjectBreakdown projects={project} metric="executions" {data} />
|
||||
{/if}
|
||||
@@ -311,8 +321,7 @@
|
||||
<ProgressBarBig
|
||||
currentUnit={currentHumanized.unit}
|
||||
currentValue={currentHumanized.value}
|
||||
maxUnit="GB"
|
||||
maxValue={max.toString()}
|
||||
maxValue={`of ${max.toString()} GB used`}
|
||||
progressValue={bytesToSize(current, 'GB')}
|
||||
progressMax={max}
|
||||
progressBarData={progressBarStorageDate} />
|
||||
@@ -332,6 +341,66 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">GB hours</Heading>
|
||||
|
||||
<p class="text">
|
||||
GB hours represent the memory usage (in gigabytes) of your function executions and
|
||||
builds, multiplied by the total execution time (in hours).
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if data.organizationUsage.storageTotal}
|
||||
{@const totalGbHours = mbSecondsToGBHours(
|
||||
data.organizationUsage.executionsMBSecondsTotal +
|
||||
data.organizationUsage.buildsMBSecondsTotal
|
||||
)}
|
||||
{@const progressBarStorageDate = [
|
||||
{
|
||||
size: mbSecondsToGBHours(data.organizationUsage.executionsMBSecondsTotal),
|
||||
color: '#85DBD8',
|
||||
tooltip: {
|
||||
title: 'Executions',
|
||||
label: `${(Math.round(mbSecondsToGBHours(data.organizationUsage.executionsMBSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours`
|
||||
}
|
||||
},
|
||||
{
|
||||
size: mbSecondsToGBHours(data.organizationUsage.buildsMBSecondsTotal),
|
||||
color: '#FE9567',
|
||||
tooltip: {
|
||||
title: 'Deployments',
|
||||
label: `${(Math.round(mbSecondsToGBHours(data.organizationUsage.buildsMBSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours`
|
||||
}
|
||||
}
|
||||
]}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4"
|
||||
>{(Math.ceil(totalGbHours * 100) / 100).toLocaleString(
|
||||
'en-US'
|
||||
)}</span>
|
||||
<span class="body-text-1 u-bold">{`GB hours`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressBarBig
|
||||
progressMax={totalGbHours}
|
||||
progressValue={totalGbHours}
|
||||
progressBarData={progressBarStorageDate} />
|
||||
{:else}
|
||||
<Card isDashed>
|
||||
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
|
||||
<span
|
||||
class="icon-chart-square-bar text-large"
|
||||
aria-hidden="true"
|
||||
style="font-size: 32px;" />
|
||||
<p class="u-bold">No data to show</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<TotalMembers members={data?.organizationMembers} />
|
||||
|
||||
<p class="text common-section u-color-text-gray">
|
||||
|
||||
@@ -24,7 +24,9 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
deploymentsStorageTotal: null,
|
||||
executions: null,
|
||||
executionsTotal: null,
|
||||
projects: null
|
||||
projects: null,
|
||||
executionsMBSecondsTotal: null,
|
||||
buildsMBSecondsTotal: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+11
-6
@@ -14,6 +14,7 @@
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
import { base } from '$app/paths';
|
||||
import { canSeeProjects } from '$lib/stores/roles';
|
||||
|
||||
type Metric = 'users' | 'storage' | 'bandwidth' | 'executions';
|
||||
export let data: PageData;
|
||||
@@ -60,7 +61,9 @@
|
||||
<TableHeader>
|
||||
<TableCellHead width={185}>Project</TableCellHead>
|
||||
<TableCellHead width={100}>Usage</TableCellHead>
|
||||
<TableCellHead width={140} />
|
||||
{#if $canSeeProjects}
|
||||
<TableCellHead width={140} />
|
||||
{/if}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project}
|
||||
@@ -69,11 +72,13 @@
|
||||
{getProjectName(project.projectId)}
|
||||
</TableCellText>
|
||||
<TableCellText title="Usage">{format(project.usage)}</TableCellText>
|
||||
<TableCellLink
|
||||
title="Go to project usage"
|
||||
href={getProjectUsageLink(project.projectId)}>
|
||||
View project usage
|
||||
</TableCellLink>
|
||||
{#if $canSeeProjects}
|
||||
<TableCellLink
|
||||
title="Go to project usage"
|
||||
href={getProjectUsageLink(project.projectId)}>
|
||||
View project usage
|
||||
</TableCellLink>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { UploadBox } from '$lib/components';
|
||||
import { MigrationBox, UploadBox } from '$lib/components';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project, stats } from './store';
|
||||
@@ -14,9 +14,15 @@
|
||||
teamSearcher,
|
||||
userSearcher
|
||||
} from '$lib/commandCenter/searchers';
|
||||
import { MigrationBox } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
canSeeBuckets,
|
||||
canSeeDatabases,
|
||||
canSeeFunctions,
|
||||
canSeeMessages,
|
||||
canWriteProjects
|
||||
} from '$lib/stores/roles';
|
||||
|
||||
onMount(() => {
|
||||
return sdk.forConsole.client.subscribe(['project', 'console'], (response) => {
|
||||
@@ -43,7 +49,8 @@
|
||||
goto(`${base}/project-${$project.$id}/databases`);
|
||||
},
|
||||
keys: ['g', 'd'],
|
||||
group: 'navigation'
|
||||
group: 'navigation',
|
||||
disabled: !$canSeeDatabases
|
||||
},
|
||||
{
|
||||
label: 'Go to Functions',
|
||||
@@ -51,7 +58,8 @@
|
||||
goto(`${base}/project-${$project.$id}/functions`);
|
||||
},
|
||||
keys: ['g', 'f'],
|
||||
group: 'navigation'
|
||||
group: 'navigation',
|
||||
disabled: !$canSeeFunctions
|
||||
},
|
||||
{
|
||||
label: 'Go to Messaging',
|
||||
@@ -59,7 +67,7 @@
|
||||
goto(`${base}/project-${$project.$id}/messaging`);
|
||||
},
|
||||
keys: ['g', 'm'],
|
||||
disabled: $page.url.pathname.endsWith('messaging'),
|
||||
disabled: $page.url.pathname.endsWith('messaging') || !$canSeeMessages,
|
||||
group: 'navigation'
|
||||
},
|
||||
{
|
||||
@@ -68,7 +76,8 @@
|
||||
goto(`${base}/project-${$project.$id}/storage`);
|
||||
},
|
||||
keys: ['g', 's'],
|
||||
group: 'navigation'
|
||||
group: 'navigation',
|
||||
disabled: !$canSeeBuckets
|
||||
},
|
||||
{
|
||||
label: 'Go to Settings',
|
||||
@@ -76,7 +85,8 @@
|
||||
goto(`${base}/project-${$project.$id}/settings`);
|
||||
},
|
||||
keys: ['g', 'e'],
|
||||
group: 'navigation'
|
||||
group: 'navigation',
|
||||
disabled: !$canWriteProjects
|
||||
},
|
||||
{
|
||||
label: 'Go to Overview',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { preferences } from '$lib/stores/preferences';
|
||||
import { failedInvoice } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.PROJECT);
|
||||
@@ -16,14 +17,22 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
const newPrefs = { ...prefs, organization: project.teamId };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
preferences.loadTeamPrefs(project.teamId);
|
||||
|
||||
let roles = isCloud ? [] : defaultScopes;
|
||||
let scopes = isCloud ? [] : defaultRoles;
|
||||
if (isCloud) {
|
||||
await failedInvoice.load(project.teamId);
|
||||
const res = await sdk.forConsole.billing.getRoles(project.teamId);
|
||||
roles = res.roles;
|
||||
scopes = res.scopes;
|
||||
if (scopes.includes('billing.read')) {
|
||||
await failedInvoice.load(project.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
organization: await (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>)
|
||||
organization: await (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>),
|
||||
roles,
|
||||
scopes
|
||||
};
|
||||
} catch (e) {
|
||||
error(e.code, e.message);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
import { TeamsPanel, UsersPanel } from '$lib/commandCenter/panels';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { canWriteTeams, canWriteUsers } from '$lib/stores/roles';
|
||||
import { GRACE_PERIOD_OVERRIDE } from '$lib/system';
|
||||
import { project } from '../store';
|
||||
import { showCreateUser } from './+page.svelte';
|
||||
@@ -23,7 +24,7 @@
|
||||
group: 'users',
|
||||
icon: 'plus',
|
||||
rank: $page.url.pathname.endsWith('auth') ? 10 : 0,
|
||||
disabled: $readOnly && !GRACE_PERIOD_OVERRIDE
|
||||
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteUsers
|
||||
},
|
||||
{
|
||||
label: 'Create team',
|
||||
@@ -38,7 +39,7 @@
|
||||
group: 'teams',
|
||||
icon: 'plus',
|
||||
rank: $page.url.pathname.endsWith('teams') ? 10 : 0,
|
||||
disabled: $readOnly && !GRACE_PERIOD_OVERRIDE
|
||||
disabled: ($readOnly && !GRACE_PERIOD_OVERRIDE) || !$canWriteTeams
|
||||
},
|
||||
{
|
||||
label: 'Go to teams',
|
||||
@@ -68,7 +69,7 @@
|
||||
},
|
||||
group: 'navigation',
|
||||
rank: 1,
|
||||
disabled: $page.url.pathname.endsWith('security')
|
||||
disabled: $page.url.pathname.endsWith('security') || !$canWriteUsers
|
||||
},
|
||||
{
|
||||
label: 'Go to settings',
|
||||
@@ -78,7 +79,7 @@
|
||||
},
|
||||
group: 'navigation',
|
||||
rank: 1,
|
||||
disabled: $page.url.pathname.endsWith('settings')
|
||||
disabled: $page.url.pathname.endsWith('settings') || !$canWriteUsers
|
||||
},
|
||||
{
|
||||
label: 'Find users',
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import Create from './createUser.svelte';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { canWriteUsers } from '$lib/stores/roles';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -44,19 +45,22 @@
|
||||
<Container>
|
||||
<ContainerHeader title="Users" isFlex={false} total={data.users.total} let:isButtonDisabled>
|
||||
<SearchQuery search={data.search} placeholder="Search by name, email, phone, or ID">
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more users`,
|
||||
disabled: !isButtonDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => ($showCreateUser = true)}
|
||||
event="create_user"
|
||||
disabled={isButtonDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create user</span>
|
||||
</Button>
|
||||
</div></SearchQuery>
|
||||
{#if $canWriteUsers}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more users`,
|
||||
disabled: !isButtonDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => ($showCreateUser = true)}
|
||||
event="create_user"
|
||||
disabled={isButtonDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create user</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</SearchQuery>
|
||||
</ContainerHeader>
|
||||
{#if data.users.total}
|
||||
<Table>
|
||||
@@ -151,6 +155,7 @@
|
||||
single
|
||||
href="https://appwrite.io/docs/references/cloud/server-nodejs/users"
|
||||
target="user"
|
||||
allowCreate={$canWriteUsers}
|
||||
on:click={() => showCreateUser.set(true)} />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Tab, Tabs } from '$lib/components';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { canWriteProjects } from '$lib/stores/roles';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const path = `${base}/project-${projectId}/auth`;
|
||||
@@ -23,13 +24,15 @@
|
||||
{
|
||||
href: `${path}/security`,
|
||||
title: 'Security',
|
||||
event: 'security'
|
||||
event: 'security',
|
||||
disabled: !$canWriteProjects
|
||||
},
|
||||
{
|
||||
href: `${path}/templates`,
|
||||
title: 'Templates',
|
||||
hasChildren: false,
|
||||
event: 'templates'
|
||||
event: 'templates',
|
||||
disabled: !$canWriteProjects
|
||||
},
|
||||
{
|
||||
href: `${path}/usage`,
|
||||
@@ -40,9 +43,10 @@
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
title: 'Settings',
|
||||
event: 'settings'
|
||||
event: 'settings',
|
||||
disabled: !$canWriteProjects
|
||||
}
|
||||
];
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { canWriteTeams } from '$lib/stores/roles';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -49,19 +50,21 @@
|
||||
buttonDisabled={isCloud && $readOnly}
|
||||
let:isButtonDisabled>
|
||||
<SearchQuery search={data.search} placeholder="Search by name">
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more teams`,
|
||||
disabled: !isButtonDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => ($showCreateTeam = true)}
|
||||
event="create_team"
|
||||
disabled={isButtonDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create team</span>
|
||||
</Button>
|
||||
</div>
|
||||
{#if $canWriteTeams}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more teams`,
|
||||
disabled: !isButtonDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => ($showCreateTeam = true)}
|
||||
event="create_team"
|
||||
disabled={isButtonDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create team</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</SearchQuery>
|
||||
</ContainerHeader>
|
||||
|
||||
@@ -109,6 +112,7 @@
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteTeams}
|
||||
on:click={() => ($showCreateTeam = true)}
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/teams"
|
||||
target="team" />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import Table from './table.svelte';
|
||||
import { registerCommands } from '$lib/commandCenter';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { canWriteDatabases } from '$lib/stores/roles';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
showCreate = true;
|
||||
},
|
||||
keys: ['c'],
|
||||
disabled: showCreate || isCreationDisabled,
|
||||
disabled: showCreate || isCreationDisabled || !$canWriteDatabases,
|
||||
icon: 'plus',
|
||||
group: 'databases',
|
||||
rank: 10
|
||||
@@ -55,19 +56,21 @@
|
||||
view={data.view}
|
||||
hideColumns={!data.databases.total}
|
||||
hideView={!data.databases.total} />
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more databases`,
|
||||
disabled: !isCreationDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => (showCreate = true)}
|
||||
event="create_database"
|
||||
disabled={isCreationDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create database</span>
|
||||
</Button>
|
||||
</div>
|
||||
{#if $canWriteDatabases}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Upgrade to add more databases`,
|
||||
disabled: !isCreationDisabled
|
||||
}}>
|
||||
<Button
|
||||
on:click={() => (showCreate = true)}
|
||||
event="create_database"
|
||||
disabled={isCreationDisabled}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create database</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ContainerHeader>
|
||||
@@ -89,6 +92,7 @@
|
||||
single
|
||||
href="https://appwrite.io/docs/products/databases/databases"
|
||||
target="database"
|
||||
allowCreate={$canWriteDatabases}
|
||||
on:click={() => (showCreate = true)} />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import CreateCollection from './createCollection.svelte';
|
||||
import { showCreate } from './store';
|
||||
import { CollectionsPanel } from '$lib/commandCenter/panels';
|
||||
import { canWriteCollections, canWriteDatabases } from '$lib/stores/roles';
|
||||
|
||||
const project = $page.params.project;
|
||||
const databaseId = $page.params.database;
|
||||
@@ -36,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
keys: $page.url.pathname.endsWith(databaseId) ? ['c'] : ['c', 'c'],
|
||||
disabled: $page.url.pathname.includes('collection-'),
|
||||
disabled: $page.url.pathname.includes('collection-') || !$canWriteCollections,
|
||||
group: 'collections',
|
||||
icon: 'plus'
|
||||
},
|
||||
@@ -68,7 +69,8 @@
|
||||
},
|
||||
disabled:
|
||||
$page.url.pathname.includes('/settings') ||
|
||||
$page.url.pathname.includes('collection-'),
|
||||
$page.url.pathname.includes('collection-') ||
|
||||
!$canWriteDatabases,
|
||||
keys: ['g', 's'],
|
||||
group: 'collections'
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import Table from './table.svelte';
|
||||
import Grid from './grid.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
@@ -17,10 +18,12 @@
|
||||
view={data.view}
|
||||
hideColumns={!data.collections.total}
|
||||
hideView={!data.collections.total}>
|
||||
<Button on:click={() => ($showCreate = true)} event="create_collection">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</Button>
|
||||
{#if $canWriteCollections}
|
||||
<Button on:click={() => ($showCreate = true)} event="create_collection">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</GridHeader>
|
||||
|
||||
{#if data.collections.total}
|
||||
@@ -38,6 +41,7 @@
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteCollections}
|
||||
href="https://appwrite.io/docs/products/databases/collections"
|
||||
target="collection"
|
||||
on:click={() => ($showCreate = true)} />
|
||||
|
||||
+12
-6
@@ -32,6 +32,7 @@
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import CreateDocument from './createDocument.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
|
||||
let unsubscribe: { (): void };
|
||||
|
||||
@@ -69,7 +70,8 @@
|
||||
addSubPanel(CreateAttributePanel);
|
||||
},
|
||||
icon: 'plus',
|
||||
group: 'attributes'
|
||||
group: 'attributes',
|
||||
disabled: !$canWriteCollections
|
||||
},
|
||||
{
|
||||
label: 'Go to documents',
|
||||
@@ -134,7 +136,7 @@
|
||||
`${base}/project-${$project?.$id}/databases/database-${$database?.$id}/collection-${$collection?.$id}/settings`
|
||||
);
|
||||
},
|
||||
disabled: $page.url.pathname.endsWith('settings'),
|
||||
disabled: $page.url.pathname.endsWith('settings') || !$canWriteCollections,
|
||||
group: 'collections'
|
||||
},
|
||||
{
|
||||
@@ -147,7 +149,8 @@
|
||||
group: 'collections',
|
||||
disabled:
|
||||
$page.url.pathname.endsWith('display-name') ||
|
||||
$page.url.pathname.endsWith('settings'),
|
||||
$page.url.pathname.endsWith('settings') ||
|
||||
!$canWriteCollections,
|
||||
icon: 'eye'
|
||||
},
|
||||
{
|
||||
@@ -160,7 +163,8 @@
|
||||
group: 'collections',
|
||||
disabled:
|
||||
$page.url.pathname.endsWith('permissions') ||
|
||||
$page.url.pathname.endsWith('settings'),
|
||||
$page.url.pathname.endsWith('settings') ||
|
||||
!$canWriteCollections,
|
||||
icon: 'puzzle'
|
||||
},
|
||||
{
|
||||
@@ -173,7 +177,8 @@
|
||||
group: 'collections',
|
||||
disabled:
|
||||
$page.url.pathname.endsWith('document-security') ||
|
||||
$page.url.pathname.endsWith('settings'),
|
||||
$page.url.pathname.endsWith('settings') ||
|
||||
!$canWriteCollections,
|
||||
icon: 'lock-closed'
|
||||
},
|
||||
{
|
||||
@@ -183,7 +188,8 @@
|
||||
initCreateIndex();
|
||||
},
|
||||
icon: 'plus',
|
||||
group: 'indexes'
|
||||
group: 'indexes',
|
||||
disabled: !$canWriteCollections
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
+23
-14
@@ -7,6 +7,7 @@
|
||||
import type { ColumnType } from '$lib/helpers/types';
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { canWriteCollections, canWriteDocuments } from '$lib/stores/roles';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import type { PageData } from './$types';
|
||||
import CreateAttributeDropdown from './attributes/createAttributeDropdown.svelte';
|
||||
@@ -36,6 +37,7 @@
|
||||
);
|
||||
|
||||
function openWizard() {
|
||||
if (!$canWriteDocuments) return;
|
||||
wizard.start(Create);
|
||||
}
|
||||
|
||||
@@ -113,13 +115,18 @@
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
allowCreate={$canWriteDocuments}
|
||||
single
|
||||
href="https://appwrite.io/docs/products/databases/documents"
|
||||
target="document"
|
||||
on:click={openWizard} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
|
||||
<Empty
|
||||
allowCreate={$canWriteCollections}
|
||||
single
|
||||
target="attribute"
|
||||
on:click={() => (showCreateDropdown = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
@@ -133,19 +140,21 @@
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{#if $canWriteCollections}
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
+24
-15
@@ -14,6 +14,7 @@
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Container } from '$lib/layout';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
import Create from '../createAttribute.svelte';
|
||||
import { isRelationship } from '../document-[document]/attributes/store';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
@@ -51,7 +52,9 @@
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Heading tag="h2" size="5">Attributes</Heading>
|
||||
|
||||
<CreateAttributeDropdown bind:showCreateDropdown bind:selectedOption bind:showCreate />
|
||||
{#if $canWriteCollections}
|
||||
<CreateAttributeDropdown bind:showCreateDropdown bind:selectedOption bind:showCreate />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $attributes.length}
|
||||
@@ -194,7 +197,11 @@
|
||||
<p class="text">Total results: {$attributes.length}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty single target="attribute" on:click={() => (showEmptyCreateDropdown = true)}>
|
||||
<Empty
|
||||
allowCreate={$canWriteCollections}
|
||||
single
|
||||
target="attribute"
|
||||
on:click={() => (showEmptyCreateDropdown = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
@@ -208,19 +215,21 @@
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown={showEmptyCreateDropdown}
|
||||
bind:selectedOption
|
||||
bind:showCreate>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showEmptyCreateDropdown = !showEmptyCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{#if $canWriteCollections}
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown={showEmptyCreateDropdown}
|
||||
bind:selectedOption
|
||||
bind:showCreate>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showEmptyCreateDropdown = !showEmptyCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
await sdk.forProject.databases.updateBooleanAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
await sdk.forProject.databases.updateDatetimeAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
|
||||
+10
-6
@@ -52,12 +52,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (showEdit) {
|
||||
currentAttr ??= { ...selectedAttribute };
|
||||
originalKey = currentAttr.key;
|
||||
error = null;
|
||||
} else {
|
||||
currentAttr = null;
|
||||
$: onShow(showEdit);
|
||||
|
||||
function onShow(show: boolean) {
|
||||
if (show) {
|
||||
currentAttr ??= { ...selectedAttribute };
|
||||
originalKey = currentAttr.key;
|
||||
error = null;
|
||||
} else {
|
||||
currentAttr = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
await sdk.forProject.databases.updateEmailAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
await sdk.forProject.databases.updateEnumAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.elements,
|
||||
data.required,
|
||||
data.default,
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
await sdk.forProject.databases.updateFloatAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.min,
|
||||
data.max,
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
await sdk.forProject.databases.updateIntegerAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.min,
|
||||
data.max,
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
await sdk.forProject.databases.updateIpAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
await sdk.forProject.databases.updateUrlAttribute(
|
||||
databaseId,
|
||||
collectionId,
|
||||
data.key,
|
||||
originalKey,
|
||||
data.required,
|
||||
data.default,
|
||||
data.key !== originalKey ? data.key : undefined
|
||||
|
||||
+4
-2
@@ -4,6 +4,7 @@
|
||||
import { Id, Tab, Tabs } from '$lib/components';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
import { collection } from './store';
|
||||
|
||||
$: projectId = $page.params.project;
|
||||
@@ -42,9 +43,10 @@
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
title: 'Settings',
|
||||
event: 'settings'
|
||||
event: 'settings',
|
||||
disabled: !$canWriteCollections
|
||||
}
|
||||
];
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
|
||||
+31
-21
@@ -21,6 +21,7 @@
|
||||
import CreateAttributeDropdown from '../attributes/createAttributeDropdown.svelte';
|
||||
import type { Option } from '../attributes/store';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
|
||||
let showDropdown = [];
|
||||
let selectedIndex: Models.Index = null;
|
||||
@@ -38,13 +39,15 @@
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Heading tag="h2" size="5">Indexes</Heading>
|
||||
|
||||
<Button
|
||||
event="create_index"
|
||||
disabled={!$collection?.attributes?.length}
|
||||
on:click={() => (showCreateIndex = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create index</span>
|
||||
</Button>
|
||||
{#if $canWriteCollections}
|
||||
<Button
|
||||
event="create_index"
|
||||
disabled={!$collection?.attributes?.length}
|
||||
on:click={() => (showCreateIndex = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create index</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $collection?.attributes?.length}
|
||||
{#if $indexes.length}
|
||||
@@ -130,13 +133,18 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Empty
|
||||
allowCreate={$canWriteCollections}
|
||||
single
|
||||
href="https://appwrite.io/docs/products/databases/collections#indexes"
|
||||
target="index"
|
||||
on:click={() => (showCreateIndex = true)} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty single target="attribute" on:click={() => (showCreateDropdown = true)}>
|
||||
<Empty
|
||||
single
|
||||
target="attribute"
|
||||
allowCreate={$canWriteCollections}
|
||||
on:click={() => (showCreateDropdown = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create an attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
@@ -150,19 +158,21 @@
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create {target}`}>Documentation</Button>
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{#if $canWriteCollections}
|
||||
<CreateAttributeDropdown
|
||||
bind:showCreateDropdown
|
||||
bind:showCreate={showCreateAttribute}
|
||||
bind:selectedOption={selectedAttribute}>
|
||||
<Button
|
||||
secondary
|
||||
event="create_attribute"
|
||||
on:click={() => {
|
||||
showCreateDropdown = !showCreateDropdown;
|
||||
}}>
|
||||
Create attribute
|
||||
</Button>
|
||||
</CreateAttributeDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { CardContainer, GridItem1, Id } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
import type { PageData } from './$types';
|
||||
export let data: PageData;
|
||||
export let showCreate = false;
|
||||
@@ -11,6 +12,7 @@
|
||||
</script>
|
||||
|
||||
<CardContainer
|
||||
showEmpty={$canWriteCollections}
|
||||
total={data.collections.total}
|
||||
on:click={() => (showCreate = true)}
|
||||
event="collection">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Id, Tab, Tabs } from '$lib/components';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { canWriteDatabases } from '$lib/stores/roles';
|
||||
import { database } from './store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
@@ -25,9 +26,10 @@
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings'
|
||||
title: 'Settings',
|
||||
disabled: !$canWriteDatabases
|
||||
}
|
||||
];
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { canWriteCollections } from '$lib/stores/roles';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
@@ -61,9 +62,11 @@
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected
|
||||
pageItemsIds={data.collections.collections.map((c) => c.$id)} />
|
||||
{#if $canWriteCollections}
|
||||
<TableCellHeadCheck
|
||||
bind:selected
|
||||
pageItemsIds={data.collections.collections.map((c) => c.$id)} />
|
||||
{/if}
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
@@ -74,7 +77,9 @@
|
||||
{#each data.collections.collections as collection}
|
||||
<TableRowLink
|
||||
href={`${base}/project-${projectId}/databases/database-${databaseId}/collection-${collection.$id}`}>
|
||||
<TableCellCheck bind:selectedIds={selected} id={collection.$id} />
|
||||
{#if $canWriteCollections}
|
||||
<TableCellCheck bind:selectedIds={selected} id={collection.$id} />
|
||||
{/if}
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { CardContainer, GridItem1, Id } from '$lib/components';
|
||||
import { canWriteDatabases } from '$lib/stores/roles';
|
||||
import type { PageData } from './$types';
|
||||
export let data: PageData;
|
||||
export let showCreate = false;
|
||||
@@ -9,6 +10,7 @@
|
||||
</script>
|
||||
|
||||
<CardContainer
|
||||
showEmpty={$canWriteDatabases}
|
||||
total={data.databases.total}
|
||||
on:click={() => (showCreate = true)}
|
||||
event="database"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { canWriteDatabases } from '$lib/stores/roles';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
@@ -58,9 +59,11 @@
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected
|
||||
pageItemsIds={data.databases.databases.map((c) => c.$id)} />
|
||||
{#if $canWriteDatabases}
|
||||
<TableCellHeadCheck
|
||||
bind:selected
|
||||
pageItemsIds={data.databases.databases.map((c) => c.$id)} />
|
||||
{/if}
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
@@ -70,7 +73,9 @@
|
||||
<TableBody>
|
||||
{#each data.databases.databases as database}
|
||||
<TableRowLink href={`${base}/project-${projectId}/databases/database-${database.$id}`}>
|
||||
<TableCellCheck bind:selectedIds={selected} id={database.$id} />
|
||||
{#if $canWriteDatabases}
|
||||
<TableCellCheck bind:selectedIds={selected} id={database.$id} />
|
||||
{/if}
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { addSubPanel, registerCommands } from '$lib/commandCenter';
|
||||
import { FunctionsPanel } from '$lib/commandCenter/panels';
|
||||
import { canSeeFunctions } from '$lib/stores/roles';
|
||||
|
||||
$registerCommands([
|
||||
{
|
||||
@@ -9,7 +10,8 @@
|
||||
addSubPanel(FunctionsPanel);
|
||||
},
|
||||
group: 'functions',
|
||||
rank: -1
|
||||
rank: -1,
|
||||
disabled: !$canSeeFunctions
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { parseExpression } from 'cron-parser';
|
||||
import { onMount } from 'svelte';
|
||||
import { functionsList } from './store';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let data;
|
||||
@@ -73,7 +74,8 @@
|
||||
keys: ['c'],
|
||||
disabled:
|
||||
$wizard.show ||
|
||||
isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total),
|
||||
isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total) ||
|
||||
!$canWriteFunctions,
|
||||
icon: 'plus',
|
||||
group: 'functions'
|
||||
}
|
||||
@@ -85,7 +87,7 @@
|
||||
<Container>
|
||||
<ContainerHeader
|
||||
title="Functions"
|
||||
buttonText="Create function"
|
||||
buttonText={$canWriteFunctions ? 'Create function' : ''}
|
||||
buttonEvent="create_function"
|
||||
buttonMethod={openWizard}
|
||||
total={data.functions.total} />
|
||||
@@ -93,6 +95,7 @@
|
||||
{#if data.functions.total}
|
||||
<CardContainer
|
||||
{offset}
|
||||
showEmpty={$canWriteFunctions}
|
||||
event="functions"
|
||||
total={data.functions.total}
|
||||
on:click={openWizard}
|
||||
@@ -136,6 +139,7 @@
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
allowCreate={$canWriteFunctions}
|
||||
href="https://appwrite.io/docs/products/functions"
|
||||
target="function"
|
||||
on:click={openWizard} />
|
||||
|
||||
+14
-7
@@ -9,6 +9,7 @@
|
||||
import { project } from '../../store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
|
||||
onMount(() => {
|
||||
let previousStatus = null;
|
||||
@@ -47,7 +48,8 @@
|
||||
},
|
||||
keys: $page.url.pathname.endsWith($func.$id) ? ['c'] : ['c', 'd'],
|
||||
group: 'functions',
|
||||
icon: 'plus'
|
||||
icon: 'plus',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Permissions',
|
||||
@@ -58,7 +60,8 @@
|
||||
scrollBy({ top: -100 });
|
||||
},
|
||||
icon: 'search',
|
||||
group: 'functions'
|
||||
group: 'functions',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Events',
|
||||
@@ -69,7 +72,8 @@
|
||||
scrollBy({ top: -100 });
|
||||
},
|
||||
icon: 'calendar',
|
||||
group: 'functions'
|
||||
group: 'functions',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Variables',
|
||||
@@ -79,7 +83,8 @@
|
||||
);
|
||||
},
|
||||
icon: 'list',
|
||||
group: 'functions'
|
||||
group: 'functions',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Timeout',
|
||||
@@ -89,7 +94,8 @@
|
||||
);
|
||||
},
|
||||
icon: 'x-circle',
|
||||
group: 'functions'
|
||||
group: 'functions',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Schedule',
|
||||
@@ -100,7 +106,8 @@
|
||||
scrollBy({ top: -100 });
|
||||
},
|
||||
icon: 'clock',
|
||||
group: 'functions'
|
||||
group: 'functions',
|
||||
disabled: !$canWriteFunctions
|
||||
},
|
||||
{
|
||||
label: 'Go to deployments',
|
||||
@@ -140,7 +147,7 @@
|
||||
keys: ['g', 's'],
|
||||
group: 'navigation',
|
||||
rank: 10,
|
||||
disabled: $page.url.pathname.endsWith('settings')
|
||||
disabled: $page.url.pathname.endsWith('settings') || !$canWriteFunctions
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import QuickFilters from './quickFilters.svelte';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { onMount } from 'svelte';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -184,15 +185,17 @@
|
||||
href={`${base}/project-${$page.params.project}/functions/function-${$page.params.function}/deployment-${activeDeployment.$id}`}>
|
||||
Build logs
|
||||
</Button>
|
||||
<Button
|
||||
text
|
||||
class="u-margin-inline-end-16"
|
||||
on:click={() => {
|
||||
selectedDeployment = activeDeployment;
|
||||
showRedeploy = true;
|
||||
}}>
|
||||
Redeploy
|
||||
</Button>
|
||||
{#if $canWriteFunctions}
|
||||
<Button
|
||||
text
|
||||
class="u-margin-inline-end-16"
|
||||
on:click={() => {
|
||||
selectedDeployment = activeDeployment;
|
||||
showRedeploy = true;
|
||||
}}>
|
||||
Redeploy
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
secondary
|
||||
href={`${base}/project-${$page.params.project}/functions/function-${$func.$id}/executions/execute-function`}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { DropList, DropListItem } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
import CreateCli from './createCli.svelte';
|
||||
import CreateGit from './createGit.svelte';
|
||||
import CreateManual from './createManual.svelte';
|
||||
@@ -24,14 +25,16 @@
|
||||
</script>
|
||||
|
||||
<DropList bind:show placement="bottom-end">
|
||||
<Button {secondary} {round} on:click={() => (show = !show)} event="create_deployment">
|
||||
<slot>
|
||||
{#if !secondary}
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="text">Create deployment</span>
|
||||
</slot>
|
||||
</Button>
|
||||
{#if $canWriteFunctions}
|
||||
<Button {secondary} {round} on:click={() => (show = !show)} event="create_deployment">
|
||||
<slot>
|
||||
{#if !secondary}
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="text">Create deployment</span>
|
||||
</slot>
|
||||
</Button>
|
||||
{/if}
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
on:click={() => {
|
||||
|
||||
+8
-6
@@ -389,12 +389,14 @@
|
||||
<DeploymentSource {deployment} />
|
||||
</span>
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Domains</p>
|
||||
<span>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</span>
|
||||
</div>
|
||||
{#if $proxyRuleList?.rules?.length}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Domains</p>
|
||||
<span>
|
||||
<DeploymentDomains domain={$proxyRuleList} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-color-text-offline">Updated</p>
|
||||
<span>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Id, Tab, Tabs } from '$lib/components';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
import { func } from './store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
@@ -36,9 +37,10 @@
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
event: 'settings',
|
||||
title: 'Settings'
|
||||
title: 'Settings',
|
||||
disabled: !$canWriteFunctions
|
||||
}
|
||||
];
|
||||
].filter((tab) => !tab.disabled);
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
import { connectTemplate } from '$lib/wizards/functions/cover.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { functionsList } from '../store';
|
||||
@@ -226,15 +227,16 @@
|
||||
text>
|
||||
<span class="text">View details</span>
|
||||
</Button>
|
||||
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonType="secondary"
|
||||
buttonMethod={() => connectTemplate(template)}
|
||||
showIcon={false}
|
||||
buttonText="Create function"
|
||||
buttonEvent="create_function" />
|
||||
{#if $canWriteFunctions}
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonType="secondary"
|
||||
buttonMethod={() => connectTemplate(template)}
|
||||
showIcon={false}
|
||||
buttonText="Create function"
|
||||
buttonEvent="create_function" />
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
+10
-7
@@ -12,6 +12,7 @@
|
||||
import { isServiceLimited } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { functionsList } from '../../store';
|
||||
import { canWriteFunctions } from '$lib/stores/roles';
|
||||
|
||||
$: buttonDisabled =
|
||||
isCloud && isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total);
|
||||
@@ -77,13 +78,15 @@
|
||||
View source
|
||||
<span class="icon-external-link" />
|
||||
</Button>
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonMethod={() => connectTemplate($template)}
|
||||
showIcon={false}
|
||||
buttonText="Create function"
|
||||
buttonEvent="create_function" />
|
||||
{#if $canWriteFunctions}
|
||||
<ContainerButton
|
||||
title="functions"
|
||||
disabled={buttonDisabled}
|
||||
buttonMethod={() => connectTemplate($template)}
|
||||
showIcon={false}
|
||||
buttonText="Create function"
|
||||
buttonEvent="create_function" />
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { messagesSearcher } from '$lib/commandCenter/searchers/messages';
|
||||
import { providersSearcher } from '$lib/commandCenter/searchers/providers';
|
||||
import { topicsSearcher } from '$lib/commandCenter/searchers/topics';
|
||||
import { canWriteMessages } from '$lib/stores/roles';
|
||||
import { project } from '../store';
|
||||
|
||||
// TODO: finalize the commands
|
||||
@@ -23,7 +24,8 @@
|
||||
addSubPanel(CreateMessagePanel);
|
||||
},
|
||||
icon: 'plus',
|
||||
group: 'messaging'
|
||||
group: 'messaging',
|
||||
disabled: !$canWriteMessages
|
||||
},
|
||||
{
|
||||
label: 'Go to Topics',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user