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:
1
assets/components.d.ts
vendored
1
assets/components.d.ts
vendored
@@ -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']
|
||||
|
||||
60
assets/components/FuzzySearchModal.spec.ts
Normal file
60
assets/components/FuzzySearchModal.spec.ts
Normal 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>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
@@ -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
0
dist/.gitkeep
vendored
Reference in New Issue
Block a user