1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-25 23:03:47 +01:00

fix: improves performance for fuzzy search dialog (#2656)

This commit is contained in:
Amir Raminfar
2024-01-05 12:47:07 -08:00
committed by GitHub
parent 589cad54d9
commit eafd2f144f
5 changed files with 101 additions and 38 deletions

View File

@@ -60,7 +60,6 @@ declare module 'vue' {
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:logout': typeof import('~icons/mdi/logout')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']

View File

@@ -0,0 +1,60 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import FuzzySearchModal from "./FuzzySearchModal.vue";
import { Container } from "@/models/Container";
import { describe, expect, test, vi } from "vitest";
import { createI18n } from "vue-i18n";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
function createFuzzySearchModal() {
global.EventSource = EventSource;
const wrapper = mount(FuzzySearchModal, {
global: {
plugins: [
createI18n({}),
createTestingPinia({
createSpy: vi.fn,
initialState: {
container: {
containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running"),
new Container("123", new Date(), "image", "foo bar", "command", "host", {}, "status", "running"),
new Container("123", new Date(), "image", "baz", "command", "host", {}, "status", "exited"),
],
},
},
}),
],
},
});
return wrapper;
}
/**
* @vitest-environment jsdom
*/
describe("<FuzzySearchModal />", () => {
test("shows all", async () => {
const wrapper = createFuzzySearchModal();
expect(wrapper.findAll("li").length).toBe(3);
});
test("search for foo", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("foo");
expect(wrapper.findAll("li").length).toBe(1);
expect(wrapper.find("ul [data-name]").html()).toMatchInlineSnapshot(
`"<span data-v-dc2e8c61="" data-name=""><mark>foo</mark> bar</span>"`,
);
});
});

View File

@@ -16,24 +16,28 @@
<mdi:keyboard-esc class="flex" />
</div>
<ul tabindex="0" class="menu dropdown-content !relative mt-2 w-full rounded-box bg-base-lighter p-2">
<li v-for="({ item }, index) in data">
<li v-for="(result, index) in data">
<a
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
@click.prevent="selected(item)"
@click.prevent="selected(result.item)"
@mouseenter="selectedIndex = index"
:class="index === selectedIndex ? 'focus' : ''"
>
<div :class="{ 'text-primary': item.state === 'running' }">
<div :class="{ 'text-primary': result.item.state === 'running' }">
<octicon:container-24 />
</div>
<div class="truncate">
<template v-if="config.hosts.length > 1">
<span class="font-light">{{ item.host }}</span> /
<span class="font-light">{{ result.item.host }}</span> /
</template>
<span v-html="item.matchedName"></span>
<span data-name v-html="matchedName(result)"></span>
</div>
<distance-time :date="item.created" class="text-xs font-light" />
<a @click.stop.prevent="addColumn(item)" :title="$t('tooltip.pin-column')" class="hover:text-secondary">
<distance-time :date="result.item.created" class="text-xs font-light" />
<a
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
class="hover:text-secondary"
>
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
</a>
@@ -45,8 +49,9 @@
<script lang="ts" setup>
import { useFuse } from "@vueuse/integrations/useFuse";
import { type FuseResultMatch } from "fuse.js";
const { maxResults: resultLimit = 5 } = defineProps<{
const { maxResults = 5 } = defineProps<{
maxResults?: number;
}>();
@@ -81,7 +86,7 @@ const { results } = useFuse(query, list, {
threshold: 0.3,
includeMatches: true,
},
resultLimit,
resultLimit: 10,
matchAllWhenSearchEmpty: true,
});
@@ -100,26 +105,7 @@ const data = computed(() => {
return 0;
}
})
.map((i) => {
const matches = (i.matches as [{ key: string; indices: [number, number][] }])?.find(
(match) => match.key === "name",
)?.indices;
i.item.matchedName = i.item.name;
if (matches) {
matches
.toSorted((a, b) => a[0] - b[0])
.toReversed()
.forEach(([start, end]) => {
i.item.matchedName =
i.item.matchedName.slice(0, start) +
`<mark>${i.item.matchedName.slice(start, end + 1)}</mark>` +
i.item.matchedName.slice(end + 1);
});
}
return i;
})
.slice(0, resultLimit);
.slice(0, maxResults);
});
watch(query, (data) => {
@@ -138,6 +124,24 @@ function addColumn(container: { id: string }) {
store.appendActiveContainer(container);
close();
}
function matchedName({ item, matches = [] }: { item: { name: string }; matches?: FuseResultMatch[] }) {
const matched = matches.find((match) => match.key === "name");
if (matched) {
const { indices } = matched;
const result = [];
let lastIndex = 0;
for (const [start, end] of indices) {
result.push(item.name.slice(lastIndex, start));
result.push(`<mark>${item.name.slice(start, end + 1)}</mark>`);
lastIndex = end + 1;
}
result.push(item.name.slice(lastIndex));
return result.join("");
} else {
return item.name;
}
}
</script>
<style scoped lang="postcss">

View File

@@ -1,16 +1,16 @@
import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { containerContext } from "@/composable/containerContext";
import { useSearchFilter } from "@/composable/search";
import { settings } from "@/stores/settings";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { computed, nextTick } from "vue";
import { createI18n } from "vue-i18n";
import { createRouter, createWebHistory } from "vue-router";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "@/stores/settings";
import { useSearchFilter } from "@/composable/search";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { computed, nextTick } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { containerContext } from "@/composable/containerContext";
import { createI18n } from "vue-i18n";
vi.mock("@/stores/config", () => ({
__esModule: true,

0
dist/.gitkeep vendored
View File