mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/fix-documents-selection-state' into fix-documents-selection-state
This commit is contained in:
@@ -5,7 +5,43 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
publish-cloud:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console-cloud
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
|
||||
publish-self-hosted:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
|
||||
+13
-10
@@ -49,7 +49,7 @@ git clone https://github.com/appwrite/console.git appwrite-console
|
||||
Navigate to the Appwrite Console repository and install dependencies.
|
||||
|
||||
```bash
|
||||
cd appwrite-console && npm install
|
||||
cd appwrite-console && pnpm install
|
||||
```
|
||||
|
||||
### 3. Install and run Appwrite locally
|
||||
@@ -62,10 +62,13 @@ Follow the [install instructions](https://appwrite.io/docs/advanced/self-hosting
|
||||
|
||||
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
|
||||
|
||||
> **Note**
|
||||
> If you are updating from Appwrite `1.5.x`, be aware that the variables for the console in the `.env` / `.env.example` file have changed in `1.6.x`.
|
||||
|
||||
Finally, start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
> **Note**
|
||||
@@ -74,7 +77,7 @@ npm run dev
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
|
||||
@@ -82,7 +85,7 @@ npm run build
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
This will run tests in the `tests/` directory.
|
||||
@@ -92,13 +95,13 @@ This will run tests in the `tests/` directory.
|
||||
Code should be consistently formatted everywhere. Before committing code, run the code-formatter.
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
pnpm run format
|
||||
```
|
||||
|
||||
### Linter
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
### Diagnostics
|
||||
@@ -110,7 +113,7 @@ Diagnostic tool that checks for the following:
|
||||
- TypeScript compiler errors
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
## Submit a Pull Request 🚀
|
||||
@@ -173,11 +176,11 @@ $ git push origin [name_of_your_new_branch]
|
||||
Before committing always make sure to run all available tools to improve the codebase:
|
||||
|
||||
- Formatter
|
||||
- `npm run format`
|
||||
- `pnpm run format`
|
||||
- Tests
|
||||
- `npm test`
|
||||
- `pnpm test`
|
||||
- Diagnostics
|
||||
- `npm run check`
|
||||
- `pnpm run check`
|
||||
|
||||
### Performance
|
||||
|
||||
|
||||
+2
-2
@@ -20,11 +20,11 @@ ADD ./static /app/static
|
||||
|
||||
ARG PUBLIC_CONSOLE_MODE
|
||||
ARG PUBLIC_APPWRITE_ENDPOINT
|
||||
ARG PUBLIC_APPWRITE_GROWTH_ENDPOINT
|
||||
ARG PUBLIC_GROWTH_ENDPOINT
|
||||
ARG PUBLIC_STRIPE_KEY
|
||||
|
||||
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
|
||||
ENV PUBLIC_APPWRITE_GROWTH_ENDPOINT=$PUBLIC_APPWRITE_GROWTH_ENDPOINT
|
||||
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
|
||||
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
|
||||
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ services:
|
||||
args:
|
||||
PUBLIC_CONSOLE_MODE: ${PUBLIC_CONSOLE_MODE}
|
||||
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
|
||||
PUBLIC_APPWRITE_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
|
||||
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
|
||||
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
|
||||
develop:
|
||||
watch:
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 523 KiB After Width: | Height: | Size: 9.3 MiB |
Generated
+9360
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -19,15 +19,15 @@
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.7",
|
||||
"@appwrite.io/console": "1.0.0",
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"ai": "^2.2.37",
|
||||
"analytics": "^0.8.13",
|
||||
"analytics": "^0.8.14",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"dayjs": "^1.11.12",
|
||||
"deep-equal": "^2.2.3",
|
||||
"echarts": "^5.5.1",
|
||||
"envfile": "^7.1.0",
|
||||
@@ -41,37 +41,37 @@
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.45.2",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.5.18",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sveltejs/adapter-static": "^3.0.4",
|
||||
"@sveltejs/kit": "^2.5.22",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@testing-library/dom": "^10.3.2",
|
||||
"@testing-library/jest-dom": "^6.4.6",
|
||||
"@testing-library/svelte": "^5.2.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/svelte": "^5.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.42.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.77.8",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.4",
|
||||
"svelte-check": "^3.8.5",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
|
||||
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
|
||||
}
|
||||
|
||||
Generated
+616
-637
File diff suppressed because it is too large
Load Diff
@@ -194,6 +194,7 @@ export enum Submit {
|
||||
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
|
||||
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
|
||||
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
|
||||
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
|
||||
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
@@ -318,5 +319,6 @@ export enum Submit {
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
|
||||
ApplyQuickFilter = 'submit_apply_quick_filter'
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate.country === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="info">
|
||||
The payment method for {$organization.name} needs to be verified.
|
||||
<svelte:fragment slot="buttons">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
export let isDowngrade = false;
|
||||
|
||||
const today = new Date();
|
||||
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
@@ -41,8 +42,8 @@
|
||||
<p class="text">{formatCurrency(currentPlan.price)}</p>
|
||||
</span>
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">Additional seats ({collaborators?.length})</p>
|
||||
<p class="text">
|
||||
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
|
||||
<p class="text" class:u-bold={isDowngrade}>
|
||||
{formatCurrency(extraSeatsCost)}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -6,3 +6,4 @@ export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte';
|
||||
export { default as PlanComparisonBox } from './planComparisonBox.svelte';
|
||||
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
|
||||
export { default as CreditsApplied } from './creditsApplied.svelte';
|
||||
export { default as PlanSelection } from './planSelection.svelte';
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
export let downgrade = false;
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-0'}
|
||||
on:click={() => (selectedTab = 'tier-0')}>
|
||||
Free
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-1'}
|
||||
on:click={() => (selectedTab = 'tier-1')}>
|
||||
Pro
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
<Card style="--card-padding: 1.5rem">
|
||||
<div class="comparison-box">
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.FREE}
|
||||
on:click={() => (selectedTab = BillingPlan.FREE)}>
|
||||
{tierFree.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.PRO}
|
||||
on:click={() => (selectedTab = BillingPlan.PRO)}>
|
||||
{tierPro.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.SCALE}
|
||||
on:click={() => (selectedTab = BillingPlan.SCALE)}>
|
||||
{tierScale.name}
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
@@ -86,16 +94,50 @@
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>Everything in the Free plan, plus:</li>
|
||||
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles <span class="inline-tag">Coming soon</span></li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.comparison-box {
|
||||
border-radius: var(--border-radius-small);
|
||||
background: hsl(var(--color-neutral-5));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box {
|
||||
background: hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-0));
|
||||
border: 1px solid hsl(var(--color-neutral-10));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-80));
|
||||
border: 1px solid hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let tier: Tier;
|
||||
export let members: number;
|
||||
|
||||
const plan = $plansInfo?.get(tier);
|
||||
let excess: {
|
||||
@@ -41,7 +42,7 @@
|
||||
$organization.billingCurrentInvoiceDate,
|
||||
new Date().toISOString()
|
||||
);
|
||||
excess = calculateExcess(usage, plan, $organization);
|
||||
excess = calculateExcess(usage, plan, members);
|
||||
showExcess = Object.values(excess).some((value) => value > 0);
|
||||
});
|
||||
</script>
|
||||
@@ -98,7 +99,8 @@
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
{humanFileSize(excess?.storage)}
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
|
||||
</script>
|
||||
|
||||
{#if billingPlan}
|
||||
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree}
|
||||
value={BillingPlan.FREE}
|
||||
tooltipShow={anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account."
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierFree.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.PRO} padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE}
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.SCALE}
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierScale.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierScale.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -44,7 +44,7 @@
|
||||
</script>
|
||||
|
||||
{#if filteredMethods?.length}
|
||||
{#if selectedPaymentMethod?.country === 'in'}
|
||||
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
|
||||
<Alert type="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
To comply with RBI regulations in India, Appwrite will ask for verification to charge up
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
bind:this={tooltip}
|
||||
class:u-width-full-line={fullWidth}
|
||||
style:--arrow-size={`${arrowSize}px`}
|
||||
style:z-index="10">
|
||||
style:z-index="21">
|
||||
<div
|
||||
class="drop-arrow"
|
||||
class:is-popover={isPopover}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
export let wrapperFullWidth = false;
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
export let noMaxWidthList = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
<Drop
|
||||
@@ -29,11 +31,10 @@
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="drop is-no-arrow"
|
||||
class="drop is-no-arrow {classes}"
|
||||
class:u-max-width-100-percent={fullWidth}
|
||||
style={`${width ? `--drop-width-size-desktop:${width}rem; ` : ''} ${
|
||||
position === 'static' ? 'position:static' : 'position:relative'
|
||||
}`}>
|
||||
style:--drop-width-size-desktop={width ? `${width}rem` : ''}
|
||||
style:position>
|
||||
{#if $$slots.list}
|
||||
<section
|
||||
class:u-overflow-y-auto={scrollable}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let icon: string = null;
|
||||
export let event: string = null;
|
||||
export let loading = false;
|
||||
export let padding: number | null = null;
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
@@ -20,6 +21,8 @@
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-main-space-between"
|
||||
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
|
||||
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
|
||||
on:click={track}
|
||||
on:click|preventDefault
|
||||
{disabled}>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script>
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { getNextTier, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { getNextTier, tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import Card from './card.svelte';
|
||||
|
||||
@@ -21,22 +19,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-stretch u-flex-vertical">
|
||||
<div class="u-stretch u-flex-vertical u-main-center">
|
||||
<h3 class="body-text-2 u-bold"><slot name="title" /></h3>
|
||||
<p class="u-margin-block-start-8">
|
||||
<slot nextTier={tierToPlan(getNextTier($organization.billingPlan)).name} />
|
||||
</p>
|
||||
<Button
|
||||
class="u-margin-block-start-32"
|
||||
secondary
|
||||
fullWidth
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source
|
||||
});
|
||||
}}>Upgrade plan</Button>
|
||||
<slot name="cta" {source} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let trimmed = true;
|
||||
export let id: string = null;
|
||||
export let style: string = undefined;
|
||||
let classes: string = undefined;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -137,7 +137,12 @@
|
||||
bind:value />
|
||||
{:else if column.type === 'datetime'}
|
||||
{#key value}
|
||||
<InputDateTime id="value" bind:value label="value" showLabel={false} />
|
||||
<InputDateTime
|
||||
id="value"
|
||||
bind:value
|
||||
label="value"
|
||||
showLabel={false}
|
||||
step={60} />
|
||||
{/key}
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
|
||||
@@ -13,13 +13,19 @@
|
||||
tags,
|
||||
ValidOperators
|
||||
} from './store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let disabled = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let singleCondition = false;
|
||||
export let clearOnClick = false; // When enabled the user doesn't have to click apply to clear the filters
|
||||
export let enableApply = false;
|
||||
export let quickFilters = false;
|
||||
let displayQuickFilters = quickFilters;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
|
||||
@@ -44,10 +50,15 @@
|
||||
function clearAll() {
|
||||
selectedColumn = null;
|
||||
queries.clearAll();
|
||||
if (clearOnClick) {
|
||||
queries.apply();
|
||||
}
|
||||
}
|
||||
|
||||
function apply() {
|
||||
if (
|
||||
if (quickFilters && displayQuickFilters) {
|
||||
dispatch('apply');
|
||||
} else if (
|
||||
selectedColumn &&
|
||||
operatorKey &&
|
||||
(operatorKey === ValidOperators.IsNotNull ||
|
||||
@@ -83,19 +94,22 @@
|
||||
arrayValues = [];
|
||||
}
|
||||
|
||||
$: isButtonDisabled = $queriesAreDirty
|
||||
? false
|
||||
: !selectedColumn ||
|
||||
!operatorKey ||
|
||||
(!value &&
|
||||
!arrayValues.length &&
|
||||
operatorKey !== ValidOperators.IsNotNull &&
|
||||
operatorKey !== ValidOperators.IsNull);
|
||||
$: isButtonDisabled =
|
||||
$queriesAreDirty || (quickFilters && displayQuickFilters && enableApply)
|
||||
? false
|
||||
: !selectedColumn ||
|
||||
!operatorKey ||
|
||||
(!value &&
|
||||
!arrayValues.length &&
|
||||
operatorKey !== ValidOperators.IsNotNull &&
|
||||
operatorKey !== ValidOperators.IsNull);
|
||||
|
||||
function toggleDropdown() {
|
||||
dispatch('toggle', { show: !showFiltersDesktop });
|
||||
showFiltersDesktop = !showFiltersDesktop;
|
||||
}
|
||||
function toggleMobileModal() {
|
||||
dispatch('toggle', { show: !showFiltersMobile });
|
||||
showFiltersMobile = !showFiltersMobile;
|
||||
}
|
||||
</script>
|
||||
@@ -115,24 +129,43 @@
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card">
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<hr />
|
||||
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button disabled={applied === 0} text on:click={clearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@@ -141,10 +174,7 @@
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<slot name="mobile" {disabled} toggle={toggleMobileModal}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showFiltersMobile = !showFiltersMobile)}
|
||||
{fullWidthMobile}>
|
||||
<Button secondary on:click={toggleMobileModal} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
@@ -160,22 +190,42 @@
|
||||
description="Apply filter rules to refine the table view"
|
||||
bind:show={showFiltersMobile}
|
||||
size="big">
|
||||
<Content
|
||||
{columns}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<Content
|
||||
{columns}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
<div
|
||||
class="u-flex u-cross-center u-width-full-line"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -13,27 +13,23 @@ export type TagValue = {
|
||||
};
|
||||
|
||||
export type Operator = {
|
||||
toTag: (
|
||||
attribute: string,
|
||||
input?: string | number | string[],
|
||||
type?: string
|
||||
) => string | TagValue;
|
||||
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
|
||||
toQuery: (attribute: string, input?: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
};
|
||||
|
||||
export function mapToQueryParams(map: Map<string | TagValue, string>) {
|
||||
export function mapToQueryParams(map: Map<TagValue, string>) {
|
||||
return encodeURIComponent(JSON.stringify(Array.from(map.entries())));
|
||||
}
|
||||
|
||||
export function queryParamToMap(queryParam: string) {
|
||||
const decodedQueryParam = decodeURIComponent(queryParam);
|
||||
const queries = JSON.parse(decodedQueryParam) as [string, string][];
|
||||
const queries = JSON.parse(decodedQueryParam) as [TagValue, string][];
|
||||
return new Map(queries);
|
||||
}
|
||||
|
||||
function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
function initQueries(initialValue = new Map<TagValue, string>()) {
|
||||
const queries = writable(initialValue);
|
||||
|
||||
type AddFilterArgs = {
|
||||
@@ -52,7 +48,7 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeFilter(tag: string | TagValue) {
|
||||
function removeFilter(tag: TagValue) {
|
||||
queries.update((map) => {
|
||||
map.delete(tag);
|
||||
return map;
|
||||
@@ -256,16 +252,22 @@ function generateDefaultOperators() {
|
||||
toQuery: operator.query,
|
||||
toTag: (attribute, input = null, type = null) => {
|
||||
if (input === null) {
|
||||
return `**${attribute}** ${operatorName}`;
|
||||
return {
|
||||
value: '',
|
||||
tag: `**${attribute}** ${operatorName}`
|
||||
};
|
||||
} else if (Array.isArray(input) && input.length > 2) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
|
||||
};
|
||||
} else if (type === ValidTypes.Datetime) {
|
||||
return `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`;
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
|
||||
};
|
||||
} else {
|
||||
return `**${attribute}** ${operatorName} **${input}**`;
|
||||
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
|
||||
}
|
||||
},
|
||||
types: operator.types,
|
||||
@@ -276,3 +278,7 @@ function generateDefaultOperators() {
|
||||
}
|
||||
|
||||
export const operators = generateDefaultOperators();
|
||||
|
||||
export function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
@@ -1,53 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
import { queries, tags, type TagValue } from './store';
|
||||
|
||||
function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
// We cast to any to not cause type errors in the input components
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
function isTypeTagValue(obj: any): obj is TagValue {
|
||||
if (typeof obj === 'string') return false;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.tag === 'string' &&
|
||||
(typeof obj.value === 'string' ||
|
||||
typeof obj.value === 'number' ||
|
||||
Array.isArray(obj.value))
|
||||
);
|
||||
}
|
||||
import { queries, tagFormat, tags } from './store';
|
||||
</script>
|
||||
|
||||
{#each $tags as tag (tag)}
|
||||
{#if isTypeTagValue(tag)}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString()
|
||||
}}
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString(),
|
||||
disabled: Array.isArray(tag.value) ? tag.value?.length < 3 : true
|
||||
}}
|
||||
type="button"
|
||||
class="tag"
|
||||
on:click|preventDefault={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
:global(.theme-dark) .floating-action-bar {
|
||||
border: 1px solid hsl(var(--color-neutral-85));
|
||||
background: hsl(var(--color-neutral-90));
|
||||
box-shadow: 0px 6px 16px 8px #14141f;
|
||||
box-shadow: 0px 6px 16px 8px hsl(var(--color-neutral-105));
|
||||
}
|
||||
|
||||
:global(.theme-light) .floating-action-bar {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let large = false;
|
||||
export let stretch = false;
|
||||
let classes: string = undefined;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
$organization?.billingPlan === BillingPlan.PRO ||
|
||||
$organization?.billingPlan === BillingPlan.SCALE;
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour('09:00')} - ${utcHourToLocaleHour('17:00')} ${localeTimezoneName()}`;
|
||||
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeTimezoneName()}`;
|
||||
</script>
|
||||
|
||||
{#if isCloud}
|
||||
|
||||
+77
-38
@@ -65,200 +65,239 @@ export const scopes: {
|
||||
scope: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
scope: 'sessions.write',
|
||||
description: "Access to create, update and delete your project's sessions",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'users.read',
|
||||
description: "Access to read your project's users",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'users.write',
|
||||
description: "Access to create, update, and delete your project's users",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'teams.read',
|
||||
description: "Access to read your project's teams",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'teams.write',
|
||||
description: "Access to create, update, and delete your project's teams",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'databases.read',
|
||||
description: "Access to read your project's databases",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'databases.write',
|
||||
description: "Access to create, update, and delete your project's databases",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'collections.read',
|
||||
description: "Access to read your project's database collections",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'collections.write',
|
||||
description: "Access to create, update, and delete your project's database collections",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'attributes.read',
|
||||
description: "Access to read your project's database collection's attributes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'attributes.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's database collection's attributes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'indexes.read',
|
||||
description: "Access to read your project's database collection's indexes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'indexes.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's database collection's indexes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'documents.read',
|
||||
description: "Access to read your project's database documents",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'documents.write',
|
||||
description: "Access to create, update, and delete your project's database documents",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'files.read',
|
||||
description: "Access to read your project's storage files and preview images",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'files.write',
|
||||
description: "Access to create, update, and delete your project's storage files",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'buckets.read',
|
||||
description: "Access to read your project's storage buckets",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'buckets.write',
|
||||
description: "Access to create, update, and delete your project's storage buckets",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'functions.read',
|
||||
description: "Access to read your project's functions and code deployments",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'functions.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's functions and code deployments",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.read',
|
||||
description: "Access to read your project's execution logs",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.write',
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'targets.read',
|
||||
description: "Access to read your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'targets.write',
|
||||
description: "Access to create, update, and delete your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'providers.read',
|
||||
description: "Access to read your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'providers.write',
|
||||
description: "Access to create, update, and delete your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'messages.read',
|
||||
description: "Access to read your project's messages",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'messages.write',
|
||||
description: "Access to create, update, and delete your project's messages",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'topics.read',
|
||||
description: "Access to read your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'topics.write',
|
||||
description: "Access to create, update, and delete your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.read',
|
||||
description: "Access to read your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'locale.read',
|
||||
description: "Access to access your project's Locale service",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'avatars.read',
|
||||
description: "Access to access your project's Avatars service",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'health.read',
|
||||
description: "Access to read your project's health status",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'migrations.read',
|
||||
description: "Access to read your project's migration status",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'migrations.write',
|
||||
description: 'Access to create migrations',
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let width = 40;
|
||||
export let height = 30;
|
||||
export let quality = 100;
|
||||
let classes: string = undefined;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
export function getFlag(country: string, width: number, height: number, quality: number) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
export let ariaLabel: string = null;
|
||||
export let noMargin = false;
|
||||
export let event: string = null;
|
||||
let classes: string = undefined;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
export let actions: MultiActionArray = [];
|
||||
export let submissionLoader = false;
|
||||
|
||||
@@ -18,7 +18,18 @@
|
||||
class:icon-exclamation={type === 'warning'}
|
||||
aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="text">
|
||||
<span class="text u-line-height-1-5">
|
||||
<slot />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-info,
|
||||
.icon-exclamation-circle,
|
||||
.icon-check-circle,
|
||||
.icon-exclamation {
|
||||
&::before {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ export { default as InputSelectSearch } from './inputSelectSearch.svelte';
|
||||
export { default as InputCheckbox } from './inputCheckbox.svelte';
|
||||
export { default as InputChoice } from './inputChoice.svelte';
|
||||
export { default as InputPhone } from './inputPhone.svelte';
|
||||
export { default as InputOTP } from './inputOTP.svelte';
|
||||
export { default as InputCron } from './inputCron.svelte';
|
||||
export { default as InputURL } from './inputURL.svelte';
|
||||
export { default as InputId } from './inputId.svelte';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let step: number | 'any' = 0.001;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -70,7 +71,7 @@
|
||||
{readonly}
|
||||
{required}
|
||||
{value}
|
||||
step=".001"
|
||||
{step}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="datetime-local"
|
||||
class="input-text"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
function isFileExtensionAllowed(fileExtension: string) {
|
||||
if (allowedFileExtensions.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
if (allowedFileExtensions?.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -42,7 +42,7 @@
|
||||
hovering = false;
|
||||
if (!ev.dataTransfer.items) return;
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.')[1];
|
||||
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.').pop();
|
||||
if (!isFileExtensionAllowed(fileExtension)) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
@@ -183,15 +183,16 @@
|
||||
{#if files?.length}
|
||||
<ul class="upload-file-box-list u-min-width-0">
|
||||
{#each fileArray as file}
|
||||
{@const fileName = file.name.split('.')}
|
||||
{@const fileName = file.name.split('.').slice(0, -1).join('.')}
|
||||
{@const extension = file.name.split('.').pop()}
|
||||
{@const fileSize = humanFileSize(file.size)}
|
||||
<li class="u-flex u-cross-center u-min-width-0">
|
||||
<span class="icon-document" aria-hidden="true" />
|
||||
<span class="upload-file-box-name u-trim u-min-width-0">
|
||||
<Trim>{fileName[0]}</Trim>
|
||||
<Trim>{fileName}</Trim>
|
||||
</span>
|
||||
<span class="upload-file-box-name u-min-width-0 u-flex-shrink-0">
|
||||
.{fileName[1]}
|
||||
.{extension}
|
||||
</span>
|
||||
<span
|
||||
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let name: string = id;
|
||||
export let value = '';
|
||||
export let pattern: string = null;
|
||||
export let patternError: string = '';
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
export let hideRequired = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let maxlength: number = null;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let tooltip: string = null;
|
||||
export let isMultiple = false;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (patternError && element.validity.patternMismatch) {
|
||||
error = patternError;
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
|
||||
type $$Events = {
|
||||
input: Event & { target: HTMLInputElement };
|
||||
};
|
||||
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{maxlength}
|
||||
{pattern}
|
||||
type="text"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if $$slots.options}
|
||||
<div class="options-list">
|
||||
<slot name="options" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning" class="u-line-height-1">{error}</Helper>
|
||||
{/if}
|
||||
</svelte:component>
|
||||
@@ -19,7 +19,7 @@
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
|
||||
const pattern = String.raw`^\+[1-9]\d{1,14}$`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -34,15 +34,21 @@
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = "Allowed characters: leading '+' and maximum of 15 digits";
|
||||
return;
|
||||
}
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = `Allowed characters: leading '+' and maximum of ${maxlength - 1} digits`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.validity.tooShort) {
|
||||
error = `The value must contain leading ‘+’ and at least ${minlength - 1} digits `;
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
@@ -92,7 +98,13 @@
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
bind:value
|
||||
bind:this={element}
|
||||
style:--amount-of-buttons={$$slots.options ? 1 : 0}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if $$slots.options}
|
||||
<div class="options-list">
|
||||
<slot name="options" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
export let fullWidth = false;
|
||||
export let autofocus = false;
|
||||
export let interactiveOutput = false;
|
||||
export let interactiveEmpty = false;
|
||||
export let hideEmpty = false;
|
||||
// stretch is used inside of a flex container to give the element flex:1
|
||||
export let stretch = true;
|
||||
export let search = '';
|
||||
@@ -51,7 +53,8 @@
|
||||
});
|
||||
|
||||
function handleInput() {
|
||||
hasFocus = true;
|
||||
if (hideEmpty && !options?.length) hasFocus = false;
|
||||
else hasFocus = true;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
@@ -202,7 +205,18 @@
|
||||
</li>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="text">There are no {name} that match your search</span>
|
||||
<button
|
||||
class:drop-button={interactiveEmpty}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (interactiveOutput && interactiveEmpty) hasFocus = !hasFocus;
|
||||
}}>
|
||||
<slot name="empty">
|
||||
<span class="text">
|
||||
There are no {name} that match your search
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
on:click|preventDefault
|
||||
class="tooltip"
|
||||
aria-label="input tooltip"
|
||||
use:tooltipAction={{ content: tooltip }}>
|
||||
use:tooltipAction={{ content: tooltip, appendTo: 'parent' }}>
|
||||
<span class="icon-info" aria-hidden="true" style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -2,3 +2,4 @@ export { default as Pill } from './pill.svelte';
|
||||
export { default as SelectSearchItem } from './selectSearchItem.svelte';
|
||||
export { default as Flag } from './flag.svelte';
|
||||
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
|
||||
export { default as SelectSearchRadio } from './selectSearchRadio.svelte';
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
export let external = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
export let eventData: Record<string, unknown> = {};
|
||||
export let style = '';
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
@@ -19,17 +23,19 @@
|
||||
}
|
||||
|
||||
trackEvent(`click_${event}`, {
|
||||
from: 'tag'
|
||||
from: 'tag',
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{style}
|
||||
{href}
|
||||
target={external ? '_blank' : '_self'}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
@@ -42,9 +48,10 @@
|
||||
<button
|
||||
on:click
|
||||
on:click={track}
|
||||
{style}
|
||||
{disabled}
|
||||
type={submit ? 'submit' : 'button'}
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
@@ -55,7 +62,8 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
{style}
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
// export let data: string[] = [];
|
||||
export let value = false;
|
||||
export let padding: number | null = null;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-gap-12"
|
||||
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
|
||||
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
|
||||
on:click|preventDefault
|
||||
type="button">
|
||||
<input type="checkbox" class="is-small" bind:checked={value} />
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let value: string;
|
||||
export let group: string;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-gap-12"
|
||||
on:click|preventDefault
|
||||
type="button">
|
||||
<input type="radio" class="is-small" {value} bind:group />
|
||||
<slot />
|
||||
</button>
|
||||
</li>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let title = '';
|
||||
export let onlyDesktop = false;
|
||||
export let width: number = null;
|
||||
export let showOverflow = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let style = '';
|
||||
export let right = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{style}
|
||||
style:--p-col-width={width?.toString() ?? ''}
|
||||
class="table-col {className}"
|
||||
class:u-overflow-visible={showOverflow}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class:u-flex={right}
|
||||
class:u-main-end={right}
|
||||
data-title={title}
|
||||
role="cell"
|
||||
on:click
|
||||
on:keypress
|
||||
data-private>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toggle } from '$lib/helpers/array';
|
||||
import { isHTMLInputElement } from '$lib/helpers/types';
|
||||
import { TableCell } from '.';
|
||||
import { TableCellButton } from '.';
|
||||
import { InputCheckbox } from '../forms';
|
||||
|
||||
export let id: string;
|
||||
@@ -12,6 +12,7 @@
|
||||
const handleClick = (e: Event) => {
|
||||
// Prevent the link from being followed
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isHTMLInputElement(el)) return;
|
||||
|
||||
selectedIds = toggle(selectedIds, id);
|
||||
@@ -24,13 +25,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<TableCell class="u-position-relative">
|
||||
<div
|
||||
class="touch-area"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
on:click={handleClick}
|
||||
on:keypress={handleClick} />
|
||||
<TableCellButton on:click={handleClick}>
|
||||
<InputCheckbox
|
||||
bind:element={el}
|
||||
id="select-{id}"
|
||||
@@ -38,11 +33,4 @@
|
||||
checked={selectedIds.includes(id)}
|
||||
{disabled}
|
||||
on:click={handleClick} />
|
||||
</TableCell>
|
||||
|
||||
<style lang="scss">
|
||||
.touch-area {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
</TableCellButton>
|
||||
|
||||
@@ -8,6 +8,7 @@ export { default as TableRow } from './row.svelte';
|
||||
export { default as TableRowLink } from './rowLink.svelte';
|
||||
export { default as TableRowButton } from './rowButton.svelte';
|
||||
export { default as TableCell } from './cell.svelte';
|
||||
export { default as TableCellButton } from './cellButton.svelte';
|
||||
export { default as TableCellHead } from './cellHead.svelte';
|
||||
export { default as TableCellHeadCheck } from './cellHeadCheck.svelte';
|
||||
export { default as TableCellLink } from './cellLink.svelte';
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let transparent = false;
|
||||
export let noStyles = false;
|
||||
export let dense = false;
|
||||
let classes: string = undefined;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
let isOverflowing = false;
|
||||
|
||||
@@ -38,7 +38,7 @@ export type Column = {
|
||||
filter?: boolean;
|
||||
array?: boolean;
|
||||
format?: string;
|
||||
elements?: string[] | { value: string; label: string }[];
|
||||
elements?: string[] | { value: string | number; label: string }[];
|
||||
};
|
||||
|
||||
export function isValueOfStringEnum<T extends Record<string, string>>(
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
export let title: string;
|
||||
export let tooltipContent =
|
||||
$organization.billingPlan === BillingPlan.FREE
|
||||
$organization?.billingPlan === BillingPlan.FREE
|
||||
? `Upgrade to add more ${title.toLocaleLowerCase()}`
|
||||
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
|
||||
tierToPlan($organization.billingPlan).name
|
||||
tierToPlan($organization?.billingPlan)?.name
|
||||
} plan`;
|
||||
export let disabled: boolean;
|
||||
export let buttonText: string;
|
||||
|
||||
+44
-12
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { hoursToDays, timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { Alert, Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
|
||||
import { Alert, Card, Code, Copy, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
|
||||
import { calculateTime } from '$lib/helpers/timeConversion';
|
||||
import {
|
||||
TableBody,
|
||||
@@ -18,6 +18,7 @@
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
let selectedRequest = 'parameters';
|
||||
let selectedResponse = 'logs';
|
||||
@@ -124,13 +125,27 @@
|
||||
{/if}
|
||||
</ul>
|
||||
<div class="status u-margin-inline-start-auto">
|
||||
<Pill
|
||||
warning={execution.status === 'waiting' ||
|
||||
execution.status === 'building'}
|
||||
danger={execution.status === 'failed'}
|
||||
info={execution.status === 'completed' || execution.status === 'ready'}>
|
||||
{execution.status}
|
||||
</Pill>
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: `Scheduled to execute on ${toLocaleDateTime(execution.scheduledAt)}`,
|
||||
disabled:
|
||||
!execution?.scheduledAt || execution.status !== 'scheduled',
|
||||
maxWidth: 180
|
||||
}}>
|
||||
<Pill
|
||||
warning={execution.status === 'waiting' ||
|
||||
execution.status === 'building'}
|
||||
danger={execution.status === 'failed'}
|
||||
info={execution.status === 'completed' ||
|
||||
execution.status === 'ready'}>
|
||||
{#if execution.status === 'scheduled'}
|
||||
<span class="icon-clock" aria-hidden="true" />
|
||||
{timeFromNow(execution.scheduledAt)}
|
||||
{:else}
|
||||
{execution.status}
|
||||
{/if}
|
||||
</Pill>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +160,23 @@
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<h4 class="text u-bold">Path:</h4>
|
||||
<span class="u-text-color-gray">{execution.requestPath}</span>
|
||||
|
||||
<Copy value={execution.requestPath}>
|
||||
<div
|
||||
class="interactive-text-output is-textarea"
|
||||
style:min-inline-size="0">
|
||||
<span class="text u-line-height-1-5 u-break-all">
|
||||
{execution.requestPath}
|
||||
</span>
|
||||
<div class="u-flex u-cross-child-start u-gap-8">
|
||||
<button
|
||||
class="interactive-text-output-button"
|
||||
aria-label="copy text">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Copy>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,8 +187,9 @@
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<h4 class="text u-bold">Status Code:</h4>
|
||||
<span class="u-text-color-gray"
|
||||
>{execution.responseStatusCode}</span>
|
||||
<span class="u-text-color-gray">
|
||||
{execution.responseStatusCode}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import Create from '$routes/(console)/feedbackWizard.svelte';
|
||||
import { showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let isOpen = false;
|
||||
|
||||
@@ -29,6 +31,9 @@
|
||||
narrow = hasSubNavigation;
|
||||
}
|
||||
|
||||
$: getContext<Writable<boolean>>('isNarrow').set(narrow);
|
||||
$: getContext<Writable<boolean>>('hasSubNavigation').set(hasSubNavigation);
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
// If Alt + S is pressed
|
||||
if (hasSubNavigation && event.altKey && event.keyCode === 83) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let isOpen = false;
|
||||
export let showSideNavigation = false;
|
||||
@@ -40,6 +42,10 @@
|
||||
wizard.hide();
|
||||
}
|
||||
});
|
||||
|
||||
const isNarrow = setContext('isNarrow', writable(false));
|
||||
const hasSubNavigation = setContext('hasSubNavigation', writable(false));
|
||||
$: sideSize = $hasSubNavigation ? ($isNarrow ? '17rem' : '25rem') : '12.5rem';
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY={y} />
|
||||
@@ -48,7 +54,8 @@
|
||||
class:grid-with-side={showSideNavigation}
|
||||
class:is-open={isOpen}
|
||||
class:u-hide={$wizard.show || $log.show || $wizard.cover}
|
||||
class:is-fixed-layout={$activeHeaderAlert?.show}>
|
||||
class:is-fixed-layout={$activeHeaderAlert?.show}
|
||||
style:--p-side-size={sideSize}>
|
||||
{#if $activeHeaderAlert?.show}
|
||||
<svelte:component this={$activeHeaderAlert.component} />
|
||||
{/if}
|
||||
|
||||
@@ -139,7 +139,18 @@
|
||||
}
|
||||
|
||||
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
|
||||
$: isLastStep = $wizard.step === steps.size;
|
||||
$: isLastStep = (() => {
|
||||
if ($wizard.step === steps.size) {
|
||||
return true;
|
||||
}
|
||||
let lastStep = true;
|
||||
steps.forEach((step, i) => {
|
||||
if (i > $wizard.step && !step.disabled) {
|
||||
lastStep = false;
|
||||
}
|
||||
});
|
||||
return lastStep;
|
||||
})();
|
||||
$: currentStep = steps.get($wizard.step);
|
||||
</script>
|
||||
|
||||
@@ -187,7 +198,7 @@
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="u-z-index-20 form-footer">
|
||||
<div class="u-z-index-5 form-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
{#if !isLastStep && currentStep?.optional}
|
||||
<Button text on:click={() => dispatch('finish')}>
|
||||
|
||||
@@ -31,17 +31,23 @@ export type Invoice = {
|
||||
$id: string;
|
||||
$createdAt: Date;
|
||||
$updatedAt: Date;
|
||||
permissions: string[];
|
||||
teamId: string;
|
||||
aggregationId: string;
|
||||
plan: Tier;
|
||||
amount: number;
|
||||
tax: number;
|
||||
taxAmount: number;
|
||||
vat: number;
|
||||
vatAmount: number;
|
||||
grossAmount: number;
|
||||
creditsUsed: number;
|
||||
currency: string;
|
||||
date: number;
|
||||
from: string;
|
||||
to: string;
|
||||
status: string;
|
||||
dueAt: string;
|
||||
clientSecret: string;
|
||||
tier: Tier;
|
||||
usage: {
|
||||
name: string;
|
||||
value: number /* service over the limit*/;
|
||||
@@ -111,6 +117,7 @@ export type Credit = {
|
||||
};
|
||||
|
||||
export type CreditList = {
|
||||
available: number;
|
||||
credits: Credit[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
@@ -150,7 +150,7 @@ export const tierPro: TierData = {
|
||||
};
|
||||
export const tierScale: TierData = {
|
||||
name: 'Scale',
|
||||
description: 'For scaling teams that need dedicated support.'
|
||||
description: 'For scaling teams and agencies that need dedicated support.'
|
||||
};
|
||||
|
||||
export const showUsageRatesModal = writable<boolean>(false);
|
||||
@@ -362,7 +362,7 @@ export async function checkForMandate(org: Organization) {
|
||||
const paymentId = org.paymentMethodId ?? org.backupPaymentMethodId;
|
||||
if (!paymentId) return;
|
||||
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentId);
|
||||
if (paymentMethod.mandateId === null && paymentMethod.country === 'in') {
|
||||
if (paymentMethod?.mandateId === null && paymentMethod?.country.toLowerCase() === 'in') {
|
||||
headerAlert.add({
|
||||
id: 'paymentMandate',
|
||||
component: PaymentMandate,
|
||||
@@ -422,14 +422,14 @@ export const upgradeURL = derived(
|
||||
|
||||
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
|
||||
|
||||
export function calculateExcess(usage: OrganizationUsage, plan: Plan, org: Organization) {
|
||||
export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: number) {
|
||||
const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0;
|
||||
return {
|
||||
bandwidth: calculateResourceSurplus(totBandwidth, plan.bandwidth),
|
||||
storage: calculateResourceSurplus(usage?.storageTotal, plan.storage, 'GB'),
|
||||
users: calculateResourceSurplus(usage?.usersTotal, plan.users),
|
||||
executions: calculateResourceSurplus(usage?.executionsTotal, plan.executions, 'GB'),
|
||||
members: calculateResourceSurplus(org.total, plan.members)
|
||||
members: calculateResourceSurplus(members, plan.members)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//campaign welcome and startup
|
||||
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export type CampaignData = {
|
||||
title: string;
|
||||
description: string;
|
||||
template: 'card' | 'review';
|
||||
data?: Record<string, unknown>;
|
||||
onlyNewOrgs?: boolean;
|
||||
plan?: BillingPlan;
|
||||
footer?: boolean;
|
||||
};
|
||||
|
||||
@@ -22,7 +25,7 @@ campaigns
|
||||
template: 'card',
|
||||
title: 'Welcome to the Startups program!',
|
||||
description:
|
||||
"We're excited to have you on board. Add the coupon code to your Appwrite Pro account to join."
|
||||
"We're excited to have you on board. Add the coupon code to your Appwrite Scale account to join."
|
||||
})
|
||||
.set('RenderATL2024', {
|
||||
template: 'card',
|
||||
@@ -52,12 +55,33 @@ campaigns
|
||||
template: 'review',
|
||||
title: 'Welcome to the Startups program',
|
||||
description:
|
||||
"We're excited to have you on board. Add your credit code to your Appwrite Pro account to join.",
|
||||
"We're excited to have you on board. Add your credit code to your Appwrite Scale account to join.",
|
||||
onlyNewOrgs: true,
|
||||
plan: BillingPlan.SCALE,
|
||||
data: {
|
||||
cta: 'Get everything out of Cloud with Scale',
|
||||
claimed: 'Your credits will be valid for 12 months.',
|
||||
unclaimed: 'Apply your code to join the Startups program.',
|
||||
reviews: [
|
||||
{
|
||||
name: 'David Foster',
|
||||
img: '1.jpeg',
|
||||
desc: 'Managing director',
|
||||
review: 'We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. It was saving us a lot of money in comparison to Firebase since the amount of users grew quite fast and we needed a quick switch.'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.set('Free100', {
|
||||
template: 'review',
|
||||
title: 'Get started with $100 credits',
|
||||
description:
|
||||
'Get $100 in Cloud credits when you upgrade or create an organization with a Pro plan.',
|
||||
onlyNewOrgs: true,
|
||||
data: {
|
||||
cta: 'Get everything out of Cloud with Pro',
|
||||
claimed: 'Your credits will be valid for 12 months.',
|
||||
unclaimed: 'Apply your code to join the Startups program.',
|
||||
claimed: 'Your credits will be valid for 3 months.',
|
||||
unclaimed: 'Apply your code to join Appwrite Pro.',
|
||||
reviews: [
|
||||
{
|
||||
name: 'David Foster',
|
||||
@@ -84,6 +108,42 @@ campaigns
|
||||
]
|
||||
}
|
||||
})
|
||||
.set('FreeCodeCamp', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 FreeCodeCamp credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('AniaKubow', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Ania Kubów credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('Fireship', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Fireship credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('Hyperplexed', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 Hyperplexed credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('TraversyMedia', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 TraversyMedia credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('VueJS', {
|
||||
template: 'card',
|
||||
title: 'Claim your $50 VueJS credits.',
|
||||
description:
|
||||
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
|
||||
})
|
||||
.set('FusionVC', {
|
||||
template: 'review',
|
||||
title: 'Welcome to Appwrite!',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
|
||||
billingAddressId?: string;
|
||||
amount: number;
|
||||
billingTaxId?: string;
|
||||
billingPlanDowngrade?: string;
|
||||
billingPlanDowngrade?: Tier;
|
||||
};
|
||||
|
||||
export type OrganizationList = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const runtimesList = derived(
|
||||
page,
|
||||
async ($page) => (await $page.data.runtimesList) as Models.RuntimeList
|
||||
);
|
||||
|
||||
export const baseRuntimesList = derived(runtimesList, async ($runtimesList) => {
|
||||
const baseRuntimes = new Map<string, Models.Runtime>();
|
||||
for (const runtime of (await $runtimesList).runtimes) {
|
||||
const runtimeBase = runtime.name.split('-')[0];
|
||||
baseRuntimes.set(runtimeBase, runtime);
|
||||
}
|
||||
return { runtimes: [...baseRuntimes.values()] };
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const templatesList = derived(
|
||||
page,
|
||||
async ($page) => (await $page.data.templatesList) as Models.TemplateFunctionList
|
||||
);
|
||||
|
||||
export const featuredTemplatesList = derived(templatesList, async ($templatesList) => {
|
||||
return {
|
||||
templates: (await $templatesList).templates
|
||||
.filter((template) => template.id !== 'starter')
|
||||
.slice(0, 2)
|
||||
};
|
||||
});
|
||||
|
||||
export const starterTemplate = derived(templatesList, async ($templatesList) => {
|
||||
return (await $templatesList).templates.find((template) => template.id === 'starter');
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Box } from '$lib/components';
|
||||
import { FormList, Helper, InputChoice, InputPassword } from '$lib/elements/forms';
|
||||
import type { Variable } from '$lib/stores/marketplace';
|
||||
import { templateConfig } from '../store';
|
||||
|
||||
export let appwriteVariable: Variable;
|
||||
</script>
|
||||
|
||||
<Box radius="small" padding={16}>
|
||||
<FormList>
|
||||
<div>
|
||||
<InputPassword
|
||||
id={appwriteVariable.name}
|
||||
label={appwriteVariable.name}
|
||||
placeholder={appwriteVariable.placeholder ?? 'Enter value'}
|
||||
showPasswordButton
|
||||
required={appwriteVariable.required && !$templateConfig.generateKey}
|
||||
bind:value={$templateConfig.appwriteApiKey}
|
||||
disabled={!!$templateConfig.generateKey} />
|
||||
<Helper type="neutral">
|
||||
This API key will allow you to interact with the Appwrite server APIs. <a
|
||||
href="https://appwrite.io/docs/advanced/platform/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more</a
|
||||
>.
|
||||
</Helper>
|
||||
</div>
|
||||
<InputChoice
|
||||
bind:value={$templateConfig.generateKey}
|
||||
id="generate"
|
||||
label="Generate API key on completion"
|
||||
disabled={!!$templateConfig.appwriteApiKey}>
|
||||
The <code class="inline-code">APPWRITE_API_KEY</code> will be automatically generated for
|
||||
your project and applied to this function once it's created.
|
||||
</InputChoice>
|
||||
</FormList>
|
||||
</Box>
|
||||
@@ -1,7 +1,10 @@
|
||||
<script context="module" lang="ts">
|
||||
import CreateTemplate from './createTemplate.svelte';
|
||||
|
||||
export function connectTemplate(template: MarketplaceTemplate, runtime: string | null = null) {
|
||||
export function connectTemplate(
|
||||
template: Models.TemplateFunction,
|
||||
runtime: string | null = null
|
||||
) {
|
||||
const variables: Record<string, string> = {};
|
||||
template.variables.forEach((variable) => {
|
||||
variables[variable.name] = variable.value ?? '';
|
||||
@@ -24,14 +27,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { AvatarGroup, Box, CardGrid, Heading } from '$lib/components';
|
||||
import { AvatarGroup, Box, Heading } from '$lib/components';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { repository, templateConfig, template as templateStore } from './store';
|
||||
import { marketplace, type MarketplaceTemplate } from '$lib/stores/marketplace';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { page } from '$app/stores';
|
||||
import { baseRuntimesList } from '$routes/(console)/project-[project]/functions/store';
|
||||
import { baseRuntimesList } from '$lib/stores/runtimes';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import WizardCover from '$lib/layout/wizardCover.svelte';
|
||||
@@ -41,14 +43,12 @@
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { isSelfHosted } from '$lib/system';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import { featuredTemplatesList, starterTemplate } from '$lib/stores/templates';
|
||||
|
||||
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
|
||||
let hasInstallations: boolean;
|
||||
let selectedRepository: string;
|
||||
|
||||
const quickStart = marketplace.find((template) => template.id === 'starter');
|
||||
const templates = marketplace.filter((template) => template.id !== 'starter').slice(0, 2);
|
||||
|
||||
function connect(event: CustomEvent<Models.ProviderRepository>) {
|
||||
trackEvent('click_connect_repository', {
|
||||
from: 'cover'
|
||||
@@ -61,55 +61,7 @@
|
||||
<WizardCover>
|
||||
<svelte:fragment slot="title">Create Function</svelte:fragment>
|
||||
<div class="wizard-container container">
|
||||
{#if isSelfHosted && !isVcsEnabled}
|
||||
<div class="u-flex-vertical u-text-center">
|
||||
<Heading tag="h5" size="5">Choose your source</Heading>
|
||||
<p>
|
||||
Connect your function to a Git repository or use a template to get started. You
|
||||
can also create a function manually.
|
||||
</p>
|
||||
</div>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="6">Create manually</Heading>
|
||||
<p class="text">Create and deploy your function manually.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-height-100-percent u-main-end u-cross-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
trackEvent('click_create_function_manual', {
|
||||
from: 'cover'
|
||||
});
|
||||
}}
|
||||
on:click={() => wizard.start(CreateManual)}>Create function</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/if}
|
||||
<div
|
||||
class="git-container u-position-relative"
|
||||
class:u-margin-block-start-24={isSelfHosted && !isVcsEnabled}>
|
||||
{#if isSelfHosted && !isVcsEnabled}
|
||||
<div
|
||||
class="overlay u-flex-vertical u-position-absolute u-height-100-percent u-width-full-line u-z-index-1 card u-text-center">
|
||||
<div
|
||||
class="u-flex-vertical u-height-100-percent u-main-center u-cross-center u-gap-16">
|
||||
<Heading size="7" tag="h6">
|
||||
Configure your self-hosted instance to connect to Git
|
||||
</Heading>
|
||||
<p>
|
||||
Connect your function to a Git repository or use a pre-made template<br />after
|
||||
configuring your self-hosted instance. Learn more in our
|
||||
<a
|
||||
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="git-container u-position-relative">
|
||||
<div class="grid-1-1 u-gap-24">
|
||||
<div class="card u-cross-child-start u-height-100-percent">
|
||||
<Heading size="6" tag="h6">Connect Git repository</Heading>
|
||||
@@ -127,7 +79,30 @@
|
||||
}}
|
||||
on:connect={connect} />
|
||||
</div>
|
||||
{#if isSelfHosted && !isVcsEnabled}
|
||||
<div
|
||||
class="overlay u-flex-vertical u-position-absolute u-height-100-percent u-width-full-line u-z-index-1 u-text-center u-inset-0"
|
||||
style="border-radius: var(--border-radius-medium)">
|
||||
<div
|
||||
class="u-flex-vertical u-height-100-percent u-main-center u-cross-center u-gap-16 u-padding-inline-24">
|
||||
<Heading size="7" tag="h6" trimmed={false}>
|
||||
Connect your self-hosted instance to Git
|
||||
</Heading>
|
||||
<p>
|
||||
Configure your self-hosted instance to connect your function to
|
||||
a Git repository.
|
||||
<a
|
||||
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card u-height-100-percent">
|
||||
<section class="common-section">
|
||||
<Heading size="6" tag="h6">Quick start</Heading>
|
||||
@@ -137,7 +112,7 @@
|
||||
style:--grid-item-size="8rem"
|
||||
style:--grid-item-size-small-screens="9rem"
|
||||
style:--grid-gap=".5rem">
|
||||
{#await $baseRuntimesList}
|
||||
{#await Promise.all([$baseRuntimesList, $starterTemplate])}
|
||||
{#each Array(6) as _i}
|
||||
<li>
|
||||
<button
|
||||
@@ -151,7 +126,7 @@
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{:then response}
|
||||
{:then [response, quickStart]}
|
||||
{@const runtimes = new Map(
|
||||
response.runtimes.map((r) => [r.$id, r])
|
||||
)}
|
||||
@@ -184,6 +159,9 @@
|
||||
</div>
|
||||
<div class="body-text-2">
|
||||
{runtimeDetail.name}
|
||||
{#if runtimeDetail.name.toLowerCase() === 'go'}
|
||||
<span class="inline-tag">New</span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
@@ -210,6 +188,13 @@
|
||||
{/await}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
text
|
||||
class="u-margin-block-start-24 u-margin-inline-start-auto"
|
||||
href={`${base}/project-${$page.params.project}/functions/templates?useCase=Starter`}>
|
||||
All starter templates <span class="icon-cheveron-right" />
|
||||
</Button>
|
||||
<div class="u-sep-block-start common-section" />
|
||||
<section class="common-section">
|
||||
<Heading size="6" tag="h6">Templates</Heading>
|
||||
@@ -218,34 +203,56 @@
|
||||
</p>
|
||||
|
||||
<ul class="clickable-list u-margin-block-start-16">
|
||||
{#each templates as template}
|
||||
<li class="clickable-list-item">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
trackEvent('click_connect_template', {
|
||||
from: 'cover',
|
||||
template: template.id
|
||||
});
|
||||
}}
|
||||
on:click={() => connectTemplate(template)}
|
||||
class="clickable-list-button u-width-full-line u-flex u-gap-12">
|
||||
<div
|
||||
class="avatar is-size-small"
|
||||
style:--p-text-size="1.25rem">
|
||||
<span class={template.icon} />
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<div class="body-text-2 u-bold u-trim">
|
||||
{template.name}
|
||||
{#await $featuredTemplatesList}
|
||||
{#each Array(3) as _i}
|
||||
<li>
|
||||
<button
|
||||
disabled
|
||||
class="clickable-list-button u-width-full-line u-flex u-gap-12">
|
||||
<div class="avatar is-size-small">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
<div class="u-trim-1 u-color-text-gray">
|
||||
{template.tagline}
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<div class="body-text-2 u-bold u-trim">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
<div class="u-trim-1 u-color-text-gray">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{:then templatesListWithoutStarter}
|
||||
{#each templatesListWithoutStarter.templates as template}
|
||||
<li class="clickable-list-item">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
trackEvent('click_connect_template', {
|
||||
from: 'cover',
|
||||
template: template.id
|
||||
});
|
||||
}}
|
||||
on:click={() => connectTemplate(template)}
|
||||
class="clickable-list-button u-width-full-line u-flex u-gap-12">
|
||||
<div
|
||||
class="avatar is-size-small"
|
||||
style:--p-text-size="1.25rem">
|
||||
<span class={template.icon} />
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-4">
|
||||
<div class="body-text-2 u-bold u-trim">
|
||||
{template.name}
|
||||
</div>
|
||||
<div class="u-trim-1 u-color-text-gray">
|
||||
{template.tagline}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/await}
|
||||
</ul>
|
||||
</section>
|
||||
<Button
|
||||
@@ -279,12 +286,17 @@
|
||||
</div>
|
||||
</WizardCover>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.git-container .overlay {
|
||||
background: linear-gradient(
|
||||
0,
|
||||
hsl(var(--p-card-bg-color)) 68.91%,
|
||||
hsl(var(--p-card-bg-color) / 0.5) 95.8%
|
||||
hsl(var(--p-card-bg-color) / 0.5) 92.8%
|
||||
);
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import Details from './steps/details.svelte';
|
||||
import Configuration from './steps/configuration.svelte';
|
||||
import Details from './steps/manualDetails.svelte';
|
||||
import Configuration from './steps/manualConfiguration.svelte';
|
||||
import ExecuteAccess from './steps/executeAccess.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { ID, Runtime } from '@appwrite.io/console';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { goto } from '$app/navigation';
|
||||
import { choices, installation, repository, template, templateConfig } from './store';
|
||||
import {
|
||||
choices,
|
||||
installation,
|
||||
repository,
|
||||
template,
|
||||
templateConfig,
|
||||
templateStepsComponents
|
||||
} from './store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import GitConfiguration from './steps/gitConfiguration.svelte';
|
||||
import TemplateConfiguration from './steps/templateConfiguration.svelte';
|
||||
import RepositoryBehaviour from './steps/repositoryBehaviour.svelte';
|
||||
import CreateRepository from './steps/createRepository.svelte';
|
||||
import TemplateVariables from './steps/templateVariables.svelte';
|
||||
import { scopes } from '$lib/constants';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import TemplateConfiguration from './steps/templateConfiguration.svelte';
|
||||
import TemplatePermissions from './steps/templatePermissions.svelte';
|
||||
import TemplateVariables from './steps/templateVariables.svelte';
|
||||
import TemplateDeployment from './steps/templateDeployment.svelte';
|
||||
import CreateRepository from './steps/createRepository.svelte';
|
||||
import GitConfiguration from './steps/gitConfiguration.svelte';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
@@ -26,22 +32,12 @@
|
||||
const runtimeDetail = $template.runtimes.find(
|
||||
(r) => r.name === $templateConfig.runtime
|
||||
);
|
||||
if ($templateConfig.appwriteApiKey) {
|
||||
$templateConfig.variables['APPWRITE_API_KEY'] = $templateConfig.appwriteApiKey;
|
||||
} else if ($templateConfig?.generateKey) {
|
||||
const key = await sdk.forConsole.projects.createKey(
|
||||
$page.params.project,
|
||||
'Generated for Template',
|
||||
scopes.map((scope) => scope.scope)
|
||||
);
|
||||
$templateConfig.variables['APPWRITE_API_KEY'] = key.secret;
|
||||
}
|
||||
|
||||
const response = await sdk.forProject.functions.create(
|
||||
$templateConfig.$id || ID.unique(),
|
||||
$templateConfig.name,
|
||||
$templateConfig.runtime,
|
||||
$template.permissions || undefined,
|
||||
$templateConfig?.execute ? $template.permissions || undefined : undefined,
|
||||
$template.events || undefined,
|
||||
$template.cron || undefined,
|
||||
$template.timeout || undefined,
|
||||
@@ -49,16 +45,20 @@
|
||||
undefined,
|
||||
runtimeDetail.entrypoint,
|
||||
runtimeDetail.commands || undefined,
|
||||
undefined,
|
||||
$installation.$id,
|
||||
$repository.id,
|
||||
$choices.branch,
|
||||
$choices.silentMode || undefined,
|
||||
$choices.rootDir || undefined,
|
||||
$template.providerRepositoryId,
|
||||
$template.providerOwner,
|
||||
runtimeDetail.providerRootDirectory,
|
||||
$template.providerBranch
|
||||
$templateConfig?.scopes?.length ? $templateConfig.scopes : undefined,
|
||||
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $installation.$id,
|
||||
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $repository.id,
|
||||
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $choices.branch,
|
||||
$templateConfig.repositoryBehaviour === 'manual'
|
||||
? undefined
|
||||
: $choices.silentMode || undefined,
|
||||
$templateConfig.repositoryBehaviour === 'manual'
|
||||
? undefined
|
||||
: $choices.rootDir || undefined,
|
||||
$template.providerRepositoryId || undefined,
|
||||
$template.providerOwner || undefined,
|
||||
runtimeDetail.providerRootDirectory || undefined,
|
||||
$template.providerVersion || undefined
|
||||
);
|
||||
|
||||
if ($templateConfig.variables) {
|
||||
@@ -75,7 +75,10 @@
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionCreate, {
|
||||
customId: !!response.$id
|
||||
customId: !!response.$id,
|
||||
runtime: response.runtime,
|
||||
deployment_type: $templateConfig.repositoryBehaviour,
|
||||
scopes: $templateConfig.scopes
|
||||
});
|
||||
resetState();
|
||||
} catch (error) {
|
||||
@@ -93,27 +96,35 @@
|
||||
installation.set(null);
|
||||
}
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
$templateStepsComponents.set(1, {
|
||||
label: 'Configuration',
|
||||
component: TemplateConfiguration
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
$templateStepsComponents.set(2, {
|
||||
label: 'Permissions',
|
||||
component: TemplatePermissions
|
||||
});
|
||||
$templateStepsComponents.set(3, {
|
||||
label: 'Variables',
|
||||
component: TemplateVariables
|
||||
component: TemplateVariables,
|
||||
disabled: !$template?.variables?.length
|
||||
});
|
||||
stepsComponents.set(3, {
|
||||
label: 'Connect',
|
||||
component: RepositoryBehaviour
|
||||
$templateStepsComponents.set(4, {
|
||||
label: 'Deployment',
|
||||
component: TemplateDeployment
|
||||
});
|
||||
stepsComponents.set(4, {
|
||||
$templateStepsComponents.set(5, {
|
||||
label: 'Repository',
|
||||
component: CreateRepository
|
||||
});
|
||||
stepsComponents.set(5, {
|
||||
$templateStepsComponents.set(6, {
|
||||
label: 'Branch',
|
||||
component: GitConfiguration
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} on:exit={resetState} />
|
||||
<Wizard
|
||||
title="Create Function"
|
||||
steps={$templateStepsComponents}
|
||||
on:finish={create}
|
||||
on:exit={resetState} />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { choices, createFunction, installation, repository } from '../store';
|
||||
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
|
||||
import { runtimesList } from '$lib/stores/runtimes';
|
||||
|
||||
let showCustomId = false;
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { createFunction } from '../store';
|
||||
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
|
||||
import { runtimesList } from '$lib/stores/runtimes';
|
||||
|
||||
let showCustomId = false;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { LabelCard } from '$lib/components';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { templateConfig } from '../store';
|
||||
|
||||
async function beforeSubmit() {
|
||||
if (!$templateConfig.repositoryBehaviour) {
|
||||
throw new Error('Please select repository behaviour.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Connect</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Connect function to a new repository or to an existing one within a selected Git
|
||||
organization.
|
||||
</svelte:fragment>
|
||||
|
||||
<ul class="u-flex u-flex-vertical u-gap-16">
|
||||
<LabelCard
|
||||
name="behaviour"
|
||||
value="new"
|
||||
backgroundColor={$app.themeInUse === 'light' ? 'var(--color-neutral-5)' : null}
|
||||
backgroundColorHover={$app.themeInUse === 'light' ? 'var(--color-neutral-10)' : null}
|
||||
bind:group={$templateConfig.repositoryBehaviour}>
|
||||
<svelte:fragment slot="title">Create a new repository</svelte:fragment>
|
||||
Clone the template and create a new repository in your selected organization.
|
||||
</LabelCard>
|
||||
<LabelCard
|
||||
name="behaviour"
|
||||
value="existing"
|
||||
backgroundColor={$app.themeInUse === 'light' ? 'var(--color-neutral-5)' : null}
|
||||
backgroundColorHover={$app.themeInUse === 'light' ? 'var(--color-neutral-10)' : null}
|
||||
bind:group={$templateConfig.repositoryBehaviour}>
|
||||
<svelte:fragment slot="title">Add to existing repository</svelte:fragment>
|
||||
Clone the template to an existing repository in your selected organization.
|
||||
</LabelCard>
|
||||
</ul>
|
||||
</WizardStep>
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Pill } from '$lib/elements';
|
||||
import { FormList, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
|
||||
import { runtimesList } from '$lib/stores/runtimes';
|
||||
import { template, templateConfig } from '../store';
|
||||
|
||||
let showCustomId = false;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { LabelCard } from '$lib/components';
|
||||
import { FormList } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { templateConfig, templateStepsComponents } from '../store';
|
||||
|
||||
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
|
||||
|
||||
onMount(() => {
|
||||
if (!isVcsEnabled) {
|
||||
$templateConfig.repositoryBehaviour = 'manual';
|
||||
}
|
||||
});
|
||||
|
||||
async function beforeSubmit() {
|
||||
if (!$templateConfig.repositoryBehaviour) {
|
||||
throw new Error('Please select repository behaviour.');
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($templateConfig.repositoryBehaviour === 'manual') {
|
||||
$templateStepsComponents.get(5).disabled = true;
|
||||
$templateStepsComponents.get(6).disabled = true;
|
||||
$templateStepsComponents = $templateStepsComponents;
|
||||
} else {
|
||||
$templateStepsComponents.get(5).disabled = false;
|
||||
$templateStepsComponents.get(6).disabled = false;
|
||||
$templateStepsComponents = $templateStepsComponents;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Deployment</svelte:fragment>
|
||||
|
||||
<h3>Connect with Git <span class="inline-code">Recommended</span></h3>
|
||||
<FormList gap={16} class="u-margin-block-start-8">
|
||||
<LabelCard
|
||||
name="behaviour"
|
||||
value="new"
|
||||
bind:group={$templateConfig.repositoryBehaviour}
|
||||
disabled={!isVcsEnabled}>
|
||||
<svelte:fragment slot="title">Create a new repository</svelte:fragment>
|
||||
Clone the template to a newly created repository in your organization.
|
||||
</LabelCard>
|
||||
<LabelCard
|
||||
name="behaviour"
|
||||
value="existing"
|
||||
bind:group={$templateConfig.repositoryBehaviour}
|
||||
disabled={!isVcsEnabled}>
|
||||
<svelte:fragment slot="title">Add to existing repository</svelte:fragment>
|
||||
Clone the template to an existing repository in your organization.
|
||||
</LabelCard>
|
||||
</FormList>
|
||||
|
||||
<h3 class="u-margin-block-start-24">Quick start</h3>
|
||||
<ul class="u-margin-block-start-8">
|
||||
<LabelCard name="behaviour" value="manual" bind:group={$templateConfig.repositoryBehaviour}>
|
||||
<svelte:fragment slot="title">Connect later</svelte:fragment>
|
||||
Deploy now and continue development via CLI, or connect Git from your function settings.
|
||||
</LabelCard>
|
||||
</ul>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { scopes } from '$lib/constants';
|
||||
import { FormList, InputChoice } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { template, templateConfig } from '../store';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
let templateScopes = [];
|
||||
onMount(() => {
|
||||
$templateConfig.execute = $template.permissions.includes('any');
|
||||
templateScopes = scopes
|
||||
.filter((scope) => $template.scopes.includes(scope.scope))
|
||||
.map((scope) => ({
|
||||
...scope,
|
||||
active: true
|
||||
}));
|
||||
});
|
||||
|
||||
$: $templateConfig.scopes = templateScopes
|
||||
?.filter((scope) => scope.active)
|
||||
?.map((scope) => scope.scope);
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Permissions</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Enable recommended scopes and execute access for when your function is deployed.
|
||||
</svelte:fragment>
|
||||
<h3 class="label">Execute permissions</h3>
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<div class="user-profile">
|
||||
<span class="avatar" style:--p-text-size="1rem">
|
||||
<span class="icon-lock-open" />
|
||||
</span>
|
||||
<span class="user-profile-info u-flex u-main-space-between u-gap-16">
|
||||
<div>
|
||||
<p class="name u-bold">
|
||||
Public (anyone can execute) <span
|
||||
class="icon-info"
|
||||
use:tooltip={{
|
||||
content:
|
||||
'You can further customize execute permissions in your function settings.'
|
||||
}} />
|
||||
</p>
|
||||
<p class="text u-margin-block-start-4">
|
||||
This could include unauthorized users and search engines.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="execute"
|
||||
label="Execute"
|
||||
showLabel={false}
|
||||
bind:value={$templateConfig.execute}></InputChoice>
|
||||
</span>
|
||||
</div>
|
||||
</FormList>
|
||||
{#if templateScopes.length > 0}
|
||||
<h3 class="label u-margin-block-start-48">Function scopes</h3>
|
||||
<FormList class="u-margin-block-start-16">
|
||||
{#each templateScopes as scope, i}
|
||||
<div class="user-profile">
|
||||
<span class="avatar" style:--p-text-size="1rem">
|
||||
<span class={`icon-${scope.icon}`} />
|
||||
</span>
|
||||
<span class="user-profile-info u-flex u-main-space-between u-gap-16">
|
||||
<div>
|
||||
<p class="name u-bold">{scope.scope}</p>
|
||||
<p class="text u-margin-block-start-4">{scope.description}</p>
|
||||
</div>
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id={scope.scope}
|
||||
label={scope.scope}
|
||||
showLabel={false}
|
||||
bind:value={scope.active}>
|
||||
</InputChoice>
|
||||
</span>
|
||||
</div>
|
||||
{#if i < templateScopes.length - 1}
|
||||
<div class="with-separators"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</FormList>
|
||||
{/if}
|
||||
</WizardStep>
|
||||
@@ -12,7 +12,6 @@
|
||||
InputNumber
|
||||
} from '$lib/elements/forms';
|
||||
import { Card, Collapsible, CollapsibleItem } from '$lib/components';
|
||||
import AppwriteVariable from '../components/appwriteVariable.svelte';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
|
||||
async function beforeSubmit() {
|
||||
@@ -20,10 +19,6 @@
|
||||
if (!variable.required) {
|
||||
continue;
|
||||
}
|
||||
if (variable.name === 'APPWRITE_API_KEY') {
|
||||
if ($templateConfig.appwriteApiKey || $templateConfig.generateKey) continue;
|
||||
else throw new Error(`Please set ${variable.name} variable or generate it.`);
|
||||
}
|
||||
if (!$templateConfig.variables[variable.name]) {
|
||||
throw new Error(`Please set ${variable.name} variable.`);
|
||||
}
|
||||
@@ -33,9 +28,7 @@
|
||||
$: requiredVariables = $template?.variables?.filter((v) => v.required);
|
||||
$: optionalVariables = $template?.variables?.filter((v) => !v.required);
|
||||
|
||||
function selectComponent(
|
||||
variableType: 'password' | 'text' | 'number' | 'email' | 'url' | 'phone'
|
||||
): typeof SvelteComponent<unknown> {
|
||||
function selectComponent(variableType: string): typeof SvelteComponent<unknown> {
|
||||
switch (variableType) {
|
||||
case 'password':
|
||||
return InputPassword;
|
||||
@@ -72,25 +65,21 @@
|
||||
|
||||
<FormList>
|
||||
{#each requiredVariables as variable}
|
||||
{#if variable.name === 'APPWRITE_API_KEY'}
|
||||
<AppwriteVariable appwriteVariable={variable} />
|
||||
{:else}
|
||||
<div>
|
||||
<svelte:component
|
||||
this={selectComponent(variable.type)}
|
||||
id={variable.name}
|
||||
label={variable.name}
|
||||
placeholder={variable.placeholder ?? 'Enter value'}
|
||||
required={variable.required}
|
||||
autocomplete={false}
|
||||
minlength={variable.type === 'password' ? 0 : null}
|
||||
showPasswordButton={variable.type === 'password'}
|
||||
bind:value={$templateConfig.variables[variable.name]} />
|
||||
<Helper type="neutral">
|
||||
{@html variable.description}
|
||||
</Helper>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<svelte:component
|
||||
this={selectComponent(variable.type)}
|
||||
id={variable.name}
|
||||
label={variable.name}
|
||||
placeholder={variable.placeholder ?? 'Enter value'}
|
||||
required={variable.required}
|
||||
autocomplete={false}
|
||||
minlength={variable.type === 'password' ? 0 : null}
|
||||
showPasswordButton={variable.type === 'password'}
|
||||
bind:value={$templateConfig.variables[variable.name]} />
|
||||
<Helper type="neutral">
|
||||
{@html variable.description}
|
||||
</Helper>
|
||||
</div>
|
||||
{/each}
|
||||
</FormList>
|
||||
</CollapsibleItem>
|
||||
@@ -107,25 +96,21 @@
|
||||
|
||||
<FormList>
|
||||
{#each optionalVariables as variable}
|
||||
{#if variable.name === 'APPWRITE_API_KEY'}
|
||||
<AppwriteVariable appwriteVariable={variable} />
|
||||
{:else}
|
||||
<div>
|
||||
<svelte:component
|
||||
this={selectComponent(variable.type)}
|
||||
id={variable.name}
|
||||
label={variable.name}
|
||||
placeholder={variable.placeholder ?? 'Enter value'}
|
||||
required={variable.required}
|
||||
autocomplete={false}
|
||||
showPasswordButton={variable.type === 'password'}
|
||||
bind:value={$templateConfig.variables[variable.name]} />
|
||||
<div>
|
||||
<svelte:component
|
||||
this={selectComponent(variable.type)}
|
||||
id={variable.name}
|
||||
label={variable.name}
|
||||
placeholder={variable.placeholder ?? 'Enter value'}
|
||||
required={variable.required}
|
||||
autocomplete={false}
|
||||
showPasswordButton={variable.type === 'password'}
|
||||
bind:value={$templateConfig.variables[variable.name]} />
|
||||
|
||||
<Helper type="neutral">
|
||||
{@html variable.description}
|
||||
</Helper>
|
||||
</div>
|
||||
{/if}
|
||||
<Helper type="neutral">
|
||||
{@html variable.description}
|
||||
</Helper>
|
||||
</div>
|
||||
{/each}
|
||||
</FormList>
|
||||
</CollapsibleItem>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { MarketplaceTemplate } from '$lib/stores/marketplace';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { derived, writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const template = writable<MarketplaceTemplate>();
|
||||
export const template = writable<Models.TemplateFunction>();
|
||||
export const templateConfig = writable<{
|
||||
$id: string;
|
||||
name: string;
|
||||
runtime: string;
|
||||
variables: { [key: string]: unknown };
|
||||
repositoryBehaviour: 'new' | 'existing';
|
||||
repositoryName: string;
|
||||
repositoryPrivate: boolean;
|
||||
repositoryBehaviour: 'new' | 'existing' | 'manual';
|
||||
repositoryName?: string;
|
||||
repositoryPrivate?: boolean;
|
||||
repositoryId: string;
|
||||
appwriteApiKey?: string;
|
||||
generateKey?: boolean;
|
||||
execute?: boolean;
|
||||
scopes?: string[];
|
||||
}>();
|
||||
export const repository = writable<Models.ProviderRepository>();
|
||||
export const installation = writable<Models.Installation>();
|
||||
@@ -59,3 +59,5 @@ function createFunctionStore() {
|
||||
export const createFunction = createFunctionStore();
|
||||
|
||||
export const createFunctionDeployment = writable<FileList>();
|
||||
|
||||
export const templateStepsComponents: Writable<WizardStepsType> = writable(new Map());
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { mfaRequired } = await parent();
|
||||
if (!mfaRequired) {
|
||||
redirect(303, base);
|
||||
}
|
||||
return {
|
||||
factors: await sdk.forConsole.account.listMfaFactors()
|
||||
};
|
||||
|
||||
@@ -119,8 +119,8 @@
|
||||
$provider.subdomain,
|
||||
$provider.region,
|
||||
$provider.adminSecret,
|
||||
$provider.database,
|
||||
$provider.username,
|
||||
$provider.database || $provider.subdomain,
|
||||
$provider.username || 'postgres',
|
||||
$provider.password
|
||||
);
|
||||
}
|
||||
@@ -186,7 +186,7 @@
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{#if report && !isVersionAtLeast(version, '1.4.0')}
|
||||
{#if report && !isVersionAtLeast(version, '1.4.0') && $provider.provider === 'appwrite'}
|
||||
<div class="u-margin-block-start-24">
|
||||
<Alert
|
||||
type="warning"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { InputSelect } from '$lib/elements/forms';
|
||||
import { FormList, InputSelect } from '$lib/elements/forms';
|
||||
import InputText from '$lib/elements/forms/inputText.svelte';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { Query, type Models, ID } from '@appwrite.io/console';
|
||||
@@ -44,7 +44,7 @@
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Project</svelte:fragment>
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<FormList>
|
||||
{#if organizations.length > 1}
|
||||
<InputSelect
|
||||
id="organization"
|
||||
@@ -107,7 +107,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -4,20 +4,17 @@
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AuthenticatorType } from '@appwrite.io/console';
|
||||
|
||||
export let showDelete = false;
|
||||
|
||||
let code: string;
|
||||
let error: string;
|
||||
|
||||
async function deleteAuthenticator() {
|
||||
try {
|
||||
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp, code);
|
||||
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.FACTORS);
|
||||
showDelete = false;
|
||||
@@ -33,7 +30,6 @@
|
||||
}
|
||||
|
||||
$: if (showDelete) {
|
||||
code = '';
|
||||
error = '';
|
||||
}
|
||||
</script>
|
||||
@@ -46,10 +42,10 @@
|
||||
state="warning"
|
||||
bind:error
|
||||
headerDivider={false}>
|
||||
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
|
||||
<FormList>
|
||||
<InputDigits autofocus required bind:value={code} autoSubmit={false} />
|
||||
</FormList>
|
||||
<p>
|
||||
Are you sure you want to delete this authentication method? You will no longer be able to
|
||||
use this method to authenticate your account.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
let step = 1;
|
||||
let error = '';
|
||||
|
||||
async function addAuthenticator(): Promise<URL> {
|
||||
async function addAuthenticator(): Promise<string> {
|
||||
type = await sdk.forConsole.account.createMfaAuthenticator(AuthenticatorType.Totp);
|
||||
trackEvent(Submit.AccountAuthenticatorCreate);
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
style:background-repeat="no-repeat"
|
||||
style:background-position="center"
|
||||
style:background-size="contain">
|
||||
<img alt="MFA QR Code" class="code" src={qr.toString()} />
|
||||
<img alt="MFA QR Code" class="code" src={qr} />
|
||||
</div>
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
let coupon: string;
|
||||
let couponData = data?.couponData;
|
||||
let campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
|
||||
let billingPlan = BillingPlan.PRO;
|
||||
|
||||
onMount(async () => {
|
||||
await loadPaymentMethods();
|
||||
@@ -79,6 +80,9 @@
|
||||
selectedOrgId = $page.url.searchParams.get('org');
|
||||
canSelectOrg = false;
|
||||
}
|
||||
if (campaign?.plan) {
|
||||
billingPlan = campaign.plan;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
@@ -100,15 +104,15 @@
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
name,
|
||||
BillingPlan.PRO,
|
||||
billingPlan,
|
||||
paymentMethodId
|
||||
);
|
||||
}
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== BillingPlan.PRO) {
|
||||
else if (selectedOrg?.billingPlan === BillingPlan.FREE) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
BillingPlan.PRO,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null
|
||||
);
|
||||
@@ -267,7 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedOrg?.billingPlan === BillingPlan.PRO}
|
||||
{#if selectedOrg?.billingPlan !== BillingPlan.FREE}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
@@ -276,7 +280,7 @@
|
||||
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
Credits will automatically be applied to your next invoice on <b
|
||||
>{toLocaleDate(selectedOrg.billingNextInvoiceDate)}.</b>
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}.</b>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">Add a coupon code to apply credits to your organization.</p>
|
||||
|
||||
@@ -3,23 +3,22 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { LabelCard } from '$lib/components';
|
||||
import {
|
||||
EstimatedTotalBox,
|
||||
PlanComparisonBox,
|
||||
PlanSelection,
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputTags, InputText, Label } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter
|
||||
} from '$lib/layout';
|
||||
import type { Coupon, PaymentList } from '$lib/sdk/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierToPlan } from '$lib/stores/billing';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -27,7 +26,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
$: anyOrgFree = $organizationList.teams?.find(
|
||||
$: anyOrgFree = $organizationList.teams?.some(
|
||||
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
|
||||
);
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
@@ -174,9 +173,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: if (billingPlan === BillingPlan.PRO) {
|
||||
$: if (billingPlan !== BillingPlan.FREE) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
</script>
|
||||
@@ -202,53 +199,9 @@
|
||||
For more details on our plans, visit our
|
||||
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
|
||||
</p>
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value="tier-0"
|
||||
disabled={!!anyOrgFree}
|
||||
tooltipShow={!!anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">{tierFree.description}</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{#if billingPlan === BillingPlan.PRO}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<PlanSelection bind:billingPlan {anyOrgFree} isNewOrg />
|
||||
{#if billingPlan !== BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
{
|
||||
value: BillingPlan.PRO,
|
||||
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
|
||||
},
|
||||
{
|
||||
value: BillingPlan.SCALE,
|
||||
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
{/if}
|
||||
{#if $organization?.billingPlanDowngrade}
|
||||
<Alert type="info" class="common-section">
|
||||
Your organization will change to a {tierToPlan(BillingPlan.FREE).name} plan once your current
|
||||
billing cycle ends and your invoice is paid on {toLocaleDate(
|
||||
Your organization will change to a {tierToPlan($organization?.billingPlanDowngrade)
|
||||
.name} plan once your current billing cycle ends and your invoice is paid on {toLocaleDate(
|
||||
$organization.billingNextInvoiceDate
|
||||
)}.
|
||||
</Alert>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Alert, CardGrid, Empty, Heading, PaginationInline } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import type { Credit, CreditList } from '$lib/sdk/billing';
|
||||
import type { CreditList } from '$lib/sdk/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
let offset = 0;
|
||||
let creditList: CreditList = {
|
||||
available: 0,
|
||||
credits: [],
|
||||
total: 0
|
||||
};
|
||||
@@ -56,9 +57,6 @@
|
||||
request();
|
||||
}
|
||||
|
||||
$: balance =
|
||||
creditList?.credits?.reduce((acc: number, curr: Credit) => acc + curr.credits, 0) ?? 0;
|
||||
|
||||
$: {
|
||||
if (reloadOnWizardClose && !$wizard.show) {
|
||||
request();
|
||||
@@ -86,8 +84,8 @@
|
||||
{:else}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<h4 class="body-text-1 u-bold">Credit balance</h4>
|
||||
<span class="inline-tag">{formatCurrency(balance)}</span>
|
||||
<h4 class="body-text-1 u-bold">Balance</h4>
|
||||
<span class="inline-tag">{formatCurrency(creditList.available)}</span>
|
||||
</div>
|
||||
{#if creditList?.total}
|
||||
<Button secondary on:click={handleCredits}>
|
||||
@@ -97,30 +95,34 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if creditList?.total}
|
||||
<Table noStyles noMargin>
|
||||
<TableScroll noStyles noMargin class="u-margin-block-start-16">
|
||||
<TableHeader>
|
||||
<TableCellHead>Date Added</TableCellHead>
|
||||
<TableCellHead>Expiry Date</TableCellHead>
|
||||
<TableCellHead>Amount</TableCellHead>
|
||||
<TableCellHead>Code</TableCellHead>
|
||||
<TableCellHead>Total</TableCellHead>
|
||||
<TableCellHead>Remaining</TableCellHead>
|
||||
<TableCellHead>Expires at</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each creditList.credits as credit}
|
||||
<TableRow>
|
||||
<TableCellText title="date added">
|
||||
{toLocaleDate(credit.$createdAt)}
|
||||
<TableCellText title="code">
|
||||
{credit?.couponId ?? '-'}
|
||||
</TableCellText>
|
||||
<TableCellText title="total">
|
||||
{formatCurrency(credit.total)}
|
||||
</TableCellText>
|
||||
<TableCellText title="remaining">
|
||||
{formatCurrency(credit.credits)}
|
||||
</TableCellText>
|
||||
<TableCellText title="expiry date">
|
||||
{toLocaleDate(credit.expiration)}
|
||||
</TableCellText>
|
||||
<TableCellText title="amount">
|
||||
{formatCurrency(credit.total)}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total results: {creditList?.total}</p>
|
||||
<p class="text">Total credits: {creditList?.total}</p>
|
||||
<PaginationInline {limit} bind:offset sum={creditList?.total} hidePages />
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellText title="due">
|
||||
{formatCurrency(invoice.amount)}
|
||||
{formatCurrency(invoice.grossAmount)}
|
||||
</TableCellText>
|
||||
<TableCell showOverflow right>
|
||||
<DropList
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage}
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage > 0}
|
||||
<CollapsibleItem isInfo gap={8}>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<span class="body-text-2"><b>Add-ons</b></span><span class="inline-tag"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { paymentMethods } from '$lib/stores/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
|
||||
export let show = false;
|
||||
export let invoice: Invoice;
|
||||
@@ -121,9 +122,9 @@
|
||||
title="Retry payment">
|
||||
<!-- TODO: format currency -->
|
||||
<p class="text">
|
||||
Your payment of <span class="inline-tag">${invoice.amount}</span> due on {toLocaleDate(
|
||||
invoice.dueAt
|
||||
)} has failed. Retry your payment to avoid service interruptions with your projects.
|
||||
Your payment of <span class="inline-tag">${formatCurrency(invoice.grossAmount)}</span> due
|
||||
on {toLocaleDate(invoice.dueAt)} has failed. Retry your payment to avoid service interruptions
|
||||
with your projects.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert, LabelCard } from '$lib/components';
|
||||
import { Alert } from '$lib/components';
|
||||
import {
|
||||
EstimatedTotalBox,
|
||||
PlanComparisonBox,
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import PlanExcess from '$lib/components/billing/planExcess.svelte';
|
||||
import PlanSelection from '$lib/components/billing/planSelection.svelte';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants';
|
||||
import {
|
||||
@@ -21,14 +22,14 @@
|
||||
InputTextarea,
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { formatCurrency } from '$lib/helpers/numbers.js';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter
|
||||
} from '$lib/layout';
|
||||
import { type Coupon, type PaymentList } from '$lib/sdk/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization, organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -92,7 +93,11 @@
|
||||
billingPlan = plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
billingPlan = BillingPlan.PRO;
|
||||
if ($organization?.billingPlan === BillingPlan.SCALE) {
|
||||
billingPlan = BillingPlan.SCALE;
|
||||
} else {
|
||||
billingPlan = BillingPlan.PRO;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
@@ -105,9 +110,9 @@
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (billingPlan === BillingPlan.FREE) {
|
||||
if (isDowngrade) {
|
||||
await downgrade();
|
||||
} else {
|
||||
} else if (isUpgrade) {
|
||||
await upgrade();
|
||||
}
|
||||
}
|
||||
@@ -237,9 +242,7 @@
|
||||
|
||||
$: isUpgrade = billingPlan > $organization.billingPlan;
|
||||
$: isDowngrade = billingPlan < $organization.billingPlan;
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: if (billingPlan === BillingPlan.PRO) {
|
||||
$: if (billingPlan !== BillingPlan.FREE) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
$: isButtonDisabled = $organization.billingPlan === billingPlan;
|
||||
@@ -258,71 +261,37 @@
|
||||
For more details on our plans, visit our
|
||||
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
|
||||
</p>
|
||||
{#if anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
<Alert type="warning" class="u-margin-block-16">
|
||||
You are limited to one {tierToPlan(BillingPlan.FREE).name} organization per account.
|
||||
Consider upgrading or deleting <Button
|
||||
link
|
||||
href={`${base}/organization-${anyOrgFree.$id}`}>{anyOrgFree.name}</Button
|
||||
>.
|
||||
</Alert>
|
||||
{/if}
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
class:u-margin-block-start-16={anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={!!anyOrgFree}
|
||||
value="tier-0"
|
||||
tooltipShow={!!anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization.billingPlan === BillingPlan.FREE}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">{tierFree.description}</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
<PlanSelection
|
||||
bind:billingPlan
|
||||
anyOrgFree={!!anyOrgFree}
|
||||
class={anyOrgFree && billingPlan !== BillingPlan.FREE
|
||||
? 'u-margin-block-start-16'
|
||||
: ''} />
|
||||
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization.billingPlan === BillingPlan.PRO}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{#if isDowngrade}
|
||||
<PlanExcess tier={BillingPlan.FREE} class="u-margin-block-start-24" />
|
||||
{#if billingPlan === BillingPlan.FREE}
|
||||
<PlanExcess
|
||||
tier={BillingPlan.FREE}
|
||||
class="u-margin-block-start-24"
|
||||
members={data?.members?.total ?? 0} />
|
||||
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE}
|
||||
{@const extraMembers = collaborators?.length ?? 0}
|
||||
<Alert type="error" class="u-margin-block-start-24">
|
||||
<svelte:fragment slot="title">
|
||||
Your monthly payments will be adjusted for the Pro plan
|
||||
</svelte:fragment>
|
||||
After switching plans,
|
||||
<b
|
||||
>you will be charged {formatCurrency(
|
||||
extraMembers *
|
||||
($plansInfo?.get(billingPlan)?.addons?.member?.price ?? 0)
|
||||
)} monthly for {extraMembers} team members.</b> This will be reflected in
|
||||
your next invoice.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if billingPlan === BillingPlan.PRO && $organization.billingPlan !== BillingPlan.PRO}
|
||||
<!-- Show email input if upgrading from free plan -->
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan === BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
@@ -344,7 +313,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isUpgrade && billingPlan === BillingPlan.FREE && $organization.billingPlan !== BillingPlan.FREE}
|
||||
{#if isDowngrade}
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputSelect
|
||||
id="reason"
|
||||
@@ -362,12 +331,13 @@
|
||||
{/if}
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== BillingPlan.PRO}
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan}
|
||||
<EstimatedTotalBox
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget />
|
||||
bind:billingBudget
|
||||
{isDowngrade} />
|
||||
{:else}
|
||||
<PlanComparisonBox downgrade={isDowngrade} />
|
||||
{/if}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ depends, parent }) => {
|
||||
const { members } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
return {
|
||||
members
|
||||
};
|
||||
|
||||
@@ -59,14 +59,12 @@
|
||||
|
||||
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
|
||||
{#if isCloud}
|
||||
<Alert type="info">
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE}
|
||||
You can add unlimited organization members on the {plan.name} plan at no cost.
|
||||
{:else if $organization?.billingPlan === BillingPlan.PRO}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO}
|
||||
<Alert type="info">
|
||||
You can add unlimited organization members on the {plan.name} plan for
|
||||
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
|
||||
{/if}
|
||||
</Alert>
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
<FormList>
|
||||
<InputEmail
|
||||
@@ -84,6 +82,6 @@
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
|
||||
<Button submit>Send invite</Button>
|
||||
<Button submit submissionLoader>Send invite</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
export let data;
|
||||
|
||||
const tier = data?.currentInvoice?.tier ?? $organization?.billingPlan;
|
||||
const tier = data?.currentInvoice?.plan ?? $organization?.billingPlan;
|
||||
const plan = tierToPlan(tier).name;
|
||||
|
||||
// let invoice = null;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
<span
|
||||
class="icon-info"
|
||||
use:tooltip={{
|
||||
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan for ${formatCurrency(plan.addons.member.price)} each per billing period.`
|
||||
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan ${$organization.billingPlan === BillingPlan.PRO ? `for ${formatCurrency(plan.addons.member.price)} each per billing period.` : '.'}`
|
||||
}}></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="bundleID"
|
||||
label="Bundle ID"
|
||||
id="servicesID"
|
||||
label="Services ID"
|
||||
autofocus={true}
|
||||
placeholder="com.company.appname"
|
||||
bind:value={appId} />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
|
||||
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
|
||||
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
|
||||
import UpdateSessionAlerts from './updateSessionAlerts.svelte';
|
||||
import UpdateSessionLength from './updateSessionLength.svelte';
|
||||
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
|
||||
import UpdateUsersLimit from './updateUsersLimit.svelte';
|
||||
@@ -11,10 +12,11 @@
|
||||
|
||||
<Container>
|
||||
<UpdateUsersLimit />
|
||||
<UpdateMockNumbers />
|
||||
<UpdateSessionLength />
|
||||
<UpdateSessionsLimit />
|
||||
<UpdatePasswordHistory />
|
||||
<UpdatePasswordDictionary />
|
||||
<UpdatePersonalDataCheck />
|
||||
<UpdateSessionAlerts />
|
||||
<UpdateMockNumbers />
|
||||
</Container>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -1,22 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { InputPhone, InputText } from '$lib/elements/forms';
|
||||
import { InputPhone, InputOTP } from '$lib/elements/forms';
|
||||
import { Button, Form, FormItem, FormItemPart } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import MockNumbersLight from './mock-numbers-light.png';
|
||||
import MockNumbersDark from './mock-numbers-dark.png';
|
||||
import EmptyCardImageCloud from '$lib/components/emptyCardImageCloud.svelte';
|
||||
import { app } from '$lib/stores/app';
|
||||
import Empty from '$lib/components/empty.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
let numbers: Models.MockNumber[] = $project?.authMockNumbers ?? [];
|
||||
let initialNumbers = [];
|
||||
let projectId: string = $project.$id;
|
||||
|
||||
$: initialNumbers = $project?.authMockNumbers?.map((num) => ({ ...num })) ?? [];
|
||||
$: submitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
|
||||
$: isSubmitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
|
||||
|
||||
let isComponentDisabled: boolean =
|
||||
isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE);
|
||||
let emptyStateTitle: string = isSelfHosted
|
||||
? 'Available on Appwrite Cloud'
|
||||
: 'Upgrade to add mock phone numbers';
|
||||
let emptyStateDescription: string = isSelfHosted
|
||||
? 'Sign up to Cloud to add mock phone numbers to your projects.'
|
||||
: 'Upgrade to a Pro plan to add mock phone numbers to your project.';
|
||||
let cta: string = isSelfHosted ? 'Sign up' : 'Upgrade plan';
|
||||
|
||||
async function updateMockNumbers() {
|
||||
try {
|
||||
@@ -24,7 +42,7 @@
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated mock phone numbers successfully'
|
||||
message: 'Mock phone numbers have been updated'
|
||||
});
|
||||
trackEvent(Submit.AuthMockNumbersUpdate);
|
||||
} catch (error) {
|
||||
@@ -48,44 +66,138 @@
|
||||
numbers.splice(index, 1);
|
||||
numbers = numbers;
|
||||
}
|
||||
|
||||
function generateNumber(): string {
|
||||
const areaCode = Math.floor(Math.random() * 800) + 200;
|
||||
const lineNumber = Math.floor(Math.random() * 10000)
|
||||
.toString()
|
||||
.padStart(4, '0');
|
||||
return `+1${areaCode}555${lineNumber}`;
|
||||
}
|
||||
|
||||
function generateOTP(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000) + '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateMockNumbers}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Mock Phone Numbers</Heading>
|
||||
<CardGrid hideFooter={isComponentDisabled}>
|
||||
<Heading tag="h6" size="7" id="variables">Mock phone numbers</Heading>
|
||||
<p>
|
||||
Generate <b>fictional</b> numbers to simulate phone verification while testing demo accounts.
|
||||
A maximum of 10 phone numbers can be generated.
|
||||
Generate <b>fictional</b> numbers to simulate phone verification when testing demo
|
||||
accounts for submitting your application to the App Store or Google Play.
|
||||
<a
|
||||
href="https://appwrite.io/docs/products/auth/security#mock-numbers"
|
||||
target="_blank"
|
||||
class="u-underline"
|
||||
rel="noopener noreferrer">
|
||||
Learn more</a>
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if numbers?.length > 0}
|
||||
<ul class="form-list">
|
||||
{#if isComponentDisabled}
|
||||
<EmptyCardImageCloud source="email_signature_card" noAspectRatio>
|
||||
<svelte:fragment slot="image">
|
||||
<div class=" is-only-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={MockNumbersDark}
|
||||
class="u-image-object-fit-cover u-only-dark u-width-full-line u-height-100-percent"
|
||||
alt="Mock Numbers Example" />
|
||||
{:else}
|
||||
<img
|
||||
src={MockNumbersLight}
|
||||
class="u-image-object-fit-cover u-only-light u-width-full-line u-height-100-percent"
|
||||
alt="Mock Numbers Example" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="is-not-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={MockNumbersDark}
|
||||
width="266"
|
||||
height="171"
|
||||
class="u-image-object-fit-contain u-block u-only-dark u-width-full-line u-height-100-percent"
|
||||
style:object-position="top"
|
||||
alt="Mock Numbers Example" />
|
||||
{:else}
|
||||
<img
|
||||
src={MockNumbersLight}
|
||||
width="266"
|
||||
height="171"
|
||||
class="u-image-object-fit-contain u-only-light u-width-full-line u-height-100-percent"
|
||||
style:object-position="top"
|
||||
alt="Mock Numbers Example" />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">{emptyStateTitle}</svelte:fragment>
|
||||
{emptyStateDescription}
|
||||
<svelte:fragment let:source slot="cta">
|
||||
<Button
|
||||
class="u-margin-block-start-32"
|
||||
secondary
|
||||
fullWidth
|
||||
href="https://cloud.appwrite.io/register"
|
||||
external
|
||||
on:click={() => {
|
||||
trackEvent('click_cloud_signup', {
|
||||
from: 'button',
|
||||
source
|
||||
});
|
||||
}}>{cta}</Button>
|
||||
</svelte:fragment>
|
||||
</EmptyCardImageCloud>
|
||||
{:else if numbers?.length > 0}
|
||||
<ul class="form-list u-gap-8">
|
||||
{#each numbers as number, index}
|
||||
<FormItem isMultiple>
|
||||
<InputPhone
|
||||
id={`key-${index}`}
|
||||
bind:value={number.phone}
|
||||
fullWidth
|
||||
placeholder="Enter Phone Number"
|
||||
label="Phone Number"
|
||||
showLabel={index === 0 ? true : false}
|
||||
minlength={8}
|
||||
placeholder="Enter phone number"
|
||||
label="Phone number"
|
||||
showLabel={index === 0}
|
||||
minlength={9}
|
||||
maxlength={16}
|
||||
required />
|
||||
<InputText
|
||||
required>
|
||||
<button
|
||||
slot="options"
|
||||
use:tooltip={{ content: 'Regenerate', placement: 'bottom' }}
|
||||
on:click={() => (number.phone = generateNumber())}
|
||||
class="options-list-button"
|
||||
aria-label="regenerate text"
|
||||
type="button">
|
||||
<span class="icon-refresh" aria-hidden="true"></span>
|
||||
</button>
|
||||
</InputPhone>
|
||||
<InputOTP
|
||||
id={`value-${index}`}
|
||||
bind:value={number.otp}
|
||||
fullWidth
|
||||
placeholder="Enter value"
|
||||
label="Verification Code"
|
||||
showLabel={index === 0 ? true : false}
|
||||
label="Verification code"
|
||||
maxlength={6}
|
||||
required />
|
||||
<FormItemPart alignEnd>
|
||||
pattern={'^[0-9]{6}$'}
|
||||
patternError="The value must contain 6 digits"
|
||||
showLabel={index === 0}
|
||||
required>
|
||||
<button
|
||||
slot="options"
|
||||
use:tooltip={{ content: 'Regenerate', placement: 'bottom' }}
|
||||
on:click={() => (number.otp = generateOTP())}
|
||||
class="options-list-button"
|
||||
aria-label="regenerate text"
|
||||
type="button">
|
||||
<span class="icon-refresh" aria-hidden="true"></span>
|
||||
</button>
|
||||
</InputOTP>
|
||||
<FormItemPart>
|
||||
<Button
|
||||
text
|
||||
disabled={numbers.length === 0}
|
||||
class={'u-padding-4 ' +
|
||||
(index === 0 ? 'u-margin-block-start-24' : '')}
|
||||
on:click={() => {
|
||||
deletePhoneNumber(index);
|
||||
}}>
|
||||
@@ -101,8 +213,8 @@
|
||||
text
|
||||
on:click={() =>
|
||||
addPhoneNumber({
|
||||
phone: '',
|
||||
otp: ''
|
||||
phone: generateNumber(),
|
||||
otp: generateOTP()
|
||||
})}
|
||||
disabled={numbers.length >= 10}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
@@ -113,15 +225,15 @@
|
||||
<Empty
|
||||
on:click={() => {
|
||||
addPhoneNumber({
|
||||
phone: '',
|
||||
otp: ''
|
||||
phone: generateNumber(),
|
||||
otp: generateOTP()
|
||||
});
|
||||
}}>Create a mock phone number</Empty>
|
||||
}}>Generate number</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={submitDisabled} submit>Update</Button>
|
||||
<Button disabled={isSubmitDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSwitch } from '$lib/elements/forms';
|
||||
import { FormList } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
|
||||
let authSessionAlerts = $project.authSessionAlerts ?? false;
|
||||
|
||||
async function updateSessionAlerts() {
|
||||
try {
|
||||
await sdk.forConsole.projects.updateSessionAlerts($project.$id, authSessionAlerts);
|
||||
await invalidate(Dependencies.PROJECT);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Updated session alerts.'
|
||||
});
|
||||
trackEvent(Submit.AuthSessionAlertsUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AuthSessionAlertsUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSessionAlerts}>
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="7" id="personal-data">Session alerts</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputSwitch
|
||||
bind:value={authSessionAlerts}
|
||||
id="authSessionAlerts"
|
||||
label="Session alerts" />
|
||||
</FormList>
|
||||
<p class="text">
|
||||
Enabling this option will send an email to the users when a new session is created.
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={authSessionAlerts === $project.authSessionAlerts} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -44,7 +44,7 @@
|
||||
<form class="form u-grid u-gap-16">
|
||||
<ul class="form-list is-multiple">
|
||||
<InputNumber id="length" label="Length" bind:value={$value} min={0} />
|
||||
<InputSelect id="period" label="Time Period" bind:value={$unit} {options} />
|
||||
<InputSelect id="period" label="Time period" bind:value={$unit} {options} />
|
||||
</ul>
|
||||
</form>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<Pill>recommended</Pill>
|
||||
</label>
|
||||
</li>
|
||||
<li class="form-item is-multiple">
|
||||
<li class="form-item is-multiple u-cross-center">
|
||||
<div class="input-text-wrapper">
|
||||
<label class="choice-item" for="limited">
|
||||
<input
|
||||
@@ -80,7 +80,7 @@
|
||||
type="radio"
|
||||
bind:group={isLimited}
|
||||
value={true} />
|
||||
<div class="choice-item-content">
|
||||
<div class="choice-item-content u-padding-inline-end-16">
|
||||
<div class="choice-item-title">Limited</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user