feat(ui): small improvements and polishments (#4862)

This commit is contained in:
Baiyi Liao
2026-07-24 07:11:13 -07:00
committed by GitHub
parent af38e3a0c0
commit 49f625a1db
33 changed files with 193 additions and 24 deletions
+4
View File
@@ -6,10 +6,12 @@
// biome-ignore lint: disable
export {}
declare global {
const DEFAULT_MENU_WIDTH: typeof import('./stores/settings').DEFAULT_MENU_WIDTH
const DEFAULT_SETTINGS: typeof import('./stores/settings').DEFAULT_SETTINGS
const EffectScope: typeof import('vue').EffectScope
const K8sNamespace: typeof import('./stores/k8s').K8sNamespace
const K8sOwner: typeof import('./stores/k8s').K8sOwner
const MIN_MENU_WIDTH: typeof import('./stores/settings').MIN_MENU_WIDTH
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const allLevels: typeof import('./composable/logContext').allLevels
const arrayEquals: typeof import('./utils/index').arrayEquals
@@ -451,10 +453,12 @@ import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly DEFAULT_MENU_WIDTH: UnwrapRef<typeof import('./stores/settings')['DEFAULT_MENU_WIDTH']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly K8sNamespace: UnwrapRef<typeof import('./stores/k8s')['K8sNamespace']>
readonly K8sOwner: UnwrapRef<typeof import('./stores/k8s')['K8sOwner']>
readonly MIN_MENU_WIDTH: UnwrapRef<typeof import('./stores/settings')['MIN_MENU_WIDTH']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
@@ -199,7 +199,7 @@ const clear = defineEmit();
const { actionStates, start, stop, restart, update } = useContainerActions(toRef(() => container));
const router = useRouter();
const { copy, copied, isSupported } = useClipboard();
const { copy, copied, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n();
const { showToast, removeToast } = useToast();
@@ -366,10 +366,25 @@ a {
@apply whitespace-nowrap;
}
/* daisyUI's .menu is width: fit-content, so nested submenus (Streams, Levels)
* shrink to their content and the hover highlight stops short. Stretch them to
* fill the dropdown so the row highlight spans the full width. */
.menu li ul {
margin-inline-start: 0;
width: 100%;
&:before {
display: none;
}
}
/* Keep the solid level colors, but use white labels in the light theme so the
* text reads against the saturated chip backgrounds. warn is a light orange,
* where dark text has better contrast than white, so it keeps the default. */
[data-theme="light"] .badge[data-level="info"],
[data-theme="light"] .badge[data-level="debug"],
[data-theme="light"] .badge[data-level="trace"],
[data-theme="light"] .badge[data-level="error"],
[data-theme="light"] .badge[data-level="fatal"] {
color: oklch(100% 0 0) !important;
}
</style>
@@ -46,8 +46,20 @@
</div>
<ContainerHealth :health="container.health" v-if="container.health" />
<VolumeWarning :container="container" />
<Tag class="hidden! font-mono @xl:block!" size="small">
{{ container.image.replace(/@sha.*/, "") }}
<Tag
class="group hidden! cursor-pointer items-center gap-1.5 pr-1! font-mono @md:inline-flex!"
size="small"
role="button"
:title="$t('toolbar.copy-image')"
:aria-label="$t('toolbar.copy-image')"
@click="copyImage"
>
<span class="truncate">{{ imageTag }}</span>
<span
class="bg-base-content/10 text-base-content/40 group-hover:text-base-content/70 flex size-4 shrink-0 items-center justify-center rounded-sm transition-colors"
>
<mdi:content-copy class="size-3" />
</span>
</Tag>
</div>
</template>
@@ -56,6 +68,21 @@
import { Container } from "@/models/Container";
const { container } = defineProps<{ container: Container }>();
const { t } = useI18n();
const { copy, copied, isSupported } = useClipboard({ legacy: true });
const { showToast } = useToast();
const imageTag = computed(() => container.image.replace(/@sha.*/, ""));
async function copyImage() {
if (!isSupported.value) return;
await copy(imageTag.value);
if (copied.value) {
showToast({ title: t("toasts.copied.title"), message: t("toasts.copied.message"), type: "info" }, { expire: 2000 });
}
}
const pinned = computed({
get: () => pinnedContainers.value.has(container.name),
set: (value) => {
+1 -1
View File
@@ -35,7 +35,7 @@
<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="pb-1">
<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"
+1 -1
View File
@@ -105,7 +105,7 @@ const { levels } = useLoggingContext();
// or by a log-level filter, so the entry can be inspected in the full log stream.
const isFiltered = computed(() => isSearching.value || allLevels.some((level) => !levels.value.has(level)));
const { copy, isSupported, copied } = useClipboard();
const { copy, isSupported, copied } = useClipboard({ legacy: true });
const { t } = useI18n();
async function copyLogMessage() {
@@ -14,10 +14,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
<li><a href="/container/abc/time/2019-06-12T10:55:42.459Z?logId=1" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M6 23H3q-.825 0-1.412-.587T1 21v-3h2v3h3zm12 0v-2h3v-3h2v3q0 .825-.587 1.413T21 23zm-6-4.5q-3 0-5.437-1.775T3 12q1.125-2.95 3.563-4.725T12 5.5t5.438 1.775T21 12q-1.125 2.95-3.562 4.725T12 18.5m0-3q1.45 0 2.475-1.025T15.5 12t-1.025-2.475T12 8.5T9.525 9.525T8.5 12t1.025 2.475T12 15.5m0-2q-.625 0-1.062-.437T10.5 12t.438-1.062T12 10.5t1.063.438T13.5 12t-.437 1.063T12 13.5M1 6V3q0-.825.588-1.412T3 1h3v2H3v3zm20 0V3h-3V1h3q.825 0 1.413.588T23 3v3z"></path>
</svg> action.see-in-context</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
@@ -55,10 +55,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
<li><a href="/container/abc/time/2019-06-12T10:55:42.459Z?logId=1" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M6 23H3q-.825 0-1.412-.587T1 21v-3h2v3h3zm12 0v-2h3v-3h2v3q0 .825-.587 1.413T21 23zm-6-4.5q-3 0-5.437-1.775T3 12q1.125-2.95 3.563-4.725T12 5.5t5.438 1.775T21 12q-1.125 2.95-3.562 4.725T12 18.5m0-3q1.45 0 2.475-1.025T15.5 12t-1.025-2.475T12 8.5T9.525 9.525T8.5 12t1.025 2.475T12 15.5m0-2q-.625 0-1.062-.437T10.5 12t.438-1.062T12 10.5t1.063.438T13.5 12t-.437 1.063T12 13.5M1 6V3q0-.825.588-1.412T3 1h3v2H3v3zm20 0V3h-3V1h3q.825 0 1.413.588T23 3v3z"></path>
</svg> action.see-in-context</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
@@ -96,10 +96,10 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
<li><a href="/container/abc/time/2019-06-12T10:55:42.459Z?logId=1" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M6 23H3q-.825 0-1.412-.587T1 21v-3h2v3h3zm12 0v-2h3v-3h2v3q0 .825-.587 1.413T21 23zm-6-4.5q-3 0-5.437-1.775T3 12q1.125-2.95 3.563-4.725T12 5.5t5.438 1.775T21 12q-1.125 2.95-3.562 4.725T12 18.5m0-3q1.45 0 2.475-1.025T15.5 12t-1.025-2.475T12 8.5T9.525 9.525T8.5 12t1.025 2.475T12 15.5m0-2q-.625 0-1.062-.437T10.5 12t.438-1.062T12 10.5t1.063.438T13.5 12t-.437 1.063T12 13.5M1 6V3q0-.825.588-1.412T3 1h3v2H3v3zm20 0V3h-3V1h3q.825 0 1.413.588T23 3v3z"></path>
</svg> action.see-in-context</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<li><a disabled="false" title="" class=""><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
+1 -1
View File
@@ -2,7 +2,7 @@
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
<header
v-if="$slots.header"
class="border-base-content/10 bg-base-200 sticky top-[calc(55px+env(safe-area-inset-top))] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
class="border-base-content/10 bg-base-200 sticky top-[var(--mobile-nav-height)] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
>
<slot name="header"></slot>
</header>
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-10 w-full border-b" data-testid="navigation">
<div class="p-2">
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-30 w-full border-b" data-testid="navigation">
<div class="px-4 py-2">
<div class="flex items-center">
<router-link :to="{ name: '/' }">
<Logo class="h-10" />
+20 -7
View File
@@ -1,11 +1,15 @@
<template>
<div>
<MobileMenu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></MobileMenu>
<Splitpanes @resized="onResized($event)">
<Pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav && !forceMenuHidden">
<SidePanel />
<Splitpanes @resized="onResized($event)" :class="{ 'nav-collapsed': collapseNav }">
<Pane
:min-size="collapseNav ? 0 : MIN_MENU_WIDTH"
:size="collapseNav ? 0 : menuWidth"
v-if="!isMobile && !forceMenuHidden"
>
<SidePanel v-show="!collapseNav" />
</Pane>
<Pane min-size="10" :size="100 - menuWidth">
<Pane :min-size="MIN_MENU_WIDTH" :size="collapseNav ? 100 : 100 - menuWidth">
<Splitpanes>
<Pane class="router-view min-h-screen">
<router-view></router-view>
@@ -35,7 +39,7 @@
</label>
</div>
<dialog ref="modal" class="modal bg-base-300/50! items-start backdrop-blur-md transition-none!" @close="closeSearch">
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
<div class="modal-box max-w-2xl overflow-visible! bg-transparent pt-20 shadow-none">
<FuzzySearchModal @close="closeSearch" v-if="open" />
</div>
<form method="dialog" class="modal-backdrop">
@@ -53,7 +57,7 @@
<script lang="ts" setup>
import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/stores/settings";
import { collapseNav, MIN_MENU_WIDTH } from "@/stores/settings";
import SideDrawer from "@/components/common/SideDrawer.vue";
const pinnedLogsStore = usePinnedLogsStore();
@@ -85,6 +89,8 @@ onKeyStroke("k", (e) => {
});
function onResized({ panes }: { panes: { size: number }[] }) {
// Ignore the resize that collapsing/expanding triggers; only persist drags.
if (collapseNav.value) return;
if (panes.length == 2) {
menuWidth.value = Math.min(panes[0].size, 50);
}
@@ -96,11 +102,18 @@ function onResized({ panes }: { panes: { size: number }[] }) {
:deep(.splitpanes--vertical > .splitpanes__splitter) {
@apply bg-base-100 hover:bg-secondary min-w-[5px];
transition: opacity 0.3s cubic-bezier(0.2, 0, 0, 1);
}
/* Hide (and disable) the resize handle while the sidebar is collapsed. */
:deep(.splitpanes.nav-collapsed > .splitpanes__splitter) {
opacity: 0;
pointer-events: none;
}
@media screen and (max-width: 768px) {
.router-view {
padding-top: 75px;
padding-top: var(--mobile-nav-height);
}
}
</style>
+28
View File
@@ -156,6 +156,14 @@
}
}
/* daisyUI darkens the ghost button's border on hover, which reads as a
* near-black ring on the dark theme. Use a theme-aware base-content tint so the
* hover ring stays visible and legible in both themes (light on dark, dark on
* light) instead of a fixed near-black. */
.btn-ghost:hover {
border-color: color-mix(in oklab, var(--color-base-content) 40%, transparent) !important;
}
@utility menu-active {
&,
&:active {
@@ -179,6 +187,14 @@
}
}
/* Reserve the document scrollbar gutter so the layout width stays constant when
* a page-level scrollbar appears (e.g. once a container's logs load). Without
* this, the width jump makes the percentage-based splitpanes recompute and the
* sidebar visibly flicks. */
html {
scrollbar-gutter: stable;
}
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
@@ -186,11 +202,23 @@ body {
padding-right: env(safe-area-inset-right);
}
/* Height of the mobile top nav (logo row + border). The container status bar
* sticks right below it and the log content is padded by the same amount, so
* they stay locked together. The safe-area inset is already handled by body. */
:root {
--mobile-nav-height: 57px;
}
[class*="shadow-"] {
@apply shadow-base-content/8;
}
.splitpanes--vertical .splitpanes__pane {
transition: width 0.3s cubic-bezier(0.2, 0, 0, 1);
}
/* Follow the pointer instantly while dragging; only animate programmatic width
* changes (collapse, expand). */
.splitpanes--vertical.splitpanes--dragging .splitpanes__pane {
transition: none !important;
}
+6 -1
View File
@@ -18,11 +18,16 @@ export type Settings = {
locale: string;
groupContainers: "always" | "at-least-2" | "never";
};
// Shared sidebar sizing (percent of the window width) so the layout and the
// collapse/expand handling always agree on one value.
export const DEFAULT_MENU_WIDTH = 15;
export const MIN_MENU_WIDTH = 10;
export const DEFAULT_SETTINGS: Settings = {
search: true,
compact: false,
size: "medium",
menuWidth: 15,
menuWidth: DEFAULT_MENU_WIDTH,
smallerScrollbars: false,
showTimestamp: true,
showStd: false,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+6 -2
View File
@@ -68,6 +68,9 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
}
}
// Inverse mode excludes lines matching the regex instead of keeping them.
inverse := r.URL.Query().Get("inverse") == "true"
// Parse level filters if provided
levels := make(map[string]struct{})
if r.URL.Query().Has("levels") {
@@ -158,8 +161,9 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
// Filter and write events
for event := range events {
// Apply regex filter if provided
if regex != nil && !support_web.Search(regex, event) {
// Apply regex filter if provided. In inverse mode a match excludes
// the line, so skip when the match result equals the inverse flag.
if regex != nil && inverse == support_web.Search(regex, event) {
continue
}
+57
View File
@@ -1,6 +1,7 @@
package web
import (
"archive/zip"
"bytes"
"io"
"time"
@@ -41,3 +42,59 @@ func Test_handler_download_logs(t *testing.T) {
require.Equal(t, http.StatusOK, rr.Code, "Status code should be 200.")
mockedClient.AssertExpectations(t)
}
func Test_handler_download_logs_inverse_filter(t *testing.T) {
id := "123456"
data := append(
makeMessage("2020-05-13T18:55:37.772853839Z ERROR boom\n", container.STDOUT),
makeMessage("2020-05-13T18:56:37.772853839Z INFO all good\n", container.STDOUT)...,
)
newHandler := func() http.Handler {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", mock.Anything, id).Return(container.Container{ID: id, Tty: false}, nil)
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, mock.Anything, mock.Anything, container.STDOUT).Return(io.NopCloser(bytes.NewReader(data)), nil)
mockedClient.On("Host").Return(container.Host{ID: "localhost"})
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
time.Sleep(1 * time.Second)
})
mockedClient.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
{ID: id, Name: "test", State: "running"},
}, nil)
return createDefaultHandler(mockedClient)
}
logContents := func(t *testing.T, body []byte) string {
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
require.NoError(t, err)
require.Len(t, zr.File, 1, "zip should contain a single log file")
f, err := zr.File[0].Open()
require.NoError(t, err)
defer f.Close()
content, err := io.ReadAll(f)
require.NoError(t, err)
return string(content)
}
t.Run("inverse excludes matching lines", func(t *testing.T) {
req, err := http.NewRequest("GET", "/api/containers/localhost~"+id+"/download?stdout=1&filter=ERROR&inverse=true", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
newHandler().ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
out := logContents(t, rr.Body.Bytes())
require.Contains(t, out, "all good")
require.NotContains(t, out, "boom")
})
t.Run("non-inverse keeps only matching lines", func(t *testing.T) {
req, err := http.NewRequest("GET", "/api/containers/localhost~"+id+"/download?stdout=1&filter=ERROR", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
newHandler().ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
out := logContents(t, rr.Body.Bytes())
require.Contains(t, out, "boom")
require.NotContains(t, out, "all good")
})
}
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Tilknyt
copy-permalink: Kopiér permanent link
copy-image: "Kopiér image-tag"
copy-logs: Kopiér logs
copy-filtered-logs: Kopiér filtrerede logs
copying-logs: Kopierer logs...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Anhängen
copy-permalink: Permanentlink kopieren
copy-image: "Image-Tag kopieren"
copy-logs: Logs kopieren
copy-filtered-logs: Gefilterte Logs kopieren
copying-logs: Logs werden kopiert...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Attach
copy-permalink: Copy permanent link
copy-image: Copy image tag
copy-logs: Copy logs
copy-filtered-logs: Copy Filtered Logs
copying-logs: Copying logs...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Conectar
copy-permalink: Copiar enlace permanente
copy-image: "Copiar etiqueta de imagen"
copy-logs: Copiar registros
copy-filtered-logs: Copiar registros filtrados
copying-logs: Copiando registros...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Attacher
copy-permalink: Copier le lien permanent
copy-image: "Copier le tag de l'image"
copy-logs: Copier les journaux
copy-filtered-logs: Copier les journaux filtrés
copying-logs: Copie des journaux...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Lampirkan
copy-permalink: Salin tautan permanen
copy-image: "Salin tag image"
copy-logs: Salin log
copy-filtered-logs: Salin log terfilter
copying-logs: Menyalin log...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Collega
copy-permalink: Copia link permanente
copy-image: "Copia tag immagine"
copy-logs: Copia log
copy-filtered-logs: Copia log filtrati
copying-logs: Copia dei log...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell:
attach: 연결
copy-permalink: 영구 링크 복사
copy-image: "이미지 태그 복사"
copy-logs: 로그 복사
copy-filtered-logs: 필터된 로그 복사
copying-logs: 로그 복사 중...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Koppelen
copy-permalink: Permanente link kopiëren
copy-image: "Image-tag kopiëren"
copy-logs: Logs kopiëren
copy-filtered-logs: Gefilterde logs kopiëren
copying-logs: Logs kopiëren...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Dołącz
copy-permalink: Kopiuj link stały
copy-image: "Kopiuj tag obrazu"
copy-logs: Kopiuj logi
copy-filtered-logs: Kopiuj filtrowane logi
copying-logs: Kopiowanie logów...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Shell
attach: Anexar
copy-permalink: Copiar link permanente
copy-image: "Copiar tag da imagem"
copy-logs: Copiar logs
copy-filtered-logs: Copiar logs filtrados
copying-logs: Copiando logs...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Оболочка
attach: Подключить
copy-permalink: Скопировать постоянную ссылку
copy-image: "Копировать тег образа"
copy-logs: Скопировать логи
copy-filtered-logs: Скопировать отфильтрованные логи
copying-logs: Копирование логов...
+1
View File
@@ -13,6 +13,7 @@ toolbar:
shell: Lupina
attach: Priloži
copy-permalink: Kopiraj trajno povezavo
copy-image: "Kopiraj oznako slike"
copy-logs: Kopiraj dnevnike
copy-filtered-logs: Kopiraj filtrirane dnevnike
copying-logs: Kopiranje dnevnikov...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: Kabuk
attach: Bağla
copy-permalink: Kalıcı bağlantıyı kopyala
copy-image: "İmaj etiketini kopyala"
copy-logs: Günlükleri kopyala
copy-filtered-logs: Filtrelenmiş günlükleri kopyala
copying-logs: Günlükler kopyalanıyor...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: 終端機
attach: 附加
copy-permalink: 複製永久連結
copy-image: "複製映像標籤"
copy-logs: 複製日誌
copy-filtered-logs: 複製篩選日誌
copying-logs: 正在複製日誌...
+1
View File
@@ -21,6 +21,7 @@ toolbar:
shell: 终端
attach: 附加
copy-permalink: 复制永久链接
copy-image: "复制镜像标签"
copy-logs: 复制日志
copy-filtered-logs: 复制筛选日志
copying-logs: 正在复制日志...