1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat!: adds services and stacks to fuzzy search. Note labels will no longer be searchable due to a bug on sorting. (#2999)

This commit is contained in:
Amir Raminfar
2024-05-30 09:47:20 -07:00
committed by GitHub
parent 4edac1174c
commit 3a30c27c9a
7 changed files with 106 additions and 24 deletions

View File

@@ -33,7 +33,7 @@ function createFuzzySearchModal() {
containers: [ containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []), new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []), new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited", []), new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "running", []),
], ],
}, },
}, },
@@ -56,7 +56,8 @@ describe("<FuzzySearchModal />", () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(useRouter().push).mockReset(); vi.mocked(useRouter().push).mockReset();
}); });
test("shows all", async () => {
test("shows running all", async () => {
const wrapper = createFuzzySearchModal(); const wrapper = createFuzzySearchModal();
expect(wrapper.findAll("li").length).toBe(3); expect(wrapper.findAll("li").length).toBe(3);
}); });

View File

@@ -24,14 +24,23 @@
:class="index === selectedIndex ? 'focus' : ''" :class="index === selectedIndex ? 'focus' : ''"
> >
<div :class="{ 'text-primary': result.item.state === 'running' }"> <div :class="{ 'text-primary': result.item.state === 'running' }">
<octicon:container-24 /> <template v-if="result.item.type === 'container'">
<octicon:container-24 />
</template>
<template v-else-if="result.item.type === 'service'">
<ph:stack-simple />
</template>
<template v-else-if="result.item.type === 'stack'">
<ph:stack />
</template>
</div> </div>
<div class="truncate"> <div class="truncate">
<template v-if="config.hosts.length > 1"> <template v-if="config.hosts.length > 1 && result.item.host">
<span class="font-light">{{ result.item.host }}</span> / <span class="font-light">{{ result.item.host }}</span> /
</template> </template>
<span data-name v-html="matchedName(result)"></span> <span data-name v-html="matchedName(result)"></span>
</div> </div>
<DistanceTime :date="result.item.created" class="text-xs font-light" /> <DistanceTime :date="result.item.created" class="text-xs font-light" />
<a <a
@click.stop.prevent="addColumn(result.item)" @click.stop.prevent="addColumn(result.item)"
@@ -48,8 +57,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ContainerState } from "@/types/Container";
import { useFuse } from "@vueuse/integrations/useFuse"; import { useFuse } from "@vueuse/integrations/useFuse";
import { type FuseResultMatch } from "fuse.js"; import { type FuseResult } from "fuse.js";
const { maxResults = 5 } = defineProps<{ const { maxResults = 5 } = defineProps<{
maxResults?: number; maxResults?: number;
@@ -64,24 +74,60 @@ const selectedIndex = ref(0);
const router = useRouter(); const router = useRouter();
const containerStore = useContainerStore(); const containerStore = useContainerStore();
const pinnedStore = usePinnedLogsStore(); const pinnedStore = usePinnedLogsStore();
const { containers } = storeToRefs(containerStore); const { visibleContainers } = storeToRefs(containerStore);
const swarmStore = useSwarmStore();
const { stacks, services } = storeToRefs(swarmStore);
type Item = {
id: string;
created: Date;
name: string;
state?: ContainerState;
host?: string;
type: "container" | "service" | "stack";
};
const list = computed(() => { const list = computed(() => {
return containers.value.map(({ id, created, name, state, labels, hostLabel: host }) => { const items: Item[] = [];
return {
id, for (const container of visibleContainers.value) {
created, items.push({
name, id: container.id,
state, created: container.created,
host, name: container.name,
labels: Object.entries(labels).map(([_, value]) => value), state: container.state,
}; host: container.hostLabel,
}); type: "container",
});
}
for (const service of services.value) {
items.push({
id: service.name,
created: service.updatedAt,
name: service.name,
state: "running",
type: "service",
});
}
for (const stack of stacks.value) {
items.push({
id: stack.name,
created: stack.updatedAt,
name: stack.name,
state: "running",
type: "stack",
});
}
return items;
}); });
const { results } = useFuse(query, list, { const { results } = useFuse(query, list, {
fuseOptions: { fuseOptions: {
keys: ["name", "host", "labels"], keys: ["name", "host"],
includeScore: true, includeScore: true,
useExtendedSearch: true, useExtendedSearch: true,
threshold: 0.3, threshold: 0.3,
@@ -93,17 +139,17 @@ const { results } = useFuse(query, list, {
const data = computed(() => { const data = computed(() => {
return [...results.value] return [...results.value]
.sort((a, b) => { .sort((a: FuseResult<Item>, b: FuseResult<Item>) => {
if (a.score === b.score) { if (a.score === b.score) {
if (a.item.state === b.item.state) { if (a.item.state === b.item.state) {
return b.item.created - a.item.created; return b.item.created.getTime() - a.item.created.getTime();
} else if (a.item.state === "running" && b.item.state !== "running") { } else if (a.item.state === "running" && b.item.state !== "running") {
return -1; return -1;
} else { } else {
return 1; return 1;
} }
} else { } else {
return a.score - b.score; return (a.score ?? 0) - (b.score ?? 0);
} }
}) })
.slice(0, maxResults); .slice(0, maxResults);
@@ -115,10 +161,16 @@ watch(query, (data) => {
} }
}); });
onMounted(() => input.value?.focus()); useFocus(input, { initialValue: true });
function selected({ id }: { id: string }) { function selected(item: Item) {
router.push({ name: "container-id", params: { id } }); if (item.type === "container") {
router.push({ name: "container-id", params: { id: item.id } });
} else if (item.type === "service") {
router.push({ name: "service-name", params: { name: item.id } });
} else if (item.type === "stack") {
router.push({ name: "stack-name", params: { name: item.id } });
}
close(); close();
} }
function addColumn(container: { id: string }) { function addColumn(container: { id: string }) {
@@ -126,7 +178,7 @@ function addColumn(container: { id: string }) {
close(); close();
} }
function matchedName({ item, matches = [] }: { item: { name: string }; matches?: FuseResultMatch[] }) { function matchedName({ item, matches = [] }: FuseResult<Item>) {
const matched = matches.find((match) => match.key === "name"); const matched = matches.find((match) => match.key === "name");
if (matched) { if (matched) {
const { indices } = matched; const { indices } = matched;

View File

@@ -20,6 +20,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeRouteLeave } from "vue-router";
const containerStore = useContainerStore(); const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore); const { ready } = storeToRefs(containerStore);
@@ -27,5 +28,13 @@ const swarmStore = useSwarmStore();
const { services, customGroups } = storeToRefs(swarmStore); const { services, customGroups } = storeToRefs(swarmStore);
const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false); const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false);
onBeforeRouteLeave((to) => {
if (to.meta.swarmMode) {
showSwarm.value = true;
} else {
showSwarm.value = false;
}
});
</script> </script>
<style scoped lang="postcss"></style> <style scoped lang="postcss"></style>

View File

@@ -10,6 +10,10 @@ export class Stack {
service.stack = this; service.stack = this;
} }
} }
get updatedAt() {
return this.containers.map((c) => c.created).reduce((acc, date) => (date > acc ? date : acc), new Date(0));
}
} }
export class Service { export class Service {
@@ -19,4 +23,8 @@ export class Service {
) {} ) {}
stack?: Stack; stack?: Stack;
get updatedAt() {
return this.containers.map((c) => c.created).reduce((acc, date) => (date > acc ? date : acc), new Date(0));
}
} }

View File

@@ -20,3 +20,7 @@ watchEffect(() => {
} }
}); });
</script> </script>
<route lang="yaml">
meta:
swarmMode: true
</route>

View File

@@ -26,3 +26,7 @@ watchEffect(() => {
} }
}); });
</script> </script>
<route lang="yaml">
meta:
swarmMode: true
</route>

View File

@@ -26,3 +26,7 @@ watchEffect(() => {
} }
}); });
</script> </script>
<route lang="yaml">
meta:
swarmMode: true
</route>