feat: command center POC

This commit is contained in:
tglide
2023-05-15 19:36:54 +01:00
parent fa2edcb5a5
commit fc2932bef8
11 changed files with 455 additions and 18 deletions
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
export let show = false;
export let size: 'small' | 'big' = null;
export let error: string = null;
export let closable = true;
export let headerDivider = true;
let dialog: HTMLDialogElement;
let alert: HTMLElement;
const dispatch = createEventDispatcher();
onMount(() => {
if (show) openModal();
});
onDestroy(() => {
if (show) closeModal();
});
function handleBLur(event: MouseEvent) {
if (event.target === dialog) {
trackEvent('click_close_modal', {
from: 'backdrop'
});
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();
trackEvent('click_close_modal', {
from: 'escape'
});
closeModal();
}
}
$: if (show) {
openModal();
} else {
closeModal();
}
$: if (error) {
alert?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
}
</script>
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
<dialog
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}
bind:this={dialog}
on:cancel|preventDefault>
{#if show}
<slot />
{/if}
</dialog>
<style>
.modal {
padding: 0.5rem;
}
</style>
+1
View File
@@ -54,3 +54,4 @@ export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
export { default as ClickableList } from './clickableList.svelte';
export { default as ClickableListItem } from './clickableListItem.svelte';
export { default as Id } from './id.svelte';
export { default as Dialog } from './dialog.svelte';
+98
View File
@@ -0,0 +1,98 @@
import { onDestroy } from 'svelte';
import { derived, writable } from 'svelte/store';
type Command = {
keys: string[];
/* Ctrl on Windows/Linux, Meta on Mac */
meta?: boolean;
shift?: boolean;
alt?: boolean;
callback: () => void;
label?: string;
disabled?: boolean;
forceEnable?: boolean;
};
type CommandCenterState = {
commandMap: Map<string, Command[]>;
enabled: boolean;
};
export const commandCenter = (function init() {
const store = writable<CommandCenterState>({
commandMap: new Map(),
enabled: true
});
return {
...store
};
})();
export const commands = derived(commandCenter, ($commandCenter) => {
return Array.from($commandCenter.commandMap.values()).flat();
});
export function CommandRegistrant() {
const uuid = crypto.randomUUID();
onDestroy(() => {
commandCenter.update((curr) => {
curr.commandMap.delete(uuid);
return curr;
});
});
return {
register(newCommands: Command[]) {
commandCenter.update((curr) => {
curr.commandMap.set(uuid, newCommands);
return curr;
});
}
};
}
export function ExtendCommandRegistrant(baseCommands: Command[]) {
return () => {
const registrant = CommandRegistrant();
return {
register: (newCommands?: Command[]) => {
registrant.register([...baseCommands, ...(newCommands ?? [])]);
}
};
};
}
export const commandCenterKeyDownHandler = derived(commandCenter, ({ commandMap, enabled }) => {
const commandsArr = Array.from(commandMap.values()).flat();
return (event: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
for (const command of commandsArr) {
if (command.disabled || (!enabled && !command.forceEnable)) continue;
const { keys, meta, shift, alt, callback } = command;
const keyCode = event.keyCode;
const isMetaPressed = meta ? (isMac ? event.metaKey : event.ctrlKey) : true;
const isShiftPressed = shift ? event.shiftKey : true;
const isAltPressed = alt ? event.altKey : true;
const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0));
if (
commandKeyCodes.includes(keyCode) &&
isMetaPressed &&
isShiftPressed &&
isAltPressed
) {
event.preventDefault();
callback();
return;
}
}
};
});
+29
View File
@@ -15,6 +15,31 @@
import { loading } from '../store';
import Create from './createOrganization.svelte';
import { goto } from '$app/navigation';
import { commandCenterKeyDownHandler, CommandRegistrant } from '$lib/helpers/commandCenter';
import CommandCenter from './commandCenter.svelte';
const { register } = CommandRegistrant();
$: register([
{
label: 'Go to Account',
callback: () => {
goto('/console/account');
},
keys: ['a'],
meta: true,
shift: true
},
{
label: 'Go to Home',
callback: () => {
goto('/console');
},
keys: ['h'],
alt: true
}
]);
onMount(() => {
loading.set(false);
@@ -46,6 +71,10 @@
}
</script>
<svelte:window on:keydown={$commandCenterKeyDownHandler} />
<CommandCenter />
<Shell
showSideNavigation={$page.url.pathname !== '/console' &&
!$page?.params.organization &&
+109
View File
@@ -0,0 +1,109 @@
<script lang="ts">
import Dialog from '$lib/components/dialog.svelte';
import { CommandRegistrant, commands, commandCenter } from '$lib/helpers/commandCenter';
import { isMac } from '$lib/helpers/platform';
let open = false;
let search = '';
let selected = 0;
const { register } = CommandRegistrant();
$: register([
{
callback: () => {
open = !open;
},
keys: ['k'],
meta: true,
forceEnable: true
}
]);
$: results = $commands.filter((command) => {
return (
!command.disabled &&
command.label &&
command.label.toLowerCase().includes(search.toLowerCase())
);
});
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowDown') {
event.preventDefault();
selected = selected === results.length - 1 ? 0 : selected + 1;
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selected = selected === 0 ? results.length - 1 : selected - 1;
} else if (event.key === 'Enter') {
event.preventDefault();
if (results[selected]) {
results[selected].callback();
open = false;
search = '';
}
}
}
$: {
results;
selected = 0;
}
$: $commandCenter.enabled = !open;
</script>
<svelte:window on:keydown={handleKeyDown} />
<Dialog bind:show={open}>
<div class="u-flex u-flex-vertical u-width-full-line">
<input type="text" placeholder="type here..." autofocus bind:value={search} />
<ul class="u-margin-block-start-16 u-flex u-flex-vertical u-gap-8">
{#each results as command, i}
<li
class="u-flex u-main-space-between result"
data-selected={selected === i ? true : undefined}>
<span>
{command.label}
</span>
<div class="u-flex u-gap-4">
{#if command.meta}
<kbd class="kbd"> {isMac ? '⌘' : 'ctrl'} </kbd>
{/if}
{#if command.shift}
<kbd class="kbd"></kbd>
{/if}
{#if command.alt}
<kbd class="kbd"> {isMac ? '⌥' : 'alt'} </kbd>
{/if}
{#each command.keys as key}
<kbd class="kbd">
{key.toUpperCase()}
</kbd>
{/each}
</div>
</li>
{:else}
<li class="result">
<span class="text">No commands found</span>
</li>
{/each}
</ul>
</div>
</Dialog>
<style>
input {
border: none;
background-color: transparent;
}
.result {
border-radius: 0.25rem;
padding: 0.5rem 0.75rem;
}
.result[data-selected] {
background-color: hsl(var(--color-neutral-200));
}
</style>
@@ -1,31 +1,32 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import {
AvatarInitials,
Copy,
Empty,
EmptySearch,
Copy,
SearchQuery,
AvatarInitials,
PaginationWithLimit
PaginationWithLimit,
SearchQuery
} from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableHeader,
TableBody,
TableCellHead,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRowLink
} from '$lib/elements/table';
import { Pill } from '$lib/elements';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import Create from './createUser.svelte';
import type { Models } from '@appwrite.io/console';
import { project, projectRegistrant } from '../store';
import type { PageData } from './$types';
import Create from './createUser.svelte';
export let data: PageData;
@@ -34,6 +35,25 @@
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
await goto(`${base}/console/project-${projectId}/auth/user-${event.detail.$id}`);
}
const { register } = $projectRegistrant();
$: register([
{
label: 'Create user',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate
},
{
label: 'Go to Overview',
callback: () => {
goto(`${base}/console/project-${$project.$id}/overview`);
},
keys: ['o']
}
]);
</script>
<Container>
@@ -1,16 +1,17 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Button } from '$lib/elements/forms';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Container, GridHeader } from '$lib/layout';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Empty, PaginationWithLimit } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container, GridHeader } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { projectRegistrant } from '../store';
import type { PageData } from './$types';
import Create from './create.svelte';
import Grid from './grid.svelte';
import Table from './table.svelte';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { columns } from './store';
import Table from './table.svelte';
export let data: PageData;
@@ -21,6 +22,18 @@
showCreate = false;
await goto(`${base}/console/project-${project}/databases/database-${event.detail.$id}`);
}
const { register } = $projectRegistrant();
$: register([
{
label: 'Create database',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate
}
]);
</script>
<Container>
@@ -14,10 +14,12 @@
import { tooltip } from '$lib/actions/tooltip';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import { beforeNavigate } from '$app/navigation';
import { beforeNavigate, goto } from '$app/navigation';
import { toLocaleDateTime } from '$lib/helpers/date';
import Create from './createFunction.svelte';
import type { PageData } from './$types';
import { CommandRegistrant } from '$lib/helpers/commandCenter';
import { projectRegistrant } from '../store';
export let data: PageData;
@@ -32,6 +34,16 @@
beforeNavigate(() => {
wizard.hide();
});
const { register } = $projectRegistrant();
$: register([
{
label: 'Create function',
callback: openWizard,
keys: ['c'],
disabled: $wizard.show
}
]);
</script>
<Container>
@@ -36,6 +36,9 @@
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import type { PageData } from './$types';
import { CommandRegistrant } from '$lib/helpers/commandCenter';
import { goto } from '$app/navigation';
import { project, projectRegistrant } from '../../store';
export let data: PageData;
@@ -70,6 +73,9 @@
return 'unknown';
}
};
const { register } = $projectRegistrant();
$: register();
</script>
<div class="common-section u-flex u-gap-12">
@@ -17,6 +17,7 @@
import { tooltip } from '$lib/actions/tooltip';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { projectRegistrant } from '../store';
export let data: PageData;
@@ -28,6 +29,18 @@
showCreate = false;
await goto(`${base}/console/project-${project}/storage/bucket-${event.detail.$id}`);
}
const { register } = $projectRegistrant();
$: register([
{
label: 'Create bucket',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate
}
]);
</script>
<Container>
@@ -2,8 +2,55 @@ import { derived, writable } from 'svelte/store';
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import type { BarSeriesOption } from 'echarts/charts';
import { ExtendCommandRegistrant } from '$lib/helpers/commandCenter';
import { goto } from '$app/navigation';
export const project = derived(page, ($page) => $page.data.project as Models.Project);
export const projectRegistrant = derived<
typeof project,
ReturnType<typeof ExtendCommandRegistrant>
>(project, ($project) => {
return ExtendCommandRegistrant([
{
label: 'Go to Overview',
keys: ['o'],
callback: () => {
goto(`/console/project-${$project.$id}`);
}
},
{
label: 'Go to Auth',
callback: () => {
goto(`/console/project-${$project.$id}/auth`);
},
keys: ['a']
},
{
label: 'Go to Databases',
callback: () => {
goto(`/console/project-${$project.$id}/databases`);
},
keys: ['d']
},
{
label: 'Go to Functions',
callback: () => {
goto(`/console/project-${$project.$id}/functions`);
},
keys: ['f']
},
{
label: 'Go to Storage',
callback: () => {
goto(`/console/project-${$project.$id}/storage`);
},
keys: ['s']
}
]);
});
export const onboarding = derived(
project,
($project) => $project.platforms.length === 0 && $project.keys.length === 0