mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 09:04:44 +00:00
feat: Command palette in Cmd+K search (#4861)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vendored
+5
@@ -219,6 +219,7 @@ declare global {
|
||||
const useCloudConfig: typeof import('./composable/cloudConfig').useCloudConfig
|
||||
const useCloudLogSearch: typeof import('./composable/cloudLogSearch').useCloudLogSearch
|
||||
const useColorMode: typeof import('@vueuse/core').useColorMode
|
||||
const useCommands: typeof import('./composable/commands').useCommands
|
||||
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
|
||||
const useContainerActions: typeof import('./composable/containerActions').useContainerActions
|
||||
const useContainerStore: typeof import('./stores/container').useContainerStore
|
||||
@@ -423,6 +424,9 @@ declare global {
|
||||
export type { CloudLogHit } from './composable/cloudLogSearch'
|
||||
import('./composable/cloudLogSearch')
|
||||
// @ts-ignore
|
||||
export type { CommandSection, Command } from './composable/commands'
|
||||
import('./composable/commands')
|
||||
// @ts-ignore
|
||||
export type { DrawerWidth } from './composable/drawer'
|
||||
import('./composable/drawer')
|
||||
// @ts-ignore
|
||||
@@ -661,6 +665,7 @@ declare module 'vue' {
|
||||
readonly useCloudConfig: UnwrapRef<typeof import('./composable/cloudConfig')['useCloudConfig']>
|
||||
readonly useCloudLogSearch: UnwrapRef<typeof import('./composable/cloudLogSearch')['useCloudLogSearch']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useCommands: UnwrapRef<typeof import('./composable/commands')['useCommands']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mount } from "@vue/test-utils";
|
||||
import FuzzySearchModal from "./FuzzySearchModal.vue";
|
||||
|
||||
import { Container } from "@/models/Container";
|
||||
import { lightTheme } from "@/stores/settings";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -16,7 +17,7 @@ vi.mock("vue-router");
|
||||
|
||||
vi.mock("@/stores/config", () => ({
|
||||
__esModule: true,
|
||||
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
|
||||
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }], enableActions: true },
|
||||
withBase: (path: string) => path,
|
||||
}));
|
||||
|
||||
@@ -119,4 +120,28 @@ describe("<FuzzySearchModal />", () => {
|
||||
await wrapper.find("input").trigger("keydown.enter");
|
||||
expect(useRouter().push).toHaveBeenCalledWith({ name: "/container/[id]", params: { id: "567" } });
|
||||
});
|
||||
|
||||
test("matches commands by keyword", async () => {
|
||||
const wrapper = createFuzzySearchModal();
|
||||
await wrapper.find("input").setValue("theme");
|
||||
const items = wrapper.findAll("li").map((li) => li.text());
|
||||
expect(items).toContain("command-palette.theme-dark");
|
||||
});
|
||||
|
||||
test("theme commands set the theme explicitly", async () => {
|
||||
lightTheme.value = "auto";
|
||||
const wrapper = createFuzzySearchModal();
|
||||
|
||||
await wrapper.find("input").setValue("dark theme");
|
||||
await wrapper.find("input").trigger("keydown.enter");
|
||||
expect(lightTheme.value).toBe("dark");
|
||||
|
||||
await wrapper.find("input").setValue("light theme");
|
||||
await wrapper.find("input").trigger("keydown.enter");
|
||||
expect(lightTheme.value).toBe("light");
|
||||
|
||||
await wrapper.find("input").setValue("system theme");
|
||||
await wrapper.find("input").trigger("keydown.enter");
|
||||
expect(lightTheme.value).toBe("auto");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
tabindex="0"
|
||||
class="text-base-content placeholder:text-base-content/40 flex-1 bg-transparent text-base outline-none"
|
||||
ref="input"
|
||||
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
|
||||
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, totalCount - 1)"
|
||||
@keydown.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
|
||||
@keydown.enter.exact="onEnter"
|
||||
@keydown.shift.enter.exact.prevent="runLogSearch"
|
||||
@keydown.alt.enter="addColumn(data[selectedIndex].item)"
|
||||
@keydown.alt.enter.exact.prevent="onPin"
|
||||
v-model="query"
|
||||
:placeholder="placeholderCopy"
|
||||
/>
|
||||
@@ -29,51 +29,80 @@
|
||||
|
||||
<!-- Body: results + log search CTA. Only renders when there is something
|
||||
to show — keeps the empty modal compact. -->
|
||||
<div v-if="results.length || logSearchVisible" class="border-base-content/10 border-t">
|
||||
<!-- Containers section -->
|
||||
<template v-if="results.length">
|
||||
<div class="text-base-content/40 px-4 pt-3 pb-1.5 text-xs font-semibold tracking-wider uppercase">
|
||||
{{ $t("cloud-search.containers-section") }} · {{ data.length }}
|
||||
</div>
|
||||
<ul class="max-h-[50vh] overflow-y-auto overscroll-contain pb-1">
|
||||
<li v-for="(result, index) in data" ref="listItems">
|
||||
<a
|
||||
class="hover:bg-base-content/5 flex cursor-pointer items-center gap-3 px-4 py-2"
|
||||
:class="{ 'bg-base-content/10': index === selectedIndex }"
|
||||
@click.prevent="selected(result.item)"
|
||||
>
|
||||
<div :class="result.item.state === 'running' ? 'text-primary' : 'text-base-content/50'">
|
||||
<template v-if="result.item.type === 'container'">
|
||||
<octicon:container-24 class="size-4" />
|
||||
</template>
|
||||
<template v-else-if="result.item.type === 'service'">
|
||||
<ph:stack-simple class="size-4" />
|
||||
</template>
|
||||
<template v-else-if="result.item.type === 'stack'">
|
||||
<ph:stack class="size-4" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 truncate text-sm">
|
||||
<template v-if="config.hosts.length > 1 && result.item.host">
|
||||
<span class="text-base-content/50 font-light">{{ result.item.host }}</span>
|
||||
<span class="text-base-content/30"> / </span>
|
||||
</template>
|
||||
<span class="text-base-content" data-name v-html="matchedName(result)"></span>
|
||||
</div>
|
||||
<RelativeTime :date="result.item.created" class="text-base-content/40 text-xs" />
|
||||
<span
|
||||
@click.stop.prevent="addColumn(result.item)"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
class="text-base-content/40 hover:text-secondary"
|
||||
<div v-if="totalCount || logSearchVisible" class="border-base-content/10 border-t">
|
||||
<!-- Scroll container spans both sections so the flat Commands + Containers
|
||||
list scrolls as one, matching the unified selection index. -->
|
||||
<div class="max-h-[50vh] overflow-y-auto overscroll-contain">
|
||||
<!-- Commands section -->
|
||||
<template v-if="commandEntries.length">
|
||||
<div class="text-base-content/40 px-4 pt-3 pb-1.5 text-xs font-semibold tracking-wider uppercase">
|
||||
{{ $t("command-palette.section-commands") }} · {{ commandEntries.length }}
|
||||
</div>
|
||||
<ul class="pb-1">
|
||||
<li v-for="(command, index) in commandEntries" :ref="(el) => setItemRef(el, index)">
|
||||
<a
|
||||
class="hover:bg-base-content/5 flex cursor-pointer items-center gap-3 px-4 py-2"
|
||||
:class="{ 'bg-base-content/10': index === selectedIndex }"
|
||||
@click.prevent="runCommand(command)"
|
||||
>
|
||||
<ic:sharp-keyboard-return v-if="index === selectedIndex" class="size-4" />
|
||||
<cil:columns v-else-if="result.item.type === 'container'" class="size-4" />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<component :is="command.icon" class="text-base-content/60 size-4 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate text-sm">{{ command.title }}</span>
|
||||
<ic:sharp-keyboard-return v-if="index === selectedIndex" class="text-base-content/40 size-4" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<!-- Containers section -->
|
||||
<template v-if="containerEntries.length">
|
||||
<div
|
||||
class="text-base-content/40 px-4 pt-3 pb-1.5 text-xs font-semibold tracking-wider uppercase"
|
||||
:class="{ 'border-base-content/10 mt-1 border-t': commandEntries.length }"
|
||||
>
|
||||
{{ $t("cloud-search.containers-section") }} · {{ containerEntries.length }}
|
||||
</div>
|
||||
<ul class="pb-1">
|
||||
<li
|
||||
v-for="(result, index) in containerEntries"
|
||||
:ref="(el) => setItemRef(el, commandEntries.length + index)"
|
||||
>
|
||||
<a
|
||||
class="hover:bg-base-content/5 flex cursor-pointer items-center gap-3 px-4 py-2"
|
||||
:class="{ 'bg-base-content/10': commandEntries.length + index === selectedIndex }"
|
||||
@click.prevent="selected(result.item)"
|
||||
>
|
||||
<div :class="result.item.state === 'running' ? 'text-primary' : 'text-base-content/50'">
|
||||
<template v-if="result.item.type === 'container'">
|
||||
<octicon:container-24 class="size-4" />
|
||||
</template>
|
||||
<template v-else-if="result.item.type === 'service'">
|
||||
<ph:stack-simple class="size-4" />
|
||||
</template>
|
||||
<template v-else-if="result.item.type === 'stack'">
|
||||
<ph:stack class="size-4" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 truncate text-sm">
|
||||
<template v-if="config.hosts.length > 1 && result.item.host">
|
||||
<span class="text-base-content/50 font-light">{{ result.item.host }}</span>
|
||||
<span class="text-base-content/30"> / </span>
|
||||
</template>
|
||||
<span class="text-base-content" data-name v-html="matchedName(result)"></span>
|
||||
</div>
|
||||
<RelativeTime :date="result.item.created" class="text-base-content/40 text-xs" />
|
||||
<span
|
||||
@click.stop.prevent="addColumn(result.item)"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
class="text-base-content/40 hover:text-secondary"
|
||||
>
|
||||
<ic:sharp-keyboard-return v-if="commandEntries.length + index === selectedIndex" class="size-4" />
|
||||
<cil:columns v-else-if="result.item.type === 'container'" class="size-4" />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
<!-- Log search CTA -->
|
||||
<div
|
||||
v-if="logSearchVisible"
|
||||
@@ -130,7 +159,7 @@
|
||||
<div
|
||||
class="bg-base-300/40 border-base-content/10 text-base-content/50 flex items-center gap-4 border-t px-4 py-2 text-xs"
|
||||
>
|
||||
<span v-if="results.length" class="flex items-center gap-1.5">
|
||||
<span v-if="totalCount" class="flex items-center gap-1.5">
|
||||
<kbd class="kbd kbd-xs">↵</kbd> {{ $t("cloud-search.open-container") }}
|
||||
</span>
|
||||
<span v-if="cloudSearch.available.value && logSearchVisible" class="flex items-center gap-1">
|
||||
@@ -164,6 +193,7 @@ import { useFuse } from "@vueuse/integrations/useFuse";
|
||||
import { type FuseResult } from "fuse.js";
|
||||
import { useCloudConfig } from "@/composable/cloudConfig";
|
||||
import { useCloudLogSearch } from "@/composable/cloudLogSearch";
|
||||
import { useCommands, type Command } from "@/composable/commands";
|
||||
|
||||
const close = defineEmit();
|
||||
|
||||
@@ -176,9 +206,15 @@ const route = useRoute();
|
||||
const initialQuery = route?.path === "/cloud/search" && typeof route.query?.q === "string" ? route.query.q : "";
|
||||
const query = ref(initialQuery);
|
||||
const input = ref<HTMLInputElement>();
|
||||
const listItems = ref<HTMLInputElement[]>();
|
||||
const listItems = ref<(Element | null)[]>([]);
|
||||
const selectedIndex = ref(0);
|
||||
|
||||
// Function ref into a single flat array so Commands and Containers share one
|
||||
// selection index for arrow-key navigation and scroll-into-view.
|
||||
function setItemRef(el: any, index: number) {
|
||||
listItems.value[index] = (el?.$el ?? el) as Element | null;
|
||||
}
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const pinnedStore = usePinnedLogsStore();
|
||||
const { visibleContainers } = storeToRefs(containerStore);
|
||||
@@ -268,6 +304,20 @@ const { results: fuseResults } = useFuse(query, list, {
|
||||
|
||||
const results = computed(() => (query.value ? fuseResults.value : []));
|
||||
|
||||
// Commands palette. Fuzzy-matched against title/keywords while typing; the
|
||||
// context commands (container actions) show up front on an empty query.
|
||||
const { commands, contextCommands } = useCommands();
|
||||
const { results: commandFuseResults } = useFuse(query, commands, {
|
||||
fuseOptions: {
|
||||
keys: ["title", "keywords"],
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.3,
|
||||
},
|
||||
});
|
||||
const commandEntries = computed<Command[]>(() =>
|
||||
query.value ? commandFuseResults.value.map((r) => r.item) : contextCommands.value,
|
||||
);
|
||||
|
||||
const data = computed(() => {
|
||||
return [...results.value].sort((a: FuseResult<Item>, b: FuseResult<Item>) => {
|
||||
if (a.score === b.score) {
|
||||
@@ -284,14 +334,26 @@ const data = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
watch(query, (data) => {
|
||||
if (data.length > 0) {
|
||||
selectedIndex.value = 0;
|
||||
// Container hits, mirrors the previously named `data` list for the template.
|
||||
const containerEntries = computed(() => data.value);
|
||||
const totalCount = computed(() => commandEntries.value.length + containerEntries.value.length);
|
||||
|
||||
// Reset to the top only when the user types. Live SSE container add/remove
|
||||
// changes totalCount too, and resetting on that would snap the selection back
|
||||
// to 0 while the palette is open.
|
||||
watch(query, () => {
|
||||
selectedIndex.value = 0;
|
||||
});
|
||||
|
||||
// Keep the selection in bounds when the result count shrinks underneath it.
|
||||
watch(totalCount, (count) => {
|
||||
if (selectedIndex.value > count - 1) {
|
||||
selectedIndex.value = Math.max(count - 1, 0);
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedIndex, () => {
|
||||
listItems.value?.[selectedIndex.value].scrollIntoView({ block: "end" });
|
||||
listItems.value?.[selectedIndex.value]?.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
|
||||
function selected(item: Item) {
|
||||
@@ -305,17 +367,35 @@ function selected(item: Item) {
|
||||
close();
|
||||
}
|
||||
|
||||
async function runCommand(command: Command) {
|
||||
close();
|
||||
await command.perform();
|
||||
}
|
||||
|
||||
function onEnter() {
|
||||
// Plain Enter prefers a container match if one is selected. With no
|
||||
// container matches (cloud-only query like "OOM"), fall back to log search
|
||||
// so the user isn't stuck on a popup that does nothing.
|
||||
if (data.value.length > 0) {
|
||||
selected(data.value[selectedIndex.value].item);
|
||||
// Commands come first in the flat list, then containers. With nothing
|
||||
// selectable (cloud-only query like "OOM"), fall back to log search so the
|
||||
// user isn't stuck on a popup that does nothing.
|
||||
const commandCount = commandEntries.value.length;
|
||||
if (selectedIndex.value < commandCount) {
|
||||
runCommand(commandEntries.value[selectedIndex.value]);
|
||||
} else if (containerEntries.value.length > 0) {
|
||||
selected(containerEntries.value[selectedIndex.value - commandCount].item);
|
||||
} else if (cloudSearch.available.value && logSearchVisible.value) {
|
||||
runLogSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function onPin() {
|
||||
// Alt+Enter pins a container column. Only meaningful when a container row is
|
||||
// selected, not a command.
|
||||
const commandCount = commandEntries.value.length;
|
||||
if (selectedIndex.value >= commandCount) {
|
||||
const entry = containerEntries.value[selectedIndex.value - commandCount];
|
||||
if (entry?.item.type === "container") addColumn(entry.item);
|
||||
}
|
||||
}
|
||||
|
||||
function runLogSearch() {
|
||||
if (!cloudSearch.available.value) return;
|
||||
const q = query.value.trim();
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import type { Component } from "vue";
|
||||
import { Container } from "@/models/Container";
|
||||
import { useContainerActions } from "@/composable/containerActions";
|
||||
import config from "@/stores/config";
|
||||
import {
|
||||
lightTheme,
|
||||
compact,
|
||||
showTimestamp,
|
||||
softWrap,
|
||||
showAllContainers,
|
||||
showStd,
|
||||
smallerScrollbars,
|
||||
} from "@/stores/settings";
|
||||
|
||||
import mdiThemeLightDark from "~icons/mdi/theme-light-dark";
|
||||
import mdiWhiteBalanceSunny from "~icons/mdi/white-balance-sunny";
|
||||
import mdiWeatherNight from "~icons/mdi/weather-night";
|
||||
import mdiFormatLineSpacing from "~icons/mdi/format-line-spacing";
|
||||
import mdiClockOutline from "~icons/mdi/clock-outline";
|
||||
import mdiWrap from "~icons/mdi/wrap";
|
||||
import mdiEyeOutline from "~icons/mdi/eye-outline";
|
||||
import mdiFormatListBulleted from "~icons/mdi/format-list-bulleted";
|
||||
import mdiUnfoldMoreHorizontal from "~icons/mdi/unfold-more-horizontal";
|
||||
import mdiCogOutline from "~icons/mdi/cog-outline";
|
||||
import carbonRestart from "~icons/carbon/restart";
|
||||
import mdiStop from "~icons/mdi/stop";
|
||||
import mdiPlay from "~icons/mdi/play";
|
||||
import mdiDownload from "~icons/mdi/download";
|
||||
|
||||
export type CommandSection = "container" | "settings" | "navigation";
|
||||
|
||||
export type Command = {
|
||||
id: string;
|
||||
title: string;
|
||||
section: CommandSection;
|
||||
icon: Component;
|
||||
keywords?: string;
|
||||
perform: () => unknown;
|
||||
};
|
||||
|
||||
// Central registry for the Cmd+K command palette. Commands are recomputed on
|
||||
// every access so context-sensitive entries (container actions, current
|
||||
// toggle labels) stay in sync with the route and settings.
|
||||
export function useCommands() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const containerStore = useContainerStore();
|
||||
|
||||
const currentId = computed(() =>
|
||||
route?.name === "/container/[id]" && typeof route.params.id === "string" ? route.params.id : "",
|
||||
);
|
||||
// Null-safe: containerStore.currentContainer is a stubbed action under
|
||||
// @pinia/testing, so guard against it being absent.
|
||||
const currentContainerRef = containerStore.currentContainer?.(currentId);
|
||||
const currentContainer = computed(() => currentContainerRef?.value as Container | undefined);
|
||||
|
||||
// Bound to the current container. The cast is safe because the action
|
||||
// handlers only read container.value when invoked, and container commands are
|
||||
// only pushed into the list when currentContainer is truthy — so the handlers
|
||||
// never run against an undefined container.
|
||||
const { start, stop, restart, update } = useContainerActions(currentContainer as Ref<Container>);
|
||||
|
||||
const commands = computed<Command[]>(() => {
|
||||
const list: Command[] = [];
|
||||
|
||||
const container = currentContainer.value;
|
||||
if (container && config.enableActions) {
|
||||
const name = container.name;
|
||||
list.push({
|
||||
id: "container.restart",
|
||||
section: "container",
|
||||
icon: carbonRestart,
|
||||
title: t("command-palette.restart-container", { name }),
|
||||
keywords: "restart reboot",
|
||||
perform: restart,
|
||||
});
|
||||
if (container.state === "running") {
|
||||
list.push({
|
||||
id: "container.stop",
|
||||
section: "container",
|
||||
icon: mdiStop,
|
||||
title: t("command-palette.stop-container", { name }),
|
||||
keywords: "stop kill halt",
|
||||
perform: stop,
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
id: "container.start",
|
||||
section: "container",
|
||||
icon: mdiPlay,
|
||||
title: t("command-palette.start-container", { name }),
|
||||
keywords: "start run",
|
||||
perform: start,
|
||||
});
|
||||
}
|
||||
list.push({
|
||||
id: "container.update",
|
||||
section: "container",
|
||||
icon: mdiDownload,
|
||||
title: t("command-palette.update-container", { name }),
|
||||
keywords: "update pull recreate upgrade",
|
||||
perform: update,
|
||||
});
|
||||
}
|
||||
|
||||
list.push(
|
||||
// lightTheme is tri-state, so expose each value as its own command rather
|
||||
// than a single toggle — that keeps "auto" (follow OS) reachable and makes
|
||||
// the target theme explicit instead of depending on the current state.
|
||||
{
|
||||
id: "settings.theme-auto",
|
||||
section: "settings",
|
||||
icon: mdiThemeLightDark,
|
||||
title: t("command-palette.theme-auto"),
|
||||
keywords: "theme auto system color mode appearance",
|
||||
perform: () => (lightTheme.value = "auto"),
|
||||
},
|
||||
{
|
||||
id: "settings.theme-light",
|
||||
section: "settings",
|
||||
icon: mdiWhiteBalanceSunny,
|
||||
title: t("command-palette.theme-light"),
|
||||
keywords: "theme light color mode appearance",
|
||||
perform: () => (lightTheme.value = "light"),
|
||||
},
|
||||
{
|
||||
id: "settings.theme-dark",
|
||||
section: "settings",
|
||||
icon: mdiWeatherNight,
|
||||
title: t("command-palette.theme-dark"),
|
||||
keywords: "theme dark color mode appearance",
|
||||
perform: () => (lightTheme.value = "dark"),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-compact",
|
||||
section: "settings",
|
||||
icon: mdiFormatLineSpacing,
|
||||
title: t("command-palette.toggle-compact"),
|
||||
keywords: "compact density spacing",
|
||||
perform: () => (compact.value = !compact.value),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-timestamps",
|
||||
section: "settings",
|
||||
icon: mdiClockOutline,
|
||||
title: t("command-palette.toggle-timestamps"),
|
||||
keywords: "timestamp time date",
|
||||
perform: () => (showTimestamp.value = !showTimestamp.value),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-soft-wrap",
|
||||
section: "settings",
|
||||
icon: mdiWrap,
|
||||
title: t("command-palette.toggle-soft-wrap"),
|
||||
keywords: "wrap soft line",
|
||||
perform: () => (softWrap.value = !softWrap.value),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-stopped",
|
||||
section: "settings",
|
||||
icon: mdiEyeOutline,
|
||||
title: t("command-palette.toggle-stopped"),
|
||||
keywords: "stopped hidden all containers exited",
|
||||
perform: () => (showAllContainers.value = !showAllContainers.value),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-std",
|
||||
section: "settings",
|
||||
icon: mdiFormatListBulleted,
|
||||
title: t("command-palette.toggle-std"),
|
||||
keywords: "stdout stderr std labels stream",
|
||||
perform: () => (showStd.value = !showStd.value),
|
||||
},
|
||||
{
|
||||
id: "settings.toggle-scrollbars",
|
||||
section: "settings",
|
||||
icon: mdiUnfoldMoreHorizontal,
|
||||
title: t("command-palette.toggle-scrollbars"),
|
||||
keywords: "scrollbar smaller thin",
|
||||
perform: () => (smallerScrollbars.value = !smallerScrollbars.value),
|
||||
},
|
||||
{
|
||||
id: "navigation.settings",
|
||||
section: "navigation",
|
||||
icon: mdiCogOutline,
|
||||
title: t("command-palette.open-settings"),
|
||||
keywords: "settings preferences options config",
|
||||
perform: () => router.push("/settings"),
|
||||
},
|
||||
);
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
// Commands shown before the user types anything: the context-sensitive
|
||||
// container actions so e.g. Restart is one keystroke away on a container page.
|
||||
const contextCommands = computed(() => commands.value.filter((c) => c.section === "container"));
|
||||
|
||||
return { commands, contextCommands };
|
||||
}
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "Containeren er slettet"
|
||||
cta-settings: "Cloud-indstillinger"
|
||||
container-removed-pill: "slettet"
|
||||
command-palette:
|
||||
section-commands: Kommandoer
|
||||
restart-container: Genstart {name}
|
||||
stop-container: Stop {name}
|
||||
start-container: Start {name}
|
||||
update-container: Opdater {name}
|
||||
theme-auto: Brug systemtema
|
||||
theme-light: Skift til lyst tema
|
||||
theme-dark: Skift til mørkt tema
|
||||
toggle-compact: Skift kompakt tilstand
|
||||
toggle-timestamps: Skift tidsstempler
|
||||
toggle-soft-wrap: Skift blød ombrydning
|
||||
toggle-stopped: Skift stoppede containere
|
||||
toggle-std: Skift stdout/stderr-etiketter
|
||||
toggle-scrollbars: Skift mindre rullepaneler
|
||||
open-settings: Åbn indstillinger
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "Container wurde gelöscht"
|
||||
cta-settings: "Cloud-Einstellungen"
|
||||
container-removed-pill: "gelöscht"
|
||||
command-palette:
|
||||
section-commands: Befehle
|
||||
restart-container: "{name} neu starten"
|
||||
stop-container: "{name} stoppen"
|
||||
start-container: "{name} starten"
|
||||
update-container: "{name} aktualisieren"
|
||||
theme-auto: Systemdesign verwenden
|
||||
theme-light: Zu hellem Design wechseln
|
||||
theme-dark: Zu dunklem Design wechseln
|
||||
toggle-compact: Kompaktmodus umschalten
|
||||
toggle-timestamps: Zeitstempel umschalten
|
||||
toggle-soft-wrap: Zeilenumbruch umschalten
|
||||
toggle-stopped: Gestoppte Container umschalten
|
||||
toggle-std: stdout/stderr-Labels umschalten
|
||||
toggle-scrollbars: Kleinere Scrollleisten umschalten
|
||||
open-settings: Einstellungen öffnen
|
||||
|
||||
@@ -160,6 +160,22 @@ cloud-search:
|
||||
container-removed: Container has been deleted
|
||||
cta-settings: "Cloud settings"
|
||||
container-removed-pill: removed
|
||||
command-palette:
|
||||
section-commands: Commands
|
||||
restart-container: Restart {name}
|
||||
stop-container: Stop {name}
|
||||
start-container: Start {name}
|
||||
update-container: Update {name}
|
||||
theme-auto: Use system theme
|
||||
theme-light: Switch to light theme
|
||||
theme-dark: Switch to dark theme
|
||||
toggle-compact: Toggle compact mode
|
||||
toggle-timestamps: Toggle timestamps
|
||||
toggle-soft-wrap: Toggle soft wrap
|
||||
toggle-stopped: Toggle stopped containers
|
||||
toggle-std: Toggle stdout/stderr labels
|
||||
toggle-scrollbars: Toggle smaller scrollbars
|
||||
open-settings: Open settings
|
||||
settings:
|
||||
help-support: >
|
||||
Please support Dozzle by donating or sponsoring us on GitHub. Your contributions help us improve Dozzle for
|
||||
|
||||
@@ -156,6 +156,22 @@ cloud-search:
|
||||
container-removed: "El contenedor ha sido eliminado"
|
||||
cta-settings: "Configuración de Cloud"
|
||||
container-removed-pill: "eliminado"
|
||||
command-palette:
|
||||
section-commands: Comandos
|
||||
restart-container: Reiniciar {name}
|
||||
stop-container: Detener {name}
|
||||
start-container: Iniciar {name}
|
||||
update-container: Actualizar {name}
|
||||
theme-auto: Usar tema del sistema
|
||||
theme-light: Cambiar a tema claro
|
||||
theme-dark: Cambiar a tema oscuro
|
||||
toggle-compact: Alternar modo compacto
|
||||
toggle-timestamps: Alternar marcas de tiempo
|
||||
toggle-soft-wrap: Alternar ajuste de línea
|
||||
toggle-stopped: Alternar contenedores detenidos
|
||||
toggle-std: Alternar etiquetas stdout/stderr
|
||||
toggle-scrollbars: Alternar barras de desplazamiento más pequeñas
|
||||
open-settings: Abrir configuración
|
||||
settings:
|
||||
help-support: >
|
||||
Por favor, apoya a Dozzle donando o patrocinándonos en GitHub. Tus contribuciones nos ayudan a mejorar Dozzle para todos. ¡Gracias! 🙏🏼
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "Le conteneur a été supprimé"
|
||||
cta-settings: "Paramètres Cloud"
|
||||
container-removed-pill: "supprimé"
|
||||
command-palette:
|
||||
section-commands: Commandes
|
||||
restart-container: Redémarrer {name}
|
||||
stop-container: Arrêter {name}
|
||||
start-container: Démarrer {name}
|
||||
update-container: Mettre à jour {name}
|
||||
theme-auto: Utiliser le thème du système
|
||||
theme-light: Passer au thème clair
|
||||
theme-dark: Passer au thème sombre
|
||||
toggle-compact: Basculer le mode compact
|
||||
toggle-timestamps: Basculer les horodatages
|
||||
toggle-soft-wrap: Basculer le retour à la ligne
|
||||
toggle-stopped: Basculer les conteneurs arrêtés
|
||||
toggle-std: Basculer les étiquettes stdout/stderr
|
||||
toggle-scrollbars: Basculer les barres de défilement réduites
|
||||
open-settings: Ouvrir les paramètres
|
||||
|
||||
@@ -420,3 +420,19 @@ cloud-search:
|
||||
container-removed: "Kontainer telah dihapus"
|
||||
cta-settings: "Pengaturan Cloud"
|
||||
container-removed-pill: "dihapus"
|
||||
command-palette:
|
||||
section-commands: Perintah
|
||||
restart-container: Mulai ulang {name}
|
||||
stop-container: Hentikan {name}
|
||||
start-container: Jalankan {name}
|
||||
update-container: Perbarui {name}
|
||||
theme-auto: Gunakan tema sistem
|
||||
theme-light: Beralih ke tema terang
|
||||
theme-dark: Beralih ke tema gelap
|
||||
toggle-compact: Alihkan mode ringkas
|
||||
toggle-timestamps: Alihkan stempel waktu
|
||||
toggle-soft-wrap: Alihkan bungkus lembut
|
||||
toggle-stopped: Alihkan kontainer yang dihentikan
|
||||
toggle-std: Alihkan label stdout/stderr
|
||||
toggle-scrollbars: Alihkan bilah gulir lebih kecil
|
||||
open-settings: Buka pengaturan
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "Il container è stato eliminato"
|
||||
cta-settings: "Impostazioni Cloud"
|
||||
container-removed-pill: "eliminato"
|
||||
command-palette:
|
||||
section-commands: Comandi
|
||||
restart-container: Riavvia {name}
|
||||
stop-container: Ferma {name}
|
||||
start-container: Avvia {name}
|
||||
update-container: Aggiorna {name}
|
||||
theme-auto: Usa tema di sistema
|
||||
theme-light: Passa al tema chiaro
|
||||
theme-dark: Passa al tema scuro
|
||||
toggle-compact: Attiva/disattiva modalità compatta
|
||||
toggle-timestamps: Attiva/disattiva timestamp
|
||||
toggle-soft-wrap: Attiva/disattiva a capo automatico
|
||||
toggle-stopped: Attiva/disattiva container fermati
|
||||
toggle-std: Attiva/disattiva etichette stdout/stderr
|
||||
toggle-scrollbars: Attiva/disattiva barre di scorrimento più piccole
|
||||
open-settings: Apri impostazioni
|
||||
|
||||
@@ -412,3 +412,19 @@ cloud-search:
|
||||
container-removed: "컨테이너가 삭제되었습니다"
|
||||
cta-settings: "Cloud 설정"
|
||||
container-removed-pill: "삭제됨"
|
||||
command-palette:
|
||||
section-commands: 명령
|
||||
restart-container: "{name} 재시작"
|
||||
stop-container: "{name} 중지"
|
||||
start-container: "{name} 시작"
|
||||
update-container: "{name} 업데이트"
|
||||
theme-auto: 시스템 테마 사용
|
||||
theme-light: 밝은 테마로 전환
|
||||
theme-dark: 어두운 테마로 전환
|
||||
toggle-compact: 컴팩트 모드 전환
|
||||
toggle-timestamps: 타임스탬프 전환
|
||||
toggle-soft-wrap: 자동 줄바꿈 전환
|
||||
toggle-stopped: 중지된 컨테이너 전환
|
||||
toggle-std: stdout/stderr 라벨 전환
|
||||
toggle-scrollbars: 작은 스크롤바 전환
|
||||
open-settings: 설정 열기
|
||||
|
||||
@@ -409,3 +409,19 @@ cloud-search:
|
||||
container-removed: "Container is verwijderd"
|
||||
cta-settings: "Cloud-instellingen"
|
||||
container-removed-pill: "verwijderd"
|
||||
command-palette:
|
||||
section-commands: Opdrachten
|
||||
restart-container: "{name} opnieuw starten"
|
||||
stop-container: "{name} stoppen"
|
||||
start-container: "{name} starten"
|
||||
update-container: "{name} bijwerken"
|
||||
theme-auto: Systeemthema gebruiken
|
||||
theme-light: Overschakelen naar licht thema
|
||||
theme-dark: Overschakelen naar donker thema
|
||||
toggle-compact: Compacte modus omschakelen
|
||||
toggle-timestamps: Tijdstempels omschakelen
|
||||
toggle-soft-wrap: Zachte terugloop omschakelen
|
||||
toggle-stopped: Gestopte containers omschakelen
|
||||
toggle-std: stdout/stderr-labels omschakelen
|
||||
toggle-scrollbars: Kleinere schuifbalken omschakelen
|
||||
open-settings: Instellingen openen
|
||||
|
||||
@@ -411,3 +411,19 @@ cloud-search:
|
||||
container-removed: "Kontener został usunięty"
|
||||
cta-settings: "Ustawienia Cloud"
|
||||
container-removed-pill: "usunięty"
|
||||
command-palette:
|
||||
section-commands: Polecenia
|
||||
restart-container: Uruchom ponownie {name}
|
||||
stop-container: Zatrzymaj {name}
|
||||
start-container: Uruchom {name}
|
||||
update-container: Zaktualizuj {name}
|
||||
theme-auto: Użyj motywu systemowego
|
||||
theme-light: Przełącz na jasny motyw
|
||||
theme-dark: Przełącz na ciemny motyw
|
||||
toggle-compact: Przełącz tryb kompaktowy
|
||||
toggle-timestamps: Przełącz znaczniki czasu
|
||||
toggle-soft-wrap: Przełącz zawijanie wierszy
|
||||
toggle-stopped: Przełącz zatrzymane kontenery
|
||||
toggle-std: Przełącz etykiety stdout/stderr
|
||||
toggle-scrollbars: Przełącz mniejsze paski przewijania
|
||||
open-settings: Otwórz ustawienia
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "O container foi excluído"
|
||||
cta-settings: "Configurações da Cloud"
|
||||
container-removed-pill: "removido"
|
||||
command-palette:
|
||||
section-commands: Comandos
|
||||
restart-container: Reiniciar {name}
|
||||
stop-container: Parar {name}
|
||||
start-container: Iniciar {name}
|
||||
update-container: Atualizar {name}
|
||||
theme-auto: Usar tema do sistema
|
||||
theme-light: Mudar para tema claro
|
||||
theme-dark: Mudar para tema escuro
|
||||
toggle-compact: Alternar modo compacto
|
||||
toggle-timestamps: Alternar carimbos de data/hora
|
||||
toggle-soft-wrap: Alternar quebra de linha
|
||||
toggle-stopped: Alternar contêineres parados
|
||||
toggle-std: Alternar rótulos stdout/stderr
|
||||
toggle-scrollbars: Alternar barras de rolagem menores
|
||||
open-settings: Abrir configurações
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "Контейнер был удалён"
|
||||
cta-settings: "Настройки Cloud"
|
||||
container-removed-pill: "удалён"
|
||||
command-palette:
|
||||
section-commands: Команды
|
||||
restart-container: Перезапустить {name}
|
||||
stop-container: Остановить {name}
|
||||
start-container: Запустить {name}
|
||||
update-container: Обновить {name}
|
||||
theme-auto: Использовать системную тему
|
||||
theme-light: Переключить на светлую тему
|
||||
theme-dark: Переключить на тёмную тему
|
||||
toggle-compact: Переключить компактный режим
|
||||
toggle-timestamps: Переключить метки времени
|
||||
toggle-soft-wrap: Переключить мягкий перенос
|
||||
toggle-stopped: Переключить остановленные контейнеры
|
||||
toggle-std: Переключить метки stdout/stderr
|
||||
toggle-scrollbars: Переключить уменьшенные полосы прокрутки
|
||||
open-settings: Открыть настройки
|
||||
|
||||
@@ -414,3 +414,19 @@ cloud-search:
|
||||
container-removed: "Vsebnik je bil izbrisan"
|
||||
cta-settings: "Nastavitve Cloud"
|
||||
container-removed-pill: "izbrisano"
|
||||
command-palette:
|
||||
section-commands: Ukazi
|
||||
restart-container: Znova zaženi {name}
|
||||
stop-container: Ustavi {name}
|
||||
start-container: Zaženi {name}
|
||||
update-container: Posodobi {name}
|
||||
theme-auto: Uporabi sistemsko temo
|
||||
theme-light: Preklopi na svetlo temo
|
||||
theme-dark: Preklopi na temno temo
|
||||
toggle-compact: Preklopi strnjeni način
|
||||
toggle-timestamps: Preklopi časovne žige
|
||||
toggle-soft-wrap: Preklopi mehko prelamljanje
|
||||
toggle-stopped: Preklopi ustavljene vsebnike
|
||||
toggle-std: Preklopi oznake stdout/stderr
|
||||
toggle-scrollbars: Preklopi manjše drsnike
|
||||
open-settings: Odpri nastavitve
|
||||
|
||||
@@ -412,3 +412,19 @@ cloud-search:
|
||||
container-removed: "Konteyner silindi"
|
||||
cta-settings: "Cloud ayarları"
|
||||
container-removed-pill: "silindi"
|
||||
command-palette:
|
||||
section-commands: Komutlar
|
||||
restart-container: "{name} yeniden başlat"
|
||||
stop-container: "{name} durdur"
|
||||
start-container: "{name} başlat"
|
||||
update-container: "{name} güncelle"
|
||||
theme-auto: Sistem temasını kullan
|
||||
theme-light: Açık temaya geç
|
||||
theme-dark: Koyu temaya geç
|
||||
toggle-compact: Kompakt modu aç/kapat
|
||||
toggle-timestamps: Zaman damgalarını aç/kapat
|
||||
toggle-soft-wrap: Yumuşak kaydırmayı aç/kapat
|
||||
toggle-stopped: Durdurulmuş konteynerleri aç/kapat
|
||||
toggle-std: stdout/stderr etiketlerini aç/kapat
|
||||
toggle-scrollbars: Daha küçük kaydırma çubuklarını aç/kapat
|
||||
open-settings: Ayarları aç
|
||||
|
||||
@@ -411,3 +411,19 @@ cloud-search:
|
||||
container-removed: "容器已被刪除"
|
||||
cta-settings: "Cloud 設定"
|
||||
container-removed-pill: "已刪除"
|
||||
command-palette:
|
||||
section-commands: 命令
|
||||
restart-container: 重新啟動 {name}
|
||||
stop-container: 停止 {name}
|
||||
start-container: 啟動 {name}
|
||||
update-container: 更新 {name}
|
||||
theme-auto: 使用系統主題
|
||||
theme-light: 切換到淺色主題
|
||||
theme-dark: 切換到深色主題
|
||||
toggle-compact: 切換精簡模式
|
||||
toggle-timestamps: 切換時間戳記
|
||||
toggle-soft-wrap: 切換軟換行
|
||||
toggle-stopped: 切換已停止的容器
|
||||
toggle-std: 切換 stdout/stderr 標籤
|
||||
toggle-scrollbars: 切換較小的捲軸
|
||||
open-settings: 開啟設定
|
||||
|
||||
@@ -408,3 +408,19 @@ cloud-search:
|
||||
container-removed: "容器已被删除"
|
||||
cta-settings: "Cloud 设置"
|
||||
container-removed-pill: "已删除"
|
||||
command-palette:
|
||||
section-commands: 命令
|
||||
restart-container: 重启 {name}
|
||||
stop-container: 停止 {name}
|
||||
start-container: 启动 {name}
|
||||
update-container: 更新 {name}
|
||||
theme-auto: 使用系统主题
|
||||
theme-light: 切换到浅色主题
|
||||
theme-dark: 切换到深色主题
|
||||
toggle-compact: 切换紧凑模式
|
||||
toggle-timestamps: 切换时间戳
|
||||
toggle-soft-wrap: 切换软换行
|
||||
toggle-stopped: 切换已停止的容器
|
||||
toggle-std: 切换 stdout/stderr 标签
|
||||
toggle-scrollbars: 切换更小的滚动条
|
||||
open-settings: 打开设置
|
||||
|
||||
Reference in New Issue
Block a user