feat(ui): small improvements and polishments (#4862)
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -21,6 +21,7 @@ toolbar:
|
||||
shell: 셸
|
||||
attach: 연결
|
||||
copy-permalink: 영구 링크 복사
|
||||
copy-image: "이미지 태그 복사"
|
||||
copy-logs: 로그 복사
|
||||
copy-filtered-logs: 필터된 로그 복사
|
||||
copying-logs: 로그 복사 중...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -21,6 +21,7 @@ toolbar:
|
||||
shell: Оболочка
|
||||
attach: Подключить
|
||||
copy-permalink: Скопировать постоянную ссылку
|
||||
copy-image: "Копировать тег образа"
|
||||
copy-logs: Скопировать логи
|
||||
copy-filtered-logs: Скопировать отфильтрованные логи
|
||||
copying-logs: Копирование логов...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -21,6 +21,7 @@ toolbar:
|
||||
shell: 終端機
|
||||
attach: 附加
|
||||
copy-permalink: 複製永久連結
|
||||
copy-image: "複製映像標籤"
|
||||
copy-logs: 複製日誌
|
||||
copy-filtered-logs: 複製篩選日誌
|
||||
copying-logs: 正在複製日誌...
|
||||
|
||||
@@ -21,6 +21,7 @@ toolbar:
|
||||
shell: 终端
|
||||
attach: 附加
|
||||
copy-permalink: 复制永久链接
|
||||
copy-image: "复制镜像标签"
|
||||
copy-logs: 复制日志
|
||||
copy-filtered-logs: 复制筛选日志
|
||||
copying-logs: 正在复制日志...
|
||||
|
||||