mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of github.com:appwrite/console into feat-card-hover-state
This commit is contained in:
Generated
+843
-844
File diff suppressed because it is too large
Load Diff
+22
-21
@@ -23,9 +23,10 @@
|
||||
"@appwrite.io/pink": "^0.0.6-rc.4",
|
||||
"@aw-labs/appwrite-console": "^13.1.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@sentry/svelte": "^7.36.0",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"@sentry/svelte": "^7.44.2",
|
||||
"@sentry/tracing": "^7.44.2",
|
||||
"analytics": "^0.8.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"echarts": "^5.4.1",
|
||||
"logrocket": "^3.0.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
@@ -34,34 +35,34 @@
|
||||
"web-vitals": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.30.0",
|
||||
"@sveltejs/adapter-static": "1.0.6",
|
||||
"@sveltejs/kit": "^1.3.10",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"@testing-library/dom": "^8.20.0",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@sveltejs/adapter-static": "2.0.1",
|
||||
"@sveltejs/kit": "1.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||
"@testing-library/dom": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@vitest/ui": "^0.28.5",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"@vitest/ui": "^0.29.7",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"jsdom": "^21.1.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier": "^2.8.6",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"sass": "^1.58.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.3",
|
||||
"sass": "^1.59.3",
|
||||
"svelte": "^3.57.0",
|
||||
"svelte-check": "^3.1.4",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.1",
|
||||
"vitest": "^0.28.5"
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.2.1",
|
||||
"vitest": "^0.29.7"
|
||||
},
|
||||
"type": "module",
|
||||
"pre-commit": [
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
export let href: string;
|
||||
export let icon: string = null;
|
||||
export let disabled = false;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
let attributes = { disabled } as Record<string, boolean>;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
|
||||
<a {href} class="drop-button" {...attributes}>
|
||||
<a {href} class="drop-button" class:is-disabled={disabled}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
import Copy from './copy.svelte';
|
||||
|
||||
export let value: string;
|
||||
export let hideCopyIcon = false;
|
||||
</script>
|
||||
|
||||
<Copy {value}>
|
||||
<div class="interactive-text-output is-textarea">
|
||||
<div
|
||||
class="interactive-text-output is-textarea"
|
||||
style:min-inline-size="0"
|
||||
style:display="inline-block">
|
||||
<span class="text u-line-height-1-5 u-break-all"><slot /></span>
|
||||
|
||||
<div class="u-flex u-cross-child-start u-gap-8">
|
||||
<button class="interactive-text-output-button" aria-label="copy text">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{#if !hideCopyIcon}
|
||||
<div class="u-flex u-cross-child-start u-gap-8">
|
||||
<button class="interactive-text-output-button" aria-label="copy text">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Copy>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<Row {role} />
|
||||
</TableCell>
|
||||
<TableCellText title="Remove">
|
||||
<div class="u-flex">
|
||||
<div class="u-flex u-main-end">
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
type="button"
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
|
||||
$: internalDisabled = (submit && $isSubmitting) || disabled;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
$: attributes = { internalDisabled } as Record<string, boolean>;
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
return;
|
||||
@@ -40,11 +37,11 @@
|
||||
{#if href}
|
||||
<a
|
||||
on:click={track}
|
||||
{...attributes}
|
||||
{href}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class="button"
|
||||
class:is-disabled={disabled}
|
||||
class:is-only-icon={round}
|
||||
class:is-secondary={secondary}
|
||||
class:is-text={text}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem } from '.';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
|
||||
export let value = '';
|
||||
|
||||
@@ -42,11 +43,7 @@
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<span class="text-counter">
|
||||
<span class="text-counter-count">{value?.length ?? 0}</span>
|
||||
<span class="text-counter-separator" />
|
||||
<span class="text-counter-max">36</span>
|
||||
</span>
|
||||
<TextCounter count={value?.length ?? 0} max={36} />
|
||||
</div>
|
||||
</FormItem>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -58,14 +59,11 @@
|
||||
type="text"
|
||||
class="input-text"
|
||||
bind:value
|
||||
class:u-padding-inline-end-56={typeof maxlength === 'number'}
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if maxlength}
|
||||
<span class="text-counter">
|
||||
<span class="text-counter-count">{value?.length ?? 0}</span>
|
||||
<span class="text-counter-separator" />
|
||||
<span class="text-counter-max">{maxlength}</span>
|
||||
</span>
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -12,6 +13,8 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let maxlength: number = null;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let tooltip: string = null;
|
||||
|
||||
let element: HTMLTextAreaElement;
|
||||
let error: string;
|
||||
@@ -37,7 +40,7 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
@@ -53,6 +56,9 @@
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if maxlength}
|
||||
<TextCounter max={maxlength} count={value?.length ?? 0} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
export let count: number;
|
||||
export let max: number;
|
||||
</script>
|
||||
|
||||
<span class="text-counter" class:active={count > 0}>
|
||||
<span class="text-counter-count">{count}</span>
|
||||
<span class="text-counter-separator" />
|
||||
<span class="text-counter-max">{max}</span>
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
.text-counter {
|
||||
--p-text-counter-color: var(--color-neutral-50);
|
||||
|
||||
&.active {
|
||||
--p-text-counter-color: var(--color-neutral-70);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .text-counter {
|
||||
--p-text-counter-color: var(--color-neutral-70);
|
||||
|
||||
&.active {
|
||||
--p-text-counter-color: var(--color-neutral-50);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,9 +11,6 @@
|
||||
export let href: string = null;
|
||||
export let trim = false;
|
||||
|
||||
//allows to add the disabled attribute to <a> tag without throwing an error
|
||||
let attributes = { disabled } as Record<string, boolean>;
|
||||
|
||||
let element: HTMLButtonElement;
|
||||
|
||||
const isOverflowing = (elem: HTMLButtonElement, iterator = 1) => {
|
||||
@@ -44,7 +41,6 @@
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{...attributes}
|
||||
{href}
|
||||
target={external ? '_blank' : '_self'}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isMac(): boolean {
|
||||
return window.navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import type { EasingFunction, TransitionConfig } from 'svelte/transition';
|
||||
|
||||
export interface SlideParams {
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
easing?: EasingFunction;
|
||||
axis?: 'x' | 'y';
|
||||
}
|
||||
|
||||
export function slide(
|
||||
node: Element,
|
||||
{ delay = 0, duration = 400, easing = cubicOut, axis = 'y' }: SlideParams = {}
|
||||
): TransitionConfig {
|
||||
const style = getComputedStyle(node);
|
||||
const opacity = +style.opacity;
|
||||
const primary_property = axis === 'y' ? 'height' : 'width';
|
||||
const primary_property_value = parseFloat(style[primary_property]);
|
||||
const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right'];
|
||||
const capitalized_secondary_properties = secondary_properties.map(
|
||||
(e) => `${e[0].toUpperCase()}${e.slice(1)}`
|
||||
);
|
||||
const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]);
|
||||
const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]);
|
||||
const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]);
|
||||
const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]);
|
||||
const border_width_start_value = parseFloat(
|
||||
style[`border${capitalized_secondary_properties[0]}Width`]
|
||||
);
|
||||
const border_width_end_value = parseFloat(
|
||||
style[`border${capitalized_secondary_properties[1]}Width`]
|
||||
);
|
||||
return {
|
||||
delay,
|
||||
duration,
|
||||
easing,
|
||||
css: (t) =>
|
||||
'overflow: hidden;' +
|
||||
`opacity: ${Math.min(t * 20, 1) * opacity};` +
|
||||
`${primary_property}: ${t * primary_property_value}px;` +
|
||||
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
|
||||
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
|
||||
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
|
||||
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
|
||||
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
|
||||
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;`
|
||||
};
|
||||
}
|
||||
@@ -5,12 +5,14 @@
|
||||
</script>
|
||||
|
||||
<Heading size="4" tag="h1" trimmed={false}>
|
||||
{#if href}
|
||||
<a class="button is-text is-only-icon" {href} aria-label="page back">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text u-trim-1" data-private>
|
||||
<slot />
|
||||
</span>
|
||||
<div class="u-flex u-cross-center">
|
||||
{#if href}
|
||||
<a class="button is-text is-only-icon" {href} aria-label="page back">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
</a>
|
||||
{/if}
|
||||
<span class="text u-trim-1" data-private>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</Heading>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<ul class="inline-links is-no-padding-first-and-last u-x-small">
|
||||
<li class="inline-links-item">
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
{#if MODE === Mode.CLOUD}
|
||||
{#if MODE !== Mode.CLOUD}
|
||||
<span class="icon-cloud" />
|
||||
{/if}
|
||||
{#if $version}
|
||||
<a
|
||||
class="text"
|
||||
href="https://github.com/appwrite/appwrite/blob/master/CHANGES.md"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Version {$version}
|
||||
@@ -62,7 +62,7 @@
|
||||
margin-block-start: auto;
|
||||
}
|
||||
|
||||
[class^='icon-']:not(:hover) {
|
||||
[class^='icon-']:not(.icon-cloud):not(:hover) {
|
||||
color: hsl(var(--color-neutral-50));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,81 +2,171 @@
|
||||
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';
|
||||
|
||||
$: project = $page.params.project;
|
||||
$: path = `${base}/console/project-${project}`;
|
||||
$: projectPath = `${base}/console/project-${project}`;
|
||||
|
||||
$: subNavigation = $page.data.subNavigation;
|
||||
// We need to have this second variable, because we only want narrow
|
||||
// to change automatically if we change from having a second side nav to
|
||||
// not having one, not when the second side nav changes to a different value.
|
||||
$: hasSubNavigation = !!subNavigation;
|
||||
|
||||
let narrow = false;
|
||||
$: {
|
||||
narrow = hasSubNavigation;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
// If Alt + S is pressed
|
||||
if (hasSubNavigation && event.altKey && event.keyCode === 83) {
|
||||
event.preventDefault();
|
||||
narrow = !narrow;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="side-nav">
|
||||
{#if project}
|
||||
<div class="side-nav-main">
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/overview`)}
|
||||
on:click={() => trackEvent('click_menu_overview')}
|
||||
href={path}>
|
||||
<span class="icon-chart-bar" aria-hidden="true" />
|
||||
<span class="text">Overview</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/auth`)}
|
||||
on:click={() => trackEvent('click_menu_auth')}
|
||||
href={`${path}/auth`}>
|
||||
<span class="icon-user-group" aria-hidden="true" />
|
||||
<span class="text">Auth</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/databases`)}
|
||||
on:click={() => trackEvent('click_menu_databases')}
|
||||
href={`${path}/databases`}>
|
||||
<span class="icon-database" aria-hidden="true" />
|
||||
<span class="text">Databases</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/functions`)}
|
||||
on:click={() => trackEvent('click_menu_functions')}
|
||||
href={`${path}/functions`}>
|
||||
<span class="icon-lightning-bolt" aria-hidden="true" />
|
||||
<span class="text">Functions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
|
||||
on:click={() => trackEvent('click_menu_storage')}
|
||||
href={`${path}/storage`}>
|
||||
<span class="icon-folder" aria-hidden="true" />
|
||||
<span class="text">Storage</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<div class="side-nav-bottom">
|
||||
<section class="drop-section">
|
||||
<a
|
||||
class="drop-button"
|
||||
href={`${path}/settings`}
|
||||
on:click={() => trackEvent('click_menu_settings')}
|
||||
class:is-selected={$page.url.pathname.startsWith(`${path}/settings`)}>
|
||||
<span class="icon-cog" aria-hidden="true" />
|
||||
<span class="text">Settings</span>
|
||||
</a>
|
||||
</section>
|
||||
<div class="side-nav" class:is-open-level-2={hasSubNavigation}>
|
||||
<div class="side-nav-level-1" class:is-narrow={narrow}>
|
||||
{#if project}
|
||||
<div class="side-nav-main">
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/overview`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_overview')}
|
||||
href={projectPath}
|
||||
use:tooltip={{
|
||||
content: 'Overview',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-chart-bar" aria-hidden="true" />
|
||||
<span class="text">Overview</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/auth`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_auth')}
|
||||
href={`${projectPath}/auth`}
|
||||
use:tooltip={{
|
||||
content: 'Auth',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-user-group" aria-hidden="true" />
|
||||
<span class="text">Auth</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/databases`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_databases')}
|
||||
href={`${projectPath}/databases`}
|
||||
use:tooltip={{
|
||||
content: 'Databases',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-database" aria-hidden="true" />
|
||||
<span class="text">Databases</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/functions`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_functions')}
|
||||
href={`${projectPath}/functions`}
|
||||
use:tooltip={{
|
||||
content: 'Functions',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-lightning-bolt" aria-hidden="true" />
|
||||
<span class="text">Functions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/storage`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_storage')}
|
||||
href={`${projectPath}/storage`}
|
||||
use:tooltip={{
|
||||
content: 'Storage',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-folder" aria-hidden="true" />
|
||||
<span class="text">Storage</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="side-nav-bottom">
|
||||
<section class="drop-section">
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if subNavigation}
|
||||
<button
|
||||
class="side-nav-button button is-small is-secondary is-not-mobile"
|
||||
aria-label="resize menu"
|
||||
on:click={() => (narrow = !narrow)}
|
||||
use:tooltip={{
|
||||
content: isMac() ? '⌥ + S' : 'Alt + S'
|
||||
}}>
|
||||
<span
|
||||
class:icon-cheveron-right={narrow}
|
||||
class:icon-cheveron-left={!narrow}
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if subNavigation}
|
||||
<div class="side-nav-level-2 is-open" transition:slide={{ axis: 'x', duration: 250 }}>
|
||||
<div class="side-nav-main">
|
||||
<svelte:component this={subNavigation} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import type { Notification } from '../stores/notifications';
|
||||
|
||||
export let type: Notification['type'] = 'info';
|
||||
@@ -9,12 +10,13 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<li
|
||||
<div
|
||||
class="alert-sticky"
|
||||
class:is-success={type === 'success'}
|
||||
class:is-warning={type === 'warning'}
|
||||
class:is-danger={type === 'error'}
|
||||
class:is-info={type === 'info'}>
|
||||
class:is-info={type === 'info'}
|
||||
transition:fly={{ x: 50 }}>
|
||||
<button
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
@@ -45,4 +47,4 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script lang="ts">
|
||||
import Notification from './notification.svelte';
|
||||
import { dismissNotification, notifications } from '../stores/notifications';
|
||||
import { flip } from 'svelte/animate';
|
||||
</script>
|
||||
|
||||
{#if $notifications}
|
||||
<section>
|
||||
<ul class="u-flex u-flex-vertical u-gap-16">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<Notification
|
||||
type={notification.type}
|
||||
title={notification.title}
|
||||
on:dismiss={() => dismissNotification(notification.id)}
|
||||
buttons={notification?.buttons}>
|
||||
{notification.message}
|
||||
</Notification>
|
||||
<li animate:flip={{ duration: 500 }}>
|
||||
<Notification
|
||||
type={notification.type}
|
||||
title={notification.title}
|
||||
on:dismiss={() => dismissNotification(notification.id)}
|
||||
buttons={notification?.buttons}>
|
||||
{notification.message}
|
||||
</Notification>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -22,8 +25,15 @@
|
||||
<style lang="scss">
|
||||
section {
|
||||
position: fixed;
|
||||
top: calc(var(--main-header-height) + 24px);
|
||||
right: 24px;
|
||||
top: calc(var(--main-header-height) + 12px);
|
||||
right: 12px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
section {
|
||||
top: calc(var(--main-header-height) + 24px);
|
||||
right: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
top: 4.5rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1199px) {
|
||||
.grid-with-side {
|
||||
grid-template-columns: auto 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -74,7 +74,7 @@ function createFeedbackStore() {
|
||||
customFields: value ? [{ id: '40655', value }] : undefined
|
||||
})
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
if (response.status >= 400) {
|
||||
throw new Error('Failed to submit feedback');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,7 @@ export const addNotification = (notification: Omit<Notification, 'id'>) => {
|
||||
|
||||
const n = { ...defaults, ...notification };
|
||||
notifications.update((all) => {
|
||||
if (all.length < 3) {
|
||||
return [n, ...all];
|
||||
} else {
|
||||
return [n, ...all.slice(0, 2)];
|
||||
}
|
||||
return [n, ...all.slice(0, 4)];
|
||||
});
|
||||
|
||||
if (n.timeout) setTimeout(() => dismissNotification(n.id), n.timeout);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import CreateCollection from './createCollection.svelte';
|
||||
import { showCreate } from './store';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
const project = $page.params.project;
|
||||
const databaseId = $page.params.database;
|
||||
|
||||
async function handleCreate(event: CustomEvent<Models.Collection>) {
|
||||
$showCreate = false;
|
||||
await invalidate(Dependencies.DATABASE);
|
||||
await goto(
|
||||
`${base}/console/project-${project}/databases/database-${databaseId}/collection-${event.detail.$id}`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Database - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
|
||||
<CreateCollection bind:showCreate={$showCreate} on:created={handleCreate} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.DATABASE);
|
||||
|
||||
+10
-22
@@ -1,38 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Empty, Copy, GridItem1, CardContainer, Heading, Pagination } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Container } from '$lib/layout';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { CardContainer, Copy, Empty, GridItem1, Heading, Pagination } from '$lib/components';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { cardLimit } from '$lib/stores/layout';
|
||||
import { createPersistentPagination } from '$lib/stores/pagination';
|
||||
import { showCreate } from '../store';
|
||||
import type { PageData } from './$types';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import Create from '../create.svelte';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
|
||||
export let data: PageData;
|
||||
let showCreate = false;
|
||||
|
||||
const project = $page.params.project;
|
||||
const databaseId = $page.params.database;
|
||||
const offset = createPersistentPagination($cardLimit);
|
||||
|
||||
async function handleCreate(event: CustomEvent<Models.Collection>) {
|
||||
showCreate = false;
|
||||
await goto(
|
||||
`${base}/console/project-${project}/databases/database-${databaseId}/collection-${event.detail.$id}`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Heading tag="h2" size="5">Collections</Heading>
|
||||
|
||||
<Button on:click={() => (showCreate = true)} event="create_collection">
|
||||
<Button on:click={() => ($showCreate = true)} event="create_collection">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</Button>
|
||||
@@ -43,7 +33,7 @@
|
||||
event="collection"
|
||||
total={data.collections.total}
|
||||
offset={$offset}
|
||||
on:click={() => (showCreate = true)}>
|
||||
on:click={() => ($showCreate = true)}>
|
||||
{#each data.collections.collections as collection}
|
||||
<GridItem1
|
||||
href={`${base}/console/project-${project}/databases/database-${databaseId}/collection-${collection.$id}`}>
|
||||
@@ -76,8 +66,6 @@
|
||||
single
|
||||
href="https://appwrite.io/docs/databases#collection"
|
||||
target="collection"
|
||||
on:click={() => (showCreate = true)} />
|
||||
on:click={() => ($showCreate = true)} />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Create bind:showCreate on:created={handleCreate} />
|
||||
|
||||
+7
-1
@@ -4,6 +4,8 @@ import type { LayoutLoad } from './$types';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import SubNavigation from './subNavigation.svelte';
|
||||
import { Query } from '@aw-labs/appwrite-console';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.COLLECTION);
|
||||
@@ -14,7 +16,11 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
collection: await sdk.forProject.databases.getCollection(
|
||||
params.database,
|
||||
params.collection
|
||||
)
|
||||
),
|
||||
subNavigation: SubNavigation,
|
||||
allCollections: await sdk.forProject.databases.listCollections(params.database, [
|
||||
Query.orderDesc('$createdAt')
|
||||
])
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(e.code, e.message);
|
||||
|
||||
+2
-2
@@ -102,9 +102,9 @@
|
||||
href={`${base}/console/project-${$page.params.project}/databases/database-${$page.params.database}/collection-${$collection.$id}/document-${document.$id}`}>
|
||||
<TableCell width={230}>
|
||||
<Copy value={document.$id}>
|
||||
<Pill button>
|
||||
<Pill button trim>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
<span class="text u-trim-start">{document.$id}</span>
|
||||
<span class="text">{document.$id}</span>
|
||||
</Pill>
|
||||
</Copy>
|
||||
</TableCell>
|
||||
|
||||
+18
-7
@@ -22,6 +22,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { InputNumber, InputText, InputChoice } from '$lib/elements/forms';
|
||||
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
|
||||
|
||||
export let selectedAttribute: Models.AttributeString;
|
||||
export let data: Partial<Models.AttributeString> = {
|
||||
@@ -50,13 +51,23 @@
|
||||
bind:value={data.size}
|
||||
required
|
||||
readonly={!!selectedAttribute} />
|
||||
<InputText
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
maxlength={data.size}
|
||||
disabled={data.required || data.array}
|
||||
readonly={!!selectedAttribute} />
|
||||
{#if data.size > 64}
|
||||
<InputTextarea
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
maxlength={data.size}
|
||||
disabled={data.required || data.array}
|
||||
readonly={!!selectedAttribute} />
|
||||
{:else}
|
||||
<InputText
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
maxlength={data.size}
|
||||
disabled={data.required || data.array}
|
||||
readonly={!!selectedAttribute} />
|
||||
{/if}
|
||||
<InputChoice
|
||||
id="required"
|
||||
label="Required"
|
||||
|
||||
+22
-9
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import InputText from '$lib/elements/forms/inputText.svelte';
|
||||
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
export let id: string;
|
||||
@@ -9,12 +10,24 @@
|
||||
export let optionalText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
placeholder="Enter string"
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
bind:value />
|
||||
{#if attribute.size > 64}
|
||||
<InputTextarea
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
placeholder="Enter string"
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
bind:value />
|
||||
{:else}
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
placeholder="Enter string"
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
bind:value />
|
||||
{/if}
|
||||
|
||||
+5
-5
@@ -6,11 +6,11 @@
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { collection } from './store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const databaseId = $page.params.database;
|
||||
const collectionId = $page.params.collection;
|
||||
const path = `/console/project-${projectId}/databases/database-${databaseId}/collection-${collectionId}`;
|
||||
const tabs = [
|
||||
$: projectId = $page.params.project;
|
||||
$: databaseId = $page.params.database;
|
||||
$: collectionId = $page.params.collection;
|
||||
$: path = `/console/project-${projectId}/databases/database-${databaseId}/collection-${collectionId}`;
|
||||
$: tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Documents',
|
||||
|
||||
+1
@@ -133,6 +133,7 @@
|
||||
|
||||
<div class="form-item-part u-cross-child-end">
|
||||
<Button
|
||||
noMargin
|
||||
text
|
||||
disabled={attributeList.length <= 1}
|
||||
on:click={() => {
|
||||
|
||||
+3
-1
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -25,6 +26,7 @@
|
||||
await goto(
|
||||
`${base}/console/project-${$page.params.project}/databases/database-${$page.params.database}`
|
||||
);
|
||||
invalidate(Dependencies.DATABASE);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { showCreate } from '../store';
|
||||
import type { PageData } from './[[page]]/$types';
|
||||
|
||||
$: data = $page.data as PageData;
|
||||
$: project = $page.params.project;
|
||||
$: databaseId = $page.params.database;
|
||||
$: collectionId = $page.params.collection;
|
||||
|
||||
$: sortedCollections = data?.allCollections?.collections?.sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
</script>
|
||||
|
||||
<section class="drop-section u-flex-vertical u-gap-8">
|
||||
<a
|
||||
class="u-flex u-cross-center u-sep-block-end u-padding-block-12 is-not-desktop"
|
||||
href={`${base}/console/project-${project}/databases/database-${databaseId}`}>
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<h5 class="eyebrow-heading-3 u-margin-inline-auto">Collections</h5>
|
||||
</a>
|
||||
<h5 class="eyebrow-heading-3 u-padding-block-12 is-not-mobile">Collections</h5>
|
||||
<button
|
||||
class="button is-text is-full-width u-main-start u-padding-inline-0"
|
||||
on:click={() => ($showCreate = true)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create collection</span>
|
||||
</button>
|
||||
{#if data?.allCollections?.total}
|
||||
<ul class="drop-list">
|
||||
{#each sortedCollections as collection}
|
||||
{@const href = `${base}/console/project-${project}/databases/database-${databaseId}/collection-${collection.$id}/documents`}
|
||||
{@const isSelected = collectionId === collection.$id}
|
||||
<li class="drop-list-item">
|
||||
<a class="drop-button" class:is-selected={isSelected} {href}>
|
||||
<span class="text">{collection.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -1,5 +1,6 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const database = derived(page, ($page) => $page.data.database as Models.Database);
|
||||
export const showCreate = writable(false);
|
||||
|
||||
+32
-17
@@ -3,7 +3,16 @@
|
||||
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, Secret } from '$lib/components';
|
||||
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';
|
||||
@@ -18,7 +27,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Variable from '../../createVariable.svelte';
|
||||
import { execute, func } from '../store';
|
||||
// import Upload from './uploadVariables.svelte';
|
||||
import UploadVariables from './uploadVariables.svelte';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -36,7 +45,7 @@
|
||||
const functionId = $page.params.function;
|
||||
let showDelete = false;
|
||||
let selectedVar: Models.Variable = null;
|
||||
// let showVariablesUpload = false;
|
||||
let showVariablesUpload = false;
|
||||
let showVariablesModal = false;
|
||||
let showVariablesDropdown = [];
|
||||
let timeout: number = null;
|
||||
@@ -44,6 +53,7 @@
|
||||
let functionSchedule: string = null;
|
||||
let permissions: string[] = [];
|
||||
let arePermsDisabled = true;
|
||||
let offset = 0;
|
||||
|
||||
onMount(async () => {
|
||||
timeout ??= $func.timeout;
|
||||
@@ -369,24 +379,26 @@
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download .env file</span>
|
||||
</Button>
|
||||
<!-- <Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<Button secondary on:click={() => (showVariablesUpload = true)}>
|
||||
<span class="icon-upload" />
|
||||
<span class="text">Import .env file</span>
|
||||
</Button> -->
|
||||
</Button>
|
||||
</div>
|
||||
{#if data.variables.total}
|
||||
{@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>Value</TableCellHead>
|
||||
<TableCellHead width={180}>Value</TableCellHead>
|
||||
<TableCellHead width={30} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.variables.variables as variable, i}
|
||||
{#each data.variables.variables.slice(offset, offset + limit) as variable, i}
|
||||
<TableRow>
|
||||
<TableCell title="key">
|
||||
<Output value={variable.key}>
|
||||
<Output value={variable.key} hideCopyIcon>
|
||||
{variable.key}
|
||||
</Output>
|
||||
</TableCell>
|
||||
@@ -435,19 +447,19 @@
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showVariablesModal = true;
|
||||
}}>
|
||||
<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>
|
||||
<Empty on:click={() => (showVariablesModal = !showVariablesModal)}>
|
||||
Create a variable to get started
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -505,3 +517,6 @@
|
||||
on:created={handleVariableCreated}
|
||||
on:updated={handleVariableUpdated} />
|
||||
{/if}
|
||||
{#if showVariablesUpload}
|
||||
<UploadVariables bind:show={showVariablesUpload} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
|
||||
export const variables = derived(
|
||||
page,
|
||||
($page) => $page.data.variables.variables as Models.Variable[]
|
||||
);
|
||||
+48
-43
@@ -6,69 +6,74 @@
|
||||
import { Button, InputFile } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { parse } from 'dotenv';
|
||||
import { variables } from './store';
|
||||
|
||||
export let show = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const functionId = $page.params.function;
|
||||
let files: FileList;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (files?.length) {
|
||||
const variables = await parseFile(files[0]);
|
||||
for (const variable of variables) {
|
||||
try {
|
||||
await sdk.forProject.functions.createVariable(
|
||||
functionId,
|
||||
variable.key,
|
||||
variable.value
|
||||
);
|
||||
invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Variable uploaded'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
let files: FileList;
|
||||
let error: string;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (!files?.length) {
|
||||
throw new Error('No file selected');
|
||||
}
|
||||
|
||||
const uploaded = parse(await files[0].text());
|
||||
|
||||
if (!Object.keys(uploaded).length) {
|
||||
throw new Error('No variables found');
|
||||
}
|
||||
|
||||
const entries = Object.entries(uploaded);
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
if (value.length > 8192) {
|
||||
throw new Error(`Variable ${key} is longer than 8192 allowed characters`);
|
||||
}
|
||||
}
|
||||
dispatch('uploaded', variables);
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function parseFile(file: File) {
|
||||
if (file) {
|
||||
let variables = [];
|
||||
let text = await file.text();
|
||||
text.split('\n').forEach((line) => {
|
||||
const [key, value] = line.split('=');
|
||||
variables.push({ key, value });
|
||||
await Promise.all(
|
||||
entries
|
||||
.filter(([, value]) => !!value)
|
||||
.map(([key, value]) => {
|
||||
const found = $variables.find((variable) => variable.key === key);
|
||||
return found
|
||||
? sdk.forProject.functions.updateVariable(
|
||||
functionId,
|
||||
found.$id,
|
||||
key,
|
||||
value
|
||||
)
|
||||
: sdk.forProject.functions.createVariable(functionId, key, value);
|
||||
})
|
||||
);
|
||||
|
||||
invalidate(Dependencies.VARIABLES);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Variables uploaded'
|
||||
});
|
||||
return variables;
|
||||
show = false;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show onSubmit={handleSubmit}>
|
||||
<Modal bind:show onSubmit={handleSubmit} bind:error>
|
||||
<svelte:fragment slot="header">Upload Variables</svelte:fragment>
|
||||
<p>
|
||||
Upload multiple variables via a .env file that will be passed to your function at runtime.
|
||||
</p>
|
||||
|
||||
<InputFile bind:files label="Attach a file" />
|
||||
<InputFile bind:files />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Create</Button>
|
||||
<Button submit>Upload</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user