mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into 'refactor-terminologies'.
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
PUBLIC_CONSOLE_MODE=self-hosted
|
||||
PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths
|
||||
PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths
|
||||
PUBLIC_APPWRITE_MULTI_REGION=false
|
||||
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
|
||||
PUBLIC_STRIPE_KEY=
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=true"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=self-hosted"
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=false"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
|
||||
publish-cloud-no-regions:
|
||||
@@ -156,6 +156,6 @@ jobs:
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=false"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,csv-import,column-encrypt,index-lengths"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
|
||||
+2
-2
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@56743f5",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
|
||||
Generated
+25
-9
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3)
|
||||
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53
|
||||
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3)
|
||||
'@popperjs/core':
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8
|
||||
@@ -261,8 +261,8 @@ packages:
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f}
|
||||
version: 1.9.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8}
|
||||
version: 2.0.0-RC.1
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -282,8 +282,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606}
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53}
|
||||
version: 2.0.0-RC.2
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -1266,6 +1266,14 @@ packages:
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tanstack/svelte-virtual@3.13.12':
|
||||
resolution: {integrity: sha512-SsOinCktvpMFbdSW61X6jeUNaJvD1tmt30AFVSLe1FQxGxjdPkZo0gAsoVPqM9FFZqidvvXQ4gpadDaTz/yGxA==}
|
||||
peerDependencies:
|
||||
svelte: ^3.48.0 || ^4.0.0 || ^5.0.0
|
||||
|
||||
'@tanstack/virtual-core@3.13.12':
|
||||
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3638,7 +3646,7 @@ snapshots:
|
||||
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@4ca420f': {}
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
@@ -3655,12 +3663,13 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@d521606(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@fee5c53(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d521606fc63e0800b996f0fbb1a8d1f6907480ed(svelte@5.25.3)
|
||||
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@fee5c539136e15c93cd3f2398d25cb217caa8dc8(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
|
||||
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
|
||||
'@tanstack/svelte-virtual': 3.13.12(svelte@5.25.3)
|
||||
ansicolor: 2.0.3
|
||||
d3: 7.9.0
|
||||
fuse.js: 7.1.0
|
||||
@@ -4711,6 +4720,13 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tanstack/svelte-virtual@3.13.12(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.12
|
||||
svelte: 5.25.3
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
|
||||
@@ -148,7 +148,12 @@
|
||||
})}
|
||||
clearOnCallback={false}
|
||||
on:keydown={(e) => {
|
||||
if (e.detail.key !== 'Escape') {
|
||||
const showingExamples = !$isLoading && !answer;
|
||||
if (e.detail.key === 'Enter' && $input.trim()) {
|
||||
e.detail.cancel();
|
||||
return;
|
||||
}
|
||||
if (e.detail.key !== 'Escape' && !showingExamples) {
|
||||
e.detail.cancel();
|
||||
}
|
||||
}}
|
||||
@@ -302,15 +307,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global(.answer ul),
|
||||
:global(.answer ol) {
|
||||
gap: 1rem;
|
||||
:global(.answer ul) {
|
||||
padding-inline-start: 1rem;
|
||||
list-style-type: disc;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:global(.answer ol) {
|
||||
padding-inline-start: 1.1rem;
|
||||
list-style-type: decimal;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:global(.answer a) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:global(.answer a:hover) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.experimental {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
$organization.$id,
|
||||
$paymentMissingMandate.$id
|
||||
);
|
||||
await confirmSetup(method.clientSecret, method.$id);
|
||||
await confirmSetup(method.clientSecret, method.providerMethodId);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Click } from '$lib/actions/analytics';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import {
|
||||
billingProjectsLimitDate,
|
||||
hideBillingHeaderRoutes,
|
||||
upgradeURL
|
||||
} from '$lib/stores/billing';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import SelectProjectCloud from './selectProjectCloud.svelte';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
let showSelectProject: boolean = $state(false);
|
||||
let selectedProjects: string[] = $state([]);
|
||||
onMount(() => {
|
||||
selectedProjects = page.data.organization?.projects || [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
|
||||
|
||||
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="warning"
|
||||
title="Action required: You have more than {$currentPlan.projects} projects.">
|
||||
<svelte:fragment>
|
||||
Choose which projects to keep before {toLocaleDate(billingProjectsLimitDate)} or upgrade
|
||||
to Pro. Projects over the limit will be blocked after this date.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
compact
|
||||
on:click={() => {
|
||||
showSelectProject = true;
|
||||
}}>Manage projects</Button>
|
||||
<Button
|
||||
href={$upgradeURL}
|
||||
event={Click.OrganizationClickUpgrade}
|
||||
eventData={{
|
||||
from: 'button',
|
||||
source: 'projects_limit_banner'
|
||||
}}
|
||||
secondary
|
||||
fullWidthMobile>
|
||||
<span class="text">Upgrade</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { billingProjectsLimitDate } from '$lib/stores/billing';
|
||||
import { page } from '$app/state';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
|
||||
let {
|
||||
showSelectProject = $bindable(false),
|
||||
selectedProjects = $bindable([])
|
||||
}: {
|
||||
showSelectProject: boolean;
|
||||
selectedProjects: string[];
|
||||
} = $props();
|
||||
|
||||
let projects = $state<Array<Models.Project>>([]);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
projects = page.data.projects?.projects || [];
|
||||
});
|
||||
|
||||
let projectsToArchive = $derived(
|
||||
projects.filter((project) => !selectedProjects.includes(project.$id))
|
||||
);
|
||||
|
||||
async function updateSelected() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateSelectedProjects(
|
||||
projects[0].teamId,
|
||||
selectedProjects
|
||||
);
|
||||
showSelectProject = false;
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Projects updated for archiving`
|
||||
});
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function formatProjectsToArchive() {
|
||||
let result = '';
|
||||
|
||||
projectsToArchive.forEach((project, index) => {
|
||||
const text = `${index === 0 ? '' : ' '}<b>${project.name}</b> `;
|
||||
result += text;
|
||||
|
||||
if (index < projectsToArchive.length - 1) {
|
||||
if (index == projectsToArchive.length - 2) {
|
||||
result += 'and ';
|
||||
}
|
||||
if (index < projectsToArchive.length - 2) {
|
||||
result += ', ';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
|
||||
<svelte:fragment slot="description">
|
||||
Choose which two projects to keep. Projects over the limit will be blocked after this date.
|
||||
</svelte:fragment>
|
||||
{#if error}
|
||||
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
|
||||
{/if}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedProjects}
|
||||
columns={[{ id: 'name' }, { id: 'created' }]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
|
||||
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each projects as project}
|
||||
<Table.Row.Base {root} id={project.$id}>
|
||||
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
|
||||
<Table.Cell column="created" {root}
|
||||
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{#if selectedProjects.length > $currentPlan?.projects}
|
||||
<div class="u-text-warning u-mb-4">
|
||||
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedProjects.length === $currentPlan?.projects}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<span>
|
||||
{@html formatProjectsToArchive()}
|
||||
will be archived.
|
||||
</span>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
|
||||
>Cancel</Button.Button>
|
||||
<Button.Button size="s" disabled={selectedProjects.length !== $currentPlan?.projects}
|
||||
>Save</Button.Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -3,7 +3,6 @@
|
||||
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
|
||||
import { app } from '$lib/stores/app';
|
||||
import {
|
||||
type BottomModalAlertAction,
|
||||
type BottomModalAlertItem,
|
||||
bottomModalAlertsConfig,
|
||||
dismissBottomModalAlert,
|
||||
@@ -20,18 +19,18 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
let currentIndex = $state(0);
|
||||
let openModalOnMobile = $state(false);
|
||||
|
||||
function getPageScope(pathname: string) {
|
||||
const isProjectPage = pathname.includes('project-[region]-[project]');
|
||||
const isOrganizationPage = pathname.includes('organization-[organization]');
|
||||
function getPageScope(route: string) {
|
||||
const isProjectPage = route.includes('project-[region]-[project]');
|
||||
const isOrganizationPage = route.includes('organization-[organization]');
|
||||
|
||||
return { isProjectPage, isOrganizationPage };
|
||||
}
|
||||
|
||||
function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) {
|
||||
const { isProjectPage, isOrganizationPage } = getPageScope(pathname);
|
||||
function filterModalAlerts(alerts: BottomModalAlertItem[], route: string) {
|
||||
const { isProjectPage, isOrganizationPage } = getPageScope(route);
|
||||
|
||||
return alerts
|
||||
.sort((a, b) => b.importance - a.importance)
|
||||
@@ -51,19 +50,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
|
||||
const bottomModalAlerts = $derived($bottomModalAlertsConfig.alerts);
|
||||
|
||||
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
|
||||
const filteredModalAlerts = $derived(filterModalAlerts(bottomModalAlerts, page.route.id));
|
||||
|
||||
$: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined';
|
||||
const currentModalAlert = $derived(filteredModalAlerts[currentIndex] as BottomModalAlertItem);
|
||||
|
||||
function handleClose() {
|
||||
filteredModalAlerts.forEach((alert) => {
|
||||
const hasOnlyPrimaryCta = $derived(typeof currentModalAlert?.learnMore === 'undefined');
|
||||
|
||||
const isOnOnboarding = $derived(page.route.id.includes('(console)/onboarding'));
|
||||
|
||||
function handleClose(id: string | null = null) {
|
||||
const alerts = !id
|
||||
? filteredModalAlerts
|
||||
: filteredModalAlerts.filter((alert) => alert.id === id);
|
||||
|
||||
alerts.forEach((alert) => {
|
||||
const modalAlert = alert;
|
||||
dismissBottomModalAlert(modalAlert.id);
|
||||
hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 });
|
||||
if (modalAlert.closed) modalAlert.closed();
|
||||
});
|
||||
|
||||
// reset `currentIndex` if we removed the last item
|
||||
if (currentIndex >= filteredModalAlerts.length - 1) {
|
||||
currentIndex = Math.max(0, filteredModalAlerts.length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
@@ -114,8 +126,13 @@
|
||||
}
|
||||
|
||||
// the button component cannot have both href and on:click!
|
||||
function triggerWindowLink(alertAction: BottomModalAlertAction, event?: string) {
|
||||
function triggerWindowLink(alert: BottomModalAlertItem, event?: string) {
|
||||
const alertAction = alert.cta;
|
||||
const shouldShowUpgrade = showUpgrade();
|
||||
|
||||
// for correct event tracking after removal
|
||||
const currentModalId = currentModalAlert.id;
|
||||
|
||||
const url = shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: alertAction.link({
|
||||
@@ -130,14 +147,12 @@
|
||||
}
|
||||
|
||||
if (alertAction?.hideOnClick === true) {
|
||||
// be careful of this one.
|
||||
// once clicked, its gone!
|
||||
handleClose();
|
||||
handleClose(alert.id); // gone!
|
||||
}
|
||||
|
||||
trackEvent(Click.PromoClick, {
|
||||
promo: currentModalAlert.id,
|
||||
type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalAlert.id}`)
|
||||
promo: currentModalId,
|
||||
type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalId}`)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,12 +171,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
addBottomModalAlerts();
|
||||
});
|
||||
onMount(addBottomModalAlerts);
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')}
|
||||
{#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
@@ -170,7 +183,7 @@
|
||||
<button
|
||||
aria-label="Close modal"
|
||||
class="icon-inline-tag"
|
||||
on:click={() => handleClose()}>
|
||||
onclick={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
@@ -208,14 +221,19 @@
|
||||
<button
|
||||
aria-label="Previous"
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
style:cursor={currentIndex !== 0 ? 'pointer' : undefined}
|
||||
onclick={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0}></button>
|
||||
|
||||
<button
|
||||
aria-label="Next"
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
onclick={showNext}
|
||||
style:cursor={currentIndex !==
|
||||
filteredModalAlerts.length - 1
|
||||
? 'pointer'
|
||||
: undefined}
|
||||
disabled={currentIndex === filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1}></button>
|
||||
@@ -243,7 +261,7 @@
|
||||
fullWidthMobile
|
||||
secondary={!hasOnlyPrimaryCta}
|
||||
class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
|
||||
on:click={() => triggerWindowLink(currentModalAlert.cta)}>
|
||||
on:click={() => triggerWindowLink(currentModalAlert)}>
|
||||
{currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
@@ -276,7 +294,7 @@
|
||||
<button
|
||||
aria-label="Close modal"
|
||||
class="icon-inline-tag"
|
||||
on:click={() => handleClose()}>
|
||||
onclick={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
@@ -314,14 +332,21 @@
|
||||
<button
|
||||
aria-label="Previous"
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
style:cursor={currentIndex !== 0
|
||||
? 'pointer'
|
||||
: undefined}
|
||||
onclick={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0}></button>
|
||||
|
||||
<button
|
||||
aria-label="Next"
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
onclick={showNext}
|
||||
style:cursor={currentIndex !==
|
||||
filteredModalAlerts.length - 1
|
||||
? 'pointer'
|
||||
: undefined}
|
||||
disabled={currentIndex ===
|
||||
filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
@@ -352,7 +377,7 @@
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
openModalOnMobile = false;
|
||||
triggerWindowLink(currentModalAlert.cta);
|
||||
triggerWindowLink(currentModalAlert);
|
||||
}}>
|
||||
{shouldShowUpgrade
|
||||
? 'Upgrade plan'
|
||||
@@ -382,13 +407,13 @@
|
||||
{:else}
|
||||
{@const mobileConfig = getMobileWindowConfig()}
|
||||
<!-- we don't need keydown because we show this only on mobile -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
if (mobileConfig.cta) {
|
||||
// navigate manually!
|
||||
triggerMobileWindowLink();
|
||||
@@ -401,7 +426,7 @@
|
||||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
|
||||
{currentModalAlert.title}
|
||||
</Typography.Text>
|
||||
<button on:click={hideAllModalAlerts} aria-label="Close">
|
||||
<button onclick={hideAllModalAlerts} aria-label="Close">
|
||||
<span class="icon-x"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
};
|
||||
|
||||
const saveColumnPreferences = () => {
|
||||
const shownColumns = $columns.filter((n) => n.hide !== true).map((n) => n.id);
|
||||
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
|
||||
|
||||
if (isCustomTable) {
|
||||
preferences.setCustomTableColumns(page.params.table, shownColumns);
|
||||
@@ -45,22 +45,22 @@
|
||||
if (isCustomTable) {
|
||||
const shownColumns = preferences.getCustomTableColumns(page.params.table);
|
||||
|
||||
columns.update((columns) => {
|
||||
return columns.map((column) => {
|
||||
column.hide = !shownColumns.includes(column.id);
|
||||
columns.update((n) =>
|
||||
n.map((column) => {
|
||||
column.hide = shownColumns?.includes(column.id) ?? false;
|
||||
return column;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const prefs = preferences.get(page.route);
|
||||
|
||||
if (prefs?.columns && prefs.columns.length > 0) {
|
||||
columns.update((cols) => {
|
||||
return cols.map((column) => {
|
||||
column.hide = !prefs.columns.includes(column.id);
|
||||
if (prefs?.columns) {
|
||||
columns.update((n) =>
|
||||
n.map((column) => {
|
||||
column.hide = prefs.columns?.includes(column.id) ?? false;
|
||||
return column;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
columns.update((cols) =>
|
||||
cols.map((col) => {
|
||||
if (col.id === column.id) {
|
||||
col.hide = !column.hide;
|
||||
column.hide = !column.hide;
|
||||
}
|
||||
return col;
|
||||
})
|
||||
@@ -90,6 +90,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={calcMaxHeight} />
|
||||
|
||||
{#if $columns?.length}
|
||||
<Popover let:toggle placement="bottom-end" padding="none">
|
||||
{@render children(toggle, selectedColumnsNumber)}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { Badge, InteractiveText, Layout, Popover, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { menuOpen } from '$lib/components/menu/store';
|
||||
|
||||
export let time: string = '';
|
||||
export let time: string | null = null;
|
||||
export let placement: ComponentProps<Popover>['placement'] = 'bottom';
|
||||
|
||||
function timeDifference(dateString: string): string {
|
||||
@@ -75,64 +75,68 @@
|
||||
$: timeToString = time ? timeDifference(time) : 'Invalid time';
|
||||
</script>
|
||||
|
||||
<Popover let:show let:hide {placement} portal>
|
||||
<button
|
||||
on:mouseenter={() => {
|
||||
if (!$menuOpen) {
|
||||
setTimeout(show, 150);
|
||||
}
|
||||
}}
|
||||
on:mouseleave={() => hidePopover(hide)}>
|
||||
<slot>{capitalize(timeFromNow(time))}</slot>
|
||||
</button>
|
||||
{#if time}
|
||||
<Popover let:show let:hide {placement} portal>
|
||||
<button
|
||||
on:mouseenter={() => {
|
||||
if (!$menuOpen) {
|
||||
setTimeout(show, 150);
|
||||
}
|
||||
}}
|
||||
on:mouseleave={() => hidePopover(hide)}>
|
||||
<slot>{capitalize(timeFromNow(time))}</slot>
|
||||
</button>
|
||||
|
||||
<div
|
||||
let:hide
|
||||
slot="tooltip"
|
||||
role="tooltip"
|
||||
style:padding="1rem"
|
||||
style:margin="-1rem"
|
||||
on:mouseenter={() => (isMouseOverTooltip = true)}
|
||||
on:mouseleave={() => hidePopover(hide, false)}>
|
||||
<Layout.Stack gap="s" alignContent="flex-start">
|
||||
<!-- `Raw time` as per design -->
|
||||
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
|
||||
{#if $$slots.title}
|
||||
<slot name="title" />
|
||||
{:else}
|
||||
{timeToString}
|
||||
{/if}
|
||||
</Typography.Caption>
|
||||
<div
|
||||
let:hide
|
||||
slot="tooltip"
|
||||
role="tooltip"
|
||||
style:padding="1rem"
|
||||
style:margin="-1rem"
|
||||
on:mouseenter={() => (isMouseOverTooltip = true)}
|
||||
on:mouseleave={() => hidePopover(hide, false)}>
|
||||
<Layout.Stack gap="s" alignContent="flex-start">
|
||||
<!-- `Raw time` as per design -->
|
||||
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
|
||||
{#if $$slots.title}
|
||||
<slot name="title" />
|
||||
{:else}
|
||||
{timeToString}
|
||||
{/if}
|
||||
</Typography.Caption>
|
||||
|
||||
<!-- `Absolute time` as per design -->
|
||||
<Layout.Stack gap="xxs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time, false, 'UTC')}
|
||||
value={toISOString(time)} />
|
||||
<!-- `Absolute time` as per design -->
|
||||
<Layout.Stack gap="xxs">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time, false, 'UTC')}
|
||||
value={toISOString(time)} />
|
||||
|
||||
<Badge variant="secondary" content="UTC" size="xs" />
|
||||
</Layout.Stack>
|
||||
<Badge variant="secondary" content="UTC" size="xs" />
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time)}
|
||||
value={toLocalDateTimeISO(time)} />
|
||||
<Badge variant="secondary" content="Local" size="xs" />
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between">
|
||||
<InteractiveText
|
||||
isVisible
|
||||
variant="copy"
|
||||
text={toLocaleDateTime(time)}
|
||||
value={toLocalDateTimeISO(time)} />
|
||||
<Badge variant="secondary" content="Local" size="xs" />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Typography.Text>{time === null || time === undefined ? 'null' : time}</Typography.Text>
|
||||
{/if}
|
||||
|
||||
@@ -90,5 +90,5 @@
|
||||
</Fieldset>
|
||||
|
||||
{#if show}
|
||||
<SelectRootModal bind:show bind:rootDir {product} />
|
||||
<SelectRootModal bind:show bind:rootDir {product} {branch} />
|
||||
{/if}
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
import { page } from '$app/state';
|
||||
import Card from '../card.svelte';
|
||||
import SkeletonRepoList from './skeletonRepoList.svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { onMount, untrack, onDestroy } from 'svelte';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
|
||||
let {
|
||||
action = $bindable('select'),
|
||||
@@ -52,6 +53,28 @@
|
||||
loadInstallations();
|
||||
});
|
||||
|
||||
const debouncedLoadRepositories = debounce(
|
||||
async (installationId: string, searchTerm: string) => {
|
||||
isLoadingRepositories = true;
|
||||
try {
|
||||
await loadRepositories(installationId, searchTerm);
|
||||
} finally {
|
||||
isLoadingRepositories = false;
|
||||
}
|
||||
},
|
||||
300
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (selectedInstallation && search !== undefined) {
|
||||
debouncedLoadRepositories(selectedInstallation, search);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
debouncedLoadRepositories.cancel();
|
||||
});
|
||||
|
||||
async function loadInstallations() {
|
||||
if (installationList) {
|
||||
if (installationList.installations.length) {
|
||||
@@ -71,7 +94,7 @@
|
||||
.vcs.listInstallations();
|
||||
if (installations.length) {
|
||||
if (!selectedInstallation) {
|
||||
untrack(() => (selectedInstallation = installationList.installations[0].$id));
|
||||
untrack(() => (selectedInstallation = installations[0].$id));
|
||||
}
|
||||
installation.set(installations.find((entry) => entry.$id === selectedInstallation));
|
||||
}
|
||||
@@ -80,22 +103,6 @@
|
||||
}
|
||||
|
||||
async function loadRepositories(installationId: string, search: string) {
|
||||
if (
|
||||
!$repositories ||
|
||||
$repositories.installationId !== installationId ||
|
||||
$repositories.search !== search
|
||||
) {
|
||||
await fetchRepos(installationId, search);
|
||||
}
|
||||
|
||||
if ($repositories.repositories.length && action === 'select') {
|
||||
selectedRepository = $repositories.repositories[0].id;
|
||||
$repository = $repositories.repositories[0];
|
||||
}
|
||||
return $repositories.repositories;
|
||||
}
|
||||
|
||||
async function fetchRepos(installationId: string, search: string) {
|
||||
if (product === 'functions') {
|
||||
$repositories.repositories = (
|
||||
(await sdk
|
||||
@@ -117,9 +124,9 @@
|
||||
)) as unknown as Models.ProviderRepositoryFrameworkList
|
||||
).frameworkProviderRepositories;
|
||||
}
|
||||
|
||||
$repositories.search = search;
|
||||
$repositories.installationId = installationId;
|
||||
return $repositories.repositories;
|
||||
}
|
||||
|
||||
selectedRepository;
|
||||
@@ -168,10 +175,7 @@
|
||||
installationsMap.find((entry) => entry.$id === selectedInstallation)
|
||||
);
|
||||
|
||||
isLoadingRepositories = true;
|
||||
loadRepositories(selectedInstallation, search).then(() => {
|
||||
isLoadingRepositories = false;
|
||||
});
|
||||
debouncedLoadRepositories.cancel();
|
||||
}}
|
||||
bind:value={selectedInstallation} />
|
||||
<InputSearch
|
||||
@@ -185,130 +189,109 @@
|
||||
<!-- manual installation change -->
|
||||
{#if isLoadingRepositories}
|
||||
<SkeletonRepoList />
|
||||
{:else if $repositories?.repositories?.length}
|
||||
<Paginator
|
||||
items={$repositories.repositories}
|
||||
hideFooter={$repositories.repositories?.length <= 6}
|
||||
limit={6}>
|
||||
{#snippet children(
|
||||
paginatedItems: Models.ProviderRepositoryRuntime[] &
|
||||
Models.ProviderRepositoryFramework[]
|
||||
)}
|
||||
<Table.Root columns={1} let:root>
|
||||
{#each paginatedItems as repo}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
{#if action === 'select'}
|
||||
<input
|
||||
class="is-small u-margin-inline-end-8"
|
||||
type="radio"
|
||||
name="repositories"
|
||||
bind:group={selectedRepository}
|
||||
onchange={() => repository.set(repo)}
|
||||
value={repo.id} />
|
||||
{/if}
|
||||
{#if product === 'sites'}
|
||||
{#if repo?.framework && repo.framework !== 'other'}
|
||||
<Avatar size="xs" alt={repo.name}>
|
||||
<SvgIcon
|
||||
name={getFrameworkIcon(repo.framework)}
|
||||
iconSize="small" />
|
||||
</Avatar>
|
||||
{:else}
|
||||
<Avatar size="xs" alt={repo.name} empty />
|
||||
{/if}
|
||||
{:else}
|
||||
{@const iconName = repo?.runtime
|
||||
? repo.runtime.split('-')[0]
|
||||
: undefined}
|
||||
<Avatar size="xs" alt={repo.name} empty={!iconName}>
|
||||
{#if iconName}
|
||||
<SvgIcon name={iconName} iconSize="small" />
|
||||
{/if}
|
||||
</Avatar>
|
||||
{/if}
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap="s"
|
||||
style="flex: 1; min-width: 0;">
|
||||
<Typography.Text
|
||||
truncate
|
||||
color="--fgcolor-neutral-secondary"
|
||||
style="flex: 1; min-width: 0;">
|
||||
{repo.name}
|
||||
</Typography.Text>
|
||||
{#if repo.private}
|
||||
<Icon
|
||||
size="s"
|
||||
icon={IconLockClosed}
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
{#if !$isSmallViewport}
|
||||
<time datetime={repo.pushedAt}>
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
truncate
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
{timeFromNow(repo.pushedAt)}
|
||||
</Typography.Caption>
|
||||
</time>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{#if action === 'button'}
|
||||
<PinkButton.Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
style="flex-shrink: 0;"
|
||||
on:click={() => connect(repo)}>
|
||||
Connect
|
||||
</PinkButton.Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{:else if search}
|
||||
<EmptySearch hidePages hidePagination bind:search target="repositories">
|
||||
<svelte:fragment slot="actions">
|
||||
{#if search}
|
||||
<Button secondary on:click={() => (search = '')}>Clear search</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
{#await loadRepositories(selectedInstallation, search)}
|
||||
<SkeletonRepoList />
|
||||
{:then response}
|
||||
{#if response?.length}
|
||||
<Paginator items={response} hideFooter={response?.length <= 6} limit={6}>
|
||||
{#snippet children(
|
||||
paginatedItems: Models.ProviderRepositoryRuntime[] &
|
||||
Models.ProviderRepositoryFramework[]
|
||||
)}
|
||||
<Table.Root columns={1} let:root>
|
||||
{#each paginatedItems as repo}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap="s">
|
||||
{#if action === 'select'}
|
||||
<input
|
||||
class="is-small u-margin-inline-end-8"
|
||||
type="radio"
|
||||
name="repositories"
|
||||
bind:group={selectedRepository}
|
||||
onchange={() => repository.set(repo)}
|
||||
value={repo.id} />
|
||||
{/if}
|
||||
{#if product === 'sites'}
|
||||
{#if repo?.framework && repo.framework !== 'other'}
|
||||
<Avatar size="xs" alt={repo.name}>
|
||||
<SvgIcon
|
||||
name={getFrameworkIcon(
|
||||
repo.framework
|
||||
)}
|
||||
iconSize="small" />
|
||||
</Avatar>
|
||||
{:else}
|
||||
<Avatar
|
||||
size="xs"
|
||||
alt={repo.name}
|
||||
empty />
|
||||
{/if}
|
||||
{:else}
|
||||
{@const iconName = repo?.runtime
|
||||
? repo.runtime.split('-')[0]
|
||||
: undefined}
|
||||
<Avatar
|
||||
size="xs"
|
||||
alt={repo.name}
|
||||
empty={!iconName}>
|
||||
<SvgIcon
|
||||
name={iconName}
|
||||
iconSize="small" />
|
||||
</Avatar>
|
||||
{/if}
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between">
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="s"
|
||||
alignItems="center">
|
||||
<Typography.Text
|
||||
truncate
|
||||
color="--fgcolor-neutral-secondary">
|
||||
{repo.name}
|
||||
</Typography.Text>
|
||||
{#if repo.private}
|
||||
<Icon
|
||||
size="s"
|
||||
icon={IconLockClosed}
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
{#if !$isSmallViewport}
|
||||
<time datetime={repo.pushedAt}>
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
truncate
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
{timeFromNow(repo.pushedAt)}
|
||||
</Typography.Caption>
|
||||
</time>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
{#if action === 'button'}
|
||||
<PinkButton.Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
on:click={() => connect(repo)}>
|
||||
Connect
|
||||
</PinkButton.Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
{/snippet}
|
||||
</Paginator>
|
||||
{:else if search}
|
||||
<EmptySearch hidePages hidePagination bind:search target="repositories">
|
||||
<svelte:fragment slot="actions">
|
||||
{#if search}
|
||||
<Button secondary on:click={() => (search = '')}>
|
||||
Clear search
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Card>
|
||||
<Layout.Stack alignItems="center" justifyContent="center">
|
||||
<Typography.Text
|
||||
variation="m-500"
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
No repositories available
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
{/if}
|
||||
{/await}
|
||||
<Card>
|
||||
<Layout.Stack alignItems="center" justifyContent="center">
|
||||
<Typography.Text variation="m-500" color="--fgcolor-neutral-tertiary">
|
||||
No repositories available
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
export let show = false;
|
||||
export let rootDir: string;
|
||||
export let product: 'sites' | 'functions' = 'functions';
|
||||
export let branch: string;
|
||||
|
||||
let isLoading = true;
|
||||
let directories: Directory[] = [
|
||||
@@ -42,7 +43,7 @@
|
||||
try {
|
||||
const content = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.vcs.getRepositoryContents($installation.$id, $repository.id, currentPath);
|
||||
.vcs.getRepositoryContents($installation.$id, $repository.id, currentPath, branch);
|
||||
directories[0].fileCount = content.contents?.length ?? 0;
|
||||
directories[0].children = content.contents
|
||||
.filter((e) => e.isDirectory)
|
||||
@@ -82,7 +83,7 @@
|
||||
try {
|
||||
const content = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.vcs.getRepositoryContents($installation.$id, $repository.id, path);
|
||||
.vcs.getRepositoryContents($installation.$id, $repository.id, path, branch);
|
||||
|
||||
const fileCount = content.contents?.length ?? 0;
|
||||
const contentDirectories = content.contents.filter((e) => e.isDirectory);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center">
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="flex-start">
|
||||
<slot name="status" />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
export let showTeam: boolean;
|
||||
export let showLabel: boolean;
|
||||
export let showCustom: boolean;
|
||||
export let hideOnClick: boolean = false;
|
||||
export let groups: Writable<Map<string, Permission>>;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -25,7 +24,7 @@
|
||||
<ActionMenu.Item.Button
|
||||
disabled={$groups.has('any')}
|
||||
on:click={(e) => {
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
dispatch('create', ['any']);
|
||||
}}>
|
||||
Any
|
||||
@@ -33,7 +32,7 @@
|
||||
<ActionMenu.Item.Button
|
||||
disabled={$groups.has('guests')}
|
||||
on:click={(e) => {
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
dispatch('create', ['guests']);
|
||||
}}>
|
||||
All guests
|
||||
@@ -41,7 +40,7 @@
|
||||
<ActionMenu.Item.Button
|
||||
disabled={$groups.has('users')}
|
||||
on:click={(e) => {
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
dispatch('create', ['users']);
|
||||
}}>
|
||||
All users
|
||||
@@ -49,22 +48,22 @@
|
||||
<ActionMenu.Item.Button
|
||||
on:click={(e) => {
|
||||
showUser = true;
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
}}>Select users</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={(e) => {
|
||||
showTeam = true;
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
}}>Select teams</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={(e) => {
|
||||
showLabel = true;
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
}}>Label</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
on:click={(e) => {
|
||||
showCustom = true;
|
||||
if (hideOnClick) hide(e);
|
||||
hide(e);
|
||||
}}>Custom permission</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import { Card } from '$lib/components';
|
||||
|
||||
export let withCreate = false;
|
||||
export let hideOnClick = false;
|
||||
export let permissions: string[] = [];
|
||||
|
||||
let showUser = false;
|
||||
@@ -197,7 +196,6 @@
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
{hideOnClick}
|
||||
on:create={create}
|
||||
let:toggle>
|
||||
<Button secondary on:click={toggle}>
|
||||
@@ -215,7 +213,6 @@
|
||||
bind:showTeam
|
||||
bind:showUser
|
||||
{groups}
|
||||
{hideOnClick}
|
||||
on:create={create}
|
||||
let:toggle>
|
||||
<Button secondary icon on:click={toggle}>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
offset = 0;
|
||||
search = '';
|
||||
selected.clear();
|
||||
show = false;
|
||||
}
|
||||
|
||||
function create() {
|
||||
@@ -88,11 +89,13 @@
|
||||
{@const exists = $groups.has(role)}
|
||||
<Table.Row.Button {root} on:click={() => onSelection(role)} disabled={exists}>
|
||||
<Table.Cell column="checkbox" {root}>
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id={team.$id}
|
||||
disabled={exists}
|
||||
checked={exists || selected.has(role)} />
|
||||
<div style:pointer-events="none">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id={team.$id}
|
||||
disabled={exists}
|
||||
checked={exists || selected.has(role)} />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="team" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
offset = 0;
|
||||
search = '';
|
||||
selected.clear();
|
||||
show = false;
|
||||
}
|
||||
|
||||
function create() {
|
||||
@@ -90,11 +91,13 @@
|
||||
{@const exists = $groups.has(role)}
|
||||
<Table.Row.Button {root} on:click={() => onSelection(role)} disabled={exists}>
|
||||
<Table.Cell column="checkbox" {root}>
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id={user.$id}
|
||||
disabled={exists}
|
||||
checked={exists || selected.has(role)} />
|
||||
<div style:pointer-events="none">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id={user.$id}
|
||||
disabled={exists}
|
||||
checked={exists || selected.has(role)} />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="user" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
|
||||
@@ -79,7 +79,13 @@
|
||||
{ name: 'Functions', icon: IconLightningBolt, slug: 'functions', category: 'build' },
|
||||
{ name: 'Messaging', icon: IconChatBubble, slug: 'messaging', category: 'build' },
|
||||
{ name: 'Storage', icon: IconFolder, slug: 'storage', category: 'build' },
|
||||
{ name: 'Sites', icon: IconGlobeAlt, slug: 'sites', category: 'deploy', badge: 'New' }
|
||||
{
|
||||
name: 'Sites',
|
||||
icon: IconGlobeAlt,
|
||||
slug: 'sites',
|
||||
category: 'deploy',
|
||||
badge: 'Early access'
|
||||
}
|
||||
];
|
||||
|
||||
const isSelected = (service: string): boolean => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export enum Dependencies {
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
UPGRADE_PLAN = 'dependency:upgrade_plan',
|
||||
CREATE_ORGANIZATION = 'dependency:create_organization',
|
||||
ORGANIZATIONS = 'dependency:organizations',
|
||||
PAYMENT_METHODS = 'dependency:paymentMethods',
|
||||
ORGANIZATION = 'dependency:organization',
|
||||
MEMBERS = 'dependency:members',
|
||||
|
||||
@@ -20,7 +20,5 @@ function isFlagEnabled(name: string) {
|
||||
|
||||
export const flags = {
|
||||
showSites: isFlagEnabled('sites'),
|
||||
showCsvImport: isFlagEnabled('csv-import'),
|
||||
showAttributeEncrypt: isFlagEnabled('column-encrypt'),
|
||||
showIndexLengths: isFlagEnabled('index-lengths')
|
||||
};
|
||||
|
||||
@@ -60,9 +60,8 @@ export type TableRootProp = {
|
||||
selectedAll: boolean;
|
||||
selectedNone: boolean;
|
||||
selectedSome: boolean;
|
||||
// columns: Array<Column> | number;
|
||||
columns: Record<PinkColumn['id'], PinkColumn>;
|
||||
// columnsMap: Record<PinkColumn['id'], PinkColumn>;
|
||||
columns: Array<PinkColumn> | number;
|
||||
columnsMap: Record<PinkColumn['id'], PinkColumn>;
|
||||
toggle: (id: string) => void;
|
||||
toggleAll: () => void;
|
||||
addAvailableId: (id: string) => void;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 949 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 415 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 717 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
@@ -1,19 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Layout, Typography, Input, Tag, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { Layout, Typography, Input, Tag, Icon, Alert } from '@appwrite.io/pink-svelte';
|
||||
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { CustomId } from '$lib/components/index.js';
|
||||
import { getFlagUrl } from '$lib/helpers/flag';
|
||||
import { isCloud } from '$lib/system.js';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export let projectName: string;
|
||||
export let id: string;
|
||||
export let regions: Array<Models.ConsoleRegion> = [];
|
||||
export let region: string;
|
||||
export let showTitle = true;
|
||||
let {
|
||||
projectName = $bindable(''),
|
||||
id = $bindable(''),
|
||||
regions = [],
|
||||
region = $bindable(''),
|
||||
showTitle = true,
|
||||
projects = undefined,
|
||||
submit
|
||||
}: {
|
||||
projectName: string;
|
||||
id: string;
|
||||
regions: Array<Models.ConsoleRegion>;
|
||||
region: string;
|
||||
showTitle: boolean;
|
||||
projects?: number;
|
||||
submit?: Snippet;
|
||||
} = $props();
|
||||
|
||||
let showCustomId = false;
|
||||
let showCustomId = $state(false);
|
||||
let projectsLimited = $derived(
|
||||
$currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -22,44 +42,57 @@
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<form on:submit|preventDefault>
|
||||
<Layout.Stack direction="column" gap="xxl">
|
||||
{#if showTitle}
|
||||
<Typography.Title size="l">Create your project</Typography.Title>
|
||||
{/if}
|
||||
{#if projectsLimited}
|
||||
<Alert.Inline status="warning" title="You've reached your limit of 2 projects">
|
||||
Extra projects are available on paid plans for an additional fee
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
compact
|
||||
size="s"
|
||||
href={`${base}/organization-${page.params.organization}/billing`}
|
||||
external
|
||||
text>Upgrade</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<Layout.Stack direction="column" gap="xxl">
|
||||
{#if showTitle}
|
||||
<Typography.Title size="l">Create your project</Typography.Title>
|
||||
{/if}
|
||||
<Layout.Stack direction="column" gap="xxl">
|
||||
<Layout.Stack direction="column" gap="xxl">
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<Input.Text
|
||||
label="Name"
|
||||
placeholder="Project name"
|
||||
required
|
||||
autofocus
|
||||
bind:value={projectName} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Tag
|
||||
size="s"
|
||||
on:click={() => {
|
||||
showCustomId = true;
|
||||
}}><Icon slot="start" icon={IconPencil} size="s" /> Project ID</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
|
||||
</Layout.Stack>
|
||||
{#if isCloud && regions.length > 0}
|
||||
<Layout.Stack gap="xs">
|
||||
<Input.Select
|
||||
required
|
||||
bind:value={region}
|
||||
placeholder="Select a region"
|
||||
options={filterRegions(regions)}
|
||||
label="Region" />
|
||||
<Typography.Text>Region cannot be changed after creation</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<Input.Text
|
||||
disabled={projectsLimited}
|
||||
label="Name"
|
||||
placeholder="Project name"
|
||||
required
|
||||
autofocus
|
||||
bind:value={projectName} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Tag
|
||||
size="s"
|
||||
on:click={() => {
|
||||
showCustomId = true;
|
||||
}}><Icon icon={IconPencil} slot="start" size="s" /> Project ID</Tag>
|
||||
</div>
|
||||
{/if}
|
||||
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
|
||||
</Layout.Stack>
|
||||
{#if isCloud && regions.length > 0}
|
||||
<Layout.Stack gap="xs">
|
||||
<Input.Select
|
||||
disabled={projectsLimited}
|
||||
required
|
||||
bind:value={region}
|
||||
placeholder="Select a region"
|
||||
options={filterRegions(regions)}
|
||||
label="Region" />
|
||||
<Typography.Text>Region cannot be changed after creation</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<slot name="submit"></slot>
|
||||
</Layout.Stack>
|
||||
</form>
|
||||
{@render submit?.()}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -313,6 +313,7 @@ export type Plan = {
|
||||
webhooks: number;
|
||||
users: number;
|
||||
teams: number;
|
||||
projects: number;
|
||||
databases: number;
|
||||
databasesAllowEncrypt: boolean;
|
||||
buckets: number;
|
||||
@@ -592,6 +593,25 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async updateSelectedProjects(
|
||||
organizationId: string,
|
||||
projects: string[]
|
||||
): Promise<Organization> {
|
||||
const path = `/organizations/${organizationId}/projects`;
|
||||
const params = {
|
||||
projects
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'patch',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async setOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
paymentMethodId: string
|
||||
|
||||
@@ -39,6 +39,7 @@ import { sdk } from './sdk';
|
||||
import { user } from './user';
|
||||
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
|
||||
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
|
||||
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
|
||||
import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
|
||||
@@ -68,6 +69,7 @@ export const roles = [
|
||||
|
||||
export const teamStatusReadonly = 'readonly';
|
||||
export const billingLimitOutstandingInvoice = 'outstanding_invoice';
|
||||
export const billingProjectsLimitDate = '2024-09-01';
|
||||
|
||||
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
|
||||
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
|
||||
@@ -287,6 +289,7 @@ export function checkForEnterpriseTrial(org: Organization) {
|
||||
}
|
||||
|
||||
export function calculateEnterpriseTrial(org: Organization) {
|
||||
if (!org || !org.billingNextInvoiceDate) return 0;
|
||||
const endDate = new Date(org.billingNextInvoiceDate);
|
||||
const startDate = new Date(org.billingCurrentInvoiceDate);
|
||||
const today = new Date();
|
||||
@@ -314,6 +317,24 @@ export function calculateTrialDay(org: Organization) {
|
||||
return days;
|
||||
}
|
||||
|
||||
export async function checkForProjectsLimit(org: Organization, projects: number) {
|
||||
if (!isCloud) return;
|
||||
if (!org || !projects) return;
|
||||
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
|
||||
if (!plan) return;
|
||||
if (plan.$id !== BillingPlan.FREE) return;
|
||||
if (org.projects?.length > 0) return;
|
||||
|
||||
if (plan.projects > 0 && projects > plan.projects) {
|
||||
headerAlert.add({
|
||||
id: 'projectsLimitReached',
|
||||
component: ProjectsLimit,
|
||||
show: true,
|
||||
importance: 12
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUsageLimit(org: Organization) {
|
||||
if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) {
|
||||
headerAlert.add({
|
||||
|
||||
@@ -34,6 +34,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
|
||||
billingInvoiceId: string;
|
||||
status: string;
|
||||
remarks: string;
|
||||
projects: string[];
|
||||
};
|
||||
|
||||
export type OrganizationList = {
|
||||
@@ -61,5 +62,4 @@ export const organizationList = derived(
|
||||
export const organization = derived(page, ($page) => $page.data?.organization as Organization);
|
||||
export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Plan);
|
||||
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
|
||||
|
||||
export const regions = writable<Models.ConsoleRegionList>({ total: 0, regions: [] });
|
||||
|
||||
@@ -180,7 +180,7 @@ function createPreferences() {
|
||||
names: TeamPreferences['names']
|
||||
) => {
|
||||
let teamPrefs: Models.Preferences;
|
||||
update((n) => {
|
||||
await updateAndSync((n) => {
|
||||
if (!n?.displayNames) {
|
||||
n ??= {};
|
||||
n.displayNames ??= {};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
checkForMarkedForDeletion,
|
||||
checkForMissingPaymentMethod,
|
||||
checkForNewDevUpgradePro,
|
||||
checkForProjectsLimit,
|
||||
checkForUsageLimit,
|
||||
checkPaymentAuthorizationRequired,
|
||||
paymentExpired,
|
||||
@@ -296,6 +297,7 @@
|
||||
if (currentOrganizationId === org.$id) return;
|
||||
if (isCloud) {
|
||||
currentOrganizationId = org.$id;
|
||||
checkForProjectsLimit(org, data.projects?.projects?.length || 0);
|
||||
checkForEnterpriseTrial(org);
|
||||
await checkForUsageLimit(org);
|
||||
checkForMarkedForDeletion(org);
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LayoutLoad } from './$types';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import type { Plan, PlanList } from '$lib/sdk/billing';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
const { organizations } = await parent();
|
||||
@@ -28,6 +29,20 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
preferences.organization ??
|
||||
(organizations.teams.length > 0 ? organizations.teams[0].$id : undefined);
|
||||
|
||||
// Load projects for the current organization if one is selected
|
||||
let projects = null;
|
||||
if (currentOrgId) {
|
||||
try {
|
||||
projects = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', currentOrgId),
|
||||
Query.limit(1000) // Get all projects for organization
|
||||
]);
|
||||
} catch (e) {
|
||||
// Handle error silently - projects might not be accessible
|
||||
projects = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plansInfo,
|
||||
roles: [],
|
||||
@@ -36,7 +51,8 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
currentOrgId,
|
||||
organizations,
|
||||
consoleVariables,
|
||||
version: versionData?.version ?? null
|
||||
version: versionData?.version ?? null,
|
||||
projects
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
if (organization) {
|
||||
org = await sdk.forConsole.billing.setBillingAddress(organization, response.$id);
|
||||
trackEvent(Submit.OrganizationBillingAddressUpdate);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.ORGANIZATIONS);
|
||||
}
|
||||
await invalidate(Dependencies.ADDRESS);
|
||||
|
||||
|
||||
@@ -1,11 +1,76 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { isSameDay } from '$lib/helpers/date';
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
import AutoIncrementDark from '$lib/images/promos/auto-increment-dark.png';
|
||||
import AutoIncrementLight from '$lib/images/promos/auto-increment-light.png';
|
||||
import EncryptedDatabasesDark from '$lib/images/promos/encrypted-attribute-dark.png';
|
||||
import EncryptedDatabasesLight from '$lib/images/promos/encrypted-attribute-light.png';
|
||||
import OptInRelationDark from '$lib/images/promos/opt-relation-dark.png';
|
||||
import OptInRelationLight from '$lib/images/promos/opt-relation-light.png';
|
||||
|
||||
const listOfPromotions: BottomModalAlertItem[] = [];
|
||||
|
||||
if (isCloud) {
|
||||
/* add promos here */
|
||||
const encryptedAttributePromo: BottomModalAlertItem = {
|
||||
id: 'modal:encrypted_attribute_announcement',
|
||||
src: {
|
||||
dark: EncryptedDatabasesDark,
|
||||
light: EncryptedDatabasesLight
|
||||
},
|
||||
title: 'Introducing Encrypted string attribute support',
|
||||
message: 'Encrypt string attributes at rest, directly from the Appwrite Console.',
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
scope: 'project',
|
||||
cta: {
|
||||
text: 'Read announcement',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-encrypted-string-attributes',
|
||||
external: true,
|
||||
hideOnClick: true
|
||||
},
|
||||
show: true
|
||||
};
|
||||
const autoIncrementPromo: BottomModalAlertItem = {
|
||||
id: 'modal:auto_increment_announcement',
|
||||
src: {
|
||||
dark: AutoIncrementDark,
|
||||
light: AutoIncrementLight
|
||||
},
|
||||
title: 'Announcing Auto-increment support',
|
||||
message:
|
||||
'Get a built-in numeric identifier that increases predictably with every new document added.',
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
scope: 'project',
|
||||
cta: {
|
||||
text: 'Read announcement',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-auto-increment-support',
|
||||
external: true,
|
||||
hideOnClick: true
|
||||
},
|
||||
show: true
|
||||
};
|
||||
const optInRelationPromo: BottomModalAlertItem = {
|
||||
id: 'modal:opt_in_relation_announcement',
|
||||
src: {
|
||||
dark: OptInRelationDark,
|
||||
light: OptInRelationLight
|
||||
},
|
||||
title: 'Introducing Opt-in relationship loading',
|
||||
message:
|
||||
'Gain full control over which related documents to fetch and drastically reduce payload sizes.',
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
scope: 'project',
|
||||
cta: {
|
||||
text: 'Read announcement',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-opt-in-relationship-loading',
|
||||
external: true,
|
||||
hideOnClick: true
|
||||
},
|
||||
show: false
|
||||
};
|
||||
listOfPromotions.push(optInRelationPromo, autoIncrementPromo, encryptedAttributePromo);
|
||||
}
|
||||
|
||||
export function addBottomModalAlerts() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Organization } from '$lib/stores/organization';
|
||||
|
||||
export const load: PageLoad = async ({ url, parent, depends }) => {
|
||||
const { organizations } = await parent();
|
||||
depends(Dependencies.CREATE_ORGANIZATION);
|
||||
depends(Dependencies.ORGANIZATIONS);
|
||||
|
||||
const [coupon, paymentMethods] = await Promise.all([
|
||||
getCoupon(url),
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
// fixes an edge case where
|
||||
// the org is not available for some reason!
|
||||
await invalidate(Dependencies.CREATE_ORGANIZATION);
|
||||
await invalidate(Dependencies.ORGANIZATIONS);
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
let isLoading = false;
|
||||
let id: string;
|
||||
let id: string = ID.unique();
|
||||
let startAnimation = false;
|
||||
let projectName = 'Appwrite project';
|
||||
let region = Region.Fra;
|
||||
@@ -99,14 +99,18 @@
|
||||
bind:projectName
|
||||
bind:id
|
||||
bind:region
|
||||
on:submit={createProject}>
|
||||
<svelte:fragment slot="submit">
|
||||
showTitle={true}>
|
||||
{#snippet submit()}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end">
|
||||
<Button.Button type="submit" variant="primary" size="s">
|
||||
<Button.Button
|
||||
on:click={createProject}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="s">
|
||||
Create
|
||||
</Button.Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</CreateProject>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
import { loading } from '$routes/store';
|
||||
import { ID, Region, type Models } from '@appwrite.io/console';
|
||||
import { openImportWizard } from '../project-[region]-[project]/settings/migrations/(import)';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { billingProjectsLimitDate, readOnly } from '$lib/stores/billing';
|
||||
import { onMount, type ComponentType } from 'svelte';
|
||||
import { canWriteProjects } from '$lib/stores/roles';
|
||||
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
|
||||
import { Badge, Icon, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Badge, Icon, Typography, Alert, Tag, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconAndroid,
|
||||
IconApple,
|
||||
@@ -37,13 +37,16 @@
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { getPlatformInfo } from '$lib/helpers/platform';
|
||||
import CreateProjectCloud from './createProjectCloud.svelte';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import { currentPlan, regions as regionsStore } from '$lib/stores/organization';
|
||||
import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
|
||||
export let data;
|
||||
|
||||
let showCreate = false;
|
||||
let showCreateProjectCloud = false;
|
||||
let addOrganization = false;
|
||||
let showSelectProject = false;
|
||||
|
||||
function filterPlatforms(platforms: { name: string; icon: string }[]) {
|
||||
return platforms.filter(
|
||||
@@ -117,8 +120,25 @@
|
||||
function findRegion(project: Models.Project) {
|
||||
return $regionsStore.regions.find((region) => region.$id === project.region);
|
||||
}
|
||||
|
||||
function isSetToArchive(project: Models.Project): boolean {
|
||||
if (!isCloud) return false;
|
||||
if (data.organization.projects?.length === 0) return false;
|
||||
if (!project || !project.$id) return false;
|
||||
return !data.organization.projects?.includes(project.$id);
|
||||
}
|
||||
|
||||
function formatName(name: string, limit: number = 19) {
|
||||
return name ? (name.length > limit ? `${name.slice(0, limit)}...` : name) : '-';
|
||||
}
|
||||
|
||||
$: projectsToArchive = data.projects.projects.filter(
|
||||
(project) => !data.organization.projects?.includes(project.$id)
|
||||
);
|
||||
</script>
|
||||
|
||||
<SelectProjectCloud selectedProjects={data.organization.projects || []} bind:showSelectProject />
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Typography.Title>Projects</Typography.Title>
|
||||
@@ -144,6 +164,27 @@
|
||||
</DropList>
|
||||
</div>
|
||||
|
||||
{#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects}
|
||||
<Alert.Inline
|
||||
title={`${data.projects.total - data.organization.projects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
|
||||
<Typography.Text>
|
||||
{#each projectsToArchive as project, index}{@const text = `<b>${project.name}</b>`}
|
||||
{@html text}{index == projectsToArchive.length - 2
|
||||
? ', and '
|
||||
: index < projectsToArchive.length - 1
|
||||
? ', '
|
||||
: ''}
|
||||
{/each}
|
||||
will be archived
|
||||
</Typography.Text>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary size="s" on:click={() => (showSelectProject = true)}>
|
||||
Manage projects
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
{#if data.projects.total}
|
||||
<CardContainer
|
||||
disableEmpty={!$canWriteProjects}
|
||||
@@ -154,13 +195,35 @@
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
)}
|
||||
{@const formatted = isSetToArchive(project)
|
||||
? formatName(project.name)
|
||||
: project.name}
|
||||
<GridItem1
|
||||
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
{project.name}
|
||||
<Tooltip
|
||||
maxWidth={project.name.length.toString()}
|
||||
placement="top"
|
||||
disabled={!isSetToArchive(project)}>
|
||||
{formatted}
|
||||
<span slot="tooltip">
|
||||
{project.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="status">
|
||||
{#if isSetToArchive(project)}
|
||||
<Tag
|
||||
size="s"
|
||||
on:click={(event) => {
|
||||
event.preventDefault();
|
||||
showSelectProject = true;
|
||||
}}>Set to archive</Tag>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#each platforms.slice(0, 2) as platform}
|
||||
@@ -210,4 +273,8 @@
|
||||
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
|
||||
<CreateProjectCloud bind:showCreateProjectCloud regions={$regionsStore.regions} />
|
||||
<CreateProjectCloud
|
||||
projects={data.projects.total}
|
||||
bind:showCreateProjectCloud
|
||||
regions={$regionsStore.regions}
|
||||
teamId={page.params.organization} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import BudgetCap from './budgetCap.svelte';
|
||||
import PlanSummary from './planSummary.svelte';
|
||||
import BillingAddress from './billingAddress.svelte';
|
||||
@@ -25,14 +24,15 @@
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let organization = data.organization;
|
||||
|
||||
// why are these reactive?
|
||||
$: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
|
||||
(method: PaymentMethodData) => method.$id === organization?.paymentMethodId
|
||||
);
|
||||
|
||||
$: backupPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.backupPaymentMethodId
|
||||
(method: PaymentMethodData) => method.$id === organization?.backupPaymentMethodId
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
@@ -52,10 +52,10 @@
|
||||
);
|
||||
|
||||
await confirmPayment(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
invoice.clientSecret,
|
||||
$organization.paymentMethodId,
|
||||
`${base}/organization-${$organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}`
|
||||
organization.paymentMethodId,
|
||||
`${base}/organization-${organization.$id}/billing?type=validate-invoice&invoice=${invoice.$id}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
page.url.searchParams.get('type') === 'validate-invoice'
|
||||
) {
|
||||
const invoiceId = page.url.searchParams.get('invoice');
|
||||
await sdk.forConsole.billing.updateInvoiceStatus($organization.$id, invoiceId);
|
||||
await sdk.forConsole.billing.updateInvoiceStatus(organization.$id, invoiceId);
|
||||
invalidate(Dependencies.INVOICES);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
}
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
if (page.url.searchParams.has('clientSecret')) {
|
||||
const clientSecret = page.url.searchParams.get('clientSecret');
|
||||
await confirmPayment($organization.$id, clientSecret, $organization.paymentMethodId);
|
||||
await confirmPayment(organization.$id, clientSecret, organization.paymentMethodId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -93,7 +93,7 @@
|
||||
{#if $failedInvoice}
|
||||
{#if $failedInvoice?.lastError}
|
||||
<Alert.Inline status="error">
|
||||
The scheduled payment for {$organization.name} failed due to following error: {$failedInvoice.lastError}
|
||||
The scheduled payment for {organization.name} failed due to following error: {$failedInvoice.lastError}
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
text
|
||||
@@ -106,7 +106,7 @@
|
||||
{:else}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
title={`The scheduled payment for ${$organization.name} failed`}>
|
||||
title={`The scheduled payment for ${organization.name} failed`}>
|
||||
To avoid service disruptions in organization's your projects, please verify your
|
||||
payment details and update them if necessary.
|
||||
</Alert.Inline>
|
||||
@@ -115,29 +115,33 @@
|
||||
{#if defaultPaymentMethod?.failed && !backupPaymentMethod}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
title={`The default payment method for ${$organization.name} has expired`}>
|
||||
title={`The default payment method for ${organization.name} has expired`}>
|
||||
To avoid service disruptions in your organization's projects, please update your payment
|
||||
details.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{#if $organization?.billingPlanDowngrade}
|
||||
{#if organization?.billingPlanDowngrade}
|
||||
<Alert.Inline status="info">
|
||||
Your organization has changed to {tierToPlan($organization?.billingPlanDowngrade).name} plan.
|
||||
You will continue to have access to {tierToPlan($organization?.billingPlan).name} plan features
|
||||
until your billing period ends on {toLocaleDate($organization.billingNextInvoiceDate)}.
|
||||
Your organization has changed to {tierToPlan(organization?.billingPlanDowngrade).name} plan.
|
||||
You will continue to have access to {tierToPlan(organization?.billingPlan).name} plan features
|
||||
until your billing period ends on {toLocaleDate(organization.billingNextInvoiceDate)}.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<Typography.Title>Billing</Typography.Title>
|
||||
<PlanSummary
|
||||
availableCredit={data?.availableCredit}
|
||||
currentPlan={data?.aggregationBillingPlan}
|
||||
currentPlan={data?.currentPlan}
|
||||
currentAggregation={data?.billingAggregation}
|
||||
currentInvoice={data?.billingInvoice} />
|
||||
<PaymentHistory />
|
||||
<PaymentMethods methods={data?.paymentMethods} />
|
||||
<BillingAddress {data} />
|
||||
<PaymentMethods organization={data?.organization} methods={data?.paymentMethods} />
|
||||
<BillingAddress
|
||||
organization={data?.organization}
|
||||
billingAddress={data?.billingAddress}
|
||||
locale={data?.locale}
|
||||
countryList={data?.countryList} />
|
||||
<TaxId />
|
||||
<BudgetCap />
|
||||
<BudgetCap organization={data?.organization} currentPlan={data?.currentPlan} />
|
||||
<AvailableCredit areCreditsSupported={data.areCreditsSupported} />
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends }) => {
|
||||
const { organization, scopes, currentPlan, countryList, locale, plansInfo } = await parent();
|
||||
const { organization, scopes, currentPlan, countryList, locale } = await parent();
|
||||
|
||||
if (!scopes.includes('billing.read')) {
|
||||
return redirect(301, `/console/organization-${organization.$id}`);
|
||||
@@ -64,10 +64,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
areCreditsSupported ? sdk.forConsole.billing.getAvailableCredit(organization.$id) : null
|
||||
]);
|
||||
|
||||
const aggregationBillingPlan = plansInfo.get(
|
||||
billingAggregation?.plan ?? organization.billingPlan
|
||||
);
|
||||
|
||||
// make number
|
||||
const credits = availableCredit ? availableCredit.available : null;
|
||||
|
||||
@@ -75,7 +71,6 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
paymentMethods,
|
||||
addressList,
|
||||
billingAddress,
|
||||
aggregationBillingPlan,
|
||||
availableCredit: credits,
|
||||
billingAggregation,
|
||||
billingInvoice,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import { addressList } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import RemoveAddress from './removeAddress.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
@@ -23,14 +23,11 @@
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const locale: Models.Locale = data.locale;
|
||||
const countryList: Models.CountryList = data.countryList;
|
||||
|
||||
let billingAddress: Address = data?.billingAddress;
|
||||
export let organization: Organization;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
export let billingAddress: Address;
|
||||
|
||||
let showCreate = false;
|
||||
let showEdit = false;
|
||||
@@ -39,16 +36,16 @@
|
||||
|
||||
async function addAddress(addressId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setBillingAddress($organization.$id, addressId);
|
||||
await sdk.forConsole.billing.setBillingAddress(organization.$id, addressId);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new billing address has been added to ${$organization.name}`
|
||||
message: `A new billing address has been added to ${organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationBillingAddressUpdate);
|
||||
|
||||
invalidate(Dependencies.ADDRESS);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
invalidate(Dependencies.ORGANIZATIONS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -63,7 +60,7 @@
|
||||
<svelte:fragment slot="title">Billing address</svelte:fragment>
|
||||
View or update your billing address. This address will be included in your invoices from Appwrite.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization?.billingAddressId && billingAddress}
|
||||
{#if organization?.billingAddressId && billingAddress}
|
||||
<Card.Base variant="secondary" padding="s">
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<div>
|
||||
@@ -140,7 +137,7 @@
|
||||
</CardGrid>
|
||||
|
||||
{#if showCreate}
|
||||
<AddressModal bind:show={showCreate} organization={$organization?.$id} {countryList} {locale} />
|
||||
<AddressModal bind:show={showCreate} organization={organization?.$id} {countryList} {locale} />
|
||||
{/if}
|
||||
{#if showEdit}
|
||||
<EditAddressModal
|
||||
|
||||
@@ -7,18 +7,20 @@
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { Alert, Icon, Table } from '@appwrite.io/pink-svelte';
|
||||
import { IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export let organization: Organization;
|
||||
export let currentPlan: Plan;
|
||||
export let alertsEnabled = false;
|
||||
|
||||
let search: string;
|
||||
let selectedAlert: number;
|
||||
let alerts: number[] = [];
|
||||
let alerts: number[] = organization?.budgetAlerts ?? [];
|
||||
|
||||
$: options = [
|
||||
{ value: 25, label: '25%', disabled: false },
|
||||
@@ -33,10 +35,6 @@
|
||||
};
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
alerts = $organization?.budgetAlerts ?? [];
|
||||
});
|
||||
|
||||
function addAlert() {
|
||||
if (alerts.some((alert) => alert === selectedAlert)) {
|
||||
return;
|
||||
@@ -50,8 +48,8 @@
|
||||
async function updateBudget() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateBudget(
|
||||
$organization.$id,
|
||||
$organization.billingBudget,
|
||||
organization.$id,
|
||||
organization.billingBudget,
|
||||
alerts
|
||||
);
|
||||
|
||||
@@ -60,7 +58,7 @@
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
message: `<span> ${alerts.length === 0 ? 'Budget alerts removed from' : alerts.length > 1 ? `Budget alerts added to` : 'A budget alert has been added to'} <b>${$organization.name}</b> </span>`
|
||||
message: `<span> ${alerts.length === 0 ? 'Budget alerts removed from' : alerts.length > 1 ? `Budget alerts added to` : 'A budget alert has been added to'} <b>${organization.name}</b> </span>`
|
||||
});
|
||||
trackEvent(Submit.BudgetAlertsUpdate, {
|
||||
alerts
|
||||
@@ -75,26 +73,26 @@
|
||||
}
|
||||
|
||||
$: isButtonDisabled =
|
||||
symmetricDifference(alerts, $organization.budgetAlerts).length === 0 || !alertsEnabled;
|
||||
symmetricDifference(alerts, organization.budgetAlerts).length === 0 || !alertsEnabled;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Billing alerts</svelte:fragment>
|
||||
{#if !$currentPlan.budgeting}
|
||||
{#if !currentPlan.budgeting}
|
||||
Get notified by email when your organization meets a percentage of your budget cap. <b
|
||||
>{tierToPlan($organization.billingPlan).name} organizations will receive one notification
|
||||
>{tierToPlan(organization.billingPlan).name} organizations will receive one notification
|
||||
at 75% resource usage.</b>
|
||||
{:else}
|
||||
Get notified by email when your organization meets or exceeds a percentage of your specified
|
||||
billing alert(s).
|
||||
{/if}
|
||||
<svelte:fragment slot="aside">
|
||||
{#if !$currentPlan.budgeting}
|
||||
{#if !currentPlan.budgeting}
|
||||
<Alert.Inline status="info" title="Billing alerts are a Pro plan feature">
|
||||
Upgrade to a Pro plan to manage when you receive billing alerts for your
|
||||
organization.
|
||||
</Alert.Inline>
|
||||
{:else if !$currentPlan.budgetCapEnabled}
|
||||
{:else if !currentPlan.budgetCapEnabled}
|
||||
<Alert.Inline status="info" title="Budget cap disabled">
|
||||
Budget caps are not supported on your current plan. For more information, please
|
||||
reach out to your customer success manager.
|
||||
@@ -150,7 +148,7 @@
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Form onSubmit={updateBudget}>
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
|
||||
{#if organization?.billingPlan === BillingPlan.FREE || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
|
||||
@@ -6,31 +6,28 @@
|
||||
import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms';
|
||||
import { showUsageRatesModal, upgradeURL } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization, currentPlan } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert, Link } from '@appwrite.io/pink-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import BudgetAlert from './budgetAlert.svelte';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
let capActive = false;
|
||||
let budget: number;
|
||||
|
||||
onMount(() => {
|
||||
budget = $organization?.billingBudget;
|
||||
capActive = $organization?.billingBudget !== null;
|
||||
});
|
||||
export let currentPlan: Plan;
|
||||
export let organization: Organization;
|
||||
let capActive = organization?.billingBudget !== null;
|
||||
let budget = organization.billingBudget;
|
||||
|
||||
async function updateBudget() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateBudget(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
budget,
|
||||
$organization.budgetAlerts
|
||||
organization.budgetAlerts
|
||||
);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Budget cap enabled for ${$organization.name}`
|
||||
message: `Budget cap enabled for ${organization.name}`
|
||||
});
|
||||
trackEvent(Submit.BudgetCapUpdate, {
|
||||
budget: capActive ? budget : undefined
|
||||
@@ -55,7 +52,7 @@
|
||||
Restrict your resource usage by setting a budget cap. Cap usage is reset at the beginning of
|
||||
each billing cycle.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if !$currentPlan.budgeting}
|
||||
{#if !currentPlan.budgeting}
|
||||
<Alert.Inline status="info" title="Budget caps are a Pro plan feature">
|
||||
Upgrade to a Pro plan to set a budget cap for your organization. For more
|
||||
information on what you can do with a Pro plan,
|
||||
@@ -64,7 +61,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">view our pricing guide.</Link.Anchor>
|
||||
</Alert.Inline>
|
||||
{:else if !$currentPlan.budgetCapEnabled}
|
||||
{:else if !currentPlan.budgetCapEnabled}
|
||||
<Alert.Inline status="info" title="Budget cap disabled">
|
||||
Budget caps are not supported on your current plan. For more information, please
|
||||
reach out to your customer success manager.
|
||||
@@ -92,7 +89,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if organization?.billingPlan === BillingPlan.FREE}
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
@@ -105,10 +102,10 @@
|
||||
>Upgrade to Pro
|
||||
</Button>
|
||||
{:else}
|
||||
<Button disabled={$organization?.billingBudget === budget} submit>Update</Button>
|
||||
<Button disabled={organization?.billingBudget === budget} submit>Update</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<BudgetAlert alertsEnabled={capActive && budget > 0} />
|
||||
<BudgetAlert {organization} {currentPlan} alertsEnabled={capActive && budget > 0} />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { CardGrid, CreditCardBrandImage, CreditCardInfo } from '$lib/components';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
@@ -34,6 +34,7 @@
|
||||
IconTrash
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let organization: Organization;
|
||||
export let methods: PaymentList;
|
||||
|
||||
let showPayment = false;
|
||||
@@ -47,12 +48,12 @@
|
||||
async function addPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethod(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new payment method has been added to ${$organization.name}`
|
||||
message: `A new payment method has been added to ${organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationPaymentUpdate);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
@@ -68,12 +69,12 @@
|
||||
async function addBackupPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethodBackup(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new payment method has been added to ${$organization.name}`
|
||||
message: `A new payment method has been added to ${organization.name}`
|
||||
});
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
} catch (error) {
|
||||
@@ -85,15 +86,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($organization?.backupPaymentMethodId) {
|
||||
$: if (organization?.backupPaymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getOrganizationPaymentMethod($organization.$id, $organization.backupPaymentMethodId)
|
||||
.getOrganizationPaymentMethod(organization.$id, organization.backupPaymentMethodId)
|
||||
.then((res) => (backupPaymentMethod = res));
|
||||
}
|
||||
|
||||
$: if ($organization?.paymentMethodId) {
|
||||
$: if (organization?.paymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getOrganizationPaymentMethod($organization.$id, $organization.paymentMethodId)
|
||||
.getOrganizationPaymentMethod(organization.$id, organization.paymentMethodId)
|
||||
.then((res) => (defaultPaymentMethod = res));
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@
|
||||
<svelte:fragment slot="title">Payment methods</svelte:fragment>
|
||||
View or update your organization payment methods here.
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization?.paymentMethodId}
|
||||
{#if organization?.paymentMethodId}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
@@ -168,7 +169,7 @@
|
||||
</Popover>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{#if $organization?.backupPaymentMethodId}
|
||||
{#if organization?.backupPaymentMethodId}
|
||||
<Table.Row.Base {root}>
|
||||
<CreditCardInfo {root} isBackup paymentMethod={backupPaymentMethod} />
|
||||
<Table.Cell column="actions" {root}>
|
||||
@@ -209,9 +210,9 @@
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
</Table.Root>
|
||||
{#if !$organization?.backupPaymentMethodId}
|
||||
{#if !organization?.backupPaymentMethodId}
|
||||
{@const filteredPaymentMethods = methods.paymentMethods.filter(
|
||||
(o) => !!o.last4 && o.$id !== $organization?.paymentMethodId
|
||||
(o) => !!o.last4 && o.$id !== organization?.paymentMethodId
|
||||
)}
|
||||
<div>
|
||||
<Popover let:toggle placement="bottom-start" padding="none">
|
||||
@@ -264,7 +265,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
{@const filteredPaymentMethods = methods.paymentMethods.filter(
|
||||
(o) => !!o.last4 && o.$id !== $organization?.backupPaymentMethodId
|
||||
(o) => !!o.last4 && o.$id !== organization?.backupPaymentMethodId
|
||||
)}
|
||||
<Card.Base>
|
||||
<Layout.Stack justifyContent="center" alignItems="center" gap="m">
|
||||
@@ -319,15 +320,15 @@
|
||||
bind:show={showEdit} />
|
||||
{/if}
|
||||
{#if isCloud && hasStripePublicKey}
|
||||
<ReplaceCard {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
<ReplaceCard {organization} {methods} bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
{/if}
|
||||
{#if showDelete && isCloud && hasStripePublicKey}
|
||||
{@const hasOtherMethod = isSelectedBackup
|
||||
? !!$organization?.paymentMethodId
|
||||
: !!$organization?.backupPaymentMethodId}
|
||||
? !!organization?.paymentMethodId
|
||||
: !!organization?.backupPaymentMethodId}
|
||||
<DeleteOrgPayment
|
||||
bind:showDelete
|
||||
{hasOtherMethod}
|
||||
isBackup={isSelectedBackup}
|
||||
disabled={$organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
|
||||
disabled={organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
|
||||
{/if}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
message: `The billing address has been removed from ${$organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationBillingAddressDelete);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
invalidate(Dependencies.ORGANIZATIONS);
|
||||
show = false;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FakeModal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { submitStripeCard } from '$lib/stores/stripe';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -12,6 +12,7 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { PaymentBoxes } from '$lib/components/billing';
|
||||
|
||||
export let organization: Organization;
|
||||
export let show = false;
|
||||
export let isBackup = false;
|
||||
export let methods: PaymentList;
|
||||
@@ -21,12 +22,12 @@
|
||||
let selectedPaymentMethodId: string;
|
||||
|
||||
onMount(async () => {
|
||||
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
|
||||
if (!organization.paymentMethodId && !organization.backupPaymentMethodId) {
|
||||
selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null;
|
||||
} else {
|
||||
selectedPaymentMethodId = isBackup
|
||||
? $organization.backupPaymentMethodId
|
||||
: $organization.paymentMethodId;
|
||||
? organization.backupPaymentMethodId
|
||||
: organization.paymentMethodId;
|
||||
// If the selected payment method does not belong to the current user, select the first one.
|
||||
if (
|
||||
methods?.total &&
|
||||
@@ -40,7 +41,7 @@
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (selectedPaymentMethodId === '$new') {
|
||||
const method = await submitStripeCard(name, $organization.$id);
|
||||
const method = await submitStripeCard(name, organization.$id);
|
||||
selectedPaymentMethodId = method.$id;
|
||||
}
|
||||
isBackup
|
||||
@@ -68,7 +69,7 @@
|
||||
async function addPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethod(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -79,7 +80,7 @@
|
||||
async function addBackupPaymentMethod(paymentMethodId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethodBackup(
|
||||
$organization.$id,
|
||||
organization.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -97,18 +98,18 @@
|
||||
methods={filteredMethods}
|
||||
bind:name
|
||||
bind:group={selectedPaymentMethodId}
|
||||
defaultMethod={$organization?.paymentMethodId}
|
||||
backupMethod={$organization?.backupPaymentMethodId}
|
||||
defaultMethod={organization?.paymentMethodId}
|
||||
backupMethod={organization?.backupPaymentMethodId}
|
||||
disabledCondition={isBackup
|
||||
? $organization.paymentMethodId
|
||||
: $organization.backupPaymentMethodId} />
|
||||
? organization.paymentMethodId
|
||||
: organization.backupPaymentMethodId} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button
|
||||
secondary
|
||||
submit
|
||||
disabled={selectedPaymentMethodId ===
|
||||
(isBackup ? $organization.backupPaymentMethodId : $organization.paymentMethodId)}>
|
||||
(isBackup ? organization.backupPaymentMethodId : organization.paymentMethodId)}>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
let showCustomId = false;
|
||||
let disabled: boolean = false;
|
||||
let name: string = 'New project';
|
||||
let showSubmissionLoader = false;
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
disabled = true;
|
||||
showSubmissionLoader = true;
|
||||
const project = await sdk.forConsole.projects.create(id ?? ID.unique(), name, teamId);
|
||||
show = false;
|
||||
dispatch('created', project);
|
||||
@@ -40,7 +42,9 @@
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.ProjectCreate);
|
||||
} finally {
|
||||
disabled = false;
|
||||
showSubmissionLoader = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -62,6 +66,10 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit {disabled}>Create</Button>
|
||||
<Button
|
||||
submit
|
||||
{disabled}
|
||||
forceShowLoader={showSubmissionLoader}
|
||||
submissionLoader={showSubmissionLoader}>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { ID, type Models, Region as ConsoleRegion, Region } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import CreateProject from '$lib/layout/createProject.svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let teamId: string;
|
||||
export let showCreateProjectCloud: boolean;
|
||||
export let projects: number;
|
||||
export let regions: Array<Models.ConsoleRegion> = [];
|
||||
|
||||
let id: string = null;
|
||||
@@ -20,7 +22,6 @@
|
||||
let region: ConsoleRegion = Region.Fra;
|
||||
|
||||
let showSubmissionLoader = false;
|
||||
const teamId = page.params.organization;
|
||||
|
||||
async function create() {
|
||||
let project: Models.Project;
|
||||
@@ -60,11 +61,18 @@
|
||||
title={'Create project'}
|
||||
onSubmit={create}
|
||||
bind:error>
|
||||
<CreateProject showTitle={false} bind:id bind:projectName={name} bind:region {regions} />
|
||||
<CreateProject
|
||||
{projects}
|
||||
showTitle={false}
|
||||
bind:id
|
||||
bind:projectName={name}
|
||||
bind:region
|
||||
{regions} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
submit
|
||||
size="s"
|
||||
disabled={$currentPlan.projects > 0 && projects && projects >= $currentPlan?.projects}
|
||||
forceShowLoader={showSubmissionLoader}
|
||||
submissionLoader={showSubmissionLoader}>Create</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
external
|
||||
href="#/"
|
||||
href="https://appwrite.io/docs/products/network/dns"
|
||||
text
|
||||
event="empty_documentation"
|
||||
size="s"
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover } from '$lib/layout';
|
||||
import { daysLeftInTrial, getServiceLimit, plansInfo, readOnly } from '$lib/stores/billing';
|
||||
import {
|
||||
daysLeftInTrial,
|
||||
getServiceLimit,
|
||||
plansInfo,
|
||||
readOnly,
|
||||
tierToPlan
|
||||
} from '$lib/stores/billing';
|
||||
import { members, newMemberModal, type Organization } from '$lib/stores/organization';
|
||||
import {
|
||||
canSeeBilling,
|
||||
@@ -114,14 +120,25 @@
|
||||
{/if}
|
||||
|
||||
{#if $isOwner}
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={areMembersLimited}>
|
||||
<Icon icon={IconPlus} size="s" slot="start" />
|
||||
Invite
|
||||
</Button>
|
||||
<Tooltip disabled={!areMembersLimited} placement="bottom-end">
|
||||
<div>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={areMembersLimited}>
|
||||
<Icon icon={IconPlus} size="s" slot="start" />
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
<div slot="tooltip">
|
||||
{organization?.billingPlan === BillingPlan.FREE
|
||||
? 'Upgrade to add more members'
|
||||
: `You've reached the members limit for the ${
|
||||
tierToPlan(organization?.billingPlan)?.name
|
||||
} plan`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,11 @@
|
||||
Icon,
|
||||
Typography,
|
||||
Popover,
|
||||
ActionMenu
|
||||
ActionMenu,
|
||||
Tooltip
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -41,6 +44,13 @@
|
||||
let showEdit = false;
|
||||
let showDropdown = [];
|
||||
|
||||
// Calculate if button should be disabled and tooltip should show
|
||||
$: memberCount = data.organizationMembers?.total ?? 0;
|
||||
$: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1;
|
||||
$: isButtonDisabled = isCloud
|
||||
? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported
|
||||
: false;
|
||||
|
||||
const resend = async (member: Models.Membership) => {
|
||||
try {
|
||||
await sdk.forConsole.teams.createMembership(
|
||||
@@ -70,14 +80,25 @@
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Title>Members</Typography.Title>
|
||||
<ConsoleButton
|
||||
size="s"
|
||||
event="create_user"
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={isCloud ? !$currentPlan?.addons?.seats?.supported : false}>
|
||||
<Icon size="s" icon={IconPlus} slot="start" />
|
||||
<span class="text">Invite</span>
|
||||
</ConsoleButton>
|
||||
<Tooltip disabled={!isButtonDisabled} placement="bottom-end">
|
||||
<div>
|
||||
<ConsoleButton
|
||||
size="s"
|
||||
event="create_user"
|
||||
on:click={() => newMemberModal.set(true)}
|
||||
disabled={isButtonDisabled}>
|
||||
<Icon size="s" icon={IconPlus} slot="start" />
|
||||
<span class="text">Invite</span>
|
||||
</ConsoleButton>
|
||||
</div>
|
||||
<div slot="tooltip">
|
||||
{$organization?.billingPlan === BillingPlan.FREE
|
||||
? 'Upgrade to add more members'
|
||||
: `You've reached the members limit for the ${
|
||||
tierToPlan($organization?.billingPlan)?.name
|
||||
} plan`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root
|
||||
|
||||
@@ -67,11 +67,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
@@ -71,11 +71,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
+3
-5
@@ -75,11 +75,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
@@ -70,11 +70,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
@@ -65,11 +65,9 @@
|
||||
To complete the setup, create an OAuth2 client ID with "Web application" as the application
|
||||
type, then add this redirect URI to your {provider.name} configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
|
||||
@@ -65,11 +65,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
+3
-5
@@ -72,11 +72,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
@@ -109,11 +109,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
@@ -82,11 +82,9 @@
|
||||
<Alert.Inline status="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert.Inline>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
<CopyInput
|
||||
label="URI"
|
||||
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
|
||||
async function updateSessionLength() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateAuthDuration($project.$id, $baseValue);
|
||||
await sdk.forConsole.projects.updateAuthDuration(project.$id, $baseValue);
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
|
||||
addNotification({
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
let name = '';
|
||||
let id: string = null;
|
||||
let showCustomId = false;
|
||||
let showPlanUpgradeAlert = true;
|
||||
|
||||
const trackEvents = (policies) => {
|
||||
policies.forEach((policy) => {
|
||||
@@ -104,10 +103,6 @@
|
||||
trackError(error, Submit.DatabaseCreate);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (!showCreate) {
|
||||
showPlanUpgradeAlert = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Create database" onSubmit={create} bind:show={showCreate}>
|
||||
@@ -133,18 +128,12 @@
|
||||
|
||||
{#if isCloud}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if showPlanUpgradeAlert}
|
||||
<Alert.Inline
|
||||
dismissible
|
||||
title="This database won't be backed up"
|
||||
status="warning"
|
||||
on:dismiss={() => (showPlanUpgradeAlert = false)}>
|
||||
Upgrade your plan to ensure your data stays safe and backed up.
|
||||
<svelte:fragment slot="actions">
|
||||
<Button compact href={$upgradeURL}>Upgrade plan</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
<Alert.Inline title="This database won't be backed up" status="warning">
|
||||
Upgrade your plan to ensure your data stays safe and backed up.
|
||||
<svelte:fragment slot="actions">
|
||||
<Button compact href={$upgradeURL}>Upgrade plan</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{:else}
|
||||
<CreatePolicy
|
||||
bind:totalPolicies
|
||||
|
||||
+11
-13
@@ -24,7 +24,6 @@
|
||||
import { base } from '$app/paths';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { flags } from '$lib/flags';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -35,12 +34,13 @@
|
||||
const filterColumns = writable<Column[]>([]);
|
||||
|
||||
$: selected = preferences.getCustomTableColumns(page.params.table);
|
||||
|
||||
$: tableColumns.set(
|
||||
$table.columns.map((column) => ({
|
||||
id: column.key,
|
||||
title: column.key,
|
||||
type: column.type as ColumnType,
|
||||
hide: !selected?.includes(column.key),
|
||||
hide: !!selected?.includes(column.key),
|
||||
array: column?.array,
|
||||
format: 'format' in column && column?.format === 'enum' ? column.format : null,
|
||||
elements: 'elements' in column ? column.elements : null
|
||||
@@ -51,13 +51,13 @@
|
||||
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
|
||||
id,
|
||||
title: id,
|
||||
hide: false,
|
||||
show: true,
|
||||
type: (id === '$id' ? 'string' : 'datetime') as ColumnType
|
||||
}))
|
||||
]);
|
||||
|
||||
$: hasColumns = !!$table.columns.length;
|
||||
$: hasValidColumns = $table?.columns?.some((attr) => attr.status === 'available');
|
||||
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
|
||||
|
||||
async function onSelect(file: Models.File) {
|
||||
$isCsvImportInProgress = true;
|
||||
@@ -100,15 +100,13 @@
|
||||
analyticsSource="database_rows" />
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<ViewSelector view={data.view} columns={tableColumns} hideView isCustomTable />
|
||||
{#if flags.showCsvImport(data)}
|
||||
<Button
|
||||
secondary
|
||||
event={Click.DatabaseImportCsv}
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
secondary
|
||||
event={Click.DatabaseImportCsv}
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
|
||||
+1
-15
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { InputChoice } from '$lib/elements/forms';
|
||||
@@ -7,11 +7,9 @@
|
||||
import { table } from '../store';
|
||||
import type { Columns } from '../store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { isRelationship } from '../row-[row]/columns/store';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
|
||||
export let showDelete = false;
|
||||
export let selectedColumn: Columns;
|
||||
@@ -21,24 +19,12 @@
|
||||
let error: string;
|
||||
let checked = false;
|
||||
|
||||
async function updateTableColumns() {
|
||||
const selectedColumns = preferences
|
||||
.getCustomTableColumns($table.$id)
|
||||
.filter((column) => column != selectedColumn.key);
|
||||
|
||||
// todo: change method name to table
|
||||
await preferences.setCustomTableColumns($table.$id, selectedColumns);
|
||||
await invalidate(Dependencies.TABLE);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.tables.deleteColumn(databaseId, $table.$id, selectedColumn.key);
|
||||
|
||||
await updateTableColumns();
|
||||
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
+57
-49
@@ -50,7 +50,6 @@
|
||||
import { ActionMenu, Selector } from '@appwrite.io/pink-svelte';
|
||||
import { InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import { Popover, Layout, Tag, Typography, Link } from '@appwrite.io/pink-svelte';
|
||||
import { flags } from '$lib/flags';
|
||||
|
||||
export let data: Partial<Models.ColumnString> = {
|
||||
required: false,
|
||||
@@ -63,8 +62,6 @@
|
||||
|
||||
let savedDefault = data.default;
|
||||
|
||||
const showEncrypt = flags.showAttributeEncrypt(page.data);
|
||||
|
||||
function handleDefaultState(hideDefault: boolean) {
|
||||
if (hideDefault) {
|
||||
savedDefault = data.default;
|
||||
@@ -128,53 +125,55 @@
|
||||
disabled={data.required || editing}
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
|
||||
{#if showEncrypt}
|
||||
<Layout.Stack gap="xs" direction="column">
|
||||
<div class="popover-holder" class:disabled-checkbox={!supportsStringEncryption || editing}>
|
||||
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
|
||||
<Popover let:toggle placement="bottom-start">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="encrypt"
|
||||
bind:checked={data.encrypt}
|
||||
disabled={!supportsStringEncryption || editing} />
|
||||
<Layout.Stack gap="xs" direction="column">
|
||||
<div
|
||||
class="popover-holder"
|
||||
class:cursor-not-allowed={editing}
|
||||
class:disabled-checkbox={!supportsStringEncryption || editing}>
|
||||
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
|
||||
<Popover let:toggle placement="bottom-start">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="encrypt"
|
||||
bind:checked={data.encrypt}
|
||||
disabled={!supportsStringEncryption || editing} />
|
||||
|
||||
<Layout.Stack gap="xxs" direction="column">
|
||||
<button
|
||||
type="button"
|
||||
class="u-cursor-pointer"
|
||||
on:click={(e) => {
|
||||
if (!supportsStringEncryption) {
|
||||
toggle(e);
|
||||
} else {
|
||||
data.encrypt = !data.encrypt;
|
||||
}
|
||||
}}>
|
||||
<Layout.Stack inline gap="xxs" direction="row" alignItems="center">
|
||||
<Typography.Text variant="m-500">Encrypted</Typography.Text>
|
||||
{#if !supportsStringEncryption}
|
||||
<Tag variant="default" size="xs" on:click={toggle}>Pro</Tag>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</button>
|
||||
<Typography.Text color="--fgcolor-neutral-tertiary">
|
||||
Indicate whether this column is encrypted. Encrypted columns cannot be
|
||||
queried.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="xxs" direction="column">
|
||||
<button
|
||||
type="button"
|
||||
disabled={editing}
|
||||
class:cursor-pointer={!editing}
|
||||
class:cursor-not-allowed={editing}
|
||||
on:click={(e) => {
|
||||
if (!supportsStringEncryption) {
|
||||
toggle(e);
|
||||
} else {
|
||||
data.encrypt = !data.encrypt;
|
||||
}
|
||||
}}>
|
||||
<Layout.Stack inline gap="xxs" direction="row" alignItems="center">
|
||||
<Typography.Text variant="m-500">Encrypted</Typography.Text>
|
||||
{#if !supportsStringEncryption}
|
||||
<Tag variant="default" size="xs" on:click={toggle}>Pro</Tag>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</button>
|
||||
<Typography.Text color="--fgcolor-neutral-tertiary">
|
||||
Protect attribute against data leaks for best privacy compliance. Encrypted
|
||||
attributes cannot be queried.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<ActionMenu.Root width="180px" slot="tooltip">
|
||||
<Typography.Text variant="m-500">
|
||||
Available on Pro plan. <Link.Anchor href={$upgradeURL}
|
||||
>Upgrade</Link.Anchor>
|
||||
to enable encrypted columns.
|
||||
</Typography.Text>
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
<ActionMenu.Root width="180px" slot="tooltip">
|
||||
<Typography.Text variant="m-500">
|
||||
Available on Pro plan. <Link.Anchor href={$upgradeURL}>Upgrade</Link.Anchor>
|
||||
to enable encrypted columns.
|
||||
</Typography.Text>
|
||||
</ActionMenu.Root>
|
||||
</Popover>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
.popover-holder {
|
||||
@@ -185,7 +184,16 @@
|
||||
|
||||
// no cursor when disabled
|
||||
&.disabled-checkbox :global(button) {
|
||||
cursor: unset !important;
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
// !important is needed
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-9
@@ -13,7 +13,6 @@
|
||||
import { base } from '$app/paths';
|
||||
import type { Columns } from './store';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
|
||||
export let showCreate = false;
|
||||
export let selectedOption: Option['name'] = null;
|
||||
@@ -29,17 +28,10 @@
|
||||
};
|
||||
let error: string;
|
||||
|
||||
async function updateTableColumns() {
|
||||
const selectedColumns = preferences.getCustomTableColumns(tableId);
|
||||
selectedColumns.push(key ?? data?.key);
|
||||
await preferences.setCustomTableColumns(tableId, selectedColumns);
|
||||
await invalidate(Dependencies.TABLE);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await $option.create(databaseId, tableId, key, data);
|
||||
await updateTableColumns();
|
||||
await invalidate(Dependencies.TABLE);
|
||||
|
||||
if (!page.url.pathname.includes('columns')) {
|
||||
await goto(
|
||||
|
||||
+9
-5
@@ -22,18 +22,22 @@
|
||||
data = null;
|
||||
selectedRelationship = null;
|
||||
}
|
||||
|
||||
$: tableColumns = [
|
||||
// take full width if no display names set
|
||||
{ id: '$id', width: args.length ? 200 : undefined },
|
||||
...args.map((id) => ({ id }))
|
||||
];
|
||||
</script>
|
||||
|
||||
<Modal bind:show title={selectedRelationship?.key} hideFooter>
|
||||
{#if data?.length}
|
||||
<Paginator items={data} {limit}>
|
||||
{#snippet children(paginatedItems: typeof data)}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[{ id: '$id', width: 150 }, ...args.map((id) => ({ id }))]}>
|
||||
<Table.Root let:root columns={tableColumns}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="$id" {root}>Row ID</Table.Header.Cell>
|
||||
{#if args?.length}
|
||||
{#if args.length}
|
||||
{#each args as arg}
|
||||
<Table.Header.Cell column={arg} {root}>{arg}</Table.Header.Cell>
|
||||
{/each}
|
||||
@@ -47,7 +51,7 @@
|
||||
<Table.Cell column="$id" {root}>
|
||||
<Id value={doc.$id}>{doc.$id}</Id>
|
||||
</Table.Cell>
|
||||
{#if args?.length}
|
||||
{#if args.length}
|
||||
{#each args as arg}
|
||||
<Table.Cell column={arg} {root}>{doc[arg]}</Table.Cell>
|
||||
{/each}
|
||||
|
||||
+2
-2
@@ -12,6 +12,7 @@
|
||||
<InputSelect
|
||||
{id}
|
||||
{label}
|
||||
bind:value
|
||||
{optionalText}
|
||||
placeholder="Select a value"
|
||||
required={column.required}
|
||||
@@ -19,5 +20,4 @@
|
||||
!column.required && { label: 'NULL', value: null },
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value />
|
||||
].filter(Boolean)} />
|
||||
|
||||
+3
-5
@@ -6,14 +6,12 @@
|
||||
export let label: string;
|
||||
export let value: string | null;
|
||||
export let column: Models.ColumnEmail;
|
||||
|
||||
$: nullable = column.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputEmail
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
placeholder="Enter Email"
|
||||
bind:value
|
||||
placeholder="Enter URL"
|
||||
required={column.required}
|
||||
bind:value />
|
||||
nullable={!column.required} />
|
||||
|
||||
+4
-6
@@ -6,16 +6,14 @@
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
export let column: Models.ColumnInteger;
|
||||
|
||||
$: nullable = column.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
required={column.required}
|
||||
bind:value
|
||||
min={column.min}
|
||||
max={column.max}
|
||||
step={column.type === 'double' ? 'any' : 1}
|
||||
bind:value />
|
||||
required={column.required}
|
||||
nullable={!column.required}
|
||||
step={column.type === 'double' ? 'any' : 1} />
|
||||
|
||||
+23
-3
@@ -116,9 +116,29 @@
|
||||
$: totalCount = relatedList?.length ?? 0;
|
||||
|
||||
$: options =
|
||||
rowList?.rows?.map((n) => {
|
||||
const data = displayNames.filter((name) => name !== '$id').map((name) => n?.[name]);
|
||||
return { value: n.$id, label: n.$id, data };
|
||||
rowList?.rows?.map((row) => {
|
||||
const names = displayNames.filter((name) => name !== '$id');
|
||||
const values = names
|
||||
.map((name) => row?.[name])
|
||||
// always supposed to be a string but just being a bit safe here
|
||||
.filter((value) => value != null && typeof value === 'string' && value !== '');
|
||||
|
||||
const displayValues = !editing
|
||||
? values
|
||||
: // on non edit routes like create, there's enough space!
|
||||
values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
|
||||
|
||||
const label = !values.length
|
||||
? row.$id
|
||||
: // values are in `$id (a | b)` format
|
||||
// previously used to have a `$id a | b`.
|
||||
`${row.$id} (${displayValues.join(' | ')})`;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: row.$id,
|
||||
data: names.map((name) => row?.[name])
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
$: hasItems = totalCount > 0;
|
||||
|
||||
+6
-8
@@ -6,26 +6,24 @@
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let column: Models.ColumnString;
|
||||
|
||||
$: nullable = column.required ? false : !value;
|
||||
</script>
|
||||
|
||||
{#if column.size >= 50}
|
||||
<InputTextarea
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
bind:value
|
||||
placeholder="Enter string"
|
||||
required={column.required}
|
||||
maxlength={column.size}
|
||||
bind:value />
|
||||
required={column.required}
|
||||
nullable={!column.required} />
|
||||
{:else}
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
{nullable}
|
||||
bind:value
|
||||
placeholder="Enter string"
|
||||
required={column.required}
|
||||
maxlength={column.size}
|
||||
bind:value />
|
||||
required={column.required}
|
||||
nullable={!column.required} />
|
||||
{/if}
|
||||
|
||||
+7
-3
@@ -6,8 +6,12 @@
|
||||
export let label: string;
|
||||
export let value: string | null;
|
||||
export let column: Models.ColumnUrl;
|
||||
|
||||
$: nullable = column.required ? false : !value;
|
||||
</script>
|
||||
|
||||
<InputURL {id} {label} {nullable} placeholder="Enter URL" required={column.required} bind:value />
|
||||
<InputURL
|
||||
{id}
|
||||
{label}
|
||||
bind:value
|
||||
placeholder="Enter URL"
|
||||
required={column.required}
|
||||
nullable={!column.required} />
|
||||
|
||||
+63
-32
@@ -17,10 +17,23 @@
|
||||
const tableId = page.params.table;
|
||||
let names: string[] = [...(preferences.getDisplayNames()?.[tableId] ?? [])];
|
||||
|
||||
function getDisplayNames() {
|
||||
return [...(preferences.getDisplayNames()?.[tableId] ?? [])].filter(
|
||||
(name) => name !== '$id'
|
||||
); // edge case with `$id`? got saved during tests!
|
||||
}
|
||||
|
||||
let names: string[] = $state(getDisplayNames());
|
||||
|
||||
async function updateDisplayName() {
|
||||
try {
|
||||
await preferences.setDisplayNames($organization.$id, tableId, names);
|
||||
names = [...(preferences.getDisplayNames()?.[tableId] ?? [])];
|
||||
// $state makes proxy,
|
||||
// structuredClone doesn't work
|
||||
const regularArray = [...names];
|
||||
|
||||
await preferences.setDisplayNames($organization.$id, tableId, regularArray);
|
||||
names = getDisplayNames();
|
||||
|
||||
await invalidate(Dependencies.TEAM);
|
||||
addNotification({
|
||||
message: 'Display names have been updated',
|
||||
@@ -36,23 +49,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: options = ($columns as Models.ColumnString[])
|
||||
.filter(
|
||||
(attr) =>
|
||||
attr.type === 'string' && !attr?.array && !names?.some((name) => name === attr.key)
|
||||
)
|
||||
.map((attr) => {
|
||||
return {
|
||||
function getValidAttributes() {
|
||||
return ($attributes as Models.AttributeString[]).filter(
|
||||
(attr) => attr.type === 'string' && !attr?.array
|
||||
);
|
||||
}
|
||||
|
||||
function getOptions(index: number) {
|
||||
const current = names?.[index];
|
||||
return getValidAttributes()
|
||||
.filter((attr) => !names?.includes(attr.key) || attr.key === current)
|
||||
.map((attr) => ({
|
||||
value: attr.key,
|
||||
label: attr.key
|
||||
};
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
$: addAttributeDisabled = names?.length >= 5 || (names?.length && !names[names?.length - 1]);
|
||||
const addAttributeDisabled = $derived(
|
||||
names?.length >= 5 || (names?.length && !names[names?.length - 1])
|
||||
);
|
||||
|
||||
$: updateBtnDisabled =
|
||||
const updateBtnDisabled = $derived(
|
||||
!symmetricDifference(names, preferences.getDisplayNames()?.[tableId] ?? [])?.length ||
|
||||
(names?.length && !last(names));
|
||||
(names?.length && !last(names))
|
||||
);
|
||||
|
||||
const hasExhaustedOptions = $derived(
|
||||
getValidAttributes().length === names.filter(Boolean).length
|
||||
);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateDisplayName}>
|
||||
@@ -72,19 +96,22 @@
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
{#if names?.length}
|
||||
{#each names as name, i}
|
||||
{#each names as name, index}
|
||||
<Layout.Stack direction="row" gap="xxs">
|
||||
{@const options = getOptions(index)}
|
||||
{@const disabled =
|
||||
(!!names[index] && names.length > index + 1) || hasExhaustedOptions}
|
||||
<InputSelect
|
||||
id={name}
|
||||
placeholder="Select column"
|
||||
bind:value={names[i]}
|
||||
disabled={!!names[i] && names.length > i + 1}
|
||||
{options} />
|
||||
{options}
|
||||
{disabled}
|
||||
bind:value={names[index]}
|
||||
placeholder="Select column" />
|
||||
<Button
|
||||
icon
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
names.splice(i, 1);
|
||||
names.splice(index, 1);
|
||||
names = names;
|
||||
}}>
|
||||
<Icon icon={IconX} />
|
||||
@@ -92,18 +119,22 @@
|
||||
</Layout.Stack>
|
||||
{/each}
|
||||
{/if}
|
||||
<div>
|
||||
<Button
|
||||
compact
|
||||
disabled={addAttributeDisabled}
|
||||
on:click={() => {
|
||||
names[names.length] = null;
|
||||
names = names;
|
||||
}}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add column
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- show only when options don't have all the attributes -->
|
||||
{#if !hasExhaustedOptions}
|
||||
<div>
|
||||
<Button
|
||||
compact
|
||||
disabled={addAttributeDisabled}
|
||||
on:click={() => {
|
||||
names[names.length] = null;
|
||||
names = names;
|
||||
}}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add column
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
|
||||
|
||||
+12
-25
@@ -10,7 +10,7 @@
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { afterUpdate, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { isRelationship, isRelationshipToMany, isString } from './row-[row]/columns/store';
|
||||
import RelationshipsModal from './relationshipsModal.svelte';
|
||||
@@ -28,36 +28,21 @@
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { flags } from '$lib/flags';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const databaseId = page.params.database;
|
||||
const tableId = page.params.table;
|
||||
|
||||
let displayNames = {};
|
||||
let showRelationships = false;
|
||||
let selectedRelationship: Models.ColumnRelationship = null;
|
||||
let relationshipData: Partial<Models.Document>[];
|
||||
let displayNames = {};
|
||||
|
||||
onMount(async () => {
|
||||
displayNames = preferences.getDisplayNames();
|
||||
updateMaxWidth();
|
||||
});
|
||||
|
||||
afterUpdate(() => updateMaxWidth());
|
||||
|
||||
function updateMaxWidth() {
|
||||
const tableCells = Array.from(document.querySelectorAll('.less-width-truncated'));
|
||||
|
||||
const visibleColumnsCount = $tableColumns.filter((col) => !col.hide).length;
|
||||
const newMaxWidth = Math.max(50 - (visibleColumnsCount - 1) * 5, 25);
|
||||
|
||||
tableCells.forEach((cell) => {
|
||||
const cellItem = cell as HTMLElement;
|
||||
cellItem.style.maxWidth = `${newMaxWidth}vw`;
|
||||
});
|
||||
}
|
||||
|
||||
function formatArray(array: unknown[]) {
|
||||
if (array.length === 0) return '[ ]';
|
||||
|
||||
@@ -104,7 +89,7 @@
|
||||
id: column.key,
|
||||
title: column.key,
|
||||
type: column.type as ColumnType,
|
||||
hide: !selected?.includes(column.key),
|
||||
hide: !!selected?.includes(column.key),
|
||||
array: column?.array,
|
||||
width: { min: 168 },
|
||||
format: 'format' in column && column?.format === 'enum' ? column.format : null,
|
||||
@@ -164,8 +149,6 @@
|
||||
) as Models.ColumnRelationship[];
|
||||
|
||||
let checked = false;
|
||||
|
||||
const showEncrypt = flags.showAttributeEncrypt(data);
|
||||
</script>
|
||||
|
||||
<Table.Root
|
||||
@@ -257,7 +240,7 @@
|
||||
<span slot="title">Timestamp</span>
|
||||
{toLocaleDateTime(datetime, true)}
|
||||
</DualTimeView>
|
||||
{:else if isEncryptedAttribute && showEncrypt}
|
||||
{:else if isEncryptedAttribute}
|
||||
<button on:click={(e) => e.preventDefault()}>
|
||||
<InteractiveText
|
||||
copy={false}
|
||||
@@ -370,8 +353,12 @@
|
||||
</Confirm>
|
||||
|
||||
<style>
|
||||
.floating-action-bar :global(div:first-of-type) {
|
||||
/* 50% > 60% because we have sub-navigation */
|
||||
left: calc(60% - var(--p-floating-action-bar-width) / 2);
|
||||
.floating-action-bar {
|
||||
position: fixed;
|
||||
|
||||
:global(div:first-of-type) {
|
||||
/* 50% > 60% because we have sub-navigation */
|
||||
left: calc(60% - var(--p-floating-action-bar-width) / 2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
-3
@@ -214,9 +214,11 @@
|
||||
>Only .tar.gz files allowed</svelte:fragment>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<Typography.Caption variant="400"
|
||||
>Max file size: {readableMaxSize.value +
|
||||
readableMaxSize.unit}</Typography.Caption>
|
||||
{#if maxSize > 0}
|
||||
<Typography.Caption variant="400"
|
||||
>Max file size: {readableMaxSize.value +
|
||||
readableMaxSize.unit}</Typography.Caption>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Upload.Dropzone>
|
||||
|
||||
+5
-3
@@ -101,9 +101,11 @@
|
||||
>Only .tar.gz files allowed</svelte:fragment>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<Typography.Caption variant="400"
|
||||
>Max file size: {readableMaxSize.value +
|
||||
readableMaxSize.unit}</Typography.Caption>
|
||||
{#if maxSize > 0}
|
||||
<Typography.Caption variant="400"
|
||||
>Max file size: {readableMaxSize.value +
|
||||
readableMaxSize.unit}</Typography.Caption>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Upload.Dropzone>
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
external
|
||||
href="#/"
|
||||
href="https://appwrite.io/docs/products/functions/domains"
|
||||
text
|
||||
event="empty_documentation"
|
||||
size="s"
|
||||
|
||||
+6
-3
@@ -83,14 +83,16 @@
|
||||
let path = '/';
|
||||
let method = ExecutionMethod.GET;
|
||||
let body = '';
|
||||
let headers: [string, string][] = [[null, '']];
|
||||
let headers: [string, string][] = [['', '']];
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const headersObject = {};
|
||||
|
||||
for (const [name, value] of headers) {
|
||||
headersObject[name] = value;
|
||||
if (name && name.trim() !== '') {
|
||||
headersObject[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
await sdk
|
||||
@@ -134,7 +136,8 @@
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
|
||||
$: filteredKeyList = keyList.filter((key) => {
|
||||
const name = last(headers)[0];
|
||||
const lastHeader = last(headers);
|
||||
const name = lastHeader ? lastHeader[0] : '';
|
||||
if (!name) return true;
|
||||
return key.value.toLowerCase().includes(name?.toLowerCase());
|
||||
});
|
||||
|
||||
+5
-1
@@ -274,5 +274,9 @@
|
||||
<DisconnectRepo bind:show={showDisconnect} on:success={loadRepository} />
|
||||
{/if}
|
||||
{#if showSelectRoot}
|
||||
<SelectRootModal bind:show={showSelectRoot} product="sites" bind:rootDir={selectedDir} />
|
||||
<SelectRootModal
|
||||
bind:show={showSelectRoot}
|
||||
product="sites"
|
||||
bind:rootDir={selectedDir}
|
||||
branch={selectedBranch} />
|
||||
{/if}
|
||||
|
||||
+6
-1
@@ -18,6 +18,8 @@
|
||||
export let func: Models.Function;
|
||||
export let specs: Models.SpecificationList;
|
||||
let specification = func.specification;
|
||||
let originalSpecification = func.specification;
|
||||
$: originalSpecification = func.specification;
|
||||
|
||||
async function updateLogging() {
|
||||
try {
|
||||
@@ -47,6 +49,9 @@
|
||||
specification || undefined
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
|
||||
originalSpecification = specification;
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Resource limits have been updated'
|
||||
@@ -96,7 +101,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={func.specification === specification} submit>Update</Button>
|
||||
<Button disabled={originalSpecification === specification} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
import { func } from '../store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { Runtime } from '@appwrite.io/console';
|
||||
import Scopes from '$routes/(console)/project-[region]-[project]/overview/keys/scopes.svelte';
|
||||
import Scopes from '$routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Link } from '$lib/elements';
|
||||
|
||||
|
||||
+15
-30
@@ -24,8 +24,7 @@
|
||||
APNSProviderParams
|
||||
} from './store';
|
||||
import TooltipPopover from './tooltipPopover.svelte';
|
||||
import { Icon, Tooltip, Upload, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Upload, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { InvalidFileType, removeFile } from '$lib/helpers/files';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
@@ -44,18 +43,6 @@
|
||||
| FCMProviderParams
|
||||
| APNSProviderParams
|
||||
>;
|
||||
const popover = input.popover ? PopoverContent : null;
|
||||
const popoverProps = getPopoverProps(input);
|
||||
|
||||
function getPopoverProps(input: ProviderInput) {
|
||||
if (!input.popover) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
lines: input.popover,
|
||||
image: input.popoverImage
|
||||
};
|
||||
}
|
||||
|
||||
function handleInvalid(e: CustomEvent) {
|
||||
const reason = e.detail?.reason ?? '';
|
||||
@@ -72,6 +59,14 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: popover = input.popover ? PopoverContent : null;
|
||||
$: popoverProps = !input.popover
|
||||
? {}
|
||||
: {
|
||||
lines: input.popover,
|
||||
image: input.popoverImage
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if input.type === 'text'}
|
||||
@@ -122,25 +117,15 @@
|
||||
{:else if input.type === 'file'}
|
||||
<Upload.Dropzone
|
||||
on:invalid={handleInvalid}
|
||||
extensions={['json']}
|
||||
extensions={input.allowedFileExtensions}
|
||||
bind:files={files[input.name]}
|
||||
maxSize={10000000}
|
||||
required={!input.optional}>
|
||||
<Layout.Stack alignItems="center" gap="s">
|
||||
<Layout.Stack alignItems="center" gap="s">
|
||||
<Layout.Stack alignItems="center" justifyContent="center" direction="row" gap="s">
|
||||
<Typography.Text variant="l-500">
|
||||
Drag and drop service account JSON here or click to upload
|
||||
</Typography.Text>
|
||||
<Tooltip>
|
||||
<Layout.Stack alignItems="center" justifyContent="center" inline>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="tooltip">Only .json files allowed</svelte:fragment>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<Typography.Caption variant="400">Max file size 10MB</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack alignItems="center" justifyContent="center" direction="row" gap="xxs">
|
||||
<Typography.Text variant="l-500">
|
||||
Drag and drop your {input.label} here or click to upload
|
||||
</Typography.Text>
|
||||
<TooltipPopover {popover} {popoverProps} tooltip={input.tooltip} />
|
||||
</Layout.Stack>
|
||||
</Upload.Dropzone>
|
||||
{#if files[input.name]?.length}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const providers: ProvidersMap = {
|
||||
],
|
||||
configure: [
|
||||
{
|
||||
label: 'Service account JSON (.json file)',
|
||||
label: 'service account JSON (.json file)',
|
||||
name: 'serviceAccountJSON',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['json'],
|
||||
@@ -164,7 +164,7 @@ export const providers: ProvidersMap = {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Auth key (.p8 file)',
|
||||
label: 'auth key (.p8 file)',
|
||||
name: 'authKey',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['p8'],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { writable } from 'svelte/store';
|
||||
import Scopes from '../keys/scopes.svelte';
|
||||
import Scopes from '../api-keys/scopes.svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const projectId = page.params.project;
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
trackEvent(Submit.KeyCreate);
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$id}`
|
||||
);
|
||||
addNotification({
|
||||
message: `API key has been created`,
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
<Wizard
|
||||
title="Create API key"
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/overview/keys/`}
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/`}
|
||||
bind:showExitModal
|
||||
column
|
||||
columnSize="s"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
import UpdateExpirationDate from './updateExpirationDate.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import Scopes from '../keys/scopes.svelte';
|
||||
import Scopes from '../api-keys/scopes.svelte';
|
||||
import { InteractiveText, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let key: Models.DevKey | Models.Key;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
const isApiKey = keyType === 'api';
|
||||
const label = isApiKey ? 'API' : 'dev';
|
||||
const slug = isApiKey ? 'keys' : 'dev-keys';
|
||||
const slug = isApiKey ? 'api-keys' : 'dev-keys';
|
||||
|
||||
function asApiKey(key: Models.Key | Models.DevKey) {
|
||||
return key as Models.Key;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
label: 'Create API Key',
|
||||
icon: IconPlus,
|
||||
callback() {
|
||||
goto(`${path}/keys/create`);
|
||||
goto(`${path}/api-keys/create`);
|
||||
},
|
||||
keys: ['c', 'k'],
|
||||
group: 'integrations',
|
||||
@@ -229,12 +229,12 @@
|
||||
selected={isTabSelected('platforms')}>Platforms</Tab>
|
||||
<Tab
|
||||
noscroll
|
||||
event="keys"
|
||||
href={`${path}/keys`}
|
||||
selected={isTabSelected('keys')}>API keys</Tab>
|
||||
event="api-keys"
|
||||
href={`${path}/api-keys`}
|
||||
selected={isTabSelected('api-keys')}>API keys</Tab>
|
||||
<Tab
|
||||
noscroll
|
||||
event="keys"
|
||||
event="dev-keys"
|
||||
href={`${path}/dev-keys`}
|
||||
selected={isTabSelected('dev-keys')}>Dev keys</Tab>
|
||||
</Tabs>
|
||||
|
||||
+2
-2
@@ -16,11 +16,11 @@
|
||||
title: $project?.name
|
||||
},
|
||||
{
|
||||
href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys`,
|
||||
href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys`,
|
||||
title: 'API keys'
|
||||
},
|
||||
{
|
||||
href: `${base}/project-${page.params.region}-${page.params.project}/overview/keys/${$key?.$id}`,
|
||||
href: `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$key?.$id}`,
|
||||
title: $key?.name
|
||||
}
|
||||
];
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/overview/keys`}>
|
||||
<CoverTitle href={`${base}/project-${page.params.region}-${projectId}/overview/api-keys`}>
|
||||
{$key?.name}
|
||||
</CoverTitle>
|
||||
</svelte:fragment>
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
{#if $canWriteKeys}
|
||||
<Button
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/overview/keys/create`}>
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/create`}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create API key
|
||||
</Button>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user