Removes fuzzysearch and uses fuse.js (#1888)

* Removes fuzzysearch and uses fuse.js

* Fix search bug

* Adds locale

* Fixes more locale

* Fixes search to not update with stats

* Fixes sort order
This commit is contained in:
Amir Raminfar
2022-09-23 09:54:55 -07:00
committed by GitHub
parent 2d54cbba9c
commit 21e5f4fc56
9 changed files with 159 additions and 92 deletions

View File

@@ -3,26 +3,29 @@
<o-autocomplete
ref="autocomplete"
v-model="query"
placeholder="Search containers using ⌘ + k or ctrl + k"
field="name"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="results"
:data="data"
@select="selected"
>
<template #default="props">
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="props.option.state">
<span class="icon is-small" :class="item.state">
<octicon-container-24 />
</span>
</div>
<div class="media-content">
{{ props.option.name }}
{{ item.name }}
</div>
<div class="media-right">
<span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
</div>
@@ -33,55 +36,61 @@
</template>
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import { type Container } from "@/types/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
const { maxResults = 20 } = defineProps<{
const { maxResults: resultLimit = 20 } = defineProps<{
maxResults?: number;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits<{
(e: "close"): void;
}>();
const query = ref("");
const autocomplete = ref<HTMLElement>();
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const preparedContainers = computed(() =>
containers.value.map(({ name, id, created, state }) =>
reactive({
name,
const list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return {
id,
created,
name,
state,
preparedName: fuzzysort.prepare(name),
})
)
);
const results = computed(() => {
const options = {
limit: maxResults,
key: "preparedName",
};
if (query.value) {
const results = fuzzysort.go(query.value, preparedContainers.value, options);
results.forEach((result) => {
if (result.obj.state === "running") {
// @ts-ignore
result.score += 1;
}
});
return [...results].sort((a, b) => b.score - a.score).map((i) => i.obj);
} else {
return [...preparedContainers.value].sort((a, b) => b.created - a.created);
}
});
onMounted(() => nextTick(() => autocomplete.value?.focus()));
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
resultLimit,
matchAllWhenSearchEmpty: true,
});
function selected(item: { id: string; name: string }) {
router.push({ name: "container-id", params: { id: item.id } });
const data = computed(() => {
return results.value
.sort((a, b) => {
if (a.score === b.score) {
if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else if (a.score && b.score) {
return a.score - b.score;
} else {
return 0;
}
})
.map(({ item }) => item);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
emit("close");
}
function addColumn(container: Container) {

View File

@@ -54,8 +54,8 @@
class="input"
type="text"
:placeholder="$t('placeholder.search-containers')"
v-model="search"
@keyup.esc="search.value = null"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
@@ -63,13 +63,13 @@
</span>
</p>
</div>
<p class="panel-tabs" v-if="!search">
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in results.slice(0, 10)"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
@@ -86,54 +86,59 @@
</template>
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = ref("running");
const search = ref();
const sort = $ref("running");
const query = ref("");
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort.value) {
switch (sort) {
case "all":
return mostRecentContainers.value;
return mostRecentContainers;
case "running":
return runningContainers.value;
return runningContainers;
default:
throw `Invalid sort order: ${sort.value}`;
throw `Invalid sort order: ${sort}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
const totalCpu = ref(0);
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
const totalMem = ref(0);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}

View File

@@ -54,8 +54,8 @@
class="input"
type="text"
:placeholder="$t('placeholder.search-containers')"
v-model="search"
@keyup.esc="search.value = null"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
@@ -63,13 +63,13 @@
</span>
</p>
</div>
<p class="panel-tabs" v-if="!search">
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in results.slice(0, 10)"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
@@ -86,54 +86,59 @@
</template>
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = ref("running");
const search = ref();
const sort = $ref("running");
const query = ref("");
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort.value) {
switch (sort) {
case "all":
return mostRecentContainers.value;
return mostRecentContainers;
case "running":
return runningContainers.value;
return runningContainers;
default:
throw `Invalid sort order: ${sort.value}`;
throw `Invalid sort order: ${sort}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
const totalCpu = ref(0);
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
const totalMem = ref(0);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}

View File

@@ -26,6 +26,6 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
it("shortcut for fuzzy search works", () => {
cy.get("body").type("{ctrl}k");
cy.get("input[placeholder='Search containers using ⌘ + k or ctrl + k']").should("be.visible");
cy.get("input[placeholder='Search containers (⌘ + k, ⌃k)']").should("be.visible");
});
});

View File

@@ -27,7 +27,7 @@ button:
logout: Logout
login: Login
placeholder:
search-containers: Search Containers
search-containers: Search containers (⌘ + k, ⌃k)
settings:
display: Display
small-scrollbars: Use smaller scrollbars

View File

@@ -27,7 +27,7 @@ button:
logout: Cerrar la sesión
login: Iniciar sesión
placeholder:
search-containers: Buscar Contenedores
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
settings:
display: Vista
small-scrollbars: Utilizar barras de desplazamiento más pequeñas

View File

@@ -27,7 +27,7 @@ button:
logout: Terminar sessão
login: Iniciar sessão
placeholder:
search-containers: Pesquisar Contentores
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
settings:
display: Visão
small-scrollbars: Usar barras de rolagem mais pequenas

View File

@@ -33,11 +33,12 @@
"@vitejs/plugin-vue": "3.1.0",
"@vue/compiler-sfc": "^3.2.39",
"@vueuse/core": "^9.2.0",
"@vueuse/integrations": "^9.2.0",
"@vueuse/router": "^9.2.0",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"date-fns": "^2.29.3",
"fuzzysort": "^2.0.1",
"fuse.js": "^6.6.2",
"lodash.debounce": "^4.0.8",
"pinia": "^2.0.22",
"sass": "^1.54.9",

55
pnpm-lock.yaml generated
View File

@@ -17,13 +17,14 @@ specifiers:
'@vue/compiler-sfc': ^3.2.39
'@vue/test-utils': ^2.0.2
'@vueuse/core': ^9.2.0
'@vueuse/integrations': ^9.2.0
'@vueuse/router': ^9.2.0
ansi-to-html: ^0.7.2
bulma: ^0.9.4
c8: ^7.12.0
date-fns: ^2.29.3
eventsourcemock: ^2.0.0
fuzzysort: ^2.0.1
fuse.js: ^6.6.2
husky: ^8.0.1
jest-serializer-vue: ^2.0.2
jsdom: ^20.0.0
@@ -62,11 +63,12 @@ dependencies:
'@vitejs/plugin-vue': 3.1.0_vite@3.1.3+vue@3.2.39
'@vue/compiler-sfc': 3.2.39
'@vueuse/core': 9.2.0_vue@3.2.39
'@vueuse/integrations': 9.2.0_fuse.js@6.6.2+vue@3.2.39
'@vueuse/router': 9.2.0_u55wpvgoaskkfdnhr4v2vlugdi
ansi-to-html: 0.7.2
bulma: 0.9.4
date-fns: 2.29.3
fuzzysort: 2.0.1
fuse.js: 6.6.2
lodash.debounce: 4.0.8
pinia: 2.0.22_arz4dztosvwy2ghjrlh2wdhejm
sass: 1.54.9
@@ -820,6 +822,50 @@ packages:
- vue
dev: false
/@vueuse/integrations/9.2.0_fuse.js@6.6.2+vue@3.2.39:
resolution: {integrity: sha512-0NerkCPUUWnbEb0ZZaJyrO8YKPPClR9+aLLF8yBbG/XRsoEo7pcpVq8d+uMhfHrXABoUpKD+9FZ+Tz/aRb7yFg==}
peerDependencies:
async-validator: '*'
axios: '*'
change-case: '*'
drauu: '*'
focus-trap: '*'
fuse.js: '*'
jwt-decode: '*'
nprogress: '*'
qrcode: '*'
universal-cookie: '*'
peerDependenciesMeta:
async-validator:
optional: true
axios:
optional: true
change-case:
optional: true
drauu:
optional: true
focus-trap:
optional: true
fuse.js:
optional: true
jwt-decode:
optional: true
nprogress:
optional: true
qrcode:
optional: true
universal-cookie:
optional: true
dependencies:
'@vueuse/core': 9.2.0_vue@3.2.39
'@vueuse/shared': 9.2.0_vue@3.2.39
fuse.js: 6.6.2
vue-demi: 0.13.11_vue@3.2.39
transitivePeerDependencies:
- '@vue/composition-api'
- vue
dev: false
/@vueuse/metadata/9.2.0:
resolution: {integrity: sha512-exN4KE6iquxDCdt72BgEhb3tlOpECtD61AUdXnUqBTIUCl70x1Ar/QXo3bYcvxmdMS2/peQyfeTzBjRTpvL5xw==}
dev: false
@@ -2195,8 +2241,9 @@ packages:
/functions-have-names/1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
/fuzzysort/2.0.1:
resolution: {integrity: sha512-SlgbPAq0eQ6JQ1h3l4MNeGH/t9DHKH8GGM0RD/6RhmJrNnSoWt3oIVaiQm9g9BPB+wAhRMeMqlUTbhbd7+Ufcg==}
/fuse.js/6.6.2:
resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
engines: {node: '>=10'}
dev: false
/get-caller-file/2.0.5: