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:
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
swarmMode: true
|
||||||
|
</route>
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
swarmMode: true
|
||||||
|
</route>
|
||||||
|
|||||||
@@ -26,3 +26,7 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
swarmMode: true
|
||||||
|
</route>
|
||||||
|
|||||||
Reference in New Issue
Block a user