mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: command center POC
This commit is contained in:
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user