mirror of
https://github.com/KevinMidboe/infra-map.git
synced 2025-10-29 17:40:28 +00:00
global search component
This commit is contained in:
185
src/lib/components/GlobalSearch.svelte
Normal file
185
src/lib/components/GlobalSearch.svelte
Normal 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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||||
|
import GlobalSearch from '$lib/components/GlobalSearch.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<GlobalSearch />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||||
import PageElement from '$lib/components/PageElement.svelte';
|
import PageElement from '$lib/components/PageElement.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let elems = [];
|
let elems = [];
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
return {
|
return {
|
||||||
bgColor: colors[counter - 1][0],
|
bgColor: colors[counter - 1][0],
|
||||||
color: colors[counter - 1][1],
|
color: colors[counter - 1][1],
|
||||||
title,
|
name: title,
|
||||||
header: null,
|
header: null,
|
||||||
description: description ? description : '',
|
description: description ? description : '',
|
||||||
link: title
|
link: `/${title}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@
|
|||||||
|
|
||||||
elems = elems.concat(createPageElement('cluster '));
|
elems = elems.concat(createPageElement('cluster '));
|
||||||
elems = elems.concat(createPageElement('health '));
|
elems = elems.concat(createPageElement('health '));
|
||||||
|
|
||||||
|
onMount(() => window.elements = elems)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
|
<PageHeader>Welcome to schleppe.cloud infra overview</PageHeader>
|
||||||
@@ -80,11 +83,11 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="shortcut-grid">
|
<div class="shortcut-grid">
|
||||||
{#each elems as shortcut (shortcut.title)}
|
{#each elems as shortcut (shortcut.name)}
|
||||||
<PageElement
|
<PageElement
|
||||||
bgColor={shortcut.bgColor}
|
bgColor={shortcut.bgColor}
|
||||||
color={shortcut.color}
|
color={shortcut.color}
|
||||||
title={shortcut.title}
|
title={shortcut.name}
|
||||||
header={shortcut.header}
|
header={shortcut.header}
|
||||||
description={shortcut.description}
|
description={shortcut.description}
|
||||||
link={shortcut.link}
|
link={shortcut.link}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import Node from '$lib/components/Node.svelte';
|
import Node from '$lib/components/Node.svelte';
|
||||||
import Deploy from '$lib/components/Deploy.svelte';
|
import Deploy from '$lib/components/Deploy.svelte';
|
||||||
import Daemon from '$lib/components/Daemon.svelte';
|
import Daemon from '$lib/components/Daemon.svelte';
|
||||||
@@ -14,10 +15,24 @@
|
|||||||
const rawDaemons: V1DaemonSet[] = data?.daemons;
|
const rawDaemons: V1DaemonSet[] = data?.daemons;
|
||||||
const rawNodes: V1Node[] = data?.nodes;
|
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 deployments = $derived(rawDeployments.filter((d) => d.metadata.name.includes(filterLC)));
|
||||||
let daemons = $derived(rawDaemons.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)));
|
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>
|
</script>
|
||||||
|
|
||||||
<PageHeader>Cluster overview</PageHeader>
|
<PageHeader>Cluster overview</PageHeader>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||||
import ServerComp from '$lib/components/Server.svelte';
|
import ServerComp from '$lib/components/Server.svelte';
|
||||||
import ServerSummary from '$lib/components/ServerSummary.svelte';
|
import ServerSummary from '$lib/components/ServerSummary.svelte';
|
||||||
@@ -7,6 +8,34 @@
|
|||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
const { cluster, nodes } = data;
|
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>
|
</script>
|
||||||
|
|
||||||
<PageHeader>Servers</PageHeader>
|
<PageHeader>Servers</PageHeader>
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
import FormSite from '$lib/components/forms/FormSite.svelte';
|
import FormSite from '$lib/components/forms/FormSite.svelte';
|
||||||
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
|
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
|
||||||
import type { Site } from '$lib/interfaces/site.ts';
|
import type { Site } from '$lib/interfaces/site.ts';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { data }: { data: { site: Site } } = $props();
|
let { data }: { data: { site: Site } } = $props();
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
|
|
||||||
const { sites } = data;
|
const { sites } = data;
|
||||||
|
|
||||||
|
onMount(() => window.elements = sites)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageHeader>Sites
|
<PageHeader>Sites
|
||||||
|
|
||||||
<button class="add-site-btn affirmative" on:click={() => (open = true)}><span>Add new site</span></button>
|
<button class="add-site-btn affirmative" on:click={() => (open = true)}><span>Add new site</span></button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user