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">
 | 
			
		||||
	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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user