global search component

This commit is contained in:
2025-10-13 20:19:13 +02:00
parent d0e107f644
commit 471b13739d
6 changed files with 243 additions and 6 deletions

View File

@@ -0,0 +1,185 @@
<script lang="ts">
import { goto, pushState } from '$app/navigation';
import { onMount } from 'svelte';
import Dialog from './Dialog.svelte';
import Input from './Input.svelte';
let open = $state(false);
const className = 'search-container';
/* search & filter */
let filterString = $state('');
let focusIndex = $state(0);
let children = $state([]);
let filteredchildren = $derived.by(() => {
return children.filter((a) => a?.name.toLowerCase().includes(filterString));
});
const updateFocus = (index: number) => {
if (index < 0) index = 0;
else if (index >= filteredchildren.length) {
index = filteredchildren.length - 1;
}
focusIndex = index;
};
/* setup & register */
const toggleSearchDialog = () => (open = !open);
const focusSearchInput = () => {
setTimeout(() => {
const input = document.getElementsByClassName(className)[0]?.getElementsByTagName('input')[0];
input.focus();
}, 50);
};
function registerShortcutKey() {
document.addEventListener('keydown', function (event) {
// listen for open/close command + k
if ((event.metaKey && event.key === 'k') || (event.ctrlKey && event.key === 'k')) {
event.preventDefault();
console.log('Command + K / Ctrl + K was pressed');
toggleSearchDialog();
// initial state
if (open) {
focusSearchInput();
filterString = '';
updateFocus(0);
children = window?.elements || [{ name: 'empty' }];
}
return;
}
// listen for text, any letter should reset focusIndex
const singleLetter = (event.key.length == 1 && event.key.match(/\D/)) || 0 > 0;
if (open && singleLetter) {
updateFocus(0);
}
// listen for number as shortcut actions
const digit = event.key.match(/\d/)?.[0];
if (open && digit?.length && digit?.length > 0) {
setTimeout(() => {
filterString = String(filterString)?.replaceAll(digit, '');
}, 1);
updateFocus(Number(digit) - 1);
}
// listen for arrow keys
if (open && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
const direction = event.key === 'ArrowDown' ? 1 : -1;
updateFocus(focusIndex + 1 * direction);
}
// listen for enter key
if (open && event.key === 'Enter' && filteredchildren.length > 0) {
const { link } = filteredchildren[focusIndex];
toggleSearchDialog();
if (String(link)?.startsWith('/')) {
goto(link);
} else {
window.open(link, '_blank');
}
}
});
}
onMount(() => {
registerShortcutKey();
});
</script>
{#if open}
<Dialog on:close={() => (open = false)} title="Search on page" description="">
<div class={className}>
<Input label="" bind:value={filterString} placeholder="attribute" />
<ul>
{#each filteredchildren as element, index (element?.name)}
<li class={index === focusIndex ? 'focus' : ''}>
<h3>{element?.name}</h3>
<div>
{#each Object.entries(element) as [key, value] (key)}
<span><b>{key}:</b> {value}</span>
{/each}
</div>
</li>
{/each}
</ul>
</div>
</Dialog>
{/if}
<style lang="scss">
.search-container {
width: 700px;
position: relative;
@media screen and (max-width: 480px) {
width: 100%;
ul {
max-height: calc(50vh);
}
}
ul {
list-style-type: none;
padding-left: 0;
overflow-y: scroll;
overflow-y: scroll;
max-height: calc(100vh - var(--offset-top) * 4);
max-height: 75vh;
}
li {
border: var(--border);
border-radius: var(--border-radius);
padding: 1rem;
margin: 0.5rem 0;
position: relative;
cursor: pointer;
&.focus,
&:hover {
&::after {
content: '';
position: absolute;
width: calc(100% - 2px);
height: calc(100% - 2px);
border: 2px solid var(--theme);
top: -1px;
left: -1px;
border-radius: var(--border-radius);
}
}
h3 {
margin: 0;
text-align: center;
font-size: 2rem;
}
div {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
column-gap: 1rem;
row-gap: 0.1rem;
}
}
}
:global(.search-container .label-input) {
position: sticky;
top: 0;
overflow: unset;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
import GlobalSearch from '$lib/components/GlobalSearch.svelte';
</script>
<div class="page">
@@ -13,6 +14,8 @@
<slot></slot>
</main>
</div>
<GlobalSearch />
</div>
<style lang="scss">

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import PageElement from '$lib/components/PageElement.svelte';
import { onMount } from 'svelte';
let elems = [];
let counter = 0;
@@ -28,10 +29,10 @@
return {
bgColor: colors[counter - 1][0],
color: colors[counter - 1][1],
title,
name: title,
header: null,
description: description ? description : '',
link: title
link: `/${title}`
};
}
@@ -59,6 +60,8 @@
elems = elems.concat(createPageElement('cluster '));
elems = elems.concat(createPageElement('health '));
onMount(() => window.elements = elems)
</script>
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
@@ -80,11 +83,11 @@
</p>
<div class="shortcut-grid">
{#each elems as shortcut (shortcut.title)}
{#each elems as shortcut (shortcut.name)}
<PageElement
bgColor={shortcut.bgColor}
color={shortcut.color}
title={shortcut.title}
title={shortcut.name}
header={shortcut.header}
description={shortcut.description}
link={shortcut.link}

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import Node from '$lib/components/Node.svelte';
import Deploy from '$lib/components/Deploy.svelte';
import Daemon from '$lib/components/Daemon.svelte';
@@ -14,10 +15,24 @@
const rawDaemons: V1DaemonSet[] = data?.daemons;
const rawNodes: V1Node[] = data?.nodes;
let filterLC = $derived(filterValue.toLowerCase())
let filterLC = $derived(filterValue.toLowerCase());
let deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterLC)));
let daemons = $derived(rawDaemons.filter((d) => d.metadata.name.includes(filterLC)));
let nodes = $derived(rawNodes.filter((n) => n.metadata.name.includes(filterLC)));
onMount(() => {
console.log(deployments);
window.elements = deployments
.map((d) => {
return {
name: d.metadata?.name || undefined,
link: `/cluster/deployment/${d.metadata?.uid}`,
...d
};
})
.filter((d) => d.name);
});
</script>
<PageHeader>Cluster overview</PageHeader>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import ServerComp from '$lib/components/Server.svelte';
import ServerSummary from '$lib/components/ServerSummary.svelte';
@@ -7,6 +8,34 @@
let { data }: { data: PageData } = $props();
const { cluster, nodes } = data;
const allVms: Array<VM> = nodes.flatMap((n) => n.vms).filter((v) => v.template !== 1);
const allLxcs: Array<LXC> = nodes.flatMap((n) => n.lxcs);
onMount(() => {
window.elements = [
...allVms
.map((vm) => {
return {
link: `/servers/vm/${vm.name}`,
...vm
};
})
.filter((d) => d.name),
...nodes.map(node => {
return {
link: `/servers/node/${node.name}`,
...node
}
}),
...allLxcs.map(lxc => {
return {
link: `/servers/lxc/${lxc.name}`,
...lxc
}
})
];
});
console.log(allLxcs)
</script>
<PageHeader>Servers</PageHeader>

View File

@@ -5,15 +5,17 @@
import FormSite from '$lib/components/forms/FormSite.svelte';
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
import type { Site } from '$lib/interfaces/site.ts';
import { onMount } from 'svelte';
let { data }: { data: { site: Site } } = $props();
let open = $state(false);
const { sites } = data;
onMount(() => window.elements = sites)
</script>
<PageHeader>Sites
<button class="add-site-btn affirmative" on:click={() => (open = true)}><span>Add new site</span></button>
</PageHeader>