mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into fix/file-input-drag-bug
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
VITE_APPWRITE_ENDPOINT=
|
||||
VITE_APPWRITE_ENDPOINT=http://localhost/v1
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=
|
||||
VITE_GA_PROJECT=
|
||||
VITE_CONSOLE_MODE=self-hosted
|
||||
@@ -22,11 +22,11 @@ jobs:
|
||||
# run: npm audit --audit-level low
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
- name: Svelte Diagnostics
|
||||
run: npm run check
|
||||
- name: Linter
|
||||
run: npm run lint
|
||||
- name: Unit Tests
|
||||
run: npm test
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
|
||||
+34
-21
@@ -10,25 +10,30 @@ If you are worried about or don’t know where to start, check out the next sect
|
||||
|
||||
```
|
||||
├── src
|
||||
│ ├── lib // All non-route components, accessible over "import ... from '$lib'"
|
||||
│ │ ├── components // Re-usable components
|
||||
│ │ ├── elements // Re-usable elements
|
||||
│ │ ├── layout // Global components for the layout (Nav/Content/Container)
|
||||
│ │ └── stores // Global stores (state management)
|
||||
│ └─── routes
|
||||
│ ├── console // Routes that need authentication
|
||||
│ │ ├──[project]
|
||||
│ │ │ ├── database // Database Service
|
||||
│ │ │ │ ├── [collection] // Nested Route for the collection "/console/[PROJECT_ID]/database/[COLLECTION_ID]"
|
||||
│ │ │ │ ├── _create.svelte // Component to Create collections
|
||||
│ │ │ │ └── index.svelte // Entrypoint for "/console/[PROJECT_ID]/database"
|
||||
│ │ │ ├── storage // Storage Service "/console/[PROJECT]/storage"
|
||||
│ │ │ └── auth // Users Service "/console/[PROJECT]/auth"
|
||||
│ │ └──...
|
||||
│ ├── login.svelte // Component for Login "/console/login"
|
||||
│ └── register.svelte // Component for Register "/console/register"
|
||||
├── build // Compiled application
|
||||
└── static // Static assets
|
||||
│ ├── lib // Reusable logic (accessible with '$lib')
|
||||
│ │ ├── actions // Svelte actions
|
||||
│ │ ├── charts // Chart components
|
||||
│ │ ├── components // Re-usable components
|
||||
│ │ ├── elements // Re-usable elements
|
||||
│ │ ├── helpers // Small functions used through out the console
|
||||
│ │ ├── images // Images used in the console
|
||||
│ │ ├── layout // Global components for the layout (Nav/Content/Container)
|
||||
│ │ ├── mock // Mock components used for testing
|
||||
│ │ └── stores // Global stores (state management)
|
||||
│ └── routes
|
||||
│ └── console // Routes that need authentication
|
||||
│ │ └── project-[project]
|
||||
│ │ │ └── database // Database Service
|
||||
│ │ │ │ ├── +layout.svelte // Layout head and other logic like realtime events is set here
|
||||
│ │ │ │ ├── +layout.ts // Layout data is set here (Header, Breadcrumbs, ...)
|
||||
│ │ │ │ ├── +page.svelte // Page displayed on "/console/project-[PROJECT_ID]/database"
|
||||
│ │ │ │ ├── +page.ts // Necessary data for the page is fetched here
|
||||
│ │ │ │ └── create.svelte // Component to create databases
|
||||
│ │ │ └── ... // Other services
|
||||
│ │ └── ...
|
||||
│ └── ... // Routes that don't need authentication
|
||||
├── build // Compiled application
|
||||
└── static // Static assets
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -41,11 +46,19 @@ git clone https://github.com/appwrite/console.git appwrite-console
|
||||
|
||||
### 2. Install dependencies with npm
|
||||
|
||||
Navigate to the Appwrite Console repository and install dependencies.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cd appwrite-console && npm install
|
||||
```
|
||||
|
||||
### 3. Setup environment variables
|
||||
### 3. Install and run Appwrite locally
|
||||
|
||||
When you run the Appwrite Console locally, it needs to point to a backend as well. The easiest way to do this is to run an Appwrite instance locally.
|
||||
|
||||
Follow the [install instructions](https://appwrite.io/docs/installation) in the Appwrite docs.
|
||||
|
||||
### 4. Setup environment variables
|
||||
|
||||
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ export enum Submit {
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
UserUpdateLabels = 'submit_user_update_labels',
|
||||
UserUpdateName = 'submit_user_update_name',
|
||||
UserUpdatePassword = 'submit_user_update_password',
|
||||
UserUpdatePhone = 'submit_user_update_phone',
|
||||
@@ -131,6 +132,7 @@ export enum Submit {
|
||||
ProjectCreate = 'submit_project_create',
|
||||
ProjectDelete = 'submit_project_delete',
|
||||
ProjectUpdateName = 'submit_project_update_name',
|
||||
ProjectUpdateTeam = 'submit_project_update_team',
|
||||
ProjectService = 'submit_project_service',
|
||||
MemberCreate = 'submit_member_create',
|
||||
MemberDelete = 'submit_member_delete',
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
{#if single}
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
on:click|preventDefault
|
||||
@@ -40,12 +41,14 @@
|
||||
</button>
|
||||
<slot>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first {target} to get started.</Heading>
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
Create your first {target} to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Check out our documentation.
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
{href}
|
||||
|
||||
+21
-30
@@ -1,40 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import {
|
||||
Form,
|
||||
FormList,
|
||||
InputTextarea,
|
||||
Button,
|
||||
InputText,
|
||||
InputEmail
|
||||
} from '$lib/elements/forms';
|
||||
import { feedback } from '$lib/stores/app';
|
||||
feedback,
|
||||
selectedFeedback,
|
||||
feedbackOptions,
|
||||
feedbackData
|
||||
} from '$lib/stores/feedback';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
let message: string;
|
||||
let name: string;
|
||||
let email: string;
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await feedback.submitFeedback('feedback-general', message, name, email);
|
||||
$: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type);
|
||||
|
||||
async function submit() {
|
||||
try {
|
||||
await feedback.submitFeedback(
|
||||
`feedback-${$feedback.type}`,
|
||||
$feedbackData.message,
|
||||
$feedbackData.name,
|
||||
$feedbackData.email
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Feedback submitted successfully'
|
||||
});
|
||||
feedback.toggleFeedback();
|
||||
feedbackData.reset();
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
feedback.toggleFeedback();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="drop-section">
|
||||
<header class="u-flex u-main-space-between u-gap-16">
|
||||
<h4 class="body-text-1 u-bold">How can we improve?</h4>
|
||||
<h4 class="body-text-1 u-bold">{$selectedFeedback.title}</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
@@ -46,21 +47,11 @@
|
||||
</button>
|
||||
</header>
|
||||
<div class="u-margin-block-start-8 u-line-height-1-5">
|
||||
Your feedback is important to us. Please be honest and tell us what you think.
|
||||
{$selectedFeedback.desc}
|
||||
</div>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormList>
|
||||
<InputText label="Name" id="name" bind:value={name} placeholder="Enter name" />
|
||||
<InputEmail label="Email" id="email" bind:value={email} placeholder="Enter email" />
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
label="Message"
|
||||
required
|
||||
bind:value={message} />
|
||||
</FormList>
|
||||
|
||||
<Form onSubmit={submit}>
|
||||
<svelte:component this={$selectedFeedback.component} />
|
||||
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
|
||||
<Button text on:click={() => feedback.toggleFeedback()}>Cancel</Button>
|
||||
<Button secondary submit>Submit</Button>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
|
||||
import { feedbackData } from '$lib/stores/feedback';
|
||||
</script>
|
||||
|
||||
<FormList>
|
||||
<InputText label="Name" id="name" bind:value={$feedbackData.name} placeholder="Enter name" />
|
||||
<InputEmail
|
||||
label="Email"
|
||||
id="email"
|
||||
bind:value={$feedbackData.email}
|
||||
placeholder="Enter email" />
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
label="Message"
|
||||
required
|
||||
bind:value={$feedbackData.message} />
|
||||
</FormList>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
|
||||
import { feedbackData } from '$lib/stores/feedback';
|
||||
import Evaluation from './evaluation.svelte';
|
||||
</script>
|
||||
|
||||
<Evaluation bind:value={$feedbackData.value}>
|
||||
How likely are you to recommend Appwrite to a friend or colleague?
|
||||
</Evaluation>
|
||||
{#if $feedbackData.value}
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
label="Message"
|
||||
required
|
||||
bind:value={$feedbackData.message}
|
||||
showLabel={false} />
|
||||
<InputText
|
||||
label="Name"
|
||||
id="name"
|
||||
bind:value={$feedbackData.name}
|
||||
placeholder="Enter name" />
|
||||
<InputEmail
|
||||
label="Email"
|
||||
id="email"
|
||||
bind:value={$feedbackData.email}
|
||||
placeholder="Enter email" />
|
||||
</FormList>
|
||||
{/if}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Feedback } from './feedback.svelte';
|
||||
export { default as FeedbackGeneral } from './feedbackGeneral.svelte';
|
||||
export { default as FeedbackNPS } from './feedbackNPS.svelte';
|
||||
@@ -1,78 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Form,
|
||||
FormList,
|
||||
InputTextarea,
|
||||
Button,
|
||||
InputText,
|
||||
InputEmail
|
||||
} from '$lib/elements/forms';
|
||||
import { feedback } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import Evaluation from './evaluation.svelte';
|
||||
|
||||
let value: number = null;
|
||||
let message: string;
|
||||
let email: string;
|
||||
let name: string;
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await feedback.submitFeedback('feedback-nps', message, name, email, value);
|
||||
feedback.switchType('general');
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Thank you for your feedback!'
|
||||
});
|
||||
} catch (error) {
|
||||
feedback.switchType('general');
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
feedback.toggleFeedback();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="drop-section">
|
||||
<header class="u-flex u-main-space-between u-gap-16">
|
||||
<h4 class="body-text-1 u-bold">How are we doing?</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() => feedback.toggleFeedback()}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="u-margin-block-start-8 u-line-height-1-5">
|
||||
At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you
|
||||
think.
|
||||
</div>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Evaluation bind:value>
|
||||
How likely are you to recommend Appwrite to a friend or colleague?
|
||||
</Evaluation>
|
||||
{#if value}
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
label="Message"
|
||||
required
|
||||
bind:value={message}
|
||||
showLabel={false} />
|
||||
<InputText label="Name" id="name" bind:value={name} placeholder="Enter name" />
|
||||
<InputEmail label="Email" id="email" bind:value={email} placeholder="Enter email" />
|
||||
</FormList>
|
||||
{/if}
|
||||
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
|
||||
<Button text on:click={() => feedback.toggleFeedback()}>No thanks</Button>
|
||||
<Button disabled={!value} secondary submit>Submit</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</section>
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
checkOverflow();
|
||||
requestAnimationFrame(checkOverflow);
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
|
||||
return {
|
||||
@@ -46,22 +46,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="interactive-text-output is-buttons-on-top"
|
||||
style:min-inline-size="0"
|
||||
style:display="inline-flex">
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
class="text u-line-height-1-5"
|
||||
style:overflow="hidden"
|
||||
use:truncateText>
|
||||
<slot />
|
||||
</span>
|
||||
<div class="interactive-text-output-buttons">
|
||||
<Copy {value} {event}>
|
||||
<Copy {value} {event}>
|
||||
<div
|
||||
class="interactive-text-output is-buttons-on-top"
|
||||
style:min-inline-size="0"
|
||||
style:display="inline-flex">
|
||||
<span
|
||||
style:white-space="nowrap"
|
||||
class="text u-line-height-1-5"
|
||||
style:overflow="hidden"
|
||||
use:truncateText>
|
||||
<slot />
|
||||
</span>
|
||||
<div class="interactive-text-output-buttons">
|
||||
<button class="interactive-text-output-button is-hidden" aria-label="copy text">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</Copy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Copy>
|
||||
|
||||
@@ -31,9 +31,6 @@ export { default as Box } from './box.svelte';
|
||||
export { default as Search } from './search.svelte';
|
||||
export { default as SearchQuery } from './searchQuery.svelte';
|
||||
export { default as GridItem1 } from './gridItem1.svelte';
|
||||
export { default as FeedbackGeneral } from './feedbackGeneral.svelte';
|
||||
export { default as FeedbackNPS } from './feedbackNPS.svelte';
|
||||
export { default as Evaluation } from './evaluation.svelte';
|
||||
export { default as Steps } from './steps.svelte';
|
||||
export { default as Step } from './step.svelte';
|
||||
export { default as Code } from './code.svelte';
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
You have no teams. Create a team to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Check out our <a
|
||||
Need a hand? Learn more in our <a
|
||||
href="https://appwrite.io/docs/client/teams"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
You have no users. Create a user to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Check out our <a
|
||||
Need a hand? Learn more in our <a
|
||||
href="https://appwrite.io/docs/server/users"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
|
||||
@@ -1,19 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { getElementDir } from '$lib/helpers/style';
|
||||
import { waitUntil } from '$lib/helpers/waitUntil';
|
||||
|
||||
export let selected = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
|
||||
function track() {
|
||||
if (!event) return;
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
async function handleClick(e: Event) {
|
||||
if (event) {
|
||||
trackEvent(`click_select_tab_${event}`);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
let el = e.target as HTMLElement;
|
||||
if (el.tagName !== 'A') {
|
||||
el = el.closest('a');
|
||||
}
|
||||
|
||||
await goto(href);
|
||||
await waitUntil(() => {
|
||||
console.log('tickUntil', el);
|
||||
return el.classList.contains('is-selected');
|
||||
}, 1000);
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const keysMap = {
|
||||
ltr: {
|
||||
next: 'ArrowRight',
|
||||
prev: 'ArrowLeft'
|
||||
},
|
||||
rtl: {
|
||||
next: 'ArrowLeft',
|
||||
prev: 'ArrowRight'
|
||||
}
|
||||
};
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const tabBtn = e.target as HTMLElement;
|
||||
const tabItem = tabBtn.closest('.tabs-item') as HTMLElement;
|
||||
const tabsList = tabItem.closest('.tabs') as HTMLElement;
|
||||
const tabItems = Array.from(tabsList.querySelectorAll('.tabs-item'));
|
||||
const currentIdx = tabItems.indexOf(tabItem);
|
||||
|
||||
const dir = getElementDir(tabsList);
|
||||
|
||||
switch (e.key) {
|
||||
case 'Home': {
|
||||
e.preventDefault();
|
||||
const firstTabBtn = tabItems[0].querySelector('.tabs-button') as HTMLElement;
|
||||
firstTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case 'End': {
|
||||
e.preventDefault();
|
||||
const lastTabBtn = last(tabItems).querySelector('.tabs-button') as HTMLElement;
|
||||
lastTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case keysMap[dir].next: {
|
||||
e.preventDefault();
|
||||
const nextIdx = currentIdx === tabItems.length - 1 ? 0 : currentIdx + 1;
|
||||
const nextTabBtn = tabItems[nextIdx].querySelector('.tabs-button') as HTMLElement;
|
||||
nextTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
case keysMap[dir].prev: {
|
||||
e.preventDefault();
|
||||
const prevIdx = currentIdx === 0 ? tabItems.length - 1 : currentIdx - 1;
|
||||
const prevTabBtn = tabItems[prevIdx].querySelector('.tabs-button') as HTMLElement;
|
||||
prevTabBtn.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="tabs-item">
|
||||
{#if href}
|
||||
<a class="tabs-button" {href} class:is-selected={selected} on:click={track}>
|
||||
<a
|
||||
class="tabs-button"
|
||||
{href}
|
||||
class:is-selected={selected}
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{:else}
|
||||
@@ -22,7 +98,10 @@
|
||||
class="tabs-button"
|
||||
class:is-selected={selected}
|
||||
on:click|preventDefault
|
||||
on:click={track}>
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
style:--amount-of-buttons={required ? 0 : 1.75} />
|
||||
style:--amount-of-buttons={nullable && !required ? 1.75 : 0} />
|
||||
<ul
|
||||
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
|
||||
{#if nullable && !required}
|
||||
|
||||
@@ -27,15 +27,17 @@
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
$: if (element && required && !value) {
|
||||
element.setCustomValidity('This field is required');
|
||||
const isNotEmpty = (value: string | number | boolean) => {
|
||||
return typeof value === 'boolean' ? true : !!value;
|
||||
};
|
||||
|
||||
$: if (required && !isNotEmpty(value)) {
|
||||
element?.setCustomValidity('This field is required');
|
||||
} else {
|
||||
element?.setCustomValidity('');
|
||||
}
|
||||
|
||||
$: if (element && required && value) {
|
||||
element.setCustomValidity('');
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
$: if (isNotEmpty(value)) {
|
||||
error = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
value = prevValue;
|
||||
}
|
||||
}
|
||||
|
||||
$: showTextCounter = !!maxlength;
|
||||
$: showNullCheckbox = nullable && !required;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
@@ -75,19 +78,21 @@
|
||||
class:u-padding-inline-end-56={typeof maxlength === 'number'}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<ul
|
||||
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
|
||||
{#if maxlength}
|
||||
<li class="buttons-list-item">
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if nullable && !required}
|
||||
<li class="buttons-list-item">
|
||||
<NullCheckbox checked={value === null} on:change={handleNullChange} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if showTextCounter || showNullCheckbox}
|
||||
<ul
|
||||
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
|
||||
{#if showTextCounter}
|
||||
<li class="buttons-list-item">
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if showNullCheckbox}
|
||||
<li class="buttons-list-item">
|
||||
<NullCheckbox checked={value === null} on:change={handleNullChange} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
value = prevValue;
|
||||
}
|
||||
}
|
||||
|
||||
$: showTextCounter = !!maxlength;
|
||||
$: showNullCheckbox = nullable && !required;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
@@ -72,20 +75,22 @@
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
style:--amount-of-buttons={required ? undefined : 0.25} />
|
||||
<ul
|
||||
class="buttons-list u-gap-8 u-cross-center u-position-absolute d u-inset-block-end-1 u-inset-inline-end-1 u-padding-block-8 u-padding-inline-12"
|
||||
style="border-end-end-radius:0.0625rem;">
|
||||
{#if maxlength}
|
||||
<li class="buttons-list-item">
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if nullable && !required}
|
||||
<li class="buttons-list-item">
|
||||
<NullCheckbox checked={value === null} on:change={handleNullChange} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if showTextCounter || showNullCheckbox}
|
||||
<ul
|
||||
class="buttons-list u-gap-8 u-cross-center u-position-absolute d u-inset-block-end-1 u-inset-inline-end-1 u-padding-block-8 u-padding-inline-12"
|
||||
style="border-end-end-radius:0.0625rem;">
|
||||
{#if showTextCounter}
|
||||
<li class="buttons-list-item">
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if showNullCheckbox}
|
||||
<li class="buttons-list-item">
|
||||
<NullCheckbox checked={value === null} on:change={handleNullChange} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -9,6 +11,7 @@
|
||||
export let value = '';
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
@@ -39,15 +42,20 @@
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
if (value === '') {
|
||||
value = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
|
||||
let prevValue = '';
|
||||
function handleNullChange(e: CustomEvent<boolean>) {
|
||||
const isNull = e.detail;
|
||||
if (isNull) {
|
||||
prevValue = value;
|
||||
value = null;
|
||||
} else {
|
||||
value = prevValue;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
@@ -66,9 +74,21 @@
|
||||
type="url"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
bind:value
|
||||
on:input={handleInput}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<ul
|
||||
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
|
||||
{#if maxlength}
|
||||
<li class="buttons-list-item">
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
</li>
|
||||
{/if}
|
||||
{#if nullable && !required}
|
||||
<li class="buttons-list-item">
|
||||
<NullCheckbox checked={value === null} on:change={handleNullChange} />
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -2,11 +2,28 @@
|
||||
import type { Action } from 'svelte/action';
|
||||
|
||||
export let isSticky = false;
|
||||
let isOverflowing = false;
|
||||
|
||||
const hasOverflow: Action<HTMLDivElement, (value: boolean) => void> = (node, callback) => {
|
||||
const hasOverflow: Action<HTMLDivElement> = (node) => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
callback(entry.contentRect.width < entry.target.scrollWidth);
|
||||
let overflowing = false;
|
||||
if (entry.contentRect.width < entry.target.scrollWidth) {
|
||||
overflowing = true;
|
||||
}
|
||||
|
||||
const cols = entry.target.querySelectorAll('.table-thead-col');
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const col = cols[i];
|
||||
const cs = getComputedStyle(col);
|
||||
const innerWidth =
|
||||
col.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
||||
if (innerWidth < 32) {
|
||||
overflowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
isOverflowing = overflowing;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,12 +35,10 @@
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let isOverflowing = false;
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll u-margin-block-start-32" data-private>
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<div class="table-wrapper" use:hasOverflow>
|
||||
<table class="table" class:is-sticky-scroll={isSticky && isOverflowing}>
|
||||
<slot />
|
||||
</table>
|
||||
|
||||
@@ -11,6 +11,12 @@ async function securedCopy(value: string) {
|
||||
function unsecuredCopy(value: string) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = value;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export const toLocaleDate = (datetime: string) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
@@ -11,6 +16,11 @@ export const toLocaleDate = (datetime: string) => {
|
||||
|
||||
export const toLocaleDateTime = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
type Direction = 'rtl' | 'ltr';
|
||||
|
||||
function isDirection(dir: string): dir is Direction {
|
||||
return dir === 'rtl' || dir === 'ltr';
|
||||
}
|
||||
|
||||
function parseDirection(dir: string): Direction {
|
||||
return isDirection(dir) ? dir : 'ltr';
|
||||
}
|
||||
|
||||
export function getElementDir(el: HTMLElement): Direction {
|
||||
if (window.getComputedStyle) {
|
||||
return parseDirection(window.getComputedStyle(el, null).getPropertyValue('direction'));
|
||||
}
|
||||
return parseDirection(el.style.direction);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export async function waitUntil(condition: () => boolean, timeout = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(interval);
|
||||
resolve(undefined);
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(interval);
|
||||
reject(new Error('Timeout'));
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<svg width="236" height="175" viewBox="0 0 236 175" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.4121 116.254C57.4121 140.493 88.4892 145.072 93.5568 119.917" stroke="url(#paint0_radial_571_90123)" stroke-width="12"/>
|
||||
<path d="M107.204 44.209L35.9817 91.6489C30.5629 100.178 37.1467 119.049 45.0168 119.528C52.887 120.008 137.394 126.511 137.394 126.511L107.204 44.209Z" fill="url(#paint1_linear_571_90123)" fill-opacity="0.4"/>
|
||||
<g filter="url(#filter0_d_571_90123)">
|
||||
<path d="M108.82 33.6465L39.8891 88.9015C34.7605 98.6703 42.5145 119.368 49.9632 119.918C57.412 120.467 137.394 127.916 137.394 127.916L108.82 33.6465Z" fill="url(#paint2_linear_571_90123)"/>
|
||||
</g>
|
||||
<path d="M142.595 74.954C146.53 88.0347 148.092 100.432 147.467 109.883C147.154 114.613 146.297 118.55 144.956 121.454C143.615 124.36 141.847 126.123 139.755 126.752C137.664 127.381 135.217 126.886 132.495 125.202C129.775 123.519 126.888 120.709 124.018 116.936C118.283 109.398 112.747 98.1959 108.812 85.1152C104.878 72.0345 103.315 59.6372 103.941 50.1857C104.254 45.456 105.111 41.5194 106.451 38.6151C107.792 35.7094 109.561 33.9466 111.652 33.3175C113.744 32.6884 116.191 33.1832 118.912 34.8671C121.633 36.55 124.519 39.3605 127.389 43.1328C133.125 50.6711 138.661 61.8733 142.595 74.954Z" fill="url(#paint3_linear_571_90123)" stroke="url(#paint4_linear_571_90123)" stroke-width="2"/>
|
||||
<path d="M114.593 98.8532C114.593 98.8532 121.351 95.3299 122.591 91.7119C123.832 88.0939 122.287 81.9135 120.565 76.1934C118.843 70.4732 105.786 69.9484 105.786 69.9484C108.725 83.3674 110.852 89.2767 114.593 98.8532Z" fill="url(#paint5_linear_571_90123)" fill-opacity="0.3"/>
|
||||
<ellipse cx="120.194" cy="82.5991" rx="2.00454" ry="10.2947" transform="rotate(-14.7165 120.194 82.5991)" fill="url(#paint6_linear_571_90123)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M172.267 33.0303C159.653 27.4164 144.876 33.0914 139.262 45.7057L138.929 46.4536C133.316 59.0679 138.991 73.8448 151.605 79.4587L156.662 81.7093C156.179 82.3785 155.895 83.211 155.916 84.1363L155.978 86.7736C156.057 90.1179 159.961 91.8943 162.534 89.7561L164.563 88.0699C165.299 87.4579 165.745 86.6643 165.921 85.8298L170.525 87.8787C183.139 93.4926 197.916 87.8176 203.53 75.2033L203.862 74.4554C209.476 61.8411 203.801 47.0642 191.187 41.4503L172.267 33.0303Z" fill="url(#paint7_linear_571_90123)"/>
|
||||
<path d="M165.517 87.4798L160.655 91.5071C159.16 92.7454 157.442 93.639 155.506 93.776C154.556 93.8432 154.265 93.1232 154 92.9993C155.828 92.8779 157.297 92.4752 157.68 91.9631L163.801 86.7594L165.517 87.4798Z" fill="#8B2A56"/>
|
||||
<path d="M158.141 26.648C152.649 24.3428 146.941 27.6001 144.975 29.3993C149.627 26.5875 154.943 27.6946 157.019 28.5997L202.118 48.1322C208.529 51.7113 207.777 59.0198 206.599 62.2266L194.603 88.8868C191.478 94.6007 186.913 95.5851 185.021 95.3631C191.256 96.2841 195.705 91.8728 197.15 89.552L209.506 62.0336C212.455 53.6616 208.162 49.1214 205.648 47.8979C191.967 41.8535 163.634 28.9532 158.141 26.648Z" fill="url(#paint8_linear_571_90123)"/>
|
||||
<mask id="path-10-inside-1_571_90123" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M155.683 27.5397C150.638 25.2942 144.727 27.5642 142.481 32.6099L129.951 60.7661C127.705 65.8118 129.975 71.7225 135.021 73.9681L152.971 81.9568L153.195 91.517C153.235 93.1892 155.187 94.0774 156.473 93.0083L163.912 86.8257L181.349 94.5859C186.395 96.8315 192.305 94.5615 194.551 89.5158L207.082 61.3596C209.327 56.3139 207.057 50.4031 202.011 48.1576L155.683 27.5397Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M155.683 27.5397C150.638 25.2942 144.727 27.5642 142.481 32.6099L129.951 60.7661C127.705 65.8118 129.975 71.7225 135.021 73.9681L152.971 81.9568L153.195 91.517C153.235 93.1892 155.187 94.0774 156.473 93.0083L163.912 86.8257L181.349 94.5859C186.395 96.8315 192.305 94.5615 194.551 89.5158L207.082 61.3596C209.327 56.3139 207.057 50.4031 202.011 48.1576L155.683 27.5397Z" fill="url(#paint9_linear_571_90123)"/>
|
||||
<path d="M142.481 32.6099L141.568 32.2033L142.481 32.6099ZM155.683 27.5397L156.09 26.6261L155.683 27.5397ZM129.951 60.7661L129.037 60.3595L129.951 60.7661ZM135.021 73.9681L135.428 73.0545L135.021 73.9681ZM152.971 81.9568L153.971 81.9333L153.956 81.3005L153.378 81.0431L152.971 81.9568ZM153.195 91.517L152.196 91.5405L153.195 91.517ZM156.473 93.0083L157.112 93.7773L156.473 93.0083ZM163.912 86.8257L164.318 85.9121L163.751 85.6594L163.273 86.0566L163.912 86.8257ZM181.349 94.5859L180.942 95.4995L181.349 94.5859ZM194.551 89.5158L193.637 89.1092L194.551 89.5158ZM207.082 61.3596L206.168 60.953L206.168 60.953L207.082 61.3596ZM202.011 48.1576L202.418 47.2439L202.418 47.2439L202.011 48.1576ZM143.395 33.0165C145.416 28.4753 150.736 26.4323 155.277 28.4533L156.09 26.6261C150.54 24.156 144.038 26.653 141.568 32.2033L143.395 33.0165ZM130.864 61.1727L143.395 33.0165L141.568 32.2033L129.037 60.3595L130.864 61.1727ZM135.428 73.0545C130.886 71.0335 128.843 65.7138 130.864 61.1727L129.037 60.3595C126.567 65.9098 129.064 72.4116 134.614 74.8817L135.428 73.0545ZM153.378 81.0431L135.428 73.0545L134.614 74.8817L152.565 82.8704L153.378 81.0431ZM154.195 91.4936L153.971 81.9333L151.972 81.9802L152.196 91.5405L154.195 91.4936ZM155.834 92.2392C155.191 92.7738 154.215 92.3297 154.195 91.4936L152.196 91.5405C152.254 94.0487 155.183 95.381 157.112 93.7773L155.834 92.2392ZM163.273 86.0566L155.834 92.2392L157.112 93.7773L164.551 87.5947L163.273 86.0566ZM181.756 93.6723L164.318 85.9121L163.505 87.7393L180.942 95.4995L181.756 93.6723ZM193.637 89.1092C191.616 93.6503 186.297 95.6933 181.756 93.6723L180.942 95.4995C186.493 97.9696 192.994 95.4727 195.465 89.9224L193.637 89.1092ZM206.168 60.953L193.637 89.1092L195.465 89.9224L207.995 61.7662L206.168 60.953ZM201.605 49.0712C206.146 51.0922 208.189 56.4118 206.168 60.953L207.995 61.7662C210.465 56.2159 207.968 49.7141 202.418 47.2439L201.605 49.0712ZM155.277 28.4533L201.605 49.0712L202.418 47.2439L156.09 26.6261L155.277 28.4533Z" fill="url(#paint10_linear_571_90123)" mask="url(#path-10-inside-1_571_90123)"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 149.662 47.835)" fill="#892450"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 148.198 47.8135)" fill="url(#paint11_linear_571_90123)"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 166.811 55.4512)" fill="#892450"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 165.348 55.4297)" fill="url(#paint12_linear_571_90123)"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 183.96 63.0664)" fill="#892450"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 182.498 63.0449)" fill="url(#paint13_linear_571_90123)"/>
|
||||
<g filter="url(#filter1_f_571_90123)">
|
||||
<path d="M63.2684 28.0889L65.3066 31.8156L69.0334 33.8538L65.3066 35.8921L63.2684 39.6188L61.2302 35.8921L57.5034 33.8538L61.2302 31.8156L63.2684 28.0889Z" fill="url(#paint14_radial_571_90123)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_571_90123)">
|
||||
<path d="M225.574 13.8984L228.553 19.3452L234 22.3242L228.553 25.3031L225.574 30.7499L222.595 25.3031L217.148 22.3242L222.595 19.3452L225.574 13.8984Z" fill="url(#paint15_radial_571_90123)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_571_90123)">
|
||||
<path d="M171.472 111.903L173.04 114.77L175.907 116.338L173.04 117.906L171.472 120.772L169.904 117.906L167.038 116.338L169.904 114.77L171.472 111.903Z" fill="url(#paint16_radial_571_90123)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_571_90123" x="0.233643" y="0.646484" width="179.161" height="174.27" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="2" dy="7"/>
|
||||
<feGaussianBlur stdDeviation="20"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.245833 0 0 0 0 0.255044 0 0 0 0 0.333333 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_571_90123"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_571_90123" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_571_90123" x="55.5034" y="26.0889" width="15.53" height="15.5303" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_571_90123"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_571_90123" x="215.148" y="11.8984" width="20.8516" height="20.8516" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_571_90123"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_571_90123" x="165.038" y="109.903" width="12.8691" height="12.8691" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_571_90123"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_571_90123" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(78.2319 119.062) rotate(53.6887) scale(22.2166 32.3419)">
|
||||
<stop stop-color="#202130"/>
|
||||
<stop offset="0.317708" stop-color="#33374B"/>
|
||||
<stop offset="0.484375" stop-color="#33374B"/>
|
||||
<stop offset="0.905609" stop-color="#20202F"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_571_90123" x1="41.4436" y1="121.838" x2="52.7291" y2="75.4951" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#292B3D"/>
|
||||
<stop offset="0.630208" stop-color="#4B506A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_571_90123" x1="71.3936" y1="52.3904" x2="98.8316" y2="131.923" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.274562" stop-color="#33364A"/>
|
||||
<stop offset="1" stop-color="#13141D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_571_90123" x1="121.215" y1="-12.9999" x2="137.25" y2="140.807" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.263084" stop-color="#101118"/>
|
||||
<stop offset="0.562167" stop-color="#3E4156"/>
|
||||
<stop offset="0.984375" stop-color="#303044"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_571_90123" x1="118.956" y1="-24.173" x2="206.164" y2="211.881" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.264071" stop-color="#33374B"/>
|
||||
<stop offset="1" stop-color="#585B7A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_571_90123" x1="115.07" y1="99.4437" x2="111.855" y2="69.0231" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0252272" stop-color="#222332"/>
|
||||
<stop offset="0.242539" stop-color="#424661"/>
|
||||
<stop offset="0.588639" stop-color="#212230"/>
|
||||
<stop offset="0.844082" stop-color="#363B58"/>
|
||||
<stop offset="1" stop-color="#34374B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_571_90123" x1="121.604" y1="109.753" x2="120.275" y2="67.1411" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#222433"/>
|
||||
<stop offset="1" stop-color="#767D9E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_571_90123" x1="199.971" y1="95.4039" x2="141.478" y2="-6.07794" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E70041"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_571_90123" x1="147.137" y1="21.1212" x2="210.784" y2="96.99" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BE2764"/>
|
||||
<stop offset="1" stop-color="#812A53"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_571_90123" x1="206.807" y1="129.438" x2="142.953" y2="21.5313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#823359"/>
|
||||
<stop offset="1" stop-color="#D02E60"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_571_90123" x1="306.52" y1="53.9106" x2="132.142" y2="70.9057" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8A2A56"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_571_90123" x1="4.40454" y1="0" x2="4.40454" y2="8.80908" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5B6074" stop-opacity="0.97"/>
|
||||
<stop offset="1" stop-color="#373B4D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_571_90123" x1="4.40454" y1="0" x2="4.40454" y2="8.80908" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5B6074" stop-opacity="0.97"/>
|
||||
<stop offset="1" stop-color="#373B4D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_571_90123" x1="4.40454" y1="0" x2="4.40454" y2="8.80908" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5B6074" stop-opacity="0.97"/>
|
||||
<stop offset="1" stop-color="#373B4D"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint14_radial_571_90123" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(63.2684 33.8538) rotate(90) scale(6.65188)">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="0.947917" stop-color="#282A3B"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint15_radial_571_90123" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(225.574 22.3242) rotate(90) scale(15.0776)">
|
||||
<stop stop-color="#DE2E62"/>
|
||||
<stop offset="0.598958" stop-color="#282A3B"/>
|
||||
<stop offset="0.75" stop-color="#282A3B"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint16_radial_571_90123" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(171.472 116.338) rotate(90) scale(6.43015)">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="0.729167" stop-color="#282A3B"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,126 @@
|
||||
<svg width="222" height="175" viewBox="0 0 222 175" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.4121 116.254C43.4121 140.493 74.4892 145.072 79.5568 119.917" stroke="url(#paint0_radial_537_80284)" stroke-width="12"/>
|
||||
<path d="M93.2039 44.209L21.9817 91.6489C16.5629 100.178 23.1467 119.049 31.0168 119.528C38.887 120.008 123.394 126.511 123.394 126.511L93.2039 44.209Z" fill="url(#paint1_linear_537_80284)" fill-opacity="0.4"/>
|
||||
<g filter="url(#filter0_d_537_80284)">
|
||||
<path d="M94.8205 33.6465L25.8891 88.9015C20.7605 98.6703 28.5145 119.368 35.9632 119.918C43.412 120.467 123.394 127.916 123.394 127.916L94.8205 33.6465Z" fill="url(#paint2_linear_537_80284)"/>
|
||||
</g>
|
||||
<path d="M128.595 74.954C132.53 88.0347 134.092 100.432 133.467 109.883C133.154 114.613 132.297 118.55 130.956 121.454C129.615 124.36 127.847 126.123 125.755 126.752C123.664 127.381 121.217 126.886 118.495 125.202C115.775 123.519 112.888 120.709 110.018 116.936C104.283 109.398 98.7468 98.1959 94.8124 85.1152C90.878 72.0345 89.3155 59.6372 89.9408 50.1857C90.2537 45.456 91.111 41.5194 92.4513 38.6151C93.7924 35.7094 95.5605 33.9466 97.6521 33.3175C99.7437 32.6884 102.191 33.1832 104.912 34.8671C107.633 36.55 110.519 39.3605 113.389 43.1328C119.125 50.6711 124.661 61.8733 128.595 74.954Z" fill="url(#paint3_linear_537_80284)" stroke="url(#paint4_linear_537_80284)" stroke-width="2"/>
|
||||
<path d="M100.593 98.8532C100.593 98.8532 107.351 95.3299 108.591 91.7119C109.831 88.0939 108.286 81.9135 106.565 76.1934C104.843 70.4732 91.7854 69.9484 91.7854 69.9484C94.7244 83.3674 96.8517 89.2767 100.593 98.8532Z" fill="url(#paint5_linear_537_80284)" fill-opacity="0.3"/>
|
||||
<ellipse cx="106.194" cy="82.5991" rx="2.00454" ry="10.2947" transform="rotate(-14.7165 106.194 82.5991)" fill="url(#paint6_linear_537_80284)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M158.267 33.0303C145.653 27.4164 130.876 33.0914 125.262 45.7057L124.929 46.4536C119.316 59.0679 124.991 73.8448 137.605 79.4587L142.662 81.7093C142.179 82.3785 141.895 83.211 141.916 84.1363L141.978 86.7736C142.057 90.1179 145.961 91.8943 148.534 89.7561L150.563 88.0699C151.299 87.4579 151.745 86.6643 151.921 85.8298L156.525 87.8787C169.139 93.4926 183.916 87.8176 189.53 75.2033L189.862 74.4554C195.476 61.8411 189.801 47.0642 177.187 41.4503L158.267 33.0303Z" fill="url(#paint7_linear_537_80284)"/>
|
||||
<path d="M151.516 87.4798L146.655 91.5071C145.16 92.7454 143.442 93.639 141.506 93.776C140.556 93.8432 140.264 93.1232 140 92.9993C141.827 92.8779 143.296 92.4752 143.68 91.9631L149.8 86.7594L151.516 87.4798Z" fill="#EB2B62"/>
|
||||
<path d="M144.141 26.648C138.649 24.3428 132.941 27.6001 130.975 29.3993C135.627 26.5875 140.943 27.6946 143.019 28.5997L188.118 48.1322C194.529 51.7113 193.777 59.0198 192.599 62.2266L180.603 88.8868C177.478 94.6007 172.913 95.5851 171.021 95.3631C177.256 96.2841 181.705 91.8728 183.15 89.552L195.506 62.0336C198.455 53.6616 194.162 49.1214 191.648 47.8979C177.967 41.8535 149.634 28.9532 144.141 26.648Z" fill="url(#paint8_linear_537_80284)"/>
|
||||
<mask id="path-10-inside-1_537_80284" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M141.683 27.5397C136.638 25.2942 130.727 27.5642 128.481 32.6099L115.951 60.7661C113.705 65.8118 115.975 71.7225 121.021 73.9681L138.971 81.9568L139.195 91.517C139.234 93.1892 141.187 94.0774 142.473 93.0083L149.912 86.8257L167.349 94.5859C172.394 96.8315 178.305 94.5615 180.551 89.5158L193.081 61.3596C195.327 56.3139 193.057 50.4031 188.011 48.1576L141.683 27.5397Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M141.683 27.5397C136.638 25.2942 130.727 27.5642 128.481 32.6099L115.951 60.7661C113.705 65.8118 115.975 71.7225 121.021 73.9681L138.971 81.9568L139.195 91.517C139.234 93.1892 141.187 94.0774 142.473 93.0083L149.912 86.8257L167.349 94.5859C172.394 96.8315 178.305 94.5615 180.551 89.5158L193.081 61.3596C195.327 56.3139 193.057 50.4031 188.011 48.1576L141.683 27.5397Z" fill="url(#paint9_linear_537_80284)"/>
|
||||
<path d="M128.481 32.6099L127.568 32.2033L128.481 32.6099ZM141.683 27.5397L142.09 26.6261L141.683 27.5397ZM115.951 60.7661L115.037 60.3595L115.951 60.7661ZM121.021 73.9681L121.427 73.0545L121.021 73.9681ZM138.971 81.9568L139.971 81.9333L139.956 81.3005L139.378 81.0431L138.971 81.9568ZM139.195 91.517L138.195 91.5405L139.195 91.517ZM142.473 93.0083L143.112 93.7773L142.473 93.0083ZM149.912 86.8257L150.318 85.9121L149.75 85.6594L149.272 86.0566L149.912 86.8257ZM167.349 94.5859L166.942 95.4995L167.349 94.5859ZM180.551 89.5158L179.637 89.1092L180.551 89.5158ZM193.081 61.3596L192.168 60.953L192.168 60.953L193.081 61.3596ZM188.011 48.1576L188.418 47.2439L188.418 47.2439L188.011 48.1576ZM129.395 33.0165C131.416 28.4753 136.735 26.4323 141.277 28.4533L142.09 26.6261C136.54 24.156 130.038 26.653 127.568 32.2033L129.395 33.0165ZM116.864 61.1727L129.395 33.0165L127.568 32.2033L115.037 60.3595L116.864 61.1727ZM121.427 73.0545C116.886 71.0335 114.843 65.7138 116.864 61.1727L115.037 60.3595C112.567 65.9098 115.064 72.4116 120.614 74.8817L121.427 73.0545ZM139.378 81.0431L121.427 73.0545L120.614 74.8817L138.565 82.8704L139.378 81.0431ZM140.195 91.4936L139.971 81.9333L137.971 81.9802L138.195 91.5405L140.195 91.4936ZM141.834 92.2392C141.191 92.7738 140.214 92.3297 140.195 91.4936L138.195 91.5405C138.254 94.0487 141.183 95.381 143.112 93.7773L141.834 92.2392ZM149.272 86.0566L141.834 92.2392L143.112 93.7773L150.551 87.5947L149.272 86.0566ZM167.755 93.6723L150.318 85.9121L149.505 87.7393L166.942 95.4995L167.755 93.6723ZM179.637 89.1092C177.616 93.6503 172.296 95.6933 167.755 93.6723L166.942 95.4995C172.492 97.9696 178.994 95.4727 181.464 89.9224L179.637 89.1092ZM192.168 60.953L179.637 89.1092L181.464 89.9224L193.995 61.7662L192.168 60.953ZM187.605 49.0712C192.146 51.0922 194.189 56.4118 192.168 60.953L193.995 61.7662C196.465 56.2159 193.968 49.7141 188.418 47.2439L187.605 49.0712ZM141.277 28.4533L187.605 49.0712L188.418 47.2439L142.09 26.6261L141.277 28.4533Z" fill="url(#paint10_linear_537_80284)" mask="url(#path-10-inside-1_537_80284)"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 135.661 47.835)" fill="#F3779A"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 134.198 47.8135)" fill="#F2F2F8"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 152.811 55.4512)" fill="#F3779A"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 151.348 55.4297)" fill="#F2F2F8"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 169.96 63.0664)" fill="#F3779A"/>
|
||||
<ellipse cx="4.40454" cy="4.40454" rx="4.40454" ry="4.40454" transform="matrix(0.894697 0.397317 -0.36312 0.950578 168.497 63.0449)" fill="#F2F2F8"/>
|
||||
<g filter="url(#filter1_f_537_80284)">
|
||||
<path d="M49.2684 28.0889L51.3066 31.8156L55.0334 33.8538L51.3066 35.8921L49.2684 39.6188L47.2302 35.8921L43.5034 33.8538L47.2302 31.8156L49.2684 28.0889Z" fill="url(#paint11_radial_537_80284)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_537_80284)">
|
||||
<path d="M211.574 13.8984L214.553 19.3452L220 22.3242L214.553 25.3031L211.574 30.7499L208.595 25.3031L203.148 22.3242L208.595 19.3452L211.574 13.8984Z" fill="url(#paint12_radial_537_80284)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_537_80284)">
|
||||
<path d="M157.472 111.903L159.04 114.77L161.907 116.338L159.04 117.906L157.472 120.772L155.904 117.906L153.038 116.338L155.904 114.77L157.472 111.903Z" fill="url(#paint13_radial_537_80284)" fill-opacity="0.6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_537_80284" x="-13.7664" y="0.646484" width="179.161" height="174.27" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="2" dy="7"/>
|
||||
<feGaussianBlur stdDeviation="20"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.768627 0 0 0 0 0.768627 0 0 0 0 0.768627 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_537_80284"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_537_80284" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_537_80284" x="41.5034" y="26.0889" width="15.53" height="15.5303" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_537_80284"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_537_80284" x="201.148" y="11.8984" width="20.8516" height="20.8516" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_537_80284"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_537_80284" x="151.038" y="109.903" width="12.8691" height="12.8691" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="1" result="effect1_foregroundBlur_537_80284"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_537_80284" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(64.2319 119.062) rotate(53.6887) scale(22.2166 32.3419)">
|
||||
<stop stop-color="#A3A8BD"/>
|
||||
<stop offset="0.317708" stop-color="#F4F4F8"/>
|
||||
<stop offset="0.484375" stop-color="white"/>
|
||||
<stop offset="0.905609" stop-color="#A3A8BD"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_537_80284" x1="27.4436" y1="121.838" x2="38.7291" y2="75.4951" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A3A8BD"/>
|
||||
<stop offset="0.633096" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_537_80284" x1="57.3936" y1="52.3904" x2="84.8316" y2="131.923" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.274562" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#D0D0E3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_537_80284" x1="107.215" y1="-12.9999" x2="123.25" y2="140.807" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.263084" stop-color="#A3A8BD"/>
|
||||
<stop offset="0.562167" stop-color="#E8E8F1"/>
|
||||
<stop offset="0.984375" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_537_80284" x1="104.956" y1="-24.173" x2="192.164" y2="211.881" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.264071" stop-color="#FCFCFF"/>
|
||||
<stop offset="1" stop-color="#A3A8BD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_537_80284" x1="101.07" y1="99.4437" x2="97.8544" y2="69.0231" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0252272" stop-color="#B5B8C7"/>
|
||||
<stop offset="0.242539" stop-color="white"/>
|
||||
<stop offset="0.588639" stop-color="#BFC2CF"/>
|
||||
<stop offset="0.844082" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#CACCD8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_537_80284" x1="107.604" y1="109.753" x2="106.275" y2="67.1411" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A3A8BD"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_537_80284" x1="185.971" y1="95.4039" x2="127.478" y2="-6.07794" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E70041"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_537_80284" x1="133.137" y1="21.1212" x2="196.784" y2="96.99" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#E90043"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_537_80284" x1="192.807" y1="129.438" x2="128.953" y2="21.5313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_537_80284" x1="292.52" y1="53.9106" x2="118.141" y2="70.9057" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint11_radial_537_80284" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(49.2684 33.8538) rotate(90) scale(6.65188)">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="0.947917" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint12_radial_537_80284" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(211.574 22.3242) rotate(90) scale(15.0776)">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="0.598958" stop-color="white"/>
|
||||
<stop offset="0.75" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint13_radial_537_80284" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(157.472 116.338) rotate(90) scale(6.43015)">
|
||||
<stop stop-color="#F02E65"/>
|
||||
<stop offset="0.729167" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F02E65"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -1,26 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
AvatarInitials,
|
||||
DropList,
|
||||
DropListItem,
|
||||
DropListLink,
|
||||
FeedbackGeneral
|
||||
} from '$lib/components';
|
||||
import { app, feedback } from '$lib/stores/app';
|
||||
import { AvatarInitials, DropList, DropListItem, DropListLink } from '$lib/components';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
|
||||
import AppwriteLogo from '$lib/images/appwrite-gray-light.svg';
|
||||
import LightMode from '$lib/images/mode/light-mode.svg';
|
||||
import DarkMode from '$lib/images/mode/dark-mode.svg';
|
||||
import SystemMode from '$lib/images/mode/system-mode.svg';
|
||||
import { FeedbackNPS } from '$lib/components';
|
||||
|
||||
import { slide } from 'svelte/transition';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { goto } from '$app/navigation';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { Feedback } from '$lib/components/feedback';
|
||||
|
||||
let showDropdown = false;
|
||||
let droplistElement: HTMLDivElement;
|
||||
@@ -77,11 +71,7 @@
|
||||
<span class="text">Feedback</span>
|
||||
</button>
|
||||
<svelte:fragment slot="other">
|
||||
{#if $feedback.type === 'nps'}
|
||||
<FeedbackNPS />
|
||||
{:else}
|
||||
<FeedbackGeneral />
|
||||
{/if}
|
||||
<Feedback />
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
<a
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
function isDeployment(data: Models.Deployment | Models.Execution): data is Models.Deployment {
|
||||
if ('buildId' in data) {
|
||||
selectedTab = 'logs';
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployment/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/deployments/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
function isExecution(data: Models.Deployment | Models.Execution): data is Models.Execution {
|
||||
if ('trigger' in data) {
|
||||
selectedTab = 'response';
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/execution/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
rawData = `${sdk.forConsole.client.config.endpoint}/functions/${$log.func.$id}/executions/${$log.data.$id}?mode=admin&project=${$page.params.project}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { slide } from '$lib/helpers/transition';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Create from '$routes/console/feedbackWizard.svelte';
|
||||
|
||||
export let isOpen = false;
|
||||
|
||||
$: project = $page.params.project;
|
||||
$: projectPath = `${base}/console/project-${project}`;
|
||||
@@ -27,6 +32,15 @@
|
||||
narrow = !narrow;
|
||||
}
|
||||
}
|
||||
|
||||
function openWizard() {
|
||||
isOpen = false;
|
||||
wizard.start(Create);
|
||||
}
|
||||
|
||||
beforeNavigate(() => {
|
||||
wizard.hide();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
@@ -122,6 +136,24 @@
|
||||
<span class="text">Storage</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item is-only-mobile">
|
||||
<a
|
||||
class="drop-button"
|
||||
href={`${projectPath}/settings`}
|
||||
on:click={() => trackEvent('click_menu_settings')}
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/settings`
|
||||
)}
|
||||
title="Settings"
|
||||
use:tooltip={{
|
||||
content: 'Settings',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-cog" aria-hidden="true" />
|
||||
<span class="text">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -129,7 +161,7 @@
|
||||
<div class="side-nav-bottom">
|
||||
<section class="drop-section">
|
||||
<a
|
||||
class="drop-button"
|
||||
class="drop-button is-only-desktop"
|
||||
href={`${projectPath}/settings`}
|
||||
on:click={() => trackEvent('click_menu_settings')}
|
||||
class:is-selected={$page.url.pathname.startsWith(`${projectPath}/settings`)}
|
||||
@@ -142,6 +174,13 @@
|
||||
<span class="icon-cog" aria-hidden="true" />
|
||||
<span class="text">Settings</span>
|
||||
</a>
|
||||
<ul class="drop-list is-only-mobile">
|
||||
<li class="drop-list-item">
|
||||
<button class="drop-button" on:click={openWizard}>
|
||||
<span class="text">Feedback</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { VARS } from '$lib/system';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type AppStore = {
|
||||
@@ -7,89 +6,11 @@ export type AppStore = {
|
||||
themeInUse: 'light' | 'dark';
|
||||
};
|
||||
|
||||
export type Feedback = {
|
||||
elapsed: number;
|
||||
visualized: number;
|
||||
notification: boolean;
|
||||
type: 'nps' | 'general';
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
export const app = writable<AppStore>({
|
||||
theme: 'auto',
|
||||
themeInUse: 'light'
|
||||
});
|
||||
|
||||
function createFeedbackStore() {
|
||||
const { subscribe, update } = writable<Feedback>({
|
||||
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
|
||||
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0,
|
||||
notification: false,
|
||||
type: 'general',
|
||||
show: false
|
||||
});
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
toggleFeedback: () => {
|
||||
update((feedback) => {
|
||||
feedback.show = !feedback.show;
|
||||
return feedback;
|
||||
});
|
||||
},
|
||||
toggleNotification: () =>
|
||||
update((feedback) => {
|
||||
feedback.notification = !feedback.notification;
|
||||
return feedback;
|
||||
}),
|
||||
switchType: (feedType: Feedback['type']) =>
|
||||
update((feedback) => {
|
||||
feedback.type = feedType;
|
||||
return feedback;
|
||||
}),
|
||||
addVisualization: () =>
|
||||
update((feedback) => {
|
||||
feedback.visualized += 1;
|
||||
localStorage.setItem('feedbackVisualized', feedback.visualized.toString());
|
||||
return feedback;
|
||||
}),
|
||||
|
||||
increaseElapsed: (time: number) => {
|
||||
update((feedback) => {
|
||||
feedback.elapsed += time;
|
||||
localStorage.setItem('feedbackElapsed', feedback.elapsed.toString());
|
||||
return feedback;
|
||||
});
|
||||
},
|
||||
submitFeedback: async (
|
||||
subject: string,
|
||||
message: string,
|
||||
firstname?: string,
|
||||
email?: string,
|
||||
value?: number
|
||||
) => {
|
||||
if (!VARS.GROWTH_ENDPOINT) return;
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject,
|
||||
message,
|
||||
email,
|
||||
firstname: firstname ? firstname : undefined,
|
||||
customFields: value ? [{ id: '40655', value }] : undefined
|
||||
})
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error('Failed to submit feedback');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export const feedback = createFeedbackStore();
|
||||
|
||||
if (browser) {
|
||||
app.update((n) => ({
|
||||
...n,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { VARS } from '$lib/system';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import FeedbackGeneral from '$lib/components/feedback/feedbackGeneral.svelte';
|
||||
import FeedbackNps from '$lib/components/feedback/feedbackNPS.svelte';
|
||||
|
||||
export type Feedback = {
|
||||
elapsed: number;
|
||||
visualized: number;
|
||||
notification: boolean;
|
||||
type: 'nps' | 'general';
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
export type FeedbackData = {
|
||||
message: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
value?: number;
|
||||
};
|
||||
|
||||
export type FeedbackOption = {
|
||||
type: Feedback['type'];
|
||||
title: string;
|
||||
desc: string;
|
||||
component: typeof SvelteComponent;
|
||||
};
|
||||
|
||||
export const feedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
type: 'general',
|
||||
title: 'How can we improve?',
|
||||
desc: 'Your feedback is important to us. Please be honest and tell us what you think.',
|
||||
component: FeedbackGeneral
|
||||
},
|
||||
{
|
||||
type: 'nps',
|
||||
title: 'How are we doing?',
|
||||
desc: "At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you think.",
|
||||
component: FeedbackNps
|
||||
}
|
||||
];
|
||||
|
||||
export const selectedFeedback = writable<FeedbackOption>();
|
||||
|
||||
function createFeedbackDataStore() {
|
||||
const { subscribe, update } = writable<FeedbackData>({
|
||||
message: '',
|
||||
name: '',
|
||||
email: '',
|
||||
value: 0
|
||||
});
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
reset: () => {
|
||||
update((feedbackData) => {
|
||||
feedbackData.message = '';
|
||||
feedbackData.name = '';
|
||||
feedbackData.email = '';
|
||||
feedbackData.value = 0;
|
||||
return feedbackData;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const feedbackData = createFeedbackDataStore();
|
||||
|
||||
function createFeedbackStore() {
|
||||
const { subscribe, update } = writable<Feedback>({
|
||||
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
|
||||
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0,
|
||||
notification: false,
|
||||
type: 'general',
|
||||
show: false
|
||||
});
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
toggleFeedback: () => {
|
||||
update((feedback) => {
|
||||
feedback.show = !feedback.show;
|
||||
return feedback;
|
||||
});
|
||||
},
|
||||
toggleNotification: () =>
|
||||
update((feedback) => {
|
||||
feedback.notification = !feedback.notification;
|
||||
return feedback;
|
||||
}),
|
||||
switchType: (feedType: Feedback['type']) =>
|
||||
update((feedback) => {
|
||||
feedback.type = feedType;
|
||||
return feedback;
|
||||
}),
|
||||
addVisualization: () =>
|
||||
update((feedback) => {
|
||||
feedback.visualized += 1;
|
||||
localStorage.setItem('feedbackVisualized', feedback.visualized.toString());
|
||||
return feedback;
|
||||
}),
|
||||
|
||||
increaseElapsed: (time: number) => {
|
||||
update((feedback) => {
|
||||
feedback.elapsed += time;
|
||||
localStorage.setItem('feedbackElapsed', feedback.elapsed.toString());
|
||||
return feedback;
|
||||
});
|
||||
},
|
||||
submitFeedback: async (
|
||||
subject: string,
|
||||
message: string,
|
||||
firstname?: string,
|
||||
email?: string,
|
||||
value?: number
|
||||
) => {
|
||||
if (!VARS.GROWTH_ENDPOINT) return;
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject,
|
||||
message,
|
||||
email,
|
||||
firstname: firstname ? firstname : undefined,
|
||||
customFields: value ? [{ id: '40655', value }] : undefined
|
||||
})
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error('Failed to submit feedback');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export const feedback = createFeedbackStore();
|
||||
@@ -1,15 +1,18 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Apple from '../../routes/console/project-[project]/auth/appleOAuth.svelte';
|
||||
import Microsoft from '../../routes/console/project-[project]/auth/microsoftOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte';
|
||||
import Auth0 from '../../routes/console/project-[project]/auth/auth0OAuth.svelte';
|
||||
import Authentik from '../../routes/console/project-[project]/auth/authentikOAuth.svelte';
|
||||
import GitLab from '../../routes/console/project-[project]/auth/gitlabOAuth.svelte';
|
||||
import Google from '../../routes/console/project-[project]/auth/googleOAuth.svelte';
|
||||
import Main from '../../routes/console/project-[project]/auth/mainOAuth.svelte';
|
||||
import Microsoft from '../../routes/console/project-[project]/auth/microsoftOAuth.svelte';
|
||||
import Oidc from '../../routes/console/project-[project]/auth/oidcOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte';
|
||||
|
||||
export type Provider = Models.Provider & {
|
||||
key: string;
|
||||
icon: string;
|
||||
docs?: string;
|
||||
component?: typeof SvelteComponent;
|
||||
@@ -22,11 +25,12 @@ export type Providers = {
|
||||
const setProviders = (project: Models.Project): Provider[] => {
|
||||
return (
|
||||
project?.providers.map((n) => {
|
||||
const p = n as Models.Provider & { key: string };
|
||||
let docs: Provider['docs'];
|
||||
let icon: Provider['icon'] = n.name.toLowerCase();
|
||||
let icon: Provider['icon'] = p.key.toLowerCase();
|
||||
let component: Provider['component'] = Main;
|
||||
|
||||
switch (n.name.toLowerCase()) {
|
||||
switch (p.key.toLowerCase()) {
|
||||
case 'amazon':
|
||||
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
|
||||
break;
|
||||
@@ -81,6 +85,7 @@ const setProviders = (project: Models.Project): Provider[] => {
|
||||
break;
|
||||
case 'google':
|
||||
docs = 'https://support.google.com/googleapi/answer/6158849';
|
||||
component = Google;
|
||||
break;
|
||||
case 'linkedin':
|
||||
docs = 'https://developer.linkedin.com/';
|
||||
@@ -92,6 +97,10 @@ const setProviders = (project: Models.Project): Provider[] => {
|
||||
case 'notion':
|
||||
docs = 'https://developers.notion.com/docs';
|
||||
break;
|
||||
case 'oidc':
|
||||
docs = 'https://openid.net/connect/faq/';
|
||||
component = Oidc;
|
||||
break;
|
||||
case 'okta':
|
||||
docs = 'https://developer.okta.com';
|
||||
component = Okta;
|
||||
@@ -149,7 +158,7 @@ const setProviders = (project: Models.Project): Provider[] => {
|
||||
}
|
||||
|
||||
return {
|
||||
...n,
|
||||
...p,
|
||||
icon,
|
||||
docs,
|
||||
component
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type WizardStore = {
|
||||
show: boolean;
|
||||
media?: string;
|
||||
component?: typeof SvelteComponent;
|
||||
component?: ComponentType;
|
||||
interceptor?: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -20,10 +20,11 @@ function createWizardStore() {
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
start: (component: typeof SvelteComponent, media: string = null) =>
|
||||
start: (component: ComponentType, media: string = null) =>
|
||||
update((n) => {
|
||||
n.show = true;
|
||||
n.component = component;
|
||||
n.interceptor = null;
|
||||
n.media = media;
|
||||
trackEvent('wizard_start');
|
||||
return n;
|
||||
@@ -39,6 +40,7 @@ function createWizardStore() {
|
||||
update((n) => {
|
||||
n.show = false;
|
||||
n.component = null;
|
||||
n.interceptor = null;
|
||||
n.media = null;
|
||||
|
||||
return n;
|
||||
|
||||
@@ -2,20 +2,56 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Heading } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
const project = $page.url.searchParams.get('project');
|
||||
if (project) {
|
||||
await goto(`appwrite-callback-${project}://${$page.url.search}`);
|
||||
const project = $page.url.searchParams.get('project');
|
||||
const link = `appwrite-callback-${project}://${$page.url.search}`;
|
||||
|
||||
const redirect = new Promise((resolve, reject) => {
|
||||
if (!project) {
|
||||
reject('no-project');
|
||||
}
|
||||
// this timeout is needed because goto does not
|
||||
// throw an exception if the redirect does not work
|
||||
setTimeout(() => reject('timeout'), 500);
|
||||
// goto will resolve on successful redirect
|
||||
goto(link).then(resolve);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Heading tag="h1" size="1">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a class="link" href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
{#await redirect then}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Login failed</Heading>
|
||||
<p class="text">You will be automatically redirected back to your app shortly.</p>
|
||||
<p class="text">
|
||||
If you are not redirected, please click on the following
|
||||
<a class="link" href={`appwrite-callback-${project}://${$page.url.search}`}>link</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{:catch}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
// override padding for screens bigger than mobile
|
||||
@media #{$break2open} {
|
||||
article.card {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,20 +2,56 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Heading } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(async () => {
|
||||
const project = $page.url.searchParams.get('project');
|
||||
if (project) {
|
||||
await goto(`appwrite-callback-${project}://${$page.url.search}`);
|
||||
const project = $page.url.searchParams.get('project');
|
||||
const link = `appwrite-callback-${project}://${$page.url.search}`;
|
||||
|
||||
const redirect = new Promise((resolve, reject) => {
|
||||
if (!project) {
|
||||
reject('no-project');
|
||||
}
|
||||
// this timeout is needed because goto does not
|
||||
// throw an exception if the redirect does not work
|
||||
setTimeout(() => reject('timeout'), 500);
|
||||
// goto will resolve on successful redirect
|
||||
goto(link).then(resolve);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Heading tag="h1" size="1">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a class="link" href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
{#await redirect then}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">You're now logged in</Heading>
|
||||
<p class="text">You will be automatically redirected back to your app shortly.</p>
|
||||
<p class="text">
|
||||
If you are not redirected, please click on the following
|
||||
<a class="link" href={`appwrite-callback-${project}://${$page.url.search}`}>link</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{:catch}
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Heading tag="h1" size="4">Missing Redirect URL</Heading>
|
||||
<p class="text">
|
||||
Your OAuth login flow is missing a proper redirect URL. Please check the
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/client/account?sdk=web#createOAuth2Session"
|
||||
>OAuth docs</a>
|
||||
and send request for new session with a valid callback URL.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
// override padding for screens bigger than mobile
|
||||
@media #{$break2open} {
|
||||
article.card {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import Header from '$lib/layout/header.svelte';
|
||||
import SideNavigation from '$lib/layout/navigation.svelte';
|
||||
import Shell from '$lib/layout/shell.svelte';
|
||||
import { feedback } from '$lib/stores/app';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { newOrgModal } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
@@ -15,6 +15,8 @@
|
||||
import { loading } from '../store';
|
||||
import Create from './createOrganization.svelte';
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
onMount(() => {
|
||||
loading.set(false);
|
||||
|
||||
@@ -47,12 +49,13 @@
|
||||
</script>
|
||||
|
||||
<Shell
|
||||
bind:isOpen
|
||||
showSideNavigation={$page.url.pathname !== '/console' &&
|
||||
!$page?.params.organization &&
|
||||
!$page.url.pathname.includes('/console/account') &&
|
||||
!$page.url.pathname.includes('/console/onboarding')}>
|
||||
<Header slot="header" />
|
||||
<SideNavigation slot="side" />
|
||||
<SideNavigation slot="side" bind:isOpen />
|
||||
<slot />
|
||||
<Footer slot="footer" />
|
||||
</Shell>
|
||||
|
||||
@@ -2,7 +2,12 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
const response = await fetch(`${sdk.forConsole.client.config.endpoint}/health/version`);
|
||||
const { endpoint, project } = sdk.forConsole.client.config;
|
||||
const response = await fetch(`${endpoint}/health/version`, {
|
||||
headers: {
|
||||
'X-Appwrite-Project': project
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import { feedbackData } from '$lib/stores/feedback';
|
||||
import Step1 from './wizardFeedback/step1.svelte';
|
||||
import Step2 from './wizardFeedback/step2.svelte';
|
||||
|
||||
onDestroy(() => {
|
||||
feedbackData.reset();
|
||||
});
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Feedback',
|
||||
component: Step1
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Thank you',
|
||||
component: Step2,
|
||||
optional: true
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard title="Feedback" steps={stepsComponents} finalAction="Close" />
|
||||
@@ -8,6 +8,8 @@
|
||||
import CreateOrganization from '../createOrganization.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { page } from '$app/stores';
|
||||
import { services } from '$lib/stores/project-services';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -38,6 +40,17 @@
|
||||
return { name, icon };
|
||||
};
|
||||
|
||||
function allServiceDisabled(project: Models.Project): boolean {
|
||||
let disabled = true;
|
||||
services.load(project);
|
||||
$services.list.forEach((service) => {
|
||||
if (service.value) {
|
||||
disabled = false;
|
||||
}
|
||||
});
|
||||
return disabled;
|
||||
}
|
||||
|
||||
function filterPlatforms(platforms: { name: string; icon: string }[]) {
|
||||
return platforms.filter(
|
||||
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
|
||||
@@ -67,6 +80,11 @@
|
||||
<svelte:fragment slot="title">
|
||||
{project.name}
|
||||
</svelte:fragment>
|
||||
{#if allServiceDisabled(project)}
|
||||
<p>
|
||||
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
|
||||
</p>
|
||||
{/if}
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
TableRowLink
|
||||
} from '$lib/elements/table';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { base } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// TODO: Remove this when the console SDK is updated
|
||||
const users = data.users.users.map((user) => {
|
||||
const labels: string[] = [];
|
||||
const accessedAt = '';
|
||||
return { accessedAt, labels, ...user };
|
||||
});
|
||||
|
||||
let showCreate = false;
|
||||
const projectId = $page.params.project;
|
||||
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
|
||||
@@ -49,10 +56,12 @@
|
||||
<TableCellHead onlyDesktop>Identifiers</TableCellHead>
|
||||
<TableCellHead onlyDesktop width={130}>Status</TableCellHead>
|
||||
<TableCellHead onlyDesktop width={100}>ID</TableCellHead>
|
||||
<TableCellHead onlyDesktop width={100}>Labels</TableCellHead>
|
||||
<TableCellHead onlyDesktop>Joined</TableCellHead>
|
||||
<TableCellHead onlyDesktop>Last Activity</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.users.users as user}
|
||||
{#each users as user}
|
||||
<TableRowLink
|
||||
href={`${base}/console/project-${projectId}/auth/user-${user.$id}`}>
|
||||
<TableCell title="Name">
|
||||
@@ -102,9 +111,15 @@
|
||||
</Pill>
|
||||
</Copy>
|
||||
</TableCell>
|
||||
<TableCellText onlyDesktop title="Labels">
|
||||
{user.labels.join(', ')}
|
||||
</TableCellText>
|
||||
<TableCellText onlyDesktop title="Joined">
|
||||
{toLocaleDateTime(user.registration)}
|
||||
</TableCellText>
|
||||
<TableCellText onlyDesktop title="Last Activity">
|
||||
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
|
||||
</TableCellText>
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -63,9 +63,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -70,9 +70,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
@@ -81,7 +79,7 @@
|
||||
disabled={(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId) ||
|
||||
!(appId && clientSecret && endpoint)}
|
||||
!(appId && clientSecret)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from './updateOAuth';
|
||||
|
||||
export let provider: Provider;
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
let enabled: boolean = null;
|
||||
let appId: string = null;
|
||||
let secret: string = null;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= provider.enabled;
|
||||
appId ??= provider.appId;
|
||||
secret ??= provider.secret;
|
||||
});
|
||||
|
||||
let error: string;
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal {error} size="big" show onSubmit={update} on:close>
|
||||
<svelte:fragment slot="header">{provider.name} OAuth2 Settings</svelte:fragment>
|
||||
<FormList>
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="App ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId} />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="App Secret"
|
||||
placeholder="Enter App Secret"
|
||||
minlength={0}
|
||||
showPasswordButton
|
||||
bind:value={secret} />
|
||||
<Alert type="info">
|
||||
To complete the setup, create an OAuth2 client ID with "Web application" as the
|
||||
application type, then add this redirect URI to your {provider.name} configuration.
|
||||
</Alert>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!appId ||
|
||||
!secret ||
|
||||
(appId === provider.appId &&
|
||||
secret === provider.secret &&
|
||||
enabled === provider.enabled)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -63,9 +63,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Alert, CopyInput, Modal } from '$lib/components';
|
||||
import { Button, FormList, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { updateOAuth } from './updateOAuth';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
export let provider: Provider;
|
||||
|
||||
let appId: string = null;
|
||||
let enabled: boolean = null;
|
||||
let clientSecret: string = null;
|
||||
let wellKnownEndpoint: string = null;
|
||||
let authorizationEndpoint: string = null;
|
||||
let tokenEndpoint: string = null;
|
||||
let userinfoEndpoint: string = null;
|
||||
let error: string;
|
||||
|
||||
// secret is valid if clientSecret is set and either wellKnownEndpoint or all of the other endpoints are set
|
||||
$: isValidSecret =
|
||||
clientSecret &&
|
||||
(wellKnownEndpoint || (authorizationEndpoint && tokenEndpoint && userinfoEndpoint));
|
||||
|
||||
onMount(() => {
|
||||
appId ??= provider.appId;
|
||||
enabled ??= provider.enabled;
|
||||
if (provider.secret) {
|
||||
({
|
||||
clientSecret,
|
||||
wellKnownEndpoint,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint
|
||||
} = JSON.parse(provider.secret));
|
||||
}
|
||||
});
|
||||
|
||||
const update = async () => {
|
||||
const result = await updateOAuth({ projectId, provider, secret, appId, enabled });
|
||||
|
||||
if (result.status === 'error') {
|
||||
error = result.message;
|
||||
} else {
|
||||
provider = null;
|
||||
}
|
||||
};
|
||||
|
||||
$: secret = isValidSecret
|
||||
? JSON.stringify({
|
||||
clientSecret,
|
||||
wellKnownEndpoint,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint
|
||||
})
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} size="big" show on:close>
|
||||
<svelte:fragment slot="header">{provider.name} OAuth2 Settings</svelte:fragment>
|
||||
<FormList>
|
||||
<p>
|
||||
To use {provider.name} authentication in your application, first fill in this form. For more
|
||||
info you can
|
||||
<a class="link" href={provider.docs} target="_blank" rel="noopener noreferrer"
|
||||
>visit the docs.</a>
|
||||
</p>
|
||||
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
|
||||
<InputText
|
||||
id="appID"
|
||||
label="Client ID"
|
||||
autofocus={true}
|
||||
placeholder="Enter ID"
|
||||
bind:value={appId} />
|
||||
<InputPassword
|
||||
id="secret"
|
||||
label="Client Secret"
|
||||
placeholder="Enter Client Secret"
|
||||
minlength={0}
|
||||
showPasswordButton
|
||||
bind:value={clientSecret} />
|
||||
<InputText
|
||||
id="well-known-endpoint"
|
||||
label="Well-Known Endpoint"
|
||||
placeholder="https://example.com/.well-known/openid-configuration"
|
||||
bind:value={wellKnownEndpoint} />
|
||||
<InputText
|
||||
id="authorization-endpoint"
|
||||
label="Authorization Endpoint"
|
||||
placeholder="https://example.com/authorize"
|
||||
bind:value={authorizationEndpoint} />
|
||||
<InputText
|
||||
id="token-endpoint"
|
||||
label="Token Endpoint"
|
||||
placeholder="https://example.com/token"
|
||||
bind:value={tokenEndpoint} />
|
||||
<InputText
|
||||
id="userinfo-endpoint"
|
||||
label="User Info Endpoint"
|
||||
placeholder="https://example.com/userinfo"
|
||||
bind:value={userinfoEndpoint} />
|
||||
|
||||
<Alert type="info">
|
||||
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
|
||||
</Alert>
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={(secret === provider.secret &&
|
||||
enabled === provider.enabled &&
|
||||
appId === provider.appId) ||
|
||||
!(appId && isValidSecret)}
|
||||
submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -82,9 +82,7 @@
|
||||
<div>
|
||||
<p>URI</p>
|
||||
<CopyInput
|
||||
value={`${
|
||||
sdk.forConsole.client.config.endpoint
|
||||
}/account/sessions/oauth2/callback/${provider.name.toLocaleLowerCase()}/${projectId}`} />
|
||||
value={`${sdk.forConsole.client.config.endpoint}/account/sessions/oauth2/callback/${provider.key}/${projectId}`} />
|
||||
</div>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputSwitch } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { OAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '../../store';
|
||||
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
|
||||
import { OAuthProviders } from '$lib/stores/oauth-providers';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { page } from '$app/stores';
|
||||
import type { Provider } from '$lib/stores/oauth-providers';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
on:click={() => {
|
||||
selectedProvider = provider;
|
||||
trackEvent(`click_select_provider`, {
|
||||
provider: provider.name.toLowerCase()
|
||||
provider: provider.key.toLowerCase()
|
||||
});
|
||||
}}>
|
||||
<div class="image-item">
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function updateOAuth({
|
||||
try {
|
||||
await sdk.forConsole.projects.updateOAuth2(
|
||||
projectId,
|
||||
provider.name.toLowerCase(),
|
||||
provider.key,
|
||||
appId || undefined,
|
||||
secret || undefined,
|
||||
enabled
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Container } from '$lib/layout';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
import UpdateEmail from './updateEmail.svelte';
|
||||
import UpdateLabels from './updateLabels.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdatePassword from './updatePassword.svelte';
|
||||
import UpdatePhone from './updatePhone.svelte';
|
||||
@@ -15,6 +16,7 @@
|
||||
<UpdateEmail />
|
||||
<UpdatePhone />
|
||||
<UpdatePassword />
|
||||
<UpdateLabels />
|
||||
<UpdatePrefs />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Box, Heading, AvatarInitials } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import DeleteUser from './deleteUser.svelte';
|
||||
import { user } from './store';
|
||||
|
||||
let showDelete = false;
|
||||
|
||||
// TODO: Remove this when the console SDK is updated
|
||||
$: accessedAt = ($user as unknown as { accessedAt: string }).accessedAt;
|
||||
</script>
|
||||
|
||||
<CardGrid danger>
|
||||
@@ -42,6 +46,7 @@
|
||||
? [$user.email, $user.phone].join(',')
|
||||
: $user.email || $user.phone}
|
||||
</p>
|
||||
<p>Last activity: {accessedAt ? toLocaleDate(accessedAt) : 'never'}</p>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
try {
|
||||
await sdk.forProject.users.deleteSession($page.params.user, selectedSessionId);
|
||||
await invalidate(Dependencies.SESSIONS);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Session has been deleted'
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { EmptySearch } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCellHead,
|
||||
TableCell,
|
||||
TableCellText
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import DeleteAllSessions from '../deleteAllSessions.svelte';
|
||||
@@ -47,7 +47,7 @@
|
||||
<TableRow>
|
||||
<TableCell title="Client">
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<div class="image-item">
|
||||
<div class="avatar">
|
||||
<img
|
||||
height="20"
|
||||
width="20"
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
<Form onSubmit={updateEmail}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Email</Heading>
|
||||
<p>
|
||||
Update user's email. An Email should be formatted as: <span class="inline-code"
|
||||
>name@example.com</span
|
||||
>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputEmail
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, Helper, InputTags } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from './store';
|
||||
import { Pill } from '$lib/elements';
|
||||
|
||||
const alphaNumericRegExp = /^[a-zA-Z0-9]+$/;
|
||||
let suggestedLabels = ['admin', 'premium', 'mvp'];
|
||||
let labels: string[] = [];
|
||||
let error = '';
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: Remove type cast when console SDK is updated
|
||||
labels = [...($user as unknown as { labels: string[] }).labels];
|
||||
});
|
||||
|
||||
async function updateLabels() {
|
||||
try {
|
||||
// TODO: Use SDK method when console SDK is updated
|
||||
// await sdk.forProject.users.updateLabels($user.$id, labels);
|
||||
const path = `/users/${$user.$id}/labels`;
|
||||
await sdk.forProject.client.call(
|
||||
'PUT',
|
||||
new URL(sdk.forConsole.client.config.endpoint + path),
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
{
|
||||
labels: labels
|
||||
}
|
||||
);
|
||||
await invalidate(Dependencies.USER);
|
||||
isDisabled = true;
|
||||
|
||||
addNotification({
|
||||
message: 'User labels have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.UserUpdateLabels);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.UserUpdateLabels);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove type cast when console SDK is updated
|
||||
$: isDisabled =
|
||||
!!error ||
|
||||
!symmetricDifference(labels, ($user as unknown as { labels: string[] }).labels).length;
|
||||
$: if (labels) {
|
||||
const invalidLabels = [];
|
||||
|
||||
labels.forEach((label) => {
|
||||
if (!alphaNumericRegExp.test(label)) {
|
||||
invalidLabels.push(label);
|
||||
}
|
||||
});
|
||||
|
||||
if (invalidLabels.length) {
|
||||
error = `Invalid labels: ${invalidLabels.join(', ')}`;
|
||||
} else {
|
||||
error = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateLabels}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Labels</Heading>
|
||||
<p class="text">
|
||||
Categorize and manage your users based on specific criteria by assigning them
|
||||
customizable labels. New label-based roles will be assigned.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<ul class="common-section">
|
||||
<InputTags
|
||||
id="user-labels"
|
||||
label="Labels"
|
||||
placeholder="Select or tyype user labels"
|
||||
bind:tags={labels} />
|
||||
<li>
|
||||
<Helper type={error ? 'warning' : 'neutral'}
|
||||
>{error ? error : 'Only alphanumeric characters are allowed'}</Helper>
|
||||
</li>
|
||||
<li class="u-flex u-gap-12 u-margin-block-start-8">
|
||||
{#each suggestedLabels as suggestedLabel}
|
||||
<Pill
|
||||
selected={labels.includes(suggestedLabel)}
|
||||
button
|
||||
on:click={() => {
|
||||
if (!labels.includes(suggestedLabel)) {
|
||||
labels = [...labels, suggestedLabel];
|
||||
} else {
|
||||
labels = labels.filter((e) => e !== suggestedLabel);
|
||||
}
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
{suggestedLabel}
|
||||
</Pill>
|
||||
{/each}
|
||||
</li>
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={isDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -33,9 +33,7 @@
|
||||
<Heading tag="h6" size="7">Password</Heading>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Enter a new password. A password must contain <b>at least 8 characters.</b>
|
||||
</p>
|
||||
<p>Enter a new password. A password must contain at least 8 characters.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputPassword
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<Form onSubmit={updatePhone}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Phone</Heading>
|
||||
<p>
|
||||
Update user's phone. A phone number must contain leading '+' and maximum of 15 digits.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputPhone
|
||||
|
||||
@@ -56,11 +56,8 @@
|
||||
|
||||
<Form onSubmit={updatePrefs}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">User Preferences</Heading>
|
||||
<p>
|
||||
You can update your user preferences by storing information on the user's objects so
|
||||
they can easily be shared across devices and sessions.
|
||||
</p>
|
||||
<Heading tag="h6" size="7">Preferences</Heading>
|
||||
<p>Add custom user preferences to share them across devices and sessions.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<form class="form u-grid u-gap-16">
|
||||
<ul class="form-list">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from './store';
|
||||
@@ -19,7 +19,7 @@
|
||||
await invalidate(Dependencies.USER);
|
||||
addNotification({
|
||||
message: `${$user.name || $user.email || $user.phone || 'The account'} has been ${
|
||||
$user.emailVerification ? 'unverified' : 'verified'
|
||||
!$user.emailVerification ? 'unverified' : 'verified'
|
||||
}`,
|
||||
type: 'success'
|
||||
});
|
||||
@@ -71,6 +71,9 @@
|
||||
trackError(error, Submit.UserUpdateStatus);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this when the console SDK is updated
|
||||
$: accessedAt = ($user as unknown as { accessedAt: string }).accessedAt;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -100,6 +103,7 @@
|
||||
<p class="title">{$user.phone}</p>
|
||||
{/if}
|
||||
<p>Joined: {toLocaleDateTime($user.registration)}</p>
|
||||
<p>Last activity: {accessedAt ? toLocaleDate(accessedAt) : 'never'}</p>
|
||||
</div>
|
||||
{#if !$user.status}
|
||||
<Pill danger>blocked</Pill>
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Check out our documentation.
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
|
||||
+2
-2
@@ -84,7 +84,7 @@
|
||||
aria-hidden="true" />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text u-trim" data-private>{attribute.key}</span>
|
||||
<span class="text u-trim-1" data-private>{attribute.key}</span>
|
||||
</div>
|
||||
{#if attribute.status !== 'available'}
|
||||
<Pill
|
||||
@@ -180,7 +180,7 @@
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Check out our documentation.
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
|
||||
+10
-12
@@ -34,8 +34,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputChoice } from '$lib/elements/forms';
|
||||
import Boolean from '../document-[document]/attributes/boolean.svelte';
|
||||
import { InputChoice, InputSelect } from '$lib/elements/forms';
|
||||
|
||||
export let editing = false;
|
||||
export let data: Partial<Models.AttributeBoolean> = {
|
||||
@@ -49,18 +48,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Boolean
|
||||
<InputSelect
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
attribute={{
|
||||
key: data.key,
|
||||
required: data.required,
|
||||
status: 'enabled',
|
||||
type: 'boolean',
|
||||
array: data.array
|
||||
}}
|
||||
disabled={data.array || data.required} />
|
||||
placeholder="Select a value"
|
||||
disabled={data.required || data.array}
|
||||
options={[
|
||||
{ label: 'NULL', value: null },
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
]}
|
||||
bind:value={data.default} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required} />
|
||||
disabled={data.required || data.array} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+8
-20
@@ -71,31 +71,19 @@
|
||||
</svelte:fragment>
|
||||
<FormList>
|
||||
{#if selectedAttribute?.type !== 'relationship'}
|
||||
<div>
|
||||
<InputText
|
||||
id="key"
|
||||
label="Attribute Key"
|
||||
placeholder="Enter Key"
|
||||
bind:value={selectedAttribute.key}
|
||||
autofocus
|
||||
required
|
||||
readonly />
|
||||
|
||||
<div class="u-flex u-gap-4 u-margin-block-start-8 u-small">
|
||||
<span
|
||||
class="icon-info u-cross-center u-margin-block-start-2 u-line-height-1 u-icon-small"
|
||||
aria-hidden="true" />
|
||||
<span class="text u-line-height-1-5">
|
||||
Allowed characters: alphanumeric, hyphen, non-leading underscore, period
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<InputText
|
||||
id="key"
|
||||
label="Attribute Key"
|
||||
placeholder="Enter Key"
|
||||
bind:value={selectedAttribute.key}
|
||||
autofocus
|
||||
readonly />
|
||||
{/if}
|
||||
{#if option}
|
||||
<svelte:component
|
||||
this={option.component}
|
||||
bind:data={selectedAttribute}
|
||||
editing
|
||||
bind:data={selectedAttribute}
|
||||
on:close={() => (option = null)} />
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required} />
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+19
-13
@@ -36,8 +36,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputChoice, InputTags } from '$lib/elements/forms';
|
||||
import Enum from '../document-[document]/attributes/enum.svelte';
|
||||
import { InputChoice, InputSelect, InputTags } from '$lib/elements/forms';
|
||||
|
||||
export let editing = false;
|
||||
export let data: Partial<Models.AttributeEnum>;
|
||||
@@ -45,6 +44,20 @@
|
||||
$: if (data.required || data.array) {
|
||||
data.default = null;
|
||||
}
|
||||
|
||||
$: options = [
|
||||
...(data?.elements ?? []).map((element) => {
|
||||
return {
|
||||
label: element,
|
||||
value: element
|
||||
};
|
||||
}),
|
||||
!data.required &&
|
||||
!data.array && {
|
||||
label: 'NULL',
|
||||
value: null
|
||||
}
|
||||
].filter(Boolean);
|
||||
</script>
|
||||
|
||||
<InputTags
|
||||
@@ -53,19 +66,12 @@
|
||||
bind:tags={data.elements}
|
||||
placeholder="Add elements here"
|
||||
required />
|
||||
|
||||
<Enum
|
||||
<InputSelect
|
||||
id="default"
|
||||
label="Default value"
|
||||
attribute={{
|
||||
key: data.key,
|
||||
type: 'string',
|
||||
status: 'enabled',
|
||||
format: 'enum',
|
||||
elements: data.elements ?? [],
|
||||
required: data.required,
|
||||
default: data.default
|
||||
}}
|
||||
disabled={data.array || data.required}
|
||||
placeholder="Select a value"
|
||||
{options}
|
||||
bind:value={data.default} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
|
||||
+15
-3
@@ -54,9 +54,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputNumber id="min" label="Min" placeholder="Enter size" bind:value={data.min} />
|
||||
<InputNumber id="max" label="Max" placeholder="Enter size" bind:value={data.max} />
|
||||
|
||||
<InputNumber
|
||||
id="min"
|
||||
label="Min"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.min}
|
||||
step="any"
|
||||
required={editing} />
|
||||
<InputNumber
|
||||
id="max"
|
||||
label="Max"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.max}
|
||||
step="any"
|
||||
required={editing} />
|
||||
<InputNumber
|
||||
id="default"
|
||||
label="Default value"
|
||||
@@ -65,6 +76,7 @@
|
||||
max={data.max}
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array}
|
||||
step="any" />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
|
||||
+14
-4
@@ -54,9 +54,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputNumber id="min" label="Min" placeholder="Enter size" bind:value={data.min} />
|
||||
<InputNumber id="max" label="Max" placeholder="Enter size" bind:value={data.max} />
|
||||
|
||||
<InputNumber
|
||||
id="min"
|
||||
label="Min"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.min}
|
||||
required={editing} />
|
||||
<InputNumber
|
||||
id="max"
|
||||
label="Max"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.max}
|
||||
required={editing} />
|
||||
<InputNumber
|
||||
id="default"
|
||||
label="Default value"
|
||||
@@ -64,7 +73,8 @@
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array} />
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array} />
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
-2
@@ -98,7 +98,6 @@
|
||||
});
|
||||
|
||||
// Reactive statements
|
||||
|
||||
$: getCollections(search).then((res) => (collectionList = res));
|
||||
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
|
||||
|
||||
@@ -219,7 +218,6 @@
|
||||
placeholder="Select a relation"
|
||||
options={relationshipType}
|
||||
disabled={editing} />
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<div class="box">
|
||||
<div class="u-flex u-align u-cross-center u-main-center u-gap-32">
|
||||
|
||||
+21
-17
@@ -34,8 +34,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import String from '../document-[document]/attributes/string.svelte';
|
||||
import { InputChoice, InputNumber, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
|
||||
export let data: Partial<Models.AttributeString> = {
|
||||
required: false,
|
||||
@@ -55,22 +54,27 @@
|
||||
label="Size"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.size}
|
||||
required
|
||||
required={!editing}
|
||||
readonly={editing} />
|
||||
<String
|
||||
id="default"
|
||||
label="Default value"
|
||||
attribute={{
|
||||
key: 'default',
|
||||
type: 'string',
|
||||
required: data.required,
|
||||
array: data.array,
|
||||
size: data.size,
|
||||
default: data.default,
|
||||
status: 'enabled'
|
||||
}}
|
||||
disabled={data.required || data.array}
|
||||
bind:value={data.default} />
|
||||
{#if data.size >= 50}
|
||||
<InputTextarea
|
||||
id="default"
|
||||
label="Default"
|
||||
placeholder="Enter string"
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array}
|
||||
maxlength={data.size}
|
||||
bind:value={data.default} />
|
||||
{:else}
|
||||
<InputText
|
||||
id="default"
|
||||
label="Default"
|
||||
placeholder="Enter string"
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array}
|
||||
maxlength={data.size}
|
||||
bind:value={data.default} />
|
||||
{/if}
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array} />
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array} />
|
||||
<InputChoice id="required" label="Required" bind:value={data.required} disabled={data.array}>
|
||||
Indicate whether this is a required attribute
|
||||
</InputChoice>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
import type { Attributes } from './store';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { feedback } from '$lib/stores/app';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
|
||||
export let showCreate = false;
|
||||
export let selectedOption: string = null;
|
||||
|
||||
+1
-3
@@ -7,7 +7,6 @@
|
||||
export let value: boolean;
|
||||
export let attribute: Models.AttributeBoolean;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<InputSelect
|
||||
@@ -22,5 +21,4 @@
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value
|
||||
{disabled} />
|
||||
bind:value />
|
||||
|
||||
+1
-3
@@ -7,7 +7,6 @@
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeEnum;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
|
||||
$: options = [
|
||||
...attribute.elements.map((element) => {
|
||||
@@ -31,5 +30,4 @@
|
||||
{optionalText}
|
||||
required={attribute.required}
|
||||
placeholder="Select a value"
|
||||
showLabel={!!label?.length}
|
||||
{disabled} />
|
||||
showLabel={!!label?.length} />
|
||||
|
||||
+2
-2
@@ -17,5 +17,5 @@
|
||||
required={attribute.required}
|
||||
min={attribute.min}
|
||||
max={attribute.max}
|
||||
bind:value
|
||||
step={attribute.type === 'double' ? 'any' : 1} />
|
||||
step={attribute.type === 'double' ? 'any' : 1}
|
||||
bind:value />
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
import { PaginationInline } from '$lib/components';
|
||||
import { SelectSearchItem } from '$lib/elements';
|
||||
import { Button, InputSelectSearch, Label } from '$lib/elements/forms';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { doc } from '../store';
|
||||
import { isRelationshipToMany } from './store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
@@ -32,7 +32,7 @@
|
||||
onMount(async () => {
|
||||
if (value) {
|
||||
if (isRelationshipToMany(attribute)) {
|
||||
relatedList = value as string[];
|
||||
relatedList = (value as string[]).slice();
|
||||
} else {
|
||||
singleRel = value as string;
|
||||
}
|
||||
|
||||
+2
-5
@@ -7,30 +7,27 @@
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeString;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
{#if attribute.size >= 50}
|
||||
<InputTextarea
|
||||
{id}
|
||||
{label}
|
||||
nullable
|
||||
nullable={!attribute.required}
|
||||
placeholder="Enter string"
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
{disabled}
|
||||
bind:value />
|
||||
{:else}
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
nullable
|
||||
nullable={!attribute.required}
|
||||
placeholder="Enter string"
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
{disabled}
|
||||
bind:value />
|
||||
{/if}
|
||||
|
||||
+1
@@ -15,5 +15,6 @@
|
||||
{optionalText}
|
||||
placeholder="Enter URL"
|
||||
showLabel={!!label?.length}
|
||||
nullable={!attribute.required}
|
||||
required={attribute.required}
|
||||
bind:value />
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2">Create your first attribute to get started.</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Check out our documentation.
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 u-main-center">
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { PageData } from './$types';
|
||||
@@ -20,7 +20,7 @@
|
||||
const databaseId = $page.params.database;
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
@@ -54,4 +54,4 @@
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { PageData } from './$types';
|
||||
@@ -19,7 +19,7 @@
|
||||
const projectId = $page.params.project;
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
@@ -55,4 +55,4 @@
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import Delete from './delete.svelte';
|
||||
import Create from './create.svelte';
|
||||
import Rebuild from './rebuild.svelte';
|
||||
import Activate from './activate.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -44,6 +45,7 @@
|
||||
let showDropdown = [];
|
||||
let showDelete = false;
|
||||
let showActivate = false;
|
||||
let showRebuild = false;
|
||||
|
||||
let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
@@ -51,6 +53,10 @@
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
function handleRebuild() {
|
||||
invalidate(Dependencies.DEPLOYMENTS);
|
||||
}
|
||||
|
||||
$: activeDeployment = data.deployments.deployments.find((d) => d.$id === $func?.deployment);
|
||||
|
||||
if (browser) {
|
||||
@@ -215,6 +221,17 @@
|
||||
}}>
|
||||
Activate
|
||||
</DropListItem>
|
||||
{#if 'failed' === deployment.status}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
selectedDeployment = deployment;
|
||||
showRebuild = true;
|
||||
showDropdown = [];
|
||||
}}>
|
||||
Retry Build
|
||||
</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
icon="terminal"
|
||||
on:click={() => {
|
||||
@@ -281,4 +298,5 @@
|
||||
{#if selectedDeployment}
|
||||
<Delete {selectedDeployment} bind:showDelete />
|
||||
<Activate {selectedDeployment} bind:showActivate on:activated={handleActivate} />
|
||||
<Rebuild {selectedDeployment} bind:showRebuild on:rebuild={handleRebuild} />
|
||||
{/if}
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
function setCodeSnippets(lang: string) {
|
||||
return {
|
||||
Unix: {
|
||||
code: `appwrite functions createDeployment \\
|
||||
--functionId=${functionId} \\
|
||||
--entrypoint='index.${lang}' \\
|
||||
--code="." \\
|
||||
code: `appwrite functions createDeployment \\
|
||||
--functionId=${functionId} \\
|
||||
--entrypoint='index.${lang}' \\
|
||||
--code="." \\
|
||||
--activate=true`,
|
||||
language: 'bash'
|
||||
},
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no execution logs. Create and activate a deployment to see it here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">Need a hand? Check out our documentation</p>
|
||||
<p class="text u-line-height-1-5">Need a hand? Learn more in our documentation</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button text external href="https://appwrite.io/docs/functions#execute">
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
<CoverTitle href={`/console/project-${projectId}/functions`}>
|
||||
{$func?.name}
|
||||
</CoverTitle>
|
||||
<Id value={$func?.$id} event="function">Function ID</Id>
|
||||
{#if $func?.$id}
|
||||
<Id value={$func.$id} event="function">{$func.$id}</Id>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let showRebuild = false;
|
||||
export let selectedDeployment: Models.Deployment = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await sdk.forProject.functions.createBuild(
|
||||
selectedDeployment.resourceId,
|
||||
selectedDeployment.$id,
|
||||
selectedDeployment.buildId
|
||||
);
|
||||
showRebuild = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Retrying build`
|
||||
});
|
||||
dispatch('rebuild');
|
||||
trackEvent(Submit.DeploymentUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.DeploymentUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showRebuild} onSubmit={handleSubmit}>
|
||||
<svelte:fragment slot="header">Retry build</svelte:fragment>
|
||||
<p>Are you sure you want to retry building this deployment?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showRebuild = false)}>Cancel</Button>
|
||||
<Button secondary submit>Retry build</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+16
-511
@@ -1,520 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import {
|
||||
Box,
|
||||
CardGrid,
|
||||
DropList,
|
||||
DropListItem,
|
||||
Empty,
|
||||
Output,
|
||||
PaginationInline,
|
||||
Secret
|
||||
} from '$lib/components';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputCron, InputNumber, InputText } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import { execute, func } from '../store';
|
||||
import UploadVariables from './uploadVariables.svelte';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import type { PageData } from './$types';
|
||||
import Delete from './delete.svelte';
|
||||
|
||||
import UpdateEvents from './updateEvents.svelte';
|
||||
import ExecuteFunction from './executeFunction.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdatePermissions from './updatePermissions.svelte';
|
||||
import UpdateSchedule from './updateSchedule.svelte';
|
||||
import UpdateVariables from './updateVariables.svelte';
|
||||
import UpdateTimeout from './updateTimeout.svelte';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let showDelete = false;
|
||||
let selectedVar: Models.Variable = null;
|
||||
let showVariablesUpload = false;
|
||||
let showVariablesModal = false;
|
||||
let showVariablesDropdown = [];
|
||||
let timeout: number = null;
|
||||
let functionName: string = null;
|
||||
let functionSchedule: string = null;
|
||||
let permissions: string[] = [];
|
||||
let arePermsDisabled = true;
|
||||
let offset = 0;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
functionName ??= $func.name;
|
||||
functionSchedule ??= $func.schedule;
|
||||
permissions = $func.execute;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
functionName,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Name has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateName);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateName);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
permissions,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSchedule() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
functionSchedule,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Cron Schedule has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateSchedule);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTimeout() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
timeout,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Timeout has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateTimeout);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
|
||||
try {
|
||||
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableCreate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
try {
|
||||
await sdk.forProject.functions.updateVariable(
|
||||
functionId,
|
||||
variable.$id,
|
||||
variable.key,
|
||||
variable.value
|
||||
);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
selectedVar = null;
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableUpdate);
|
||||
}
|
||||
}
|
||||
async function handleVariableDeleted(variable: Models.Variable) {
|
||||
try {
|
||||
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Variable has been deleted`
|
||||
});
|
||||
trackEvent(Submit.VariableDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadVariables() {
|
||||
if (data.variables.total) {
|
||||
let content = data.variables.variables
|
||||
.map((variable) => `${variable.key}=${variable.value}`)
|
||||
.join('\n');
|
||||
const file = new File([content], '.env', {
|
||||
type: 'application/x-envoy'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $func.execute).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
}
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-medium">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${
|
||||
$func.runtime.split('-')[0]
|
||||
}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">{$func.name}</Heading>
|
||||
|
||||
<p class="text u-capitalize">{$func.runtime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div>
|
||||
<p>Function ID: {$func.$id}</p>
|
||||
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
|
||||
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($execute = $func)}>Execute now</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Name</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autocomplete={false}
|
||||
bind:value={functionName} />
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={functionName === $func.name || !functionName} submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Execute Access</Heading>
|
||||
<p>
|
||||
Choose who can execute this function using the client API. For more information,
|
||||
check out the <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<ExecuteFunction />
|
||||
<UpdateName />
|
||||
<UpdatePermissions />
|
||||
<UpdateEvents />
|
||||
|
||||
<Form onSubmit={updateSchedule}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Schedule</Heading>
|
||||
<p>
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://en.wikipedia.org/wiki/Cron"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
More details on Cron syntax here.</a>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={functionSchedule}
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.schedule === functionSchedule} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Variables</Heading>
|
||||
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-margin-inline-start-auto u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
event="download_env"
|
||||
disabled={!data.variables.total}
|
||||
on:click={downloadVariables}>
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download .env file</span>
|
||||
</Button>
|
||||
<Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<span class="icon-upload" />
|
||||
<span class="text">Import .env file</span>
|
||||
</Button>
|
||||
</div>
|
||||
{@const limit = 10}
|
||||
{@const sum = data.variables.total}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Key</TableCellHead>
|
||||
<TableCellHead width={180}>Value</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.variables.variables.slice(offset, offset + limit) as variable, i}
|
||||
<TableRow>
|
||||
<TableCell title="key">
|
||||
<Output value={variable.key} hideCopyIcon>
|
||||
{variable.key}
|
||||
</Output>
|
||||
</TableCell>
|
||||
|
||||
<TableCell showOverflow title="value">
|
||||
<Secret copyEvent="variable" value={variable.value} />
|
||||
</TableCell>
|
||||
<TableCell showOverflow title="options">
|
||||
<DropList
|
||||
bind:show={showVariablesDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
text
|
||||
round
|
||||
ariaLabel="more options"
|
||||
on:click={() =>
|
||||
(showVariablesDropdown[i] =
|
||||
!showVariablesDropdown[i])}>
|
||||
<span
|
||||
class="icon-dots-horizontal"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
selectedVar = variable;
|
||||
showVariablesDropdown[i] = false;
|
||||
showVariablesModal = true;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={async () => {
|
||||
handleVariableDeleted(variable);
|
||||
showVariablesDropdown[i] = false;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button text noMargin on:click={() => (showVariablesModal = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create variable</span>
|
||||
</Button>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total variables: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
|
||||
Create a variable to get started
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Form onSubmit={updateTimeout}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Timeout</Heading>
|
||||
<p>
|
||||
Limit the execution time of your function. Maximum value is 900 seconds (15
|
||||
minutes).
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={900}
|
||||
id="time"
|
||||
label="Time (in seconds)"
|
||||
bind:value={timeout} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.timeout === timeout || timeout < 1} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete Function</Heading>
|
||||
<p>
|
||||
The function will be permanently deleted, including all deployments associated with it.
|
||||
This action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$func.name}</h6>
|
||||
</svelte:fragment>
|
||||
<p>Last Updated: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<UpdateSchedule />
|
||||
<UpdateVariables variableList={data.variables} />
|
||||
<UpdateTimeout />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
{#if showVariablesModal}
|
||||
<Variable
|
||||
bind:selectedVar
|
||||
bind:showCreate={showVariablesModal}
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
{#if showVariablesUpload}
|
||||
<UploadVariables bind:show={showVariablesUpload} />
|
||||
{/if}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Box, CardGrid } from '$lib/components';
|
||||
import Heading from '$lib/components/heading.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import Delete from './deleteModal.svelte';
|
||||
|
||||
import { func } from '../store';
|
||||
|
||||
let showDelete = false;
|
||||
</script>
|
||||
|
||||
<CardGrid danger>
|
||||
<Heading tag="h6" size="7">Delete Function</Heading>
|
||||
<p>
|
||||
The function will be permanently deleted, including all deployments associated with it. This
|
||||
action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$func.name}</h6>
|
||||
</svelte:fragment>
|
||||
<p>Last Updated: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { execute, func } from '../store';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<div class="avatar is-medium">
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${$func.runtime.split('-')[0]}.svg`}
|
||||
alt="technology" />
|
||||
</div>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">{$func.name}</Heading>
|
||||
|
||||
<p class="text u-capitalize">{$func.runtime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div>
|
||||
<p>Function ID: {$func.$id}</p>
|
||||
<p>Created at: {toLocaleDateTime($func.$createdAt)}</p>
|
||||
<p>Updated at: {toLocaleDateTime($func.$updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($execute = $func)}>Execute now</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let functionName: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
functionName ??= $func.name;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
functionName,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Name has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateName);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateName);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Name</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<ul>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autocomplete={false}
|
||||
bind:value={functionName} />
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={functionName === $func.name || !functionName} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
|
||||
let arePermsDisabled = true;
|
||||
let permissions: string[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
permissions = $func.execute;
|
||||
});
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
permissions,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $func.execute).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Execute Access</Heading>
|
||||
<p>
|
||||
Choose who can execute this function using the client API. For more information, check
|
||||
out the <a
|
||||
href="https://appwrite.io/docs/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions Guide
|
||||
</a>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputCron } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let functionSchedule: string = null;
|
||||
|
||||
onMount(async () => {
|
||||
functionSchedule ??= $func.schedule;
|
||||
});
|
||||
|
||||
async function updateSchedule() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
functionSchedule,
|
||||
$func.timeout || undefined,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Cron Schedule has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateSchedule);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateSchedule);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateSchedule}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Schedule</Heading>
|
||||
<p>
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://appwrite.io/docs/functions#scheduled-execution"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
More details on Cron syntax here.</a>
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputCron
|
||||
bind:value={functionSchedule}
|
||||
label="Schedule (Cron Syntax)"
|
||||
id="schedule" />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.schedule === functionSchedule} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputNumber } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { func } from '../store';
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let timeout: number = null;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
});
|
||||
|
||||
async function updateTimeout() {
|
||||
try {
|
||||
await sdk.forProject.functions.update(
|
||||
functionId,
|
||||
$func.name,
|
||||
$func.execute || undefined,
|
||||
$func.events || undefined,
|
||||
$func.schedule || undefined,
|
||||
timeout,
|
||||
$func.enabled
|
||||
);
|
||||
await invalidate(Dependencies.FUNCTION);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Timeout has been updated'
|
||||
});
|
||||
trackEvent(Submit.FunctionUpdateTimeout);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FunctionUpdateTimeout);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateTimeout}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Timeout</Heading>
|
||||
<p>Limit the execution time of your function. Maximum value is 900 seconds (15 minutes).</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={900}
|
||||
id="time"
|
||||
label="Time (in seconds)"
|
||||
bind:value={timeout} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={$func.timeout === timeout || timeout < 1} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
CardGrid,
|
||||
Heading,
|
||||
DropList,
|
||||
DropListItem,
|
||||
Empty,
|
||||
Output,
|
||||
PaginationInline,
|
||||
Secret
|
||||
} from '$lib/components';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import UploadVariables from './uploadVariablesModal.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { func } from '../store';
|
||||
|
||||
export let variableList: Models.VariableList;
|
||||
const functionId = $page.params.function;
|
||||
let showVariablesDropdown = [];
|
||||
let selectedVar: Models.Variable = null;
|
||||
let showVariablesUpload = false;
|
||||
let showVariablesModal = false;
|
||||
let offset = 0;
|
||||
|
||||
async function handleVariableCreated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
|
||||
try {
|
||||
await sdk.forProject.functions.createVariable(functionId, variable.key, variable.value);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableCreate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVariableUpdated(event: CustomEvent<Models.Variable>) {
|
||||
const variable = event.detail;
|
||||
try {
|
||||
await sdk.forProject.functions.updateVariable(
|
||||
functionId,
|
||||
variable.$id,
|
||||
variable.key,
|
||||
variable.value
|
||||
);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
selectedVar = null;
|
||||
showVariablesModal = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$func.name} variables have been updated`
|
||||
});
|
||||
trackEvent(Submit.VariableUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableUpdate);
|
||||
}
|
||||
}
|
||||
async function handleVariableDeleted(variable: Models.Variable) {
|
||||
try {
|
||||
await sdk.forProject.functions.deleteVariable(variable.functionId, variable.$id);
|
||||
await invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Variable has been deleted`
|
||||
});
|
||||
trackEvent(Submit.VariableDelete);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.VariableDelete);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadVariables() {
|
||||
if (variableList.total) {
|
||||
let content = variableList.variables
|
||||
.map((variable) => `${variable.key}=${variable.value}`)
|
||||
.join('\n');
|
||||
const file = new File([content], '.env', {
|
||||
type: 'application/x-envoy'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Variables</Heading>
|
||||
<p>Set the variables (or secret keys) that will be passed to your function at runtime.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-margin-inline-start-auto u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
event="download_env"
|
||||
disabled={!variableList.total}
|
||||
on:click={downloadVariables}>
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download .env file</span>
|
||||
</Button>
|
||||
<Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<span class="icon-upload" />
|
||||
<span class="text">Import .env file</span>
|
||||
</Button>
|
||||
</div>
|
||||
{@const limit = 10}
|
||||
{@const sum = variableList.total}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Key</TableCellHead>
|
||||
<TableCellHead width={180}>Value</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each variableList.variables.slice(offset, offset + limit) as variable, i}
|
||||
<TableRow>
|
||||
<TableCell title="key">
|
||||
<Output value={variable.key} hideCopyIcon>
|
||||
{variable.key}
|
||||
</Output>
|
||||
</TableCell>
|
||||
|
||||
<TableCell showOverflow title="value">
|
||||
<Secret copyEvent="variable" value={variable.value} />
|
||||
</TableCell>
|
||||
<TableCell showOverflow title="options">
|
||||
<DropList
|
||||
bind:show={showVariablesDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
text
|
||||
round
|
||||
ariaLabel="more options"
|
||||
on:click={() =>
|
||||
(showVariablesDropdown[i] =
|
||||
!showVariablesDropdown[i])}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
selectedVar = variable;
|
||||
showVariablesDropdown[i] = false;
|
||||
showVariablesModal = true;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
<DropListItem
|
||||
icon="trash"
|
||||
on:click={async () => {
|
||||
handleVariableDeleted(variable);
|
||||
showVariablesDropdown[i] = false;
|
||||
}}>
|
||||
Delete
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button text noMargin on:click={() => (showVariablesModal = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create variable</span>
|
||||
</Button>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total variables: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
|
||||
Create a variable to get started
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
{#if showVariablesModal}
|
||||
<Variable
|
||||
bind:selectedVar
|
||||
bind:showCreate={showVariablesModal}
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
{#if showVariablesUpload}
|
||||
<UploadVariables bind:show={showVariablesUpload} />
|
||||
{/if}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user