Merge branch 'main' of github.com:appwrite/appwrite-console-poc into feat-tag-trim-overflow

This commit is contained in:
Arman
2022-11-11 12:34:45 +01:00
510 changed files with 13390 additions and 9774 deletions
+1 -1
View File
@@ -147,7 +147,7 @@ Adding a new dependency should have vital value on the product with minimum poss
│ │ │ │ ├── _create.svelte // Component to Create collections
│ │ │ │ └── index.svelte // Entrypoint for "/console/[PROJECT_ID]/database"
│ │ │ ├── storage // Storage Service "/console/[PROJECT]/storage"
│ │ │ └── authentication // Users Service "/console/[PROJECT]/authentication"
│ │ │ └── auth // Users Service "/console/[PROJECT]/auth"
│ │ └──...
│ ├── login.svelte // Component for Login "/console/login"
│ └── register.svelte // Component for Register "/console/register"
+239 -707
View File
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -8,6 +8,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"sync": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint .",
@@ -18,30 +19,31 @@
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
"@aw-labs/icons": "0.0.0-64",
"@aw-labs/ui": "0.0.0-64",
"@aw-labs/appwrite-console": "^10.0.0",
"@aw-labs/icons": "0.0.0-74",
"@aw-labs/ui": "0.0.0-74",
"@popperjs/core": "^2.11.6",
"echarts": "^5.4.0",
"pretty-bytes": "^6.0.0",
"prismjs": "^1.29.0",
"tippy.js": "^6.3.7",
"web-vitals": "^3.0.3"
},
"devDependencies": {
"@playwright/test": "^1.27.1",
"@sveltejs/adapter-static": "1.0.0-next.44",
"@sveltejs/kit": "1.0.0-next.516",
"@sveltejs/vite-plugin-svelte": "^1.0.9",
"@sveltejs/adapter-static": "1.0.0-next.46",
"@sveltejs/kit": "1.0.0-next.525",
"@sveltejs/vite-plugin-svelte": "^1.1.0",
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "3.1.3",
"@testing-library/svelte": "^3.2.2",
"@testing-library/user-event": "^14.4.3",
"@types/gtag.js": "^0.0.12",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"@vitest/ui": "^0.24.3",
"eslint": "^8.25.0",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"pre-commit": "^1.2.2",
@@ -54,7 +56,7 @@
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"vite": "^3.1.8",
"vite": "^3.2.1",
"vitest": "^0.24.3"
},
"type": "module",
+6 -4
View File
@@ -3,21 +3,23 @@ import type { Action } from 'svelte/action';
export type AnalyticsActionParam = {
name: string;
action: string;
category?: string;
label?: string;
parameters?: Record<string, string>;
event?: keyof HTMLElementEventMap;
};
export const event: Action<HTMLElement, AnalyticsActionParam> = (node, param) => {
export const event: Action<HTMLElement, Partial<AnalyticsActionParam>> = (node, param) => {
if (!isTrackingAllowed()) {
return;
}
node.addEventListener(param.event ?? 'click', () =>
node.addEventListener(param.event ?? 'click', () => {
gtag('event', param.name, {
...param.parameters,
action: param.action
})
);
});
});
};
const isTrackingAllowed = () => {
+3
View File
@@ -1,11 +1,14 @@
<script lang="ts">
import type { EChartsOption } from 'echarts';
import type { BarSeriesOption } from 'echarts/charts';
import Base from './base.svelte';
export let series: BarSeriesOption[];
export let options: EChartsOption = null;
</script>
<Base
{options}
series={series.map((s) => {
s.type = 'bar';
s.stack = 'total';
+1 -1
View File
@@ -6,7 +6,7 @@
"borderWidth": 1
},
"lineStyle": {
"width": 2
"width": 1
},
"symbolSize": 4,
"symbol": "emptyCircle",
+6 -2
View File
@@ -8,10 +8,12 @@
import dark from './dark.json';
import type { ECharts } from 'echarts/core';
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
import type { EChartsOption } from 'echarts';
registerTheme('light', { ...base, ...light });
registerTheme('dark', { ...base, ...dark });
export let options: EChartsOption;
export let series: (BarSeriesOption | LineSeriesOption)[];
let chart: ECharts;
@@ -19,6 +21,7 @@
$: option = {
...defaultConfig,
...options,
series
};
@@ -39,7 +42,7 @@
(await import('echarts/components')).TransformComponent,
(await import('echarts/components')).LegendComponent,
(await import('echarts/features')).LabelLayout,
(await import('echarts/renderers')).CanvasRenderer
(await import('echarts/renderers')).SVGRenderer
]);
makeChart();
});
@@ -85,6 +88,7 @@
<style>
.echart {
width: 100%;
min-height: 12rem;
min-height: 10rem;
height: 100%;
}
</style>
+8 -3
View File
@@ -17,8 +17,8 @@ export const defaultConfig: EChartsOption = {
containLabel: true,
left: 0,
right: 0,
bottom: 15,
top: 15
bottom: 0,
top: 45
},
xAxis: {
type: 'time',
@@ -27,6 +27,11 @@ export const defaultConfig: EChartsOption = {
yAxis: {
type: 'value',
splitNumber: 3,
minInterval: 1
minInterval: 1,
splitLine: {
lineStyle: {
width: 1
}
}
}
};
+2 -2
View File
@@ -45,11 +45,11 @@
}
},
"axisLabel": {
"color": "#666666"
"color": "#868EA3"
},
"splitLine": {
"lineStyle": {
"color": ["#333333"]
"color": ["#4F5769"]
}
},
"splitArea": {
+1 -1
View File
@@ -34,7 +34,7 @@
},
"splitLine": {
"lineStyle": {
"color": ["#E0E6F1"]
"color": ["#E8E9F0"]
}
}
},
+10
View File
@@ -1,14 +1,24 @@
<script lang="ts">
import type { LineSeriesOption } from 'echarts/charts';
import type { EChartsOption } from 'echarts';
import { Colors } from './config';
import Base from './base.svelte';
export let series: LineSeriesOption[];
export let options: EChartsOption = null;
</script>
<Base
{options}
series={series.map((s) => {
s.type = 'line';
s.stack = 'total';
s.lineStyle = {
shadowBlur: 38,
shadowColor: Colors.Primary,
shadowOffsetY: 15,
shadowOffsetX: 0
};
s.showSymbol = false;
return s;
-8
View File
@@ -1,8 +0,0 @@
<script lang="ts">
export let href: string;
</script>
<a {href}>
<span class="icon-cheveron-left" aria-hidden="true" />
<slot />
</a>
+27 -2
View File
@@ -1,7 +1,32 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
export let isTile = false;
export let isDashed = false;
export let isButton = false;
export let danger = false;
export let href: string = null;
function getElement(): string {
switch (true) {
case !!href:
return 'a';
case isButton:
return 'button';
default:
return 'article';
}
}
</script>
<article class="card" class:common-section={!isTile}>
<svelte:element
this={getElement()}
class="card"
class:common-section={!isTile}
class:is-border-dashed={isDashed}
class:is-danger={danger}
on:click
on:keyup={clickOnEnter}
{href}>
<slot />
</article>
</svelte:element>
+2 -2
View File
@@ -12,8 +12,8 @@
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}>
<slot />
{#if limit + offset > total && (total % 2 !== 0 || total % 4 === 0)}
<Empty isButton on:click>
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
<Empty on:click>
<slot name="empty" />
</Empty>
{/if}
+3 -1
View File
@@ -1,8 +1,10 @@
<script>
import { Card } from './';
export let danger = false;
</script>
<Card>
<Card {danger}>
<div class="common-section grid-1-2">
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<slot />
+4 -5
View File
@@ -10,7 +10,6 @@
import 'prismjs/plugins/autoloader/prism-autoloader';
import 'prismjs/plugins/custom-class/prism-custom-class';
import 'prismjs/plugins/line-numbers/prism-line-numbers';
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
import { afterUpdate } from 'svelte';
import { Copy } from '.';
@@ -49,16 +48,16 @@
{/if}
</div>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}>
<code>{code}</code>
</pre>
<pre class={`language-${language}`} class:line-numbers={withLineNumbers}><code
>{code}</code></pre>
</section>
<style lang="scss" global>
@import 'prismjs/themes/prism.css';
@import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
.box {
--p-box-background-color: var(--color-neutral-300) !important;
--p-box-background-color: var(--color-neutral-400) !important;
body.theme-light & {
--p-box-background-color: var(--color-neutral-5) !important;
+13 -3
View File
@@ -1,13 +1,23 @@
<script lang="ts">
export let withIndentation = false;
</script>
<li class="collapsible-item">
<details class="collapsible-wrapper">
<summary class="collapsible-button">
<span class="text"><slot name="title" /></span>
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
<slot name="beforetitle" />
<div>
<span class="text"><slot name="title" /></span>
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
</div>
<div class="icon">
<span class="icon-cheveron-down" aria-hidden="true" />
</div>
</summary>
<div class="collapsible-content">
<div
class="collapsible-content"
class:u-margin-block-start-8={withIndentation}
class:u-padding-inline-32={withIndentation}>
<slot />
</div>
</details>
+16 -14
View File
@@ -22,21 +22,23 @@
};
</script>
<div class="input-text-wrapper is-with-end-button">
<div>
<label class:u-hide={!showLabel} class="label" for={label}>{label}</label>
<div class="input-text-wrapper">
<div class="input-text-wrapper" style="--amount-of-buttons:1">
<input {value} id={label} type="text" class="input-text" readonly />
<button
type="button"
class="input-button"
aria-label="Click to copy."
on:click={copy}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
content,
hideOnClick: false
}}>
<span class="icon-duplicate" aria-hidden="true" />
</button>
<div class="options-list">
<button
type="button"
class="input-button"
aria-label="Click to copy."
on:click={copy}
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
use:tooltip={{
content,
hideOnClick: false
}}>
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
</div>
</div>
+46 -43
View File
@@ -11,39 +11,23 @@
type Placement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
let arrow: HTMLDivElement;
let instance: Instance;
let currentArrow: Placement;
$: [arrowHorizontal, arrowPosition] = applyArrow(currentArrow);
function applyArrow(value: Placement): [string: 'start' | 'end', string: 'top' | 'bottom'] {
switch (value) {
case 'bottom-start':
return ['start', 'bottom'];
case 'bottom-end':
return ['end', 'bottom'];
case 'top-start':
return ['start', 'top'];
case 'top-end':
return ['end', 'top'];
default:
return ['start', 'bottom'];
}
}
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
onFirstUpdate(state) {
if (currentArrow !== state.placement) {
currentArrow = state.placement as Placement;
}
},
modifiers: [
{
name: 'arrow',
options: {
element: arrow
}
},
{
name: 'offset',
options: {
offset: [0, noArrow ? 0 : 12]
offset: [0, noArrow ? 0 : 6]
}
},
{
@@ -51,16 +35,6 @@
options: {
fallbackPlacements: ['top-start', 'top-end', 'bottom-start', 'bottom-end']
}
},
{
name: 'placementLogger',
enabled: true,
phase: 'main',
fn({ state }) {
if (currentArrow !== state.placement) {
currentArrow = state.placement as Placement;
}
}
}
]
});
@@ -87,16 +61,10 @@
<slot />
</div>
<div bind:this={tooltip} style="z-index: 10">
<div class="drop-tooltip" bind:this={tooltip} style="z-index: 10">
<div class="drop-arrow" class:u-hide={!show} bind:this={arrow} />
{#if show}
<div
class="drop"
style="position: revert"
class:is-no-arrow={noArrow}
class:is-arrow-start={arrowHorizontal === 'start'}
class:is-arrow-end={arrowHorizontal === 'end'}
class:is-block-start={arrowPosition === 'top'}
class:is-block-end={arrowPosition === 'bottom'}>
<div class="drop is-no-arrow" style="position: revert">
<section
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
@@ -109,3 +77,38 @@
</div>
{/if}
</div>
<style global lang="scss">
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
bottom: -4px;
}
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
top: -4px;
}
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
right: -4px;
}
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
left: -4px;
}
.drop-arrow,
.drop-arrow::before {
position: absolute;
width: 8px;
height: 8px;
z-index: -1;
}
.drop-arrow::before {
content: '';
transform: rotate(45deg);
background: hsl(var(--color-neutral-200));
body.theme-light & {
background: hsl(var(--color-neutral-10));
}
}
</style>
+2 -1
View File
@@ -3,10 +3,11 @@
export let href: string;
export let icon: string = null;
export let disabled = false;
</script>
<li class="drop-list-item" on:click on:keyup={clickOnEnter}>
<a {href} class="drop-button">
<a {href} class="drop-button" {disabled}>
<span class="text"><slot /></span>
{#if icon}
<span class={`icon-${icon}`} aria-hidden="true" />
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
export let href: string;
export let disabled = false;
</script>
<li class="drop-tabs-item">
{#if disabled}
<button class="drop-tabs-button" disabled>
<span class="text"><slot /></span>
</button>
{:else}
<a class="drop-tabs-button" {href}>
<span class="text"><slot /></span>
</a>
{/if}
</li>
+33 -41
View File
@@ -3,52 +3,44 @@
import { Button } from '$lib/elements/forms';
import EmptyLight from '$lib/images/empty-light.svg';
import EmptyDark from '$lib/images/empty-dark.svg';
import { Heading } from '.';
export let isButton = false;
export let single = false;
export let target: string = null;
export let href: string = null;
</script>
{#if isButton}
{#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">
<button type="button" on:click|preventDefault>
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" />
{/if}
</button>
<slot>
<div class="u-text-center">
<Heading size="7" tag="h4">Create your first {target} to get started.</Heading>
<p class="body-text-2 u-margin-block-start-4">
Need a hand? Check out our documentation.
</p>
</div>
<div class="u-flex u-gap-16 u-main-center">
<Button external {href} text>Documentation</Button>
<Button secondary on:click>Create {target}</Button>
</div>
</slot>
</div>
</article>
{:else}
<button
on:click|preventDefault
type="button"
class="card u-grid u-cross-center u-width-full-line dashed"
class:common-section={single}>
{#if single}
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" />
{/if}
<slot />
</div>
{:else}
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<Button secondary round>
<i class="icon-plus" />
</Button>
</div>
<div class="common-section">
<slot />
</div>
</div>
{/if}
</button>
{:else}
<article class="card u-grid u-cross-center u-width-full-line" class:common-section={single}>
{#if single}
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" />
{/if}
<slot />
</div>
{:else}
<div class="common-section u-main-center u-flex">
class="card u-grid u-cross-center u-width-full-line dashed">
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<Button secondary round>
<i class="icon-plus" />
</Button>
@@ -56,6 +48,6 @@
<div class="common-section">
<slot />
</div>
{/if}
</article>
</div>
</button>
{/if}
+4 -2
View File
@@ -2,7 +2,9 @@
import { app } from '$lib/stores/app';
import Light from '$lib/images/search-light.svg';
import Dark from '$lib/images/search-dark.svg';
import Pagination from './pagination.svelte';
import PaginationInline from './paginationInline.svelte';
export let hidePages = false;
</script>
<article class="card u-grid u-cross-center u-width-full-line common-section">
@@ -18,5 +20,5 @@
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: 0</p>
<Pagination limit={1} offset={0} sum={0} />
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
+128 -133
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Form } from '$lib/elements/forms';
import { Button } from '$lib/elements/forms';
import { Modal, Copy } from '$lib/components';
import { Pill } from '$lib/elements';
import { createEventDispatcher } from 'svelte';
@@ -157,152 +157,147 @@
//TODO: remove inline style
</script>
<Form noStyle noMargin on:submit={create}>
<Modal bind:show size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<Modal bind:show on:submit={create} size="big">
<svelte:fragment slot="header">Create Event</svelte:fragment>
<div>
<p class="u-text">Choose a service</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each services as service}
<Pill
disabled={showInput}
selected={service.name === selectedService?.name}
button
on:click={() => {
selectedService = service;
inputData = null;
}}>{service.name}</Pill>
{/each}
</div>
</div>
{#if selectedService}
<div>
<p class="u-text">Choose a service</p>
<p class="u-text">Choose a request (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each services as service}
{#each selectedService.requests as request}
<Pill
disabled={showInput}
selected={service.name === selectedService?.name}
selected={request.name === selectedRequest?.name}
button
on:click={() => {
selectedService = service;
selectedRequest === request
? (selectedRequest = null)
: (selectedRequest = request);
inputData = null;
}}>{service.name}</Pill>
}}>{request.name}</Pill>
{/each}
</div>
</div>
{#if selectedService}
<div>
<p class="u-text">Choose a request (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedService.requests as request}
<Pill
disabled={showInput}
selected={request.name === selectedRequest?.name}
button
on:click={() => {
selectedRequest === request
? (selectedRequest = null)
: (selectedRequest = request);
inputData = null;
}}>{request.name}</Pill>
{/each}
</div>
</div>
<div>
<p class="u-text">Choose an event (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedRequest?.events ?? selectedService?.events as event}
<Pill
disabled={showInput}
selected={selectedEvent === event}
button
on:click={() => {
selectedEvent === event
? (selectedEvent = null)
: (selectedEvent = event);
inputData = null;
}}>{event}</Pill>
{/each}
</div>
</div>
{#if selectedService?.name === 'users' && !selectedRequest?.name}
<div>
<p class="u-text">Choose an attribute (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedService?.attributes as attribute}
<Pill
disabled={showInput}
selected={selectedAttribute === attribute}
button
on:click={() => {
selectedAttribute === attribute
? (selectedAttribute = null)
: (selectedAttribute = attribute);
inputData = null;
}}>{attribute}</Pill>
{/each}
</div>
</div>
{/if}
{/if}
{#if showInput}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
<div class="options-list">
<button
on:click|preventDefault={() => {
showInput = false;
}}
class="options-list-button"
aria-label="confirm">
<span class="icon-check" aria-hidden="true" />
</button>
<button
on:click|preventDefault={() => {
<div>
<p class="u-text">Choose an event (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedRequest?.events ?? selectedService?.events as event}
<Pill
disabled={showInput}
selected={selectedEvent === event}
button
on:click={() => {
selectedEvent === event
? (selectedEvent = null)
: (selectedEvent = event);
inputData = null;
showInput = false;
}}
class="options-list-button"
aria-label="cancel">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
}}>{event}</Pill>
{/each}
</div>
{:else}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<div type="text" readonly style="min-height: 2.5rem;">
{#if inputData}
<span>{inputData}</span>
{:else}
{#each Array.from(eventString.values()) as route, i}
<button
class:u-opacity-0-5={helper !== route.description}
on:mouseenter={() => {
helper = route.description;
}}
on:mouseleave={() => {
helper = null;
}}>
{route.value}
</button>
<span class="u-opacity-0-5">
{i + 1 < eventString?.size ? '.' : ''}
</span>
{/each}
{/if}
</div>
{#if selectedService?.name === 'users' && !selectedRequest?.name}
<div>
<p class="u-text">Choose an attribute (optional)</p>
<div class="u-flex u-gap-8 u-margin-block-start-8">
{#each selectedService?.attributes as attribute}
<Pill
disabled={showInput}
selected={selectedAttribute === attribute}
button
on:click={() => {
selectedAttribute === attribute
? (selectedAttribute = null)
: (selectedAttribute = attribute);
inputData = null;
}}>{attribute}</Pill>
{/each}
</div>
<div class="options-list">
<button
on:click|preventDefault={() => {
inputData = copyValue;
showInput = true;
}}
class="options-list-button"
aria-label="edit event">
<span class="icon-pencil" aria-hidden="true" />
</button>
<button
disabled={!copyValue}
class="options-list-button"
aria-label="copy text">
<Copy value={copyValue}>
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
</button>
</div>
<p style="height: 2rem;">{helper ?? ''}</p>
</div>
{/if}
{/if}
{#if showInput}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<input type="text" placeholder="Enter custom event" bind:value={inputData} />
<div class="options-list">
<button
on:click|preventDefault={() => {
showInput = false;
}}
class="options-list-button"
aria-label="confirm">
<span class="icon-check" aria-hidden="true" />
</button>
<button
on:click|preventDefault={() => {
inputData = null;
showInput = false;
}}
class="options-list-button"
aria-label="cancel">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</div>
{:else}
<div class="input-text-wrapper" style="--amount-of-buttons:2">
<div type="text" readonly style="min-height: 2.5rem;">
{#if inputData}
<span>{inputData}</span>
{:else}
{#each Array.from(eventString.values()) as route, i}
<button
class:u-opacity-0-5={helper !== route.description}
on:mouseenter={() => {
helper = route.description;
}}
on:mouseleave={() => {
helper = null;
}}>
{route.value}
</button>
<span class="u-opacity-0-5">
{i + 1 < eventString?.size ? '.' : ''}
</span>
{/each}
{/if}
</div>
<div class="options-list">
<button
on:click|preventDefault={() => {
inputData = copyValue;
showInput = true;
}}
class="options-list-button"
aria-label="edit event">
<span class="icon-pencil" aria-hidden="true" />
</button>
<button disabled={!copyValue} class="options-list-button" aria-label="copy text">
<Copy value={copyValue}>
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
</button>
</div>
<p style="height: 2rem;">{helper ?? ''}</p>
</div>
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button disabled={showInput || !copyValue} submit>Create</Button>
</svelte:fragment>
</Modal>
</Form>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button disabled={showInput || !copyValue} submit>Create</Button>
</svelte:fragment>
</Modal>
+2 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export let size: '1' | '2' | '3' | '4' | '5' | '6' | '7';
export let trimmed = true;
</script>
<svelte:element this={tag} class={`heading-level-${size}`}>
<svelte:element this={tag} class={`heading-level-${size}`} class:u-trim-1={trimmed}>
<slot />
</svelte:element>
+4 -1
View File
@@ -1,12 +1,12 @@
export { default as Modal } from './modal.svelte';
export { default as Pagination } from './pagination.svelte';
export { default as PaginationInline } from './paginationInline.svelte';
export { default as Card } from './card.svelte';
export { default as CardGrid } from './cardGrid.svelte';
export { default as CardContainer } from './cardContainer.svelte';
export { default as InnerModal } from './innerModal.svelte';
export { default as Tile } from './tile.svelte';
export { default as Tiles } from './tiles.svelte';
export { default as Back } from './back.svelte';
export { default as Copy } from './copy.svelte';
export { default as CopyInput } from './copyInput.svelte';
export { default as UploadBox } from './uploadBox.svelte';
@@ -21,12 +21,14 @@ export { default as Collapsible } from './collapsible.svelte';
export { default as CollapsibleItem } from './collapsibleItem.svelte';
export { default as DropTabs } from './dropTabs.svelte';
export { default as DropTabsItem } from './dropTabsItem.svelte';
export { default as DropTabsLink } from './dropTabsLink.svelte';
export { default as Avatar } from './avatar.svelte';
export { default as AvatarInitials } from './avatarInitials.svelte';
export { default as AvatarGroup } from './avatarGroup.svelte';
export { default as Alert } from './alert.svelte';
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 Steps } from './steps.svelte';
export { default as Step } from './step.svelte';
@@ -40,3 +42,4 @@ export { default as Tab } from './tab.svelte';
export { default as EventModal } from './eventModal.svelte';
export { default as Status } from './status.svelte';
export { default as Heading } from './heading.svelte';
export { default as Output } from './output.svelte';
+27 -21
View File
@@ -1,27 +1,33 @@
<script>
import { FormItem } from '$lib/elements/forms';
export let show = false;
</script>
{#if show}
<section class="modal is-inner-modal">
<header class="modal-header u-flex-wrap">
<h4 class="modal-title body-text-1">
<slot name="title" />
</h4>
<button
type="button"
class="x-button"
aria-label="Close"
title="Close"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
<p class="u-flex-basis-100-percent">
<slot name="subtitle" />
</p>
</header>
<div class="modal-content">
<slot name="content" />
</div>
</section>
<FormItem>
<section class="modal is-inner-modal">
<div class="modal-form">
<header class="modal-header u-flex-wrap">
<h4 class="modal-title body-text-1">
<slot name="title" />
</h4>
<button
type="button"
class="x-button"
aria-label="Close"
title="Close"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
<p class="u-flex-basis-100-percent">
<slot name="subtitle" />
</p>
</header>
<div class="modal-content">
<slot name="content" />
</div>
</div>
</section>
</FormItem>
{/if}
+58 -55
View File
@@ -1,9 +1,6 @@
<script lang="ts">
import { browser } from '$app/environment';
import { createEventDispatcher } from 'svelte';
import { fade, fly, type FadeParams, type FlyParams } from 'svelte/transition';
import { Alert, Heading } from '$lib/components';
import { clickOnEnter } from '$lib/helpers/a11y';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { Alert } from '$lib/components';
export let show = false;
export let size: 'small' | 'big' = null;
@@ -11,44 +8,53 @@
export let error: string = null;
export let closable = true;
let dialog: HTMLDialogElement;
let alert: HTMLElement;
const dispatch = createEventDispatcher();
const transitionFly: FlyParams = {
duration: 150,
y: 50
};
const transitionFade: FadeParams = {
duration: 150
};
const handleKeydown = (event: KeyboardEvent) => {
const dispatch = createEventDispatcher();
onMount(() => {
if (show) openModal();
});
onDestroy(() => {
if (show) closeModal();
});
function handleBLur(event: MouseEvent) {
if (event.target === dialog) {
closeModal();
}
}
function openModal() {
if (dialog && !dialog.open) {
dialog.showModal();
document.documentElement.classList.add('u-overflow-hidden');
}
}
function closeModal() {
if (closable) {
if (dialog && dialog.open) {
dispatch('close');
dialog.close();
show = false;
document.documentElement.classList.remove('u-overflow-hidden');
}
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
closeModal();
}
};
const handleBLur = (event: MouseEvent) => {
const target: Partial<HTMLElement> = event.target;
if (target.hasAttribute('data-curtain')) {
closeModal();
}
};
const closeModal = () => {
if (closable) {
dispatch('close');
show = false;
}
};
}
/**
* Workaround until https://github.com/sveltejs/svelte/issues/3105 is resolved.
*/
$: if (browser) {
if (show) {
document.body.classList.add('u-overflow-hidden');
} else {
document.body.classList.remove('u-overflow-hidden');
}
$: if (show) {
openModal();
} else {
closeModal();
}
$: if (error) {
@@ -56,29 +62,25 @@
}
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window on:click={handleBLur} on:keydown={handleKeydown} />
{#if show}
<div
class="modal-curtain"
data-curtain
on:click={handleBLur}
on:keyup|self={clickOnEnter}
transition:fade={transitionFade}>
<section
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class="modal"
transition:fly={transitionFly}>
<dialog
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
bind:this={dialog}>
{#if show}
<!-- svelte-ignore a11y-no-redundant-roles -->
<form class="modal-form" role="form" on:submit|preventDefault>
<header class="modal-header">
{#if warning}
<div class="avatar is-color-orange is-medium">
<span class="icon-exclamation" aria-hidden="true" />
</div>
{/if}
<Heading tag="h4" size="5">
<h4 class="modal-title heading-level-5">
<slot name="header" />
</Heading>
</h4>
{#if closable}
<button
type="button"
@@ -105,13 +107,14 @@
{/if}
<slot />
</div>
{#if $$slots.footer}
<div class="modal-footer">
<div class="u-flex u-main-end u-gap-12">
<div class="u-flex u-main-end u-gap-16">
<slot name="footer" />
</div>
</div>
{/if}
</section>
</div>
{/if}
</form>
{/if}
</dialog>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import Copy from './copy.svelte';
export let value: string;
</script>
<Copy {value}>
<div class="interactive-text-output is-textarea">
<span class="text u-line-height-1-5 u-break-word"><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>
</div>
</Copy>
+13 -37
View File
@@ -1,37 +1,15 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { page } from '$app/stores';
export let sum: number;
export let limit: number;
export let offset: number;
export let path: string;
export let hidePages = false;
const dispatch = createEventDispatcher();
$: totalPages = Math.ceil(sum / limit);
$: currentPage = Math.floor(offset / limit + 1);
$: pages = pagination(currentPage, totalPages);
function handleOptionClick(page: number) {
if (currentPage !== page) {
offset = limit * (page - 1);
currentPage = page;
dispatch('change');
}
}
function handleButtonPage(direction: string) {
if (direction === 'next' && currentPage < totalPages) {
currentPage += 1;
offset = limit * (currentPage - 1);
dispatch('change');
} else if (direction === 'prev' && currentPage > 1) {
currentPage -= 1;
offset = limit * (currentPage - 1);
dispatch('change');
}
}
function pagination(page: number, total: number) {
const pagesShown = 5;
const start = Math.max(
@@ -51,30 +29,29 @@
</script>
{#if totalPages > 1}
{@const search = $page.url.search}
<nav class="pagination">
<button
type="button"
on:click={() => handleButtonPage('prev')}
<a
class:is-disabled={currentPage <= 1}
class="button is-text"
aria-label="prev page">
aria-label="prev page"
href={`${path}/${currentPage - 1}${search}`}>
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</button>
</a>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
<button
type="button"
<a
href={`${path}/${page}${search}`}
class="button"
on:click={() => handleOptionClick(+page)}
class:is-disabled={currentPage === page}
class:is-text={currentPage !== page}
aria-label="page">
<span class="text">{page}</span>
</button>
</a>
</li>
{:else}
<li class="li is-text">
@@ -84,15 +61,14 @@
{/each}
</ol>
{/if}
<button
on:click={() => handleButtonPage('next')}
<a
class:is-disabled={currentPage === totalPages}
class="button is-text"
type="button"
href={`${path}/${currentPage + 1}${search}`}
aria-label="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</button>
</a>
</nav>
{:else}
<nav class="pagination">
+117
View File
@@ -0,0 +1,117 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let sum: number;
export let limit: number;
export let offset: number;
export let hidePages = false;
const dispatch = createEventDispatcher();
$: totalPages = Math.ceil(sum / limit);
$: currentPage = Math.floor(offset / limit + 1);
$: pages = pagination(currentPage, totalPages);
function handleOptionClick(page: number) {
if (currentPage !== page) {
offset = limit * (page - 1);
currentPage = page;
dispatch('change');
}
}
function handleButtonPage(direction: string) {
if (direction === 'next' && currentPage < totalPages) {
currentPage += 1;
offset = limit * (currentPage - 1);
dispatch('change');
} else if (direction === 'prev' && currentPage > 1) {
currentPage -= 1;
offset = limit * (currentPage - 1);
dispatch('change');
}
}
function pagination(page: number, total: number) {
const pagesShown = 5;
const start = Math.max(
1,
Math.min(page - Math.floor((pagesShown - 3) / 2), total - pagesShown + 2)
);
const end = Math.min(
total,
Math.max(page + Math.floor((pagesShown - 2) / 2), pagesShown - 1)
);
return [
...(start > 2 ? [1, '...'] : start > 1 ? [1] : []),
...Array.from({ length: end + 1 - start }, (_, i) => i + start),
...(end < total - 1 ? ['...', total] : end < total ? [total] : [])
];
}
</script>
{#if totalPages > 1}
<nav class="pagination">
<button
type="button"
on:click={() => handleButtonPage('prev')}
class:is-disabled={currentPage <= 1}
class="button is-text"
aria-label="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</button>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
{#each pages as page}
{#if typeof page === 'number'}
<li class="pagination-item">
<button
type="button"
class="button"
on:click={() => handleOptionClick(+page)}
class:is-disabled={currentPage === page}
class:is-text={currentPage !== page}
aria-label="page">
<span class="text">{page}</span>
</button>
</li>
{:else}
<li class="li is-text">
<span class="icon">...</span>
</li>
{/if}
{/each}
</ol>
{/if}
<button
on:click={() => handleButtonPage('next')}
class:is-disabled={currentPage === totalPages}
class="button is-text"
type="button"
aria-label="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</button>
</nav>
{:else}
<nav class="pagination">
<button type="button" class="button is-text is-disabled" aria-label="prev page">
<span class="icon-cheveron-left" aria-hidden="true" />
<span class="text">Prev</span>
</button>
{#if !hidePages}
<ol class="pagination-list is-only-desktop">
<li class="pagination-item">
<button type="button" class="button is-disabled" aria-label="page">
<span class="text">1</span>
</button>
</li>
</ol>
{/if}
<button type="button" class="button is-text is-disabled" aria-label="next page">
<span class="text">Next</span>
<span class="icon-cheveron-right" aria-hidden="true" />
</button>
</nav>
{/if}
@@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Button } from '$lib/elements/forms';
import { DropList, DropListItem } from '..';
import Custom from './custom.svelte';
import Team from './team.svelte';
@@ -22,10 +21,7 @@
</script>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add role</span>
</Button>
<slot />
<svelte:fragment slot="list">
<DropListItem disabled={$groups.has('any')} on:click={() => dispatch('create', ['any'])}>
Any
+18 -22
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Form, FormList, Helper, InputText } from '$lib/elements/forms';
import { Button, FormList, Helper, InputText } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { Modal } from '..';
import type { Writable } from 'svelte/store';
@@ -25,26 +25,22 @@
$: disabled = !value || $groups.has(value);
</script>
{#if show}
<Form noMargin noStyle on:submit={create}>
<Modal bind:show on:close={reset}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<Modal bind:show on:close={reset} on:submit={create}>
<svelte:fragment slot="header">Custom permission</svelte:fragment>
<FormList>
<InputText
showLabel={false}
id="custom-permission"
label="Custom permission"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
</FormList>
<FormList>
<InputText
showLabel={false}
id="custom-permission"
label="Custom permission"
placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]"
bind:value />
<Helper type="neutral">
A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸
</Helper>
</FormList>
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
{/if}
<svelte:fragment slot="footer">
<Button submit {disabled}>Create</Button>
</svelte:fragment>
</Modal>
+1
View File
@@ -1,3 +1,4 @@
export { default as Permissions } from './permissions.svelte';
export { default as Roles } from './roles.svelte';
export { default as Team } from './team.svelte';
export { default as User } from './user.svelte';
@@ -10,26 +10,23 @@
</script>
<script lang="ts">
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { difference } from '$lib/helpers/array';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import { Empty } from '../';
import Actions from './actions.svelte';
import Row from './row.svelte';
export let withCreate = false;
export let withCrud = true;
export let permissions: string[] = [];
export let executeAccess: string[] = [];
let showUser = false;
let showTeam = false;
@@ -43,10 +40,7 @@
permissions.forEach(fromPermissionString);
unsubscribe = groups.subscribe(() => {
const current = exportRoles();
if (
difference(current, permissions).length ||
difference(permissions, current).length
) {
if (symmetricDifference(current, permissions).length) {
permissions = current;
}
});
@@ -145,76 +139,61 @@
return a.localeCompare(b);
}
$: if (executeAccess.length && !permissions.length) {
executeAccess.forEach((permission) => {
addRole(permission);
});
}
$: if ([...$groups]?.length && !permissions.length) {
executeAccess = [...$groups].sort(sortRoles).map(([role]) => role);
}
//TODO: click on Empty components opens the dropdown
</script>
{#if [...$groups]?.length}
<Table noMargin noStyles noMobile>
<TableHeader>
<TableCellHead>Role</TableCellHead>
{#if withCrud}
{#if withCreate}
<TableCellHead width={70}>Create</TableCellHead>
{/if}
<TableCellHead width={70}>Read</TableCellHead>
<TableCellHead width={70}>Update</TableCellHead>
<TableCellHead width={70}>Delete</TableCellHead>
{#if withCreate}
<TableCellHead width={70}>Create</TableCellHead>
{/if}
<TableCellHead width={70}>Read</TableCellHead>
<TableCellHead width={70}>Update</TableCellHead>
<TableCellHead width={70}>Delete</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each [...$groups].sort(sortRoles) as [role, permission]}
<TableRow>
<TableCellText title="Role">
<TableCell title="Role">
<Row {role} />
</TableCellText>
</TableCell>
{#if withCrud}
{#if withCreate}
<TableCell title="Create">
<input
type="checkbox"
class="icon-check"
aria-label="Create"
checked={permission.create}
on:change={() => togglePermission(role, 'create')} />
</TableCell>
{/if}
<TableCell title="Read">
{#if withCreate}
<TableCell title="Create">
<input
type="checkbox"
class="icon-check"
aria-label="Read"
checked={permission.read}
on:change={() => togglePermission(role, 'read')} />
</TableCell>
<TableCell title="Update">
<input
type="checkbox"
class="icon-check"
aria-label="Update"
checked={permission.update}
on:change={() => togglePermission(role, 'update')} />
</TableCell>
<TableCell title="Delete">
<input
type="checkbox"
class="icon-check"
aria-label="Delete"
checked={permission.delete}
on:change={() => togglePermission(role, 'delete')} />
aria-label="Create"
checked={permission.create}
on:change={() => togglePermission(role, 'create')} />
</TableCell>
{/if}
<TableCell title="Read">
<input
type="checkbox"
class="icon-check"
aria-label="Read"
checked={permission.read}
on:change={() => togglePermission(role, 'read')} />
</TableCell>
<TableCell title="Update">
<input
type="checkbox"
class="icon-check"
aria-label="Update"
checked={permission.update}
on:change={() => togglePermission(role, 'update')} />
</TableCell>
<TableCell title="Delete">
<input
type="checkbox"
class="icon-check"
aria-label="Delete"
checked={permission.delete}
on:change={() => togglePermission(role, 'delete')} />
</TableCell>
<TableCell title="Remove" width={40}>
<div class="u-flex">
@@ -231,18 +210,38 @@
{/each}
</TableBody>
</Table>
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add role</span>
</Button>
</Actions>
{:else}
<span class="eyebrow-heading-3 u-sep-block-end">ROLE</span>
<Empty isButton on:click={() => (showDropdown = !showDropdown)}>
Add a role to get started
</Empty>
<article class="card u-grid u-cross-center u-width-full-line dashed">
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button secondary round on:click={() => (showDropdown = !showDropdown)}>
<i class="icon-plus" />
</Button>
</Actions>
</div>
<div class="common-section">
<span class="text"> Add a role to get started </span>
</div>
</div>
</article>
{/if}
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create} />
+83 -36
View File
@@ -1,13 +1,15 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { difference } from '$lib/helpers/array';
import { symmetricDifference } from '$lib/helpers/array';
import { onDestroy, onMount } from 'svelte';
import { writable, type Unsubscriber } from 'svelte/store';
import Actions from './actions.svelte';
@@ -28,7 +30,7 @@
roles.forEach(addRole);
unsubscribe = groups.subscribe((n) => {
const current = Array.from(n.keys());
if (difference(current, roles).length || difference(roles, current).length) {
if (symmetricDifference(current, roles).length) {
roles = current;
}
});
@@ -69,39 +71,84 @@
return n;
});
}
function sortRoles(a: string, b: string) {
if ((a === 'any') !== (b === 'any')) {
return a === 'any' ? -1 : 1;
}
if ((a === 'users') !== (b === 'users')) {
return a === 'users' ? -1 : 1;
}
if ((a === 'guests') !== (b === 'guests')) {
return a === 'guests' ? -1 : 1;
}
return a.localeCompare(b);
}
$: if (roles) {
roles.forEach(addRole);
}
</script>
<Table noMargin noStyles noMobile>
<TableHeader>
<TableCellHead>Role</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each [...$groups.keys()] as role}
<TableRow>
<TableCellText title="Role">
<Row {role} />
</TableCellText>
<TableCellText title="Remove">
<div class="u-flex">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => deleteRole(role)}>
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create} />
{#if [...$groups.keys()]?.length}
<Table noMargin noStyles noMobile>
<TableHeader>
<TableCellHead>Role</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each [...$groups.keys()].sort(sortRoles) as role}
<TableRow>
<TableCell title="Role">
<Row {role} />
</TableCell>
<TableCellText title="Remove">
<div class="u-flex">
<button
class="button is-text is-only-icon"
type="button"
aria-label="delete"
on:click={() => deleteRole(role)}>
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button text noMargin on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add role</span>
</Button>
</Actions>
{:else}
<article class="card u-grid u-cross-center u-width-full-line dashed">
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<Actions
bind:showCustom
bind:showDropdown
bind:showTeam
bind:showUser
{groups}
on:create={create}>
<Button secondary round on:click={() => (showDropdown = !showDropdown)}>
<i class="icon-plus" />
</Button>
</Actions>
</div>
<div class="common-section">
<span class="text"> Add a role to get started </span>
</div>
</div>
</article>
{/if}
+1 -1
View File
@@ -11,7 +11,7 @@
{:else if role === 'any'}
<div>Any</div>
{:else}
<div>{role}</div>
<div class="u-trim-1">{role}</div>
{/if}
</div>
</div>
+77 -51
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { Button, Form, InputSearch } from '$lib/elements/forms';
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import { AvatarInitials, Modal, Pagination } from '..';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import type { Writable } from 'svelte/store';
import type { Permission } from './permissions.svelte';
@@ -58,54 +58,80 @@
}
</script>
{#if show}
<Form noStyle noMargin on:submit={create}>
<Modal bind:show on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<InputSearch bind:value={search} />
{#if results?.teams}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.teams as team (team.$id)}
{@const role = `team:${team.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td
class="table-col"
data-title="Enabled"
style="--p-col-width:40">
<input
id={team.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="Team">
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
<AvatarInitials size={32} name={team.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{team.name}</div>
<div class="u-x-small">{team.$id}</div>
</div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
<Modal bind:show on:submit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select teams</svelte:fragment>
<InputSearch autofocus disabled={!results?.teams?.length && !search} bind:value={search} />
{#if results?.teams?.length}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.teams as team (team.$id)}
{@const role = `team:${team.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
<input
id={team.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="Team">
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
<AvatarInitials size={32} name={team.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">{team.name}</div>
<div class="u-x-small">{team.$id}</div>
</div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {results?.total}</p>
<PaginationInline limit={5} bind:offset sum={results?.total} hidePages />
</div>
{:else if search}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2">Sorry we couldn't find "{search}"</b>
<p>There are no teams that match your search.</p>
</div>
<div class="u-flex u-gap-16 common-section u-main-center">
<Button external href="https://appwrite.io/docs/client/teams" text
>Documentation</Button>
<Button secondary on:click={() => (search = '')}>Clear search</Button>
</div>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {results?.total}</p>
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
</div>
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
{/if}
</EmptySearch>
{:else}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<p class="text u-line-height-1-5">
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
href="https://appwrite.io/docs/client/teams"
target="_blank"
rel="noopener noreferrer">
documentation</a
>.
</p>
</div>
</div>
</EmptySearch>
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
+99 -42
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button, Form, InputSearch } from '$lib/elements/forms';
import { Button, InputSearch } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
import { AvatarInitials, Modal, Pagination } from '..';
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
import { sdkForProject } from '$lib/stores/sdk';
import { Query, type Models } from '@aw-labs/appwrite-console';
import type { Writable } from 'svelte/store';
@@ -58,49 +58,106 @@
}
</script>
<Form noStyle noMargin on:submit={create}>
<Modal bind:show on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<InputSearch bind:value={search} />
{#if results?.users}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.users as user (user.$id)}
{@const role = `user:${user.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
<input
id={user.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="User">
<label class="u-flex u-cross-center u-gap-8" for={user.$id}>
<AvatarInitials size={32} name={user.name} />
<Modal bind:show on:submit={create} on:close={reset} size="big">
<svelte:fragment slot="header">Select users</svelte:fragment>
<InputSearch autofocus disabled={!results?.users?.length && !search} bind:value={search} />
{#if results?.users?.length}
<div class="table-wrapper">
<table class="table is-table-layout-auto is-remove-outer-styles">
<tbody class="table-tbody">
{#each results.users as user (user.$id)}
{@const role = `user:${user.$id}`}
{@const exists = $groups.has(role)}
<tr class="table-row">
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
<input
id={user.$id}
type="checkbox"
class="icon-check"
aria-label="Create"
checked={exists || selected.has(role)}
disabled={exists}
on:change={(event) => onSelection(event, role)} />
</td>
<td class="table-col" data-title="User">
<label class="u-flex u-cross-center u-gap-8" for={user.$id}>
{#if user.email || user.phone}
{#if user.name}
<AvatarInitials size={32} name={user.name} />
<div class="u-line-height-1-5">
<div class="body-text-2">
{user.name}
</div>
<div class="u-x-small">{user.$id}</div>
</div>
{:else}
<div class="avatar is-size-small ">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
<div class="u-line-height-1-5">
<div class="body-text-2">
{user.email ? user.email : user.phone}
</div>
<div class="u-x-small">{user.$id}</div>
</div>
{/if}
{:else}
<div class="avatar is-size-small ">
<span class="icon-anonymous" aria-hidden="true" />
</div>
<div class="u-line-height-1-5">
<div class="body-text-2">{user.name}</div>
<div class="body-text-2">
{user.name ? user.name : '-'}
</div>
<div class="u-x-small">{user.$id}</div>
</div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {results?.total}</p>
<Pagination limit={5} bind:offset sum={results?.total} hidePages />
<PaginationInline limit={5} bind:offset sum={results?.total} hidePages />
</div>
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
</Form>
{:else if search}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<b class="body-text-2">Sorry we couldn't find "{search}"</b>
<p>There are no Users that match your search.</p>
</div>
<div class="u-flex u-gap-16 common-section u-main-center">
<Button external href="https://appwrite.io/docs/server/users" text
>Documentation</Button>
<Button secondary on:click={() => (search = '')}>Clear search</Button>
</div>
</div>
</EmptySearch>
{:else}
<EmptySearch hidePages>
<div class="common-section">
<div class="u-text-center common-section">
<p class="text u-line-height-1-5">
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
href="https://appwrite.io/docs/server/users"
target="_blank"
rel="noopener noreferrer">
documentation</a
>.
</p>
</div>
</div>
</EmptySearch>
{/if}
<svelte:fragment slot="footer">
<Button submit disabled={!hasSelection}>Create</Button>
</svelte:fragment>
</Modal>
+73
View File
@@ -0,0 +1,73 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { onDestroy } from 'svelte';
export let search = '';
export let placeholder = '';
export let debounce = 250;
export let required = false;
export let disabled = false;
export let autofocus = false;
export let isWithEndButton = true;
let element: HTMLInputElement;
let timer: ReturnType<typeof setTimeout>;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
onDestroy(() => {
search = '';
if (timer) {
clearTimeout(timer);
}
});
$: valueChange(search);
const valueChange = (value: string) => {
clearTimeout(timer);
timer = setTimeout(async () => {
const url = new URL($page.url);
if (url.search === value) {
return;
}
/**
* Reset to first page if search changes.
*/
if ($page.data.page > 1) {
url.pathname = url.pathname.replace(new RegExp(`/${$page.data.page}*$`), '/');
}
url.search = value;
goto(url, { keepfocus: true });
}, debounce);
};
</script>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<div class="u-flex-basis-50-percent">
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
<input
{placeholder}
{disabled}
{required}
type="search"
class="input-text"
bind:value={search} />
<span class="icon-search" aria-hidden="true" />
{#if isWithEndButton && search}
<button class="x-button" aria-label="Clear search" on:click={() => (search = '')}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
</div>
</div>
<slot />
</div>
+44 -5
View File
@@ -1,23 +1,62 @@
export const scopes = [
export const PAGE_LIMIT = 12; // default page limit
export const CARD_LIMIT = 6; // default card limit
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
PROJECTS = 'dependency:projects',
ACCOUNT = 'dependency:account',
ACCOUNT_SESSIONS = 'dependency:account_sessions',
USER = 'dependency:user',
USERS = 'dependency:users',
SESSIONS = 'dependency:sessions',
TEAM = 'dependency:team',
TEAMS = 'dependency:teams',
MEMBERSHIPS = 'dependency:memberships',
DATABASE = 'dependency:database',
COLLECTION = 'dependency:collection',
DOCUMENT = 'dependency:document',
DOCUMENTS = 'dependency:documents',
BUCKET = 'dependency:bucket',
FILE = 'dependency:file',
FILES = 'dependency:files',
FUNCTION = 'dependency:function',
FUNCTIONS = 'dependency:functions',
VARIABLES = 'dependency:variables',
DEPLOYMENTS = 'dependency:deployments',
EXECUTIONS = 'dependency:executions',
PLATFORM = 'dependency:platform',
PLATFORMS = 'dependency:platforms',
KEY = 'dependency:key',
KEYS = 'dependency:keys',
DOMAINS = 'dependency:domains',
WEBHOOK = 'dependency:webhook',
WEBHOOKS = 'dependency:webhooks'
}
export const scopes: {
scope: string;
description: string;
category: string;
}[] = [
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Authentication'
category: 'Auth'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Authentication'
category: 'Auth'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Authentication'
category: 'Auth'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Authentication'
category: 'Auth'
},
{
scope: 'databases.read',
+1 -1
View File
@@ -16,7 +16,7 @@
<a
{disabled}
{href}
target={external ? '_blank' : '_self'}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class="button"
class:is-only-icon={round}
+3
View File
@@ -18,6 +18,8 @@
let element: HTMLInputElement;
let error: string;
const pattern = String.raw`^[^]+\s+[^]+\s+[^]+\s+[^]+\s+[^]`;
onMount(() => {
if (element && autofocus) {
element.focus();
@@ -57,6 +59,7 @@
{maxlength}
{readonly}
{step}
{pattern}
type="text"
class="input-text"
bind:value
+3 -2
View File
@@ -21,7 +21,7 @@
}
});
const handleInvalid = (event: Event) => {
function handleInvalid(event: Event) {
event.preventDefault();
if (element.validity.valueMissing) {
@@ -30,7 +30,7 @@
}
error = element.validationMessage;
};
}
$: if (value) {
error = null;
@@ -45,6 +45,7 @@
{disabled}
{readonly}
{required}
step=".001"
autocomplete={autocomplete ? 'on' : 'off'}
type="datetime-local"
class="input-text"
+1 -1
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { FormItem, Helper } from '.';
export let id: string;
export let label: string;
export let showLabel = true;
export let id: string;
export let value: string | number | boolean;
export let placeholder = '';
export let required = false;
+1 -1
View File
@@ -90,7 +90,7 @@
</div>
<input
{id}
placeholder={!tags.length ? placeholder : ''}
{placeholder}
{disabled}
{readonly}
type="text"
+8 -2
View File
@@ -2,6 +2,12 @@
import { clickOnEnter } from '$lib/helpers/a11y';
</script>
<div class="table-row" role="row" on:keyup={clickOnEnter} on:click|preventDefault>
<!-- svelte-ignore a11y-unknown-role -->
<tr
class="table-row"
role="row button"
tabindex="0"
on:keyup={clickOnEnter}
on:click|preventDefault>
<slot />
</div>
</tr>
+4
View File
@@ -8,3 +8,7 @@ export function difference(arr1: unknown[], arr2: unknown[]) {
const intersection = new Set(arr1.filter((elem) => !set.has(elem)));
return Array.from(intersection);
}
export function symmetricDifference(arr1: unknown[], arr2: unknown[]) {
return difference(arr1, arr2).concat(difference(arr2, arr1));
}
+26
View File
@@ -0,0 +1,26 @@
export function pageToOffset(page: number, limit: number): number {
return page ? page * limit - limit : 0;
}
type TabElement = { href: string; title: string; hasChildren?: boolean };
export function isTabSelected(
tab: TabElement,
pathname: string,
basePath: string,
tabs: TabElement[]
) {
if (!tab?.hasChildren) {
return tab.href === pathname;
} else {
if (tab.href === pathname) return true;
if (tab.href === basePath) {
if (!tabs.some((t) => pathname.includes(t.href) && t.href !== tab.href)) {
return pathname.includes(tab.href);
}
} else {
return pathname.includes(tab.href);
}
}
}
+8 -45
View File
@@ -1,3 +1,5 @@
import prettyBytes from 'pretty-bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
export function calculateSize(bytes: number, decimals = 1) {
@@ -20,55 +22,16 @@ export function bytesToSize(value: number, unit: string) {
return value / Math.pow(1024, index);
}
export function humanFileSize(value: number): {
export function humanFileSize(bytes: number): {
value: string;
unit: string;
} {
if (!value) return { value: 'No', unit: 'Bytes' };
const length = value.toString().length;
const unit =
length < 7
? 'kilobyte'
: length < 11
? 'megabyte'
: length < 15
? 'gigabyte'
: length < 19
? 'terabyte'
: 'petabyte';
const formatter = Intl.NumberFormat('en', {
style: 'unit',
maximumSignificantDigits: 3,
maximumFractionDigits: 2,
unit
});
while (value > 1000) {
value /= 1000;
}
const parts: {
integer: string;
unit: string;
fraction?: string;
decimal?: string;
} = formatter.formatToParts(value).reduce(
(prev, curr) => {
prev[curr.type] = curr.value;
return prev;
},
{
integer: '',
unit: '',
decimal: '',
fraction: ''
}
);
const value = prettyBytes(bytes, {
locale: 'en'
}).split(' ');
return {
value: `${parts.integer}${parts.decimal}${parts.fraction}`,
unit: parts.unit
value: value[0],
unit: value[1]
};
}
@@ -1,6 +1,5 @@
<script lang="ts">
import { Empty, Pagination } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { EmptySearch, Pagination, Trim } from '$lib/components';
import {
Table,
TableBody,
@@ -13,19 +12,12 @@
import { Container } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageLimit } from '$lib/stores/layout';
import { onMount } from 'svelte';
import { accountActivity } from '../store';
import { Query } from '@aw-labs/appwrite-console';
import { createPersistentPagination } from '$lib/stores/pagination';
import { PAGE_LIMIT } from '$lib/constants';
import type { Models } from '@aw-labs/appwrite-console';
const offset = createPersistentPagination($pageLimit);
onMount(async () => {
await accountActivity.load([Query.offset($offset), Query.limit($pageLimit)]);
});
$: accountActivity.load([Query.offset($offset), Query.limit($pageLimit)]);
export let logs: Models.LogList;
export let path: string;
export let offset = 0;
const getBrowser = (clientCode: string) => {
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
@@ -33,21 +25,18 @@
</script>
<Container>
{#if $accountActivity?.total}
{#if logs.total}
<Table>
<TableHeader>
<TableCellHead>Date</TableCellHead>
<TableCellHead>Event</TableCellHead>
<TableCellHead>Client</TableCellHead>
<TableCellHead>Event</TableCellHead>
<TableCellHead>Location</TableCellHead>
<TableCellHead>IP</TableCellHead>
<TableCellHead>Date</TableCellHead>
</TableHeader>
<TableBody>
{#each $accountActivity.logs as log}
{#each logs.logs as log}
<TableRow>
<TableCellText title="Date">{toLocaleDateTime(log.time)}</TableCellText>
<TableCellText title="Event">{log.event}</TableCellText>
<TableCell title="Client">
{#if log.clientName}
<div class="u-flex u-cross-center u-gap-12">
@@ -58,14 +47,12 @@
src={getBrowser(log.clientCode).toString()}
alt={log.clientName} />
</div>
<p
class="text u-trim"
title={`${log.clientName} ${log.clientVersion} on ${log.osName} ${log.osVersion}`}>
<Trim>
{log.clientName}
{log.clientVersion}
on {log.osName}
{log.osVersion}
</p>
</Trim>
</div>
{:else}
<div class="u-flex u-cross-center u-gap-12">
@@ -74,6 +61,7 @@
</div>
{/if}
</TableCell>
<TableCellText title="Event">{log.event}</TableCellText>
<TableCellText title="Location">
{#if log.countryCode !== '--'}
@@ -83,28 +71,25 @@
{/if}
</TableCellText>
<TableCellText title="IP">{log.ip}</TableCellText>
<TableCellText title="Date">{toLocaleDateTime(log.time)}</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {logs.total}</p>
<Pagination limit={PAGE_LIMIT} {path} {offset} sum={logs.total} />
</div>
{:else}
<Empty>
<EmptySearch>
<div class="u-flex u-flex-vertical u-cross-center">
<div class="common-section">
<p>No logs available</p>
</div>
<div class="common-section">
<Button
external
secondary
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetLogs"
>Documentation</Button>
<div class="u-text-center">
<p class="text u-line-height-1-5">
You have no activity. Once your users start interacting with your app you'll
see their activity here.
</p>
</div>
</div>
</Empty>
</EmptySearch>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$accountActivity?.total}</p>
<Pagination limit={$pageLimit} bind:offset={$offset} sum={$accountActivity?.total} />
</div>
</Container>
+13 -42
View File
@@ -1,53 +1,24 @@
<script lang="ts" context="module">
export type Breadcrumb = {
title: string;
href?: string;
};
</script>
<script lang="ts">
import { breadcrumbs as breadcrumbsStore, level, type Breadcrumb } from '$lib/stores/layout';
$: breadcrumbs = [...$breadcrumbsStore]
.map(([level, value]) => ({ level, value }))
.sort((a, b) => (a.level > b.level ? 1 : -1))
.filter((n) => n.level <= $level);
function truncate(
breadcrumbs: {
value: Breadcrumb;
}[]
) {
/**
* Maximum of 4 Breadcrumbs, truncated from the left.
*/
if ($level > 4) {
breadcrumbs = [
{
value: {
href: null,
title: '...'
}
},
...breadcrumbs.slice(-3)
];
}
return breadcrumbs;
}
function buildHref(index: number) {
return breadcrumbs
.slice(0, index + 1)
.reduce((prev, next) => `${prev}/${next.value.href}`, '');
}
export let breadcrumbs: Breadcrumb[];
</script>
<nav class="breadcrumbs is-only-desktop" aria-label="breadcrumb">
<ol class="breadcrumbs-list">
{#each truncate(breadcrumbs) as breadcrumb, i}
{#each breadcrumbs as breadcrumb}
<li class="breadcrumbs-item">
{#if breadcrumb.value.href}
<a
href={buildHref(breadcrumbs.length > 4 ? i + breadcrumbs.length - 4 : i)}
title={breadcrumb.value.title}>
{breadcrumb.value.title}
{#if breadcrumb.href}
<a href={breadcrumb.href} title={breadcrumb.title}>
{breadcrumb.title}
</a>
{:else}
<span>{breadcrumb.value.title}</span>
<span>{breadcrumb.title}</span>
{/if}
</li>
{/each}
+6
View File
@@ -1,9 +1,15 @@
<script lang="ts">
export let overlapCover = false;
export let size: 'small' | 'medium' | 'large' | 'xl' = null;
$: style = size
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
: '';
</script>
<div
class="container"
{style}
class:u-margin-block-start-24={!overlapCover}
class:u-margin-block-start-negative-56={overlapCover}>
<slot />
+7 -1
View File
@@ -1,9 +1,15 @@
<script lang="ts">
export let adjustContentToCover = false;
export let size: 'small' | 'medium' | 'large' | 'xl' = null;
$: style = size
? `--p-container-max-size: var(--container-max-size, var(--container-size-${size}))`
: '';
</script>
<div class="top-cover" class:is-adjust-content-to-cover={adjustContentToCover}>
<div class="container">
<div class="container" {style}>
<div class="u-flex u-cross-center u-gap-16 u-margin-block-start-20">
<slot name="header" />
</div>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import { Heading } from '$lib/components';
export let href: string = null;
</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">
<slot />
</span>
</Heading>
+6 -12
View File
@@ -1,27 +1,19 @@
<script lang="ts">
import { base } from '$app/paths';
import { onMount } from 'svelte';
import { Breadcrumbs } from '.';
import { AvatarInitials, 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 { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
let showDropdown = false;
let droplistElement: HTMLDivElement;
onMount(async () => {
await organizationList.load();
if (!$organization) {
await organization.load($organizationList.teams[0].$id);
}
});
const onBlur = (event: MouseEvent) => {
if (
showDropdown &&
@@ -40,7 +32,9 @@
<img src={AppwriteLogo} width="132" height="34" alt="Appwrite" />
</a>
<Breadcrumbs />
{#if $page.data.breadcrumbs}
<svelte:component this={$page.data.breadcrumbs} />
{/if}
<div class="main-header-end">
<nav class="u-flex is-only-desktop">
@@ -53,7 +47,7 @@
<span class="text">Support</span>
</a>
</nav>
<nav class="user-profile">
<nav class="u-flex u-height-100-percents u-sep-inline-start">
{#if $user}
<div class="drop-wrapper" class:is-open={showDropdown} bind:this={droplistElement}>
<button class="user-profile-button" on:click={() => (showDropdown = !showDropdown)}>
+3
View File
@@ -1,5 +1,6 @@
export { default as Container } from './container.svelte';
export { default as Cover } from './cover.svelte';
export { default as CoverTitle } from './coverTitle.svelte';
export { default as Footer } from './footer.svelte';
export { default as Header } from './header.svelte';
export { default as Navigation } from './navigation.svelte';
@@ -12,3 +13,5 @@ export { default as WizardStep } from './wizardStep.svelte';
export { default as Breadcrumbs } from './breadcrumbs.svelte';
export { default as Unauthenticated } from './unauthenticated.svelte';
export { default as Usage, type UsagePeriods } from './usage.svelte';
export { default as Activity } from './activity.svelte';
export { default as Progress } from './progress.svelte';
+22 -3
View File
@@ -2,7 +2,7 @@
import { toLocaleDateTime } from '$lib/helpers/date';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { log } from '$lib/stores/logs';
import { Status, Tab, Tabs } from '../components';
import { Output, Status, Tab, Tabs } from '../components';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
@@ -35,8 +35,17 @@
.getElementsByClassName('code-panel-content')[0]
.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
$log.show = false;
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if $log.data}
<section class="cover-frame">
<header class="cover-frame-header u-flex u-gap-16 u-main-space-between u-cross-center">
@@ -59,7 +68,12 @@
alt="technology" />
</div>
<div>
<h2 class="body-text-2">Deployment ID: {$log.data.$id}</h2>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2">Deployment ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
</div>
<time class="u-block"
>Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
<div>Size: {size.value} {size.unit}</div>
@@ -122,7 +136,12 @@
alt="technology" />
</div>
<div>
<h2 class="body-text-2">Execution ID: {$log.data.$id}</h2>
<div class="u-flex u-gap-12 u-cross-center">
<h2 class="body-text-2">Execution ID:</h2>
<Output value={$log.data.$id}>
{$log.data.$id}
</Output>
</div>
<time class="u-block">
Created at: {toLocaleDateTime($log.data.$createdAt)}</time>
</div>
+30 -21
View File
@@ -1,9 +1,16 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { event, type AnalyticsActionParam } from '$lib/actions/analytics';
$: project = $page.params.project;
$: path = `${base}/console/project-${project}`;
const defaultEventOptions: Partial<AnalyticsActionParam> = {
category: 'console/navigation',
name: 'click_menu_link',
action: 'click'
};
</script>
<div class="side-nav">
@@ -15,11 +22,24 @@
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/overview`)}
href={path}>
href={path}
use:event={{
...defaultEventOptions,
label: 'Home Link'
}}>
<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`)}
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"
@@ -29,26 +49,6 @@
<span class="text">Databases</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(`${path}/storage`)}
href={`${path}/storage`}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
class:is-selected={$page.url.pathname.startsWith(
`${path}/authentication`
)}
href={`${path}/authentication`}>
<span class="icon-user-group" aria-hidden="true" />
<span class="text">Authentication</span>
</a>
</li>
<li class="drop-list-item">
<a
class="drop-button"
@@ -58,6 +58,15 @@
<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`)}
href={`${path}/storage`}>
<span class="icon-folder" aria-hidden="true" />
<span class="text">Storage</span>
</a>
</li>
</ul>
</section>
</div>
+84
View File
@@ -0,0 +1,84 @@
<script lang="ts">
import { navigating } from '$app/stores';
let width: number;
let updater: ReturnType<typeof setTimeout>;
let completed = false;
const minimum = 0.08;
const maximum = 0.994;
const settleTime = 700;
const intervalTime = 800;
const stepSizes = [0, 0.005, 0.01, 0.02];
function animate(): void {
if (updater) {
clearInterval(updater);
}
updater = setInterval(() => {
const randomStep = stepSizes[Math.floor(Math.random() * stepSizes.length)];
const step = getIncrement(width) + randomStep;
if (width < maximum) {
width = width + step;
}
if (width > maximum) {
width = maximum;
stop();
}
}, intervalTime);
}
function start(): void {
width = minimum;
animate();
}
function complete(): void {
clearInterval(updater);
width = 1;
setTimeout(() => {
completed = true;
setTimeout(() => {
completed = false;
width = 0;
}, settleTime);
}, settleTime);
}
function getIncrement(number: number): number {
if (number >= 0 && number < 0.2) return 0.1;
else if (number >= 0.2 && number < 0.5) return 0.04;
else if (number >= 0.5 && number < 0.8) return 0.02;
else if (number >= 0.8 && number < 0.99) return 0.005;
return 0;
}
$: if ($navigating) {
start();
} else {
complete();
}
$: barStyle = (width && width * 100 && `width: ${width * 100}%;`) || '';
</script>
{#if width}
<div class="progress-bar" class:progress-bar-hiding={completed} style={barStyle} />
{/if}
<style>
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 2px;
transition: width 0.2s ease-in-out;
z-index: 5;
background: hsl(var(--color-primary-200));
}
.progress-bar-hiding {
transition: top 0.8s ease;
top: -8px;
}
</style>
+14 -121
View File
@@ -1,57 +1,19 @@
<script lang="ts">
import { navigating, page } from '$app/stores';
import { tabs, title, backButton, copyData, titleDropdown } from '$lib/stores/layout';
import { Cover } from '.';
import {
Copy,
DropList,
DropListItem,
DropListLink,
AvatarGroup,
Tabs,
Tab,
Heading
} from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
organization,
memberList,
newOrgModal,
newMemberModal
} from '$lib/stores/organization';
import { base } from '$app/paths';
import { user } from '$lib/stores/user';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { wizard } from '$lib/stores/wizard';
import { log } from '$lib/stores/logs';
import { beforeNavigate } from '$app/navigation';
export let isOpen = false;
export let showSideNavigation = false;
let y: number;
let showDropdown = false;
navigating.subscribe(() => {
if (isOpen) isOpen = false;
});
let avatars = [];
let avatarsTotal = 0;
memberList.subscribe((value) => {
if (value?.total > 0) {
avatarsTotal = value.total;
avatars = value.memberships.map((team) => team.userName);
}
});
const logout = async () => {
await user.logout();
await goto(`${base}/login`);
};
const toggleMenu = () => {
y = 0;
isOpen = !isOpen;
@@ -63,6 +25,16 @@
}
}
};
beforeNavigate((n) => {
/**
* Hide wizard when navigation is triggered by popstate.
*/
if (n.type === 'popstate' && $wizard.show) {
wizard.hide();
n.cancel();
}
});
</script>
<svelte:window bind:scrollY={y} />
@@ -86,89 +58,10 @@
<slot name="side" />
</nav>
<section class="main-content">
<Cover>
<svelte:fragment slot="header">
{#if $backButton}
<a class="back-button" href={$backButton} aria-label="page back">
<span class="icon-cheveron-left" aria-hidden="true" />
</a>
{/if}
{#if $titleDropdown?.length && $organization}
<DropList bind:show={showDropdown} scrollable={true} noArrow>
<button
class="button is-text u-padding-inline-0"
on:click={() => (showDropdown = !showDropdown)}>
<Heading tag="h1" size="4">
<span class="text"> {$organization.name}</span>
{#if $page.data?.header}
<svelte:component this={$page.data.header} />
{/if}
<span
class={`icon-cheveron-${showDropdown ? 'up' : 'down'}`}
aria-hidden="true" />
</Heading>
</button>
<svelte:fragment slot="list">
{#each $titleDropdown as org}
<DropListLink
href={`${base}/console/organization-${org.$id}`}
on:click={() => {
showDropdown = false;
}}>
{org.name}
</DropListLink>
{/each}
</svelte:fragment>
<svelte:fragment slot="other">
<section class="drop-section">
<ul class="drop-list">
<DropListItem
icon="plus"
on:click={() => {
showDropdown = false;
newOrgModal.set(true);
}}>New Organization</DropListItem>
</ul>
</section></svelte:fragment>
</DropList>
<div class="u-margin-inline-start-auto">
<div class="u-flex u-gap-16">
<AvatarGroup size={40} {avatars} total={avatarsTotal} />
<Button secondary on:click={() => newMemberModal.set(true)}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Invite</span>
</Button>
</div>
</div>
{:else}
<Heading tag="h1" size="4">
<span class="text">{$title ? $title : '-'}</span>
</Heading>
{/if}
{#if $page.url.pathname.includes('/console/account')}
<div class="u-margin-inline-start-auto">
<Button secondary on:click={logout}>Logout</Button>
</div>
{/if}
{#if $copyData?.value}
<Copy value={$copyData.value}>
<Pill button>
<span class="icon-duplicate" aria-hidden="true" />
{$copyData.text}
</Pill>
</Copy>
{/if}
</svelte:fragment>
{#if $tabs.length}
<Tabs>
{#each $tabs as tab}
<Tab href={tab.href} selected={$page.url.pathname === tab.href}>
{tab.title}
</Tab>
{/each}
</Tabs>
{/if}
</Cover>
<slot />
</section>
</main>
+1 -1
View File
@@ -82,7 +82,7 @@
<div class="u-margin-block-start-auto" />
<p class="u-max-width-500 u-width-full-line u-margin-block-start-24">
version 0.15.2.402
<!-- version 0.15.2.402 -->
</p>
<div class="u-margin-block-start-20" />
</div>
+61 -48
View File
@@ -13,14 +13,16 @@
<script lang="ts">
import { Container } from '$lib/layout';
import { BarChart, LineChart } from '$lib/charts';
import { Card, DropTabs, DropTabsItem, Heading, Tiles } from '$lib/components';
import { Card, DropTabs, DropTabsLink, Heading, Tiles } from '$lib/components';
import { Colors } from '$lib/charts/config';
import type { Models } from '@aw-labs/appwrite-console';
import { page } from '$app/stores';
type MetricMetadata = {
title: string;
legend: string;
};
export let title: string;
export let count: Models.Metric[];
export let created: Models.Metric[];
@@ -33,23 +35,24 @@
export let readMetadata: MetricMetadata;
export let updatedMetadata: MetricMetadata;
export let deletedMetadata: MetricMetadata;
export let range: UsagePeriods;
export let path: string = null;
</script>
<Container>
<div class="u-flex u-main-space-between common-section">
<Heading tag="h2" size="5">{title}</Heading>
<DropTabs>
<DropTabsItem on:click={() => (range = '24h')} disabled={range === '24h'}>
<DropTabsLink href={`${path}/24h`} disabled={$page.params.period === '24h'}>
24h
</DropTabsItem>
<DropTabsItem on:click={() => (range = '30d')} disabled={range === '30d'}>
</DropTabsLink>
<DropTabsLink
href={`${path}/30d`}
disabled={!$page.params.period || $page.params.period === '30d'}>
30d
</DropTabsItem>
<DropTabsItem on:click={() => (range = '90d')} disabled={range === '90d'}>
</DropTabsLink>
<DropTabsLink href={`${path}/90d`} disabled={$page.params.period === '90d'}>
90d
</DropTabsItem>
</DropTabsLink>
</DropTabs>
</div>
<Card>
@@ -57,13 +60,15 @@
<Heading tag="h6" size="6">{last(count).value}</Heading>
<p>{countMetadata.title}</p>
<div class="u-margin-block-start-16" />
<BarChart
series={[
{
name: countMetadata.legend,
data: [...count.map((e) => [e.date, e.value])]
}
]} />
<div style="height: 12rem;">
<BarChart
series={[
{
name: countMetadata.legend,
data: [...count.map((e) => [e.date, e.value])]
}
]} />
</div>
{/if}
</Card>
<Tiles>
@@ -72,14 +77,16 @@
<Heading tag="h6" size="6">{total(created)}</Heading>
<p>{createdMetadata.title}</p>
<div class="u-margin-block-start-16" />
<LineChart
series={[
{
name: createdMetadata.legend,
data: [...created.map((e) => [e.date, e.value])],
color: Colors.Secondary
}
]} />
<div style="height: 12rem;">
<LineChart
series={[
{
name: createdMetadata.legend,
data: [...created.map((e) => [e.date, e.value])],
color: Colors.Secondary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
@@ -87,14 +94,16 @@
<Heading tag="h6" size="6">{total(read)}</Heading>
<p>{readMetadata.title}</p>
<div class="u-margin-block-start-16" />
<LineChart
series={[
{
name: readMetadata.legend,
data: [...read.map((e) => [e.date, e.value])],
color: Colors.Tertiary
}
]} />
<div style="height: 12rem;">
<LineChart
series={[
{
name: readMetadata.legend,
data: [...read.map((e) => [e.date, e.value])],
color: Colors.Tertiary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
@@ -102,14 +111,16 @@
<Heading tag="h6" size="6">{total(updated)}</Heading>
<p>{updatedMetadata.title}</p>
<div class="u-margin-block-start-16" />
<LineChart
series={[
{
name: updatedMetadata.legend,
data: [...updated.map((e) => [e.date, e.value])],
color: Colors.Quaternary
}
]} />
<div style="height: 12rem;">
<LineChart
series={[
{
name: updatedMetadata.legend,
data: [...updated.map((e) => [e.date, e.value])],
color: Colors.Quaternary
}
]} />
</div>
{/if}
</Card>
<Card isTile>
@@ -117,14 +128,16 @@
<Heading tag="h6" size="6">{total(deleted)}</Heading>
<p>{deletedMetadata.title}</p>
<div class="u-margin-block-start-16" />
<LineChart
series={[
{
name: deletedMetadata.legend,
data: [...deleted.map((e) => [e.date, e.value])],
color: Colors.Quinary
}
]} />
<div style="height: 12rem;">
<LineChart
series={[
{
name: deletedMetadata.legend,
data: [...deleted.map((e) => [e.date, e.value])],
color: Colors.Quinary
}
]} />
</div>
{/if}
</Card>
</Tiles>
+9 -5
View File
@@ -28,7 +28,8 @@
let showExitModal = false;
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && !showExitModal) {
const openDialog = document.querySelectorAll('dialog[open]');
if (event.key === 'Escape' && !showExitModal && !openDialog?.length) {
event.preventDefault();
dispatch('exit');
wizard.hide();
@@ -102,7 +103,9 @@
{currentStep} />
</aside>
<div class="wizard-media">
<slot name="media" />
{#if $wizard.media}
<img src={$wizard.media} alt="wizard media" loading="lazy" />
{/if}
</div>
<div class="wizard-main">
<Form noStyle on:submit={submit}>
@@ -114,11 +117,12 @@
<div class="form-footer">
<div class="u-flex u-main-end u-gap-12">
{#if !isLastStep && sortedSteps[currentStep - 1][1].optional}
<Button text on:click={() => dispatch('finish')}
>Skip optional steps</Button>
<Button text on:click={() => dispatch('finish')}>
Skip optional steps
</Button>
{/if}
{#if currentStep === 1}
<Button secondary on:click={wizard.hide}>Cancel</Button>
<Button secondary on:click={handleExit}>Cancel</Button>
<Button submit>Next</Button>
{:else if isLastStep}
<Button secondary on:click={() => currentStep--}>Back</Button>
+12 -14
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { Button } from '$lib/elements/forms';
import { createEventDispatcher } from 'svelte';
export let show = false;
@@ -11,16 +11,14 @@
}
</script>
<Form noMargin on:submit={handleSubmit}>
<Modal bind:show warning>
<svelte:fragment slot="header">Exit Process</svelte:fragment>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
irreversible.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Exit</Button>
</svelte:fragment>
</Modal>
</Form>
<Modal bind:show on:submit={handleSubmit} warning>
<svelte:fragment slot="header">Exit Process</svelte:fragment>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
irreversible.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Exit</Button>
</svelte:fragment>
</Modal>
+2 -2
View File
@@ -1,7 +1,7 @@
<script>
<script lang="ts">
import { Copy } from '../components';
export let value = false;
export let value: string;
</script>
<Copy {value}>
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { Modal } from '../components';
export let show = false;
+9 -59
View File
@@ -1,7 +1,5 @@
import { project } from '../../routes/console/project-[project]/store';
import { get, writable, readable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
import type { Navigation } from '@sveltejs/kit';
import { writable, readable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
export type Tab = {
href: string;
@@ -11,68 +9,20 @@ export type Tab = {
export type Breadcrumb = {
href: string;
title: string;
level?: number;
};
export type updateLayoutArguments = {
title: string;
titleDropdown?: Models.Team[];
level?: number;
tabs?: Tab[];
back?: string;
breadcrumbs?: Breadcrumb[] | Breadcrumb;
copy?: {
text: string;
value: string;
};
customBase?: string;
navigate?: Navigation;
header?: typeof SvelteComponent;
breadcrumb?: typeof SvelteComponent;
};
export const level = writable<number>();
export const title = writable<string>('');
export const titleDropdown = writable<Models.Team[]>([]);
export const backButton = writable<string>('');
export const tabs = writable<Tab[]>([]);
export const breadcrumbs = writable<Map<number, Breadcrumb>>(new Map());
export const copyData = writable({
text: '',
value: ''
});
export const header = writable<typeof SvelteComponent>();
export const breadcrumb = writable<typeof SvelteComponent>();
export function updateLayout(args: updateLayoutArguments) {
const projectId = get(project)?.$id;
const base = args?.customBase ?? projectId ? `/console/project-${projectId}` : ``;
if (args?.tabs) {
args.tabs.map((tab) => {
tab.href = `${base}/${tab.href}`;
});
}
if (args?.navigate?.to) {
const previousTabs = get(tabs);
if (previousTabs.some((t) => `${base}/${t.href}` === args.navigate.to.url.pathname)) {
return;
}
}
title.set(args.title);
titleDropdown.set(args.titleDropdown ?? []);
backButton.set(args.back ?? null);
copyData.set(args.copy ?? null);
level.set(args.level ?? null);
tabs.set(args.tabs ?? []);
if (args.breadcrumbs) {
if (!Array.isArray(args.breadcrumbs)) {
args.breadcrumbs = [args.breadcrumbs];
}
for (const breadcrumb of args.breadcrumbs) {
breadcrumbs.update((n) => n.set(breadcrumb.level ?? args.level, breadcrumb));
}
}
header.set(args.header ?? null);
breadcrumb.set(args.breadcrumb ?? null);
}
export const pageLimit = readable(12); // default page limit
export const cardLimit = readable(6); // default page limit
export const cardLimit = readable(6); // default card limit
+121 -282
View File
@@ -1,18 +1,14 @@
import { writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
import type { SvelteComponent } from 'svelte';
import Apple from '../../routes/console/project-[project]/authentication/_appleOAuth.svelte';
import Microsoft from '../../routes/console/project-[project]/authentication/_microsoftOAuth.svelte';
import Okta from '../../routes/console/project-[project]/authentication/_oktaOAuth.svelte';
import Auth0 from '../../routes/console/project-[project]/authentication/_auth0OAuth.svelte';
import Main from '../../routes/console/project-[project]/authentication/_mainOAuth.svelte';
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 Main from '../../routes/console/project-[project]/auth/_mainOAuth.svelte';
export type Provider = {
name: string;
export type Provider = Models.Provider & {
icon: string;
active: boolean;
id: string | null;
secret: string | null;
docs?: string;
component?: typeof SvelteComponent;
};
@@ -22,279 +18,122 @@ export type Providers = {
};
const setProviders = (project: Models.Project): Provider[] => {
return [
{
name: 'Amazon',
icon: 'amazon',
active: false,
id: project?.providerAmazonAppid,
secret: project?.providerAmazonSecret,
docs: 'https://developer.amazon.com/apps-and-games/services-and-apis',
component: Main
},
{
name: 'Apple',
icon: 'apple',
active: false,
id: project?.providerAppleAppid,
secret: project?.providerAppleSecret,
docs: 'https://developer.apple.com/',
component: Apple
},
{
name: 'Auth0',
icon: 'auth0',
active: false,
id: project?.providerAuth0Appid,
secret: project?.providerAuth0Secret,
docs: 'https://auth0.com/developers',
component: Auth0
},
{
name: 'Bitbucket',
icon: 'bitBucket',
active: false,
id: project?.providerBitbucketAppid,
secret: project?.providerBitbucketSecret,
docs: 'https://developer.atlassian.com/bitbucket',
component: Main
},
{
name: 'Bitly',
icon: 'bitly',
active: false,
id: project?.providerBitlyAppid,
secret: project?.providerBitlySecret,
docs: 'https://dev.bitly.com/v4_documentation.html',
component: Main
},
{
name: 'Box',
icon: 'box',
active: false,
id: project?.providerBoxAppid,
secret: project?.providerBoxSecret,
docs: 'https://developer.box.com/reference/',
component: Main
},
{
name: 'Discord',
icon: 'discord',
active: false,
id: project?.providerDiscordAppid,
secret: project?.providerDiscordSecret,
docs: 'https://discordapp.com/developers/docs/topics/oauth2',
component: Main
},
{
name: 'Dropbox',
icon: 'dropbox',
active: false,
id: project?.providerDropboxAppid,
secret: project?.providerDropboxSecret,
docs: 'https://www.dropbox.com/developers/documentation',
component: Main
},
{
name: 'Facebook',
icon: 'facebook',
active: false,
id: project?.providerFacebookAppid,
secret: project?.providerFacebookSecret,
docs: 'https://developers.facebook.com/',
component: Main
},
{
name: 'Github',
icon: 'github',
active: false,
id: project?.providerGithubAppid,
secret: project?.providerGithubSecret,
docs: 'https://developer.github.com/',
component: Main
},
{
name: 'Gitlab',
icon: 'gitlab',
active: false,
id: project?.providerGitlabAppid,
secret: project?.providerGitlabSecret,
docs: 'https://docs.gitlab.com/ee/api/',
component: Main
},
{
name: 'Google',
icon: 'google',
active: false,
id: project?.providerGoogleAppid,
secret: project?.providerGoogleSecret,
docs: 'https://support.google.com/googleapi/answer/6158849',
component: Main
},
{
name: 'Linkedin',
icon: 'linkedin',
active: false,
id: project?.providerLinkedinAppid,
secret: project?.providerLinkedinSecret,
docs: 'https://developer.linkedin.com/',
component: Main
},
{
name: 'Microsoft',
icon: 'microsoft',
active: false,
id: project?.providerMicrosoftAppid,
secret: project?.providerMicrosoftSecret,
docs: 'https://developer.microsoft.com/en-us/',
component: Microsoft
},
{
name: 'Notion',
icon: 'notion',
active: false,
id: project?.providerNotionAppid,
secret: project?.providerNotionSecret,
docs: 'https://developers.notion.com/docs',
component: Main
},
{
name: 'Okta',
icon: 'okta',
active: false,
id: project?.providerOktaAppid,
secret: project?.providerOktaSecret,
docs: 'https://developer.okta.com/',
component: Okta
},
{
name: 'Paypal',
icon: 'paypal',
active: false,
id: project?.providerPaypalAppid,
secret: project?.providerPaypalSecret,
docs: 'https://developer.paypal.com/docs/api/overview/',
component: Main
},
{
name: 'Paypal (sandbox)',
icon: 'paypal',
active: false,
id: project?.providerPaypalSandboxAppid,
secret: project?.providerPaypalSandboxSecret,
docs: 'https://developer.paypal.com/docs/api/overview/',
component: Main
},
{
name: 'Salesforce',
icon: 'salesforce',
active: false,
id: project?.providerSalesforceAppid,
secret: project?.providerSalesforceSecret,
docs: 'https://developer.salesforce.com/docs/',
component: Main
},
{
name: 'Slack',
icon: 'slack',
active: false,
id: project?.providerSlackAppid,
secret: project?.providerSlackSecret,
docs: 'https://api.slack.com/',
component: Main
},
{
name: 'Spotify',
icon: 'spotify',
active: false,
id: project?.providerSpotifyAppid,
secret: project?.providerSpotifySecret,
docs: 'https://developer.spotify.com/documentation/general/guides/authorization-guide/',
component: Main
},
{
name: 'Stripe',
icon: 'stripe',
active: false,
id: project?.providerStripeAppid,
secret: project?.providerStripeSecret,
docs: 'https://stripe.com/docs/api',
component: Main
},
{
name: 'Tradeshift',
icon: 'tradeshift',
active: false,
id: project?.providerTradeshiftAppid,
secret: project?.providerTradeshiftSecret,
docs: 'https://developers.tradeshift.com/docs/api',
component: Main
},
{
name: 'Tradeshift(sandbox)',
icon: 'tradeshift',
active: false,
id: project?.providerTradeshiftBoxAppid,
secret: project?.providerTradeshiftBoxSecret,
docs: 'https://developers.tradeshift.com/docs/api',
component: Main
},
{
name: 'Twitch',
icon: 'twitch',
active: false,
id: project?.providerTwitchAppid,
secret: project?.providerTwitchSecret,
docs: 'https://dev.twitch.tv/docs/authentication',
component: Main
},
return (
project?.providers.map((n) => {
let docs: Provider['docs'];
let icon: Provider['icon'] = n.name.toLowerCase();
let component: Provider['component'] = Main;
{
name: 'Wordpress',
icon: 'wordpress',
active: false,
id: project?.providerWordpressAppid,
secret: project?.providerWordpressSecret,
docs: 'https://developer.wordpress.com/docs/oauth2/',
component: Main
},
{
name: 'Yahoo',
icon: 'yahoo',
active: false,
id: project?.providerYahooAppid,
secret: project?.providerYahooSecret,
docs: 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
component: Main
},
{
name: 'Yammer',
icon: 'ms_yammer',
active: false,
id: project?.providerYammerAppid,
secret: project?.providerYammerSecret,
docs: 'https://developer.yammer.com/docs/oauth-2',
component: Main
},
{
name: 'Yandex',
icon: 'yandex',
active: false,
id: project?.providerYandexAppid,
secret: project?.providerYandexSecret,
docs: 'https://tech.yandex.com/oauth/',
component: Main
},
{
name: 'Zoom',
icon: 'zoom',
active: false,
id: project?.providerZoomAppid,
secret: project?.providerZoomSecret,
docs: 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
component: Main
}
];
switch (n.name.toLowerCase()) {
case 'amazon':
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
break;
case 'apple':
docs = 'https://developer.apple.com/';
component = Apple;
break;
case 'auth0':
docs = 'https://auth0.com/developers';
component = Auth0;
break;
case 'bitbucket':
docs = 'https://developer.atlassian.com/bitbucket';
break;
case 'bitly':
docs = 'https://dev.bitly.com/v4_documentation.html';
break;
case 'box':
docs = 'https://developer.box.com/reference/';
break;
case 'discord':
docs = 'https://discordapp.com/developers/docs/topics/oauth2';
break;
case 'dropbox':
docs = 'https://www.dropbox.com/developers/documentation';
break;
case 'facebook':
docs = 'https://developers.facebook.com/';
break;
case 'github':
docs = 'https://developer.github.com';
break;
case 'gitlab':
docs = 'https://docs.gitlab.com/ee/api/';
break;
case 'google':
docs = 'https://support.google.com/googleapi/answer/6158849';
break;
case 'linkedin':
docs = 'https://developer.linkedin.com/';
break;
case 'microsoft':
docs = 'https://developer.microsoft.com/en-us/';
component = Microsoft;
break;
case 'notion':
docs = 'https://developers.notion.com/docs';
break;
case 'okta':
docs = 'https://developer.okta.com';
component = Okta;
break;
case 'paypal':
docs = 'https://developer.paypal.com/docs/api/overview/';
break;
case 'paypalsandbox':
icon = 'paypal';
docs = 'https://developer.paypal.com/docs/api/overview/';
break;
case 'salesforce':
docs = 'https://developer.salesforce.com/docs/';
break;
case 'slack':
docs = 'https://api.slack.com/';
break;
case 'spotify':
docs =
'https://developer.spotify.com/documentation/general/guides/authorization-guide/';
break;
case 'stripe':
docs = 'https://stripe.com/docs/api';
break;
case 'tradeshift':
docs = 'https://developers.tradeshift.com/docs/api';
break;
case 'tradeshiftbox':
icon = 'tradeshift';
docs = 'https://developers.tradeshift.com/docs/api';
break;
case 'twitch':
docs = 'https://dev.twitch.tv/docs/auth';
break;
case 'wordpress':
docs = 'https://developer.wordpress.com/docs/oauth2/';
break;
case 'yahoo':
docs = 'https://developer.yahoo.com/oauth2/guide/flows_authcode/';
break;
case 'yammer':
docs = 'https://developer.yammer.com/docs/oauth-2';
break;
case 'yandex':
docs = 'https://tech.yandex.com/oauth/';
break;
case 'zoom':
docs = 'https://marketplace.zoom.us/docs/guides/auth/oauth/';
break;
default:
break;
}
return {
...n,
icon,
docs,
component
};
}) ?? []
);
};
function createOAuthProviders() {
+8 -80
View File
@@ -1,84 +1,12 @@
import { sdkForConsole } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { derived, writable } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { get } from 'svelte/store';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { cachedStore } from '$lib/helpers/cache';
export const newOrgModal = writable<boolean>(false);
export const newMemberModal = writable<boolean>(false);
export const organizationList = cachedStore<
Models.TeamList,
{
load: () => Promise<void>;
}
>('organizationList', function ({ set }) {
return {
load: async () => {
const response = await sdkForConsole.teams.list();
set(response);
}
};
});
export const organization = cachedStore<
Models.Team,
{
load: (teamId: string) => Promise<void>;
}
>('organization', function ({ set }) {
return {
load: async (teamId) => {
const response = await sdkForConsole.teams.get(teamId);
set(response);
}
};
});
export const projectList = cachedStore<
Models.ProjectList,
{
load: (queries: string[], search?: string) => Promise<void>;
}
>('projectList', function ({ set }) {
return {
load: async (queries, search) => {
const response = await sdkForConsole.projects.list(queries, search);
set(response);
}
};
});
export const memberList = cachedStore<
Models.MembershipList,
{
load: (teamId: string, queries?: string[], search?: string) => Promise<void>;
}
>('memberList', function ({ set }) {
return {
load: async (teamId, queries, search) => {
const response = await sdkForConsole.teams.listMemberships(teamId, queries, search);
set(response);
}
};
});
export const redirectTo = async () => {
let org = get(organization);
if (org) {
await goto(`${base}/console/organization-${org.$id}`);
} else {
await organizationList.load();
const orgList = get(organizationList);
if (orgList?.total) {
await organization.load(orgList.teams[0].$id);
org = get(organization);
await goto(`${base}/console/organization-${org.$id}`);
} else {
newOrgModal.set(true);
await goto(`${base}/console`);
}
}
};
export const organizationList = derived(
page,
($page) => $page.data.organizations as Models.TeamList
);
export const organization = derived(page, ($page) => $page.data.organization as Models.Team);
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
+2 -1
View File
@@ -12,7 +12,8 @@ import {
Users
} from '@aw-labs/appwrite-console';
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT.toString();
const endpoint =
import.meta.env.VITE_APPWRITE_ENDPOINT?.toString() ?? `${globalThis?.location?.origin}/v1`;
const clientConsole = new Client();
clientConsole.setEndpoint(endpoint).setProject('console');
+6 -22
View File
@@ -1,24 +1,8 @@
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import type { Models } from '@aw-labs/appwrite-console';
import { writable } from 'svelte/store';
import { sdkForConsole } from './sdk';
function createUserStore() {
const { subscribe, set } = writable<Models.Account<Record<string, unknown>>>();
return {
subscribe,
set,
fetchUser: async () => {
set(await sdkForConsole.account.get());
},
logout: async () => {
await sdkForConsole.account.deleteSession('current');
/**
* Clear cache.
*/
globalThis.sessionStorage.clear();
}
};
}
export const user = createUserStore();
export const user = derived(
page,
($page) => $page.data.account as Models.Account<Record<string, string>>
);
+8 -3
View File
@@ -3,23 +3,27 @@ import { writable } from 'svelte/store';
export type WizardStore = {
show: boolean;
media?: string;
component?: typeof SvelteComponent;
interceptor?: () => Promise<void>;
};
function createWizardStore() {
const { subscribe, update } = writable<WizardStore>({
const { subscribe, update, set } = writable<WizardStore>({
show: false,
component: null,
interceptor: null
interceptor: null,
media: null
});
return {
subscribe,
start: (component: typeof SvelteComponent) =>
set,
start: (component: typeof SvelteComponent, media: string = null) =>
update((n) => {
n.show = true;
n.component = component;
n.media = media;
return n;
}),
@@ -34,6 +38,7 @@ function createWizardStore() {
update((n) => {
n.show = false;
n.component = null;
n.media = null;
return n;
})
-2
View File
@@ -1,2 +0,0 @@
import '@aw-labs/ui/src/_index.scss';
import 'tippy.js/dist/tippy.css';
+21 -26
View File
@@ -2,25 +2,21 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { redirectTo } from '$lib/stores/organization';
import { onCLS, onFID, onLCP, onFCP, onINP, onTTFB } from 'web-vitals';
import { reportWebVitals } from '$lib/helpers/vitals';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { browser, dev } from '$app/environment';
import { app } from '$lib/stores/app';
import Notifications from '$lib/layout/notifications.svelte';
import Loading from './_loading.svelte';
let loaded = false;
import { Progress, Notifications } from '$lib/layout';
import { loading } from './store';
import Loading from './loading.svelte';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
window.GOOGLE_ANALYTICS = import.meta.env.VITE_GOOGLE_ANALYTICS?.toString() ?? false;
}
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
onMount(async () => {
/**
* Reporting Web Vitals.
@@ -34,28 +30,25 @@
onTTFB(reportWebVitals);
}
try {
if (!$user) {
await user.fetchUser();
} else {
user.fetchUser();
}
/**
* Handle initial load.
*/
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
if ($user) {
if (!$page.url.pathname.startsWith('/console')) {
await redirectTo();
await goto(`${base}/console`, {
replaceState: true
});
}
} catch (error) {
} else {
if (acceptedRoutes.includes($page.url.pathname)) {
await goto(`${base}${$page.url.pathname}${$page.url.search}`);
} else {
/**
* Clear cache.
*/
window.sessionStorage.clear();
await goto(`${base}/login`);
await goto(`${base}/login`, {
replaceState: true
});
}
} finally {
loaded = true;
loading.set(false);
}
});
@@ -96,12 +89,14 @@
<Notifications />
{#if loaded}
<slot />
{:else}
<slot />
{#if $loading}
<Loading />
{/if}
<Progress />
<style lang="scss" global>
.tippy-box {
--p-tooltip-text-color: var(--color-neutral-10);
+26
View File
@@ -0,0 +1,26 @@
import '@aw-labs/ui/src/_index.scss';
import 'tippy.js/dist/tippy.css';
import { sdkForConsole } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
try {
const account = await sdkForConsole.account.get();
return {
account,
organizations: sdkForConsole.teams.list()
};
} catch (error) {
const acceptedRoutes = ['/login', '/register', '/recover', '/invite'];
if (!acceptedRoutes.includes(url.pathname)) {
throw redirect(303, '/login');
}
}
};
+2 -1
View File
@@ -1,6 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import type { PageLoad } from './$types';
export const load = async () => {
export const load: PageLoad = async () => {
throw redirect(302, `${base}/login`);
};
+10 -33
View File
@@ -2,36 +2,18 @@
import Shell from '$lib/layout/shell.svelte';
import SideNavigation from '$lib/layout/navigation.svelte';
import Header from '$lib/layout/header.svelte';
import { updateLayout } from '$lib/stores/layout';
import {
organization,
organizationList,
newOrgModal,
redirectTo
} from '$lib/stores/organization';
import Create from './_createOrganization.svelte';
import { newOrgModal } from '$lib/stores/organization';
import Create from './createOrganization.svelte';
import { page } from '$app/stores';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { onMount } from 'svelte';
import { beforeNavigate } from '$app/navigation';
import { wizard } from '$lib/stores/wizard';
import { Logs } from '$lib/layout';
import { log } from '$lib/stores/logs';
import { onMount } from 'svelte';
import { loading } from '../store';
updateLayout({
title: $organization?.name ?? '',
level: 0
});
onMount(async () => {
if ($page.url.pathname === '/console' && !$newOrgModal) {
await redirectTo();
}
});
afterNavigate(async () => {
if ($page.url.pathname === '/console' && !$newOrgModal) {
await redirectTo();
}
onMount(() => {
loading.set(false);
});
beforeNavigate(() => {
@@ -44,14 +26,11 @@
}
</script>
<svelte:head>
<title>Console - Appwrite</title>
</svelte:head>
<Shell
showSideNavigation={$page.url.pathname !== '/console' &&
!$page?.params.organization &&
!$page.url.pathname.includes('/console/account')}>
!$page.url.pathname.includes('/console/account') &&
!$page.url.pathname.includes('/console/onboarding')}>
<svelte:fragment slot="header">
<Header />
</svelte:fragment>
@@ -66,9 +45,7 @@
<svelte:component this={$wizard.component} />
{/if}
{#if $newOrgModal}
<Create bind:show={$newOrgModal} closable={!!$organizationList?.total} />
{/if}
<Create bind:show={$newOrgModal} />
{#if $log.show}
<Logs />
View File
+13
View File
@@ -0,0 +1,13 @@
import { base } from '$app/paths';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { organizations } = await parent();
if (organizations.total) {
const teamId = localStorage.getItem('organization') ?? organizations.teams[0].$id;
throw redirect(303, `${base}/console/organization-${teamId}`);
} else {
throw redirect(303, `${base}/console/onboarding`);
}
};
@@ -1,70 +0,0 @@
<script lang="ts">
import { Modal, CustomId } from '$lib/components';
import { Pill } from '$lib/elements';
import { InputText, Button, Form, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { organization, organizationList } from '$lib/stores/organization';
import { titleDropdown } from '$lib/stores/layout';
import { goto } from '$app/navigation';
export let show = false;
export let closable = true;
const dispatch = createEventDispatcher();
let name: string;
let id: string;
let showCustomId = false;
let error: string;
const create = async () => {
try {
const team = await sdkForConsole.teams.create(id ?? 'unique()', name);
dispatch('created');
await organizationList.load();
titleDropdown.set($organizationList.teams);
organization.set(team);
await goto(`/console/organization-${$organization.$id}`);
addNotification({
type: 'success',
message: `${name} has been created`
});
name = null;
id = null;
show = false;
} catch ({ message }) {
error = message;
}
};
</script>
<Form on:submit={create}>
<Modal {error} size="big" bind:show {closable}>
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
<FormList>
<InputText
id="name"
label="Name"
placeholder="Enter name"
bind:value={name}
autofocus={true}
required />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Organization ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="organization" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
<Button disabled={!closable} secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
</Form>
-43
View File
@@ -1,46 +1,3 @@
<script lang="ts">
import { updateLayout } from '$lib/stores/layout';
import { user } from '$lib/stores/user';
import { afterNavigate } from '$app/navigation';
import { onMount } from 'svelte';
const path = 'console/account';
onMount(handle);
afterNavigate(handle);
async function handle(event = null) {
updateLayout({
navigate: event,
title: $user?.name,
level: 0,
customBase: '',
breadcrumbs: {
title: `${$user.name}`,
href: path
},
tabs: [
{
href: path,
title: 'Overview'
},
{
href: `${path}/sessions`,
title: 'Sessions'
},
{
href: `${path}/activity`,
title: 'Activity'
},
{
href: `${path}/organizations`,
title: 'Organizations'
}
]
});
}
</script>
<svelte:head>
<title>User - Appwrite</title>
</svelte:head>
+10
View File
@@ -0,0 +1,10 @@
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
return {
header: Header,
breadcrumbs: Breadcrumbs
};
};
+7 -10
View File
@@ -6,9 +6,10 @@
import { user } from '$lib/stores/user';
import { sdkForConsole } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { title, breadcrumbs } from '$lib/stores/layout';
import { base } from '$app/paths';
import Delete from './_delete.svelte';
import Delete from './delete.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
let name: string = null,
email: string = null,
@@ -25,11 +26,7 @@
async function updateName() {
try {
await sdkForConsole.account.updateName(name);
$user.name = name;
title.set(name);
const breadcrumb = $breadcrumbs.get(0);
breadcrumb.title = name;
$breadcrumbs = $breadcrumbs.set($breadcrumbs.size, breadcrumb);
invalidate(Dependencies.ACCOUNT);
addNotification({
message: 'Name has been updated',
type: 'success'
@@ -44,7 +41,7 @@
async function updateEmail() {
try {
await sdkForConsole.account.updateEmail(email, emailPassword);
$user.email = email;
invalidate(Dependencies.ACCOUNT);
addNotification({
message: 'Email has been updated',
type: 'success'
@@ -158,7 +155,7 @@
</svelte:fragment>
</CardGrid>
</Form>
<CardGrid>
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Delete Account</Heading>
</div>
@@ -172,7 +169,7 @@
<AvatarInitials size={48} name={$user.name} />
</svelte:fragment>
<svelte:fragment slot="title">
<h6 class="u-bold">{$user.name}</h6>
<h6 class="u-bold u-trim-1">{$user.name}</h6>
</svelte:fragment>
</Box>
</svelte:fragment>
@@ -0,0 +1,9 @@
<script lang="ts">
import { Activity } from '$lib/layout';
import type { PageData } from './$types';
export let data: PageData;
const path = '/console/account/activity';
</script>
<Activity {path} logs={data.logs} offset={data.offset} />
@@ -0,0 +1,15 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const page = Number(params.page);
const offset = pageToOffset(page, PAGE_LIMIT);
return {
offset,
logs: await sdkForConsole.account.listLogs([Query.offset(offset), Query.limit(PAGE_LIMIT)])
};
};
@@ -0,0 +1,13 @@
<script lang="ts">
import { Breadcrumbs } from '$lib/layout';
import { user } from '$lib/stores/user';
$: breadcrumbs = [
{
href: `/console/account`,
title: $user?.name
}
];
</script>
<Breadcrumbs {breadcrumbs} />
@@ -1,6 +1,6 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
@@ -24,13 +24,11 @@
};
</script>
<Form on:submit={deleteAccount}>
<Modal bind:show={showDelete} warning>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
</Form>
<Modal bind:show={showDelete} on:submit={deleteAccount} warning>
<svelte:fragment slot="header">Delete Account</svelte:fragment>
<p>Are you sure you want to delete your account?</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
+57
View File
@@ -0,0 +1,57 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Tab, Tabs } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { isTabSelected } from '$lib/helpers/load';
import { Cover, CoverTitle } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
const path = `/console/account`;
$: tabs = [
{
href: path,
title: 'Overview'
},
{
href: `${path}/sessions`,
title: 'Sessions'
},
{
href: `${path}/activity`,
title: 'Activity',
hasChildren: true
},
{
href: `${path}/organizations`,
title: 'Organizations',
hasChildren: true
}
];
async function logout() {
await sdkForConsole.account.deleteSession('current');
await goto(`${base}/login`);
}
</script>
<Cover>
<svelte:fragment slot="header">
<CoverTitle>
{$user.name}
</CoverTitle>
<div class="u-margin-inline-start-auto">
<Button secondary on:click={logout}>Logout</Button>
</div>
</svelte:fragment>
<Tabs>
{#each tabs as tab}
<Tab href={tab.href} selected={isTabSelected(tab, $page.url.pathname, path, tabs)}>
{tab.title}
</Tab>
{/each}
</Tabs>
</Cover>
@@ -10,16 +10,12 @@
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import CreateOrganization from '../../_createOrganization.svelte';
import { organizationList } from '$lib/stores/organization';
import { onMount } from 'svelte';
import CreateOrganization from '../../../createOrganization.svelte';
import { sdkForConsole } from '$lib/stores/sdk';
import { cardLimit } from '$lib/stores/layout';
import { createPersistentPagination } from '$lib/stores/pagination';
import { CARD_LIMIT } from '$lib/constants';
import type { PageData } from './$types';
onMount(async () => {
await organizationList.load();
});
export let data: PageData;
const getMemberships = async (teamId: string) => {
const memberships = await sdkForConsole.teams.listMemberships(teamId);
@@ -27,7 +23,6 @@
};
let addOrganization = false;
const offset = createPersistentPagination($cardLimit);
</script>
<Container>
@@ -43,17 +38,18 @@
</Button>
</div>
{#if $organizationList?.teams?.length}
{#if data.organizations.teams.length}
<CardContainer
total={$organizationList.total}
offset={$offset}
total={data.organizations.total}
offset={data.offset}
on:click={() => (addOrganization = true)}>
{#each $organizationList.teams as organization, index}
{#each data.organizations.teams as organization, index}
{@const avatarList = getMemberships(organization.$id)}
{#if index >= $offset && index < $cardLimit + $offset}
{#if index >= data.offset && index < CARD_LIMIT + data.offset}
<GridItem1 href={`${base}/console/organization-${organization.$id}`}>
<svelte:fragment slot="eyebrow"
>{organization?.total ? organization?.total : 'No'} projects</svelte:fragment>
<svelte:fragment slot="eyebrow">
{organization?.total ? organization?.total : 'No'} projects
</svelte:fragment>
<svelte:fragment slot="title">
{organization.name}
</svelte:fragment>
@@ -70,13 +66,17 @@
</svelte:fragment>
</CardContainer>
{:else}
<Empty isButton single on:click={() => (addOrganization = true)}>
<Empty single on:click={() => (addOrganization = true)}>
<p>Create a new organization</p>
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$organizationList?.total}</p>
<Pagination limit={$cardLimit} bind:offset={$offset} sum={$organizationList?.total} />
<p class="text">Total results: {data.organizations.total}</p>
<Pagination
limit={CARD_LIMIT}
path="console/account/organizations"
offset={data.offset}
sum={data.organizations.total} />
</div>
</Container>
@@ -0,0 +1,18 @@
import { Query } from '@aw-labs/appwrite-console';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const page = Number(params.page);
const offset = pageToOffset(page, PAGE_LIMIT);
return {
offset,
organizations: await sdkForConsole.teams.list([
Query.offset(offset),
Query.limit(PAGE_LIMIT)
])
};
};
@@ -1,5 +1,5 @@
<script lang="ts">
import { Empty, Heading, Pagination } from '$lib/components';
import { Empty, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import {
@@ -13,17 +13,12 @@
} from '$lib/elements/table';
import { Container } from '$lib/layout';
import { sdkForConsole } from '$lib/stores/sdk';
import { pageLimit } from '$lib/stores/layout';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import type { Models } from '@aw-labs/appwrite-console';
import { accountSession } from '../store';
import { onMount } from 'svelte';
import { createPersistentPagination } from '$lib/stores/pagination';
import type { PageData } from './$types';
const offset = createPersistentPagination($pageLimit);
onMount(async () => {
await accountSession.load();
});
export let data: PageData;
const getBrowser = (clientCode: string) => {
return sdkForConsole.avatars.getBrowser(clientCode, 40, 40);
@@ -31,11 +26,11 @@
const logout = async (session: Models.Session) => {
await sdkForConsole.account.deleteSession(session.$id);
await accountSession.load();
invalidate(Dependencies.ACCOUNT_SESSIONS);
};
const logoutAll = async () => {
await sdkForConsole.account.deleteSessions();
await accountSession.load();
invalidate(Dependencies.ACCOUNT_SESSIONS);
};
</script>
@@ -48,7 +43,7 @@
</Button>
</div>
{#if $accountSession?.total}
{#if data.sessions.total}
<Table>
<TableHeader>
<TableCellHead>Client</TableCellHead>
@@ -57,10 +52,10 @@
<TableCellHead width={100} />
</TableHeader>
<TableBody>
{#each $accountSession.sessions as session}
{#each data.sessions.sessions as session}
<TableRow>
<TableCell title="Client">
<div class="u-flex u-gap-12">
<div class="u-flex u-gap-12 u-cross-center">
<div class="u-flex u-cross-center u-gap-12">
{#if session.clientName}
<div class="avatar is-small">
@@ -113,7 +108,7 @@
<Button
external
secondary
href="https://appwrite.io/docs/server/authentication?sdk=nodejs-default#usersGetsessions">
href="https://appwrite.io/docs/server/auth?sdk=nodejs-default#usersGetsessions">
Documentation
</Button>
</div>
@@ -121,7 +116,7 @@
</Empty>
{/if}
<div class="u-flex u-margin-block-start-32 u-main-space-between">
<p class="text">Total results: {$accountSession?.total}</p>
<Pagination limit={$pageLimit} bind:offset={$offset} sum={$accountSession?.total} />
<p class="text">Total results: {data.sessions.total}</p>
<!-- <Pagination limit={PAGE_LIMIT} offset={data.offset} sum={data.sessions.total} /> -->
</div>
</Container>
@@ -0,0 +1,11 @@
import { Dependencies } from '$lib/constants';
import { sdkForConsole } from '$lib/stores/sdk';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ depends }) => {
depends(Dependencies.ACCOUNT_SESSIONS);
return {
sessions: await sdkForConsole.account.listSessions()
};
};
-30
View File
@@ -1,30 +0,0 @@
import { cachedStore } from '$lib/helpers/cache';
import { sdkForConsole } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
export const accountSession = cachedStore<
Models.SessionList,
{
load: () => Promise<void>;
}
>('accountSession', function ({ set }) {
return {
load: async () => {
const response = await sdkForConsole.account.listSessions();
set(response);
}
};
});
export const accountActivity = cachedStore<
Models.LogList,
{
load: (queries: string[]) => Promise<void>;
}
>('accountActivity', function ({ set }) {
return {
load: async (queries) => {
const response = await sdkForConsole.account.listLogs(queries);
set(response);
}
};
});
@@ -0,0 +1,65 @@
<script lang="ts">
import { Modal, CustomId } from '$lib/components';
import { Pill } from '$lib/elements';
import { InputText, Button, FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
export let show = false;
let name: string;
let id: string;
let showCustomId = false;
let error: string;
const dispatch = createEventDispatcher();
const create = async () => {
try {
const org = await sdkForConsole.teams.create(id ?? 'unique()', name);
await invalidate(Dependencies.ACCOUNT);
dispatch('created');
await goto(`/console/organization-${org.$id}`);
addNotification({
type: 'success',
message: `${name} has been created`
});
name = null;
id = null;
show = false;
} catch ({ message }) {
error = message;
}
};
</script>
<Modal {error} on:submit={create} size="big" bind:show>
<svelte:fragment slot="header">Create New Organization</svelte:fragment>
<FormList>
<InputText
id="name"
label="Name"
placeholder="Enter name"
bind:value={name}
autofocus={true}
required />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Organization ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Organization" bind:id />
{/if}
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,5 @@
<svelte:head>
<title>Onboarding - Appwrite</title>
</svelte:head>
<slot />
+8
View File
@@ -0,0 +1,8 @@
import Header from './header.svelte';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
return {
header: Header
};
};
@@ -0,0 +1,70 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { Card } from '$lib/components';
import CustomId from '$lib/components/customId.svelte';
import { Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button, Form, InputText } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
import { sdkForConsole } from '$lib/stores/sdk';
import { ID } from '@aw-labs/appwrite-console';
let name: string;
let id: string;
let showCustomId = false;
let loading = false;
async function createProject() {
try {
loading = true;
const org = await createOrganization();
const project = await sdkForConsole.projects.create(
id ?? ID.unique(),
name,
org.$id,
'default'
);
await invalidate(Dependencies.ACCOUNT);
goto(`/console/project-${project.$id}`);
} catch ({ message }) {
addNotification({
message,
type: 'error'
});
} finally {
loading = false;
}
}
async function createOrganization() {
return await sdkForConsole.teams.create(ID.unique(), 'Personal Projects');
}
</script>
<Container overlapCover size="large">
<Card>
<Form on:submit={createProject}>
<FormList>
<InputText
id="name"
label="Project name"
placeholder="First Appwrite Project"
disabled={loading}
bind:value={name} />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Project ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Project ID" bind:id />
{/if}
<Button fullWidth submit disabled={loading}>Create project</Button>
</FormList>
</Form>
</Card>
</Container>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Cover, CoverTitle } from '$lib/layout';
</script>
<Cover size="large">
<CoverTitle>Let's create your first project</CoverTitle>
</Cover>
@@ -1,70 +1,14 @@
<script lang="ts">
import { pageLimit, updateLayout } from '$lib/stores/layout';
import { onMount } from 'svelte';
import { afterNavigate } from '$app/navigation';
import {
organizationList,
organization,
memberList,
newOrgModal,
newMemberModal
} from '$lib/stores/organization';
import CreateMember from './_createMember.svelte';
import Create from '../_createOrganization.svelte';
import { page } from '$app/stores';
import { Query } from '@aw-labs/appwrite-console';
$: organizationId = $page.params.organization;
$: path = `console/organization-${organizationId}`;
onMount(handle);
afterNavigate(handle);
async function handle(event = null) {
const promises = Promise.all([
organization.load(organizationId),
memberList.load(organizationId, [Query.limit($pageLimit)])
]);
if ($organization?.$id !== organizationId) {
await promises;
}
updateLayout({
navigate: event,
title: $organization?.name,
titleDropdown: $organizationList.teams,
level: 0,
customBase: '',
breadcrumbs: {
title: `${$organization.name}`,
href: path
},
tabs: [
{
href: path,
title: 'Projects'
},
{
href: `${path}/members`,
title: 'Members'
},
{
href: `${path}/settings`,
title: 'Settings'
}
]
});
}
import { newOrgModal, newMemberModal } from '$lib/stores/organization';
import CreateMember from './createMember.svelte';
import Create from '../createOrganization.svelte';
</script>
<svelte:head>
<title>Organizations - Appwrite</title>
</svelte:head>
{#if $organization}
<slot />
{/if}
<slot />
<Create bind:show={$newOrgModal} />
<CreateMember bind:showCreate={$newMemberModal} />

Some files were not shown because too many files have changed in this diff Show More