diff --git a/assets/auto-imports.d.ts b/assets/auto-imports.d.ts index ebae61d0..0be5c422 100644 --- a/assets/auto-imports.d.ts +++ b/assets/auto-imports.d.ts @@ -58,6 +58,8 @@ declare global { const getDeep: typeof import('./utils/index')['getDeep'] const globalShowPopup: typeof import('./composable/popup')['globalShowPopup'] const h: typeof import('vue')['h'] + const hash: typeof import('./utils/index')['hash'] + const hashCode: typeof import('./utils/index')['hashCode'] const hourStyle: typeof import('./stores/settings')['hourStyle'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const inject: typeof import('vue')['inject'] @@ -71,7 +73,10 @@ declare global { const isRef: typeof import('vue')['isRef'] const lightTheme: typeof import('./stores/settings')['lightTheme'] const locale: typeof import('./stores/settings')['locale'] + const logContext: typeof import('./composable/logContext')['logContext'] + const logContextKey: typeof import('./composable/logContext')['logContextKey'] const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext'] + const loggingContextKey: typeof import('./composable/logContext')['loggingContextKey'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const mapActions: typeof import('pinia')['mapActions'] const mapGetters: typeof import('pinia')['mapGetters'] @@ -102,10 +107,14 @@ declare global { const onUpdated: typeof import('vue')['onUpdated'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const persistentVisibleKeys: typeof import('./composable/storage')['persistentVisibleKeys'] + const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer'] const pinnedContainers: typeof import('./composable/storage')['pinnedContainers'] const provide: typeof import('vue')['provide'] const provideContainerContext: typeof import('./composable/containerContext')['provideContainerContext'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] + const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext'] + const provideServiceContext: typeof import('./composable/serviceContext')['provideServiceContext'] + const provideStackContext: typeof import('./composable/stackContext')['provideStackContext'] const reactify: typeof import('@vueuse/core')['reactify'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactive: typeof import('vue')['reactive'] @@ -123,6 +132,7 @@ declare global { const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const search: typeof import('./stores/settings')['search'] + const serviceContext: typeof import('./composable/serviceContext')['serviceContext'] const sessionHost: typeof import('./composable/storage')['sessionHost'] const setActivePinia: typeof import('pinia')['setActivePinia'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] @@ -137,6 +147,7 @@ declare global { const size: typeof import('./stores/settings')['size'] const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars'] const softWrap: typeof import('./stores/settings')['softWrap'] + const stackContext: typeof import('./composable/stackContext')['stackContext'] const storeToRefs: typeof import('pinia')['storeToRefs'] const stripVersion: typeof import('./utils/index')['stripVersion'] const syncRef: typeof import('@vueuse/core')['syncRef'] @@ -189,7 +200,9 @@ declare global { const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useContainerActions: typeof import('./composable/containerActions')['useContainerActions'] const useContainerContext: typeof import('./composable/containerContext')['useContainerContext'] + const useContainerContextLogStream: typeof import('./composable/eventStreams')['useContainerContextLogStream'] const useContainerStore: typeof import('./stores/container')['useContainerStore'] + const useContainerStream: typeof import('./composable/eventStreams')['useContainerStream'] const useCounter: typeof import('@vueuse/core')['useCounter'] const useCssModule: typeof import('vue')['useCssModule'] const useCssVar: typeof import('@vueuse/core')['useCssVar'] @@ -229,6 +242,7 @@ declare global { const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] const useGamepad: typeof import('@vueuse/core')['useGamepad'] const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] + const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream'] const useHead: typeof import('@vueuse/head')['useHead'] const useHosts: typeof import('./stores/hosts')['useHosts'] const useI18n: typeof import('vue-i18n')['useI18n'] @@ -243,13 +257,15 @@ declare global { const useLink: typeof import('vue-router')['useLink'] const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] const useLogSearchContext: typeof import('./composable/logSearchContext')['useLogSearchContext'] - const useLogStream: typeof import('./composable/eventsource')['useLogStream'] + const useLogStream: typeof import('./composable/stackEventStream')['useLogStream'] + const useLoggingContext: typeof import('./composable/logContext')['useLoggingContext'] const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] const useMemoize: typeof import('@vueuse/core')['useMemoize'] const useMemory: typeof import('@vueuse/core')['useMemory'] + const useMergedStream: typeof import('./composable/eventStreams')['useMergedStream'] const useMounted: typeof import('@vueuse/core')['useMounted'] const useMouse: typeof import('@vueuse/core')['useMouse'] const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] @@ -266,6 +282,10 @@ declare global { const useParentElement: typeof import('@vueuse/core')['useParentElement'] const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] const usePermission: typeof import('@vueuse/core')['usePermission'] + const usePinned: typeof import('./stores/pinned')['usePinned'] + const usePinnedContainers: typeof import('./stores/pinned')['usePinnedContainers'] + const usePinnedLogsStore: typeof import('./stores/pinned')['usePinnedLogsStore'] + const usePinnedStore: typeof import('./stores/pinned')['usePinnedStore'] const usePointer: typeof import('@vueuse/core')['usePointer'] const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] @@ -289,6 +309,9 @@ declare global { const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] const useSearchFilter: typeof import('./composable/search')['useSearchFilter'] const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta'] + const useServiceContext: typeof import('./composable/serviceContext')['useServiceContext'] + const useServiceContextLogStream: typeof import('./composable/eventStreams')['useServiceContextLogStream'] + const useServiceStream: typeof import('./composable/eventStreams')['useServiceStream'] const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] const useShare: typeof import('@vueuse/core')['useShare'] const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory'] @@ -296,11 +319,16 @@ declare global { const useSorted: typeof import('@vueuse/core')['useSorted'] const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] + const useStackContext: typeof import('./composable/stackContext')['useStackContext'] + const useStackContextLogStream: typeof import('./composable/eventStreams')['useStackContextLogStream'] + const useStackStore: typeof import('./stores/swarm')['useStackStore'] + const useStackStream: typeof import('./composable/eventStreams')['useStackStream'] const useStepper: typeof import('@vueuse/core')['useStepper'] const useStorage: typeof import('@vueuse/core')['useStorage'] const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] const useSupported: typeof import('@vueuse/core')['useSupported'] + const useSwarmStore: typeof import('./stores/swarm')['useSwarmStore'] const useSwipe: typeof import('@vueuse/core')['useSwipe'] const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] @@ -417,6 +445,7 @@ declare module 'vue' { readonly getDeep: UnwrapRef readonly globalShowPopup: UnwrapRef readonly h: UnwrapRef + readonly hashCode: UnwrapRef readonly hourStyle: UnwrapRef readonly ignorableWatch: UnwrapRef readonly inject: UnwrapRef @@ -430,6 +459,7 @@ declare module 'vue' { readonly isRef: UnwrapRef readonly lightTheme: UnwrapRef readonly locale: UnwrapRef + readonly loggingContextKey: UnwrapRef readonly makeDestructurable: UnwrapRef readonly mapActions: UnwrapRef readonly mapGetters: UnwrapRef @@ -459,11 +489,12 @@ declare module 'vue' { readonly onUnmounted: UnwrapRef readonly onUpdated: UnwrapRef readonly pausableWatch: UnwrapRef - readonly persistentVisibleKeys: UnwrapRef + readonly persistentVisibleKeysForContainer: UnwrapRef readonly pinnedContainers: UnwrapRef readonly provide: UnwrapRef readonly provideContainerContext: UnwrapRef readonly provideLocal: UnwrapRef + readonly provideLoggingContext: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef @@ -548,6 +579,7 @@ declare module 'vue' { readonly useContainerActions: UnwrapRef readonly useContainerContext: UnwrapRef readonly useContainerStore: UnwrapRef + readonly useContainerStream: UnwrapRef readonly useCounter: UnwrapRef readonly useCssModule: UnwrapRef readonly useCssVar: UnwrapRef @@ -587,6 +619,7 @@ declare module 'vue' { readonly useFullscreen: UnwrapRef readonly useGamepad: UnwrapRef readonly useGeolocation: UnwrapRef + readonly useGroupedStream: UnwrapRef readonly useHead: UnwrapRef readonly useHosts: UnwrapRef readonly useI18n: UnwrapRef @@ -601,13 +634,14 @@ declare module 'vue' { readonly useLink: UnwrapRef readonly useLocalStorage: UnwrapRef readonly useLogSearchContext: UnwrapRef - readonly useLogStream: UnwrapRef + readonly useLoggingContext: UnwrapRef readonly useMagicKeys: UnwrapRef readonly useManualRefHistory: UnwrapRef readonly useMediaControls: UnwrapRef readonly useMediaQuery: UnwrapRef readonly useMemoize: UnwrapRef readonly useMemory: UnwrapRef + readonly useMergedStream: UnwrapRef readonly useMounted: UnwrapRef readonly useMouse: UnwrapRef readonly useMouseInElement: UnwrapRef @@ -624,6 +658,7 @@ declare module 'vue' { readonly useParentElement: UnwrapRef readonly usePerformanceObserver: UnwrapRef readonly usePermission: UnwrapRef + readonly usePinnedLogsStore: UnwrapRef readonly usePointer: UnwrapRef readonly usePointerLock: UnwrapRef readonly usePointerSwipe: UnwrapRef @@ -647,6 +682,7 @@ declare module 'vue' { readonly useScrollLock: UnwrapRef readonly useSearchFilter: UnwrapRef readonly useSeoMeta: UnwrapRef + readonly useServiceStream: UnwrapRef readonly useSessionStorage: UnwrapRef readonly useShare: UnwrapRef readonly useSimpleRefHistory: UnwrapRef @@ -654,11 +690,13 @@ declare module 'vue' { readonly useSorted: UnwrapRef readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef + readonly useStackStream: UnwrapRef readonly useStepper: UnwrapRef readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef readonly useStyleTag: UnwrapRef readonly useSupported: UnwrapRef + readonly useSwarmStore: UnwrapRef readonly useSwipe: UnwrapRef readonly useTemplateRefsList: UnwrapRef readonly useTextDirection: UnwrapRef @@ -768,6 +806,7 @@ declare module '@vue/runtime-core' { readonly getDeep: UnwrapRef readonly globalShowPopup: UnwrapRef readonly h: UnwrapRef + readonly hashCode: UnwrapRef readonly hourStyle: UnwrapRef readonly ignorableWatch: UnwrapRef readonly inject: UnwrapRef @@ -781,6 +820,7 @@ declare module '@vue/runtime-core' { readonly isRef: UnwrapRef readonly lightTheme: UnwrapRef readonly locale: UnwrapRef + readonly loggingContextKey: UnwrapRef readonly makeDestructurable: UnwrapRef readonly mapActions: UnwrapRef readonly mapGetters: UnwrapRef @@ -810,11 +850,12 @@ declare module '@vue/runtime-core' { readonly onUnmounted: UnwrapRef readonly onUpdated: UnwrapRef readonly pausableWatch: UnwrapRef - readonly persistentVisibleKeys: UnwrapRef + readonly persistentVisibleKeysForContainer: UnwrapRef readonly pinnedContainers: UnwrapRef readonly provide: UnwrapRef readonly provideContainerContext: UnwrapRef readonly provideLocal: UnwrapRef + readonly provideLoggingContext: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef @@ -899,6 +940,7 @@ declare module '@vue/runtime-core' { readonly useContainerActions: UnwrapRef readonly useContainerContext: UnwrapRef readonly useContainerStore: UnwrapRef + readonly useContainerStream: UnwrapRef readonly useCounter: UnwrapRef readonly useCssModule: UnwrapRef readonly useCssVar: UnwrapRef @@ -938,6 +980,7 @@ declare module '@vue/runtime-core' { readonly useFullscreen: UnwrapRef readonly useGamepad: UnwrapRef readonly useGeolocation: UnwrapRef + readonly useGroupedStream: UnwrapRef readonly useHead: UnwrapRef readonly useHosts: UnwrapRef readonly useI18n: UnwrapRef @@ -952,13 +995,14 @@ declare module '@vue/runtime-core' { readonly useLink: UnwrapRef readonly useLocalStorage: UnwrapRef readonly useLogSearchContext: UnwrapRef - readonly useLogStream: UnwrapRef + readonly useLoggingContext: UnwrapRef readonly useMagicKeys: UnwrapRef readonly useManualRefHistory: UnwrapRef readonly useMediaControls: UnwrapRef readonly useMediaQuery: UnwrapRef readonly useMemoize: UnwrapRef readonly useMemory: UnwrapRef + readonly useMergedStream: UnwrapRef readonly useMounted: UnwrapRef readonly useMouse: UnwrapRef readonly useMouseInElement: UnwrapRef @@ -975,6 +1019,7 @@ declare module '@vue/runtime-core' { readonly useParentElement: UnwrapRef readonly usePerformanceObserver: UnwrapRef readonly usePermission: UnwrapRef + readonly usePinnedLogsStore: UnwrapRef readonly usePointer: UnwrapRef readonly usePointerLock: UnwrapRef readonly usePointerSwipe: UnwrapRef @@ -998,6 +1043,7 @@ declare module '@vue/runtime-core' { readonly useScrollLock: UnwrapRef readonly useSearchFilter: UnwrapRef readonly useSeoMeta: UnwrapRef + readonly useServiceStream: UnwrapRef readonly useSessionStorage: UnwrapRef readonly useShare: UnwrapRef readonly useSimpleRefHistory: UnwrapRef @@ -1005,11 +1051,13 @@ declare module '@vue/runtime-core' { readonly useSorted: UnwrapRef readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef + readonly useStackStream: UnwrapRef readonly useStepper: UnwrapRef readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef readonly useStyleTag: UnwrapRef readonly useSupported: UnwrapRef + readonly useSwarmStore: UnwrapRef readonly useSwipe: UnwrapRef readonly useTemplateRefsList: UnwrapRef readonly useTextDirection: UnwrapRef diff --git a/assets/components.d.ts b/assets/components.d.ts index 8302228e..70c620fb 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -7,7 +7,6 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { - BarChart: typeof import('./components/BarChart.vue')['default'] 'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default'] 'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default'] 'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default'] @@ -25,34 +24,35 @@ declare module 'vue' { 'Cil:columns': typeof import('~icons/cil/columns')['default'] 'Cil:xCircle': typeof import('~icons/cil/x-circle')['default'] ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default'] - ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default'] - ContainerLogViewer: typeof import('./components/LogViewer/ContainerLogViewer.vue')['default'] - ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default'] - ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default'] + ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default'] + ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default'] + ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default'] + ContainerName: typeof import('./components/LogViewer/ContainerName.vue')['default'] + ContainerPopup: typeof import('./components/ContainerPopup.vue')['default'] ContainerTable: typeof import('./components/ContainerTable.vue')['default'] - ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default'] + ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default'] DateTime: typeof import('./components/common/DateTime.vue')['default'] DistanceTime: typeof import('./components/common/DistanceTime.vue')['default'] DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default'] Dropdown: typeof import('./components/common/Dropdown.vue')['default'] DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default'] + EventSource: typeof import('./components/LogViewer/EventSource.vue')['default'] FieldList: typeof import('./components/LogViewer/FieldList.vue')['default'] FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default'] + GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default'] HostList: typeof import('./components/HostList.vue')['default'] + HostMenu: typeof import('./components/HostMenu.vue')['default'] 'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default'] InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default'] KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default'] LabeledInput: typeof import('./components/common/LabeledInput.vue')['default'] Links: typeof import('./components/Links.vue')['default'] - LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default'] - LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default'] LogDate: typeof import('./components/LogViewer/LogDate.vue')['default'] - LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default'] LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default'] + LogList: typeof import('./components/LogViewer/LogList.vue')['default'] LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default'] LogStd: typeof import('./components/LogViewer/LogStd.vue')['default'] LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default'] - LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default'] 'MaterialSymbols:collapseAllRounded': typeof import('~icons/material-symbols/collapse-all-rounded')['default'] 'MaterialSymbols:expandAllRounded': typeof import('~icons/material-symbols/expand-all-rounded')['default'] 'Mdi:announcement': typeof import('~icons/mdi/announcement')['default'] @@ -67,15 +67,22 @@ declare module 'vue' { 'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default'] 'Mdi:magnify': typeof import('~icons/mdi/magnify')['default'] MobileMenu: typeof import('./components/common/MobileMenu.vue')['default'] + MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default'] + MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default'] 'Octicon:container24': typeof import('~icons/octicon/container24')['default'] 'Octicon:download24': typeof import('~icons/octicon/download24')['default'] 'Octicon:trash24': typeof import('~icons/octicon/trash24')['default'] PageWithLinks: typeof import('./components/PageWithLinks.vue')['default'] + 'Ph:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default'] + 'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default'] + 'Ph:circlesFour': typeof import('~icons/ph/circles-four')['default'] 'Ph:command': typeof import('~icons/ph/command')['default'] 'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default'] 'Ph:controlBold': typeof import('~icons/ph/control-bold')['default'] 'Ph:cpu': typeof import('~icons/ph/cpu')['default'] 'Ph:memory': typeof import('~icons/ph/memory')['default'] + 'Ph:stack': typeof import('~icons/ph/stack')['default'] + 'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default'] Popup: typeof import('./components/Popup.vue')['default'] Releases: typeof import('./components/Releases.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] @@ -83,15 +90,20 @@ declare module 'vue' { ScrollableView: typeof import('./components/ScrollableView.vue')['default'] ScrollProgress: typeof import('./components/ScrollProgress.vue')['default'] Search: typeof import('./components/Search.vue')['default'] + ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default'] SideMenu: typeof import('./components/SideMenu.vue')['default'] SidePanel: typeof import('./components/SidePanel.vue')['default'] SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default'] SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default'] + SlideTransition: typeof import('./components/common/SlideTransition.vue')['default'] + StackLog: typeof import('./components/StackViewer/StackLog.vue')['default'] StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default'] StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default'] + SwarmMenu: typeof import('./components/SwarmMenu.vue')['default'] Tag: typeof import('./components/common/Tag.vue')['default'] TimedButton: typeof import('./components/common/TimedButton.vue')['default'] Toggle: typeof import('./components/common/Toggle.vue')['default'] + ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default'] ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default'] } } diff --git a/assets/components/BarChart.vue b/assets/components/BarChart.vue deleted file mode 100644 index 36ecd024..00000000 --- a/assets/components/BarChart.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/assets/components/LogViewer/ContainerPopup.vue b/assets/components/ContainerPopup.vue similarity index 87% rename from assets/components/LogViewer/ContainerPopup.vue rename to assets/components/ContainerPopup.vue index 83443b08..c65d0705 100644 --- a/assets/components/LogViewer/ContainerPopup.vue +++ b/assets/components/ContainerPopup.vue @@ -2,7 +2,7 @@
RUNNING - +
diff --git a/assets/components/LogViewer/LogActionsToolbar.vue b/assets/components/ContainerViewer/ContainerActionsToolbar.vue similarity index 90% rename from assets/components/LogViewer/LogActionsToolbar.vue rename to assets/components/ContainerViewer/ContainerActionsToolbar.vue index c789ea34..a54368af 100644 --- a/assets/components/LogViewer/LogActionsToolbar.vue +++ b/assets/components/ContainerViewer/ContainerActionsToolbar.vue @@ -8,7 +8,7 @@
  • {{ $t("toolbar.clear") }} - +
  • @@ -17,7 +17,7 @@
  • {{ $t("toolbar.search") }} - +
  • @@ -101,15 +101,18 @@ diff --git a/assets/components/LogViewer/ContainerTitle.vue b/assets/components/ContainerViewer/ContainerTitle.vue similarity index 69% rename from assets/components/LogViewer/ContainerTitle.vue rename to assets/components/ContainerViewer/ContainerTitle.vue index cfdbc5e7..953b36d1 100644 --- a/assets/components/LogViewer/ContainerTitle.vue +++ b/assets/components/ContainerViewer/ContainerTitle.vue @@ -17,22 +17,24 @@ {{ container.swarmId }}
    - - diff --git a/assets/components/HostMenu.vue b/assets/components/HostMenu.vue index 8a38931e..97605338 100644 --- a/assets/components/HostMenu.vue +++ b/assets/components/HostMenu.vue @@ -30,11 +30,12 @@ {{ label.startsWith("label.") ? $t(label) : label }} - all +
      @@ -43,7 +44,7 @@
      @@ -52,8 +53,8 @@ @@ -83,9 +84,11 @@ import Stack from "~icons/ph/stack"; // @ts-ignore import Containers from "~icons/octicon/container-24"; -const store = useContainerStore(); +const containerStore = useContainerStore(); +const { visibleContainers } = storeToRefs(containerStore); + +const pinnedStore = usePinnedLogsStore(); -const { activeContainers, visibleContainers } = storeToRefs(store); const { hosts } = useHosts(); const setHost = (host: string | null) => (sessionHost.value = host); @@ -121,7 +124,7 @@ const menuItems = computed(() => { const singular = []; for (const item of sortedContainers.value) { - const namespace = item.labels["com.docker.stack.namespace"] ?? item.labels["com.docker.compose.project"]; + const namespace = item.group; if (debouncedPinnedContainers.value.has(item.name)) { pinned.push(item); } else if (namespace) { @@ -156,16 +159,6 @@ const menuItems = computed(() => { return items; }); - -const activeContainersById = computed(() => - activeContainers.value.reduce( - (acc, item) => { - acc[item.id] = item; - return acc; - }, - {} as Record, - ), -); diff --git a/assets/components/LogViewer/ContainerName.vue b/assets/components/LogViewer/ContainerName.vue new file mode 100644 index 00000000..77ee7d0a --- /dev/null +++ b/assets/components/LogViewer/ContainerName.vue @@ -0,0 +1,45 @@ + + + + diff --git a/assets/components/LogViewer/ContainerStat.vue b/assets/components/LogViewer/ContainerStat.vue deleted file mode 100644 index 6fc76699..00000000 --- a/assets/components/LogViewer/ContainerStat.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/assets/components/LogViewer/DockerEventLogItem.vue b/assets/components/LogViewer/DockerEventLogItem.vue index 30dcc41b..37f70b8d 100644 --- a/assets/components/LogViewer/DockerEventLogItem.vue +++ b/assets/components/LogViewer/DockerEventLogItem.vue @@ -1,25 +1,33 @@ @@ -31,20 +39,19 @@ const { t } = useI18n(); const { logEntry } = defineProps<{ logEntry: DockerEventLogEntry; + showContainerName?: boolean; }>(); -const store = useContainerStore(); -const { containers } = storeToRefs(store); -const { container } = useContainerContext(); +const { containers } = useLoggingContext(); const nextContainer = computed( () => [ ...containers.value.filter( (c) => - c.host === container.value.host && + c.host === containers.value[0].host && c.created > logEntry.date && - c.name === container.value.name && + c.name === containers.value[0].name && c.state === "running", ), ].sort((a, b) => +a.created - +b.created)[0], diff --git a/assets/components/LogViewer/LogEventSource.spec.ts b/assets/components/LogViewer/EventSource.spec.ts similarity index 87% rename from assets/components/LogViewer/LogEventSource.spec.ts rename to assets/components/LogViewer/EventSource.spec.ts index 36235b98..09448f69 100644 --- a/assets/components/LogViewer/LogEventSource.spec.ts +++ b/assets/components/LogViewer/EventSource.spec.ts @@ -1,6 +1,5 @@ import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; -import { containerContext } from "@/composable/containerContext"; import { useSearchFilter } from "@/composable/search"; import { settings } from "@/stores/settings"; // @ts-ignore @@ -9,8 +8,9 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { computed, nextTick } from "vue"; import { createI18n } from "vue-i18n"; import { createRouter, createWebHistory } from "vue-router"; -import LogEventSource from "./LogEventSource.vue"; -import ContainerLogViewer from "./ContainerLogViewer.vue"; +import { default as Component } from "./EventSource.vue"; +import LogViewer from "@/components/LogViewer/LogViewer.vue"; +import { Container } from "@/models/Container"; vi.mock("@/stores/config", () => ({ __esModule: true, @@ -21,7 +21,7 @@ vi.mock("@/stores/config", () => ({ /** * @vitest-environment jsdom */ -describe("", () => { +describe("", () => { const search = useSearchFilter(); beforeEach(() => { @@ -67,26 +67,29 @@ describe("", () => { ], }); - return mount(LogEventSource, { + return mount(Component, { global: { plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})], components: { - ContainerLogViewer, + LogViewer, }, provide: { - [containerContext as symbol]: { - container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })), + scrollingPaused: computed(() => false), + [loggingContextKey as symbol]: { + containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]), streamConfig: reactive({ stdout: true, stderr: true }), }, - scrollingPaused: computed(() => false), }, }, slots: { default: ` - + `, }, - props: {}, + props: { + streamSource: useContainerStream, + entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "status", "created", []), + }, }); } diff --git a/assets/components/LogViewer/EventSource.vue b/assets/components/LogViewer/EventSource.vue new file mode 100644 index 00000000..ac870a61 --- /dev/null +++ b/assets/components/LogViewer/EventSource.vue @@ -0,0 +1,28 @@ + + + diff --git a/assets/components/LogViewer/FieldList.vue b/assets/components/LogViewer/FieldList.vue index aa6aca83..0ead59f6 100644 --- a/assets/components/LogViewer/FieldList.vue +++ b/assets/components/LogViewer/FieldList.vue @@ -3,12 +3,7 @@
    • diff --git a/assets/stores/container.ts b/assets/stores/container.ts index cc46d6f1..cf140fff 100644 --- a/assets/stores/container.ts +++ b/assets/stores/container.ts @@ -11,7 +11,7 @@ const { t } = i18n.global; export const useContainerStore = defineStore("container", () => { const containers: Ref = ref([]); - const activeContainerIds: Ref = ref([]); + let es: EventSource | null = null; const ready = ref(false); @@ -30,8 +30,6 @@ export const useContainerStore = defineStore("container", () => { return containers.value.filter(filter); }); - const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id])); - function connect() { es?.close(); ready.value = false; @@ -135,6 +133,7 @@ export const useContainerStore = defineStore("container", () => { c.status, c.state, c.stats, + c.group, c.health, ); }), @@ -142,19 +141,23 @@ export const useContainerStore = defineStore("container", () => { }; const currentContainer = (id: Ref) => computed(() => allContainersById.value[id.value]); - const appendActiveContainer = ({ id }: { id: string }) => activeContainerIds.value.push(id); - const removeActiveContainer = ({ id }: { id: string }) => - activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1); + + const containerNames = computed(() => + containers.value.reduce( + (acc, container) => { + acc[container.id] = container.name; + return acc; + }, + {} as Record, + ), + ); return { containers, - activeContainerIds, allContainersById, visibleContainers, - activeContainers, currentContainer, - appendActiveContainer, - removeActiveContainer, + containerNames, ready, }; }); diff --git a/assets/stores/pinned.ts b/assets/stores/pinned.ts new file mode 100644 index 00000000..f473b05b --- /dev/null +++ b/assets/stores/pinned.ts @@ -0,0 +1,27 @@ +import { acceptHMRUpdate, defineStore } from "pinia"; +import { Ref } from "vue"; + +export const usePinnedLogsStore = defineStore("pinnedLogs", () => { + const containerStore = useContainerStore(); + const { allContainersById } = storeToRefs(containerStore); + const pinnedContainerIds: Ref = ref([]); + + const pinnedLogs = computed(() => pinnedContainerIds.value.map((id) => allContainersById.value[id])); + + const pinContainer = ({ id }: { id: string }) => pinnedContainerIds.value.push(id); + const unPinContainer = ({ id }: { id: string }) => + pinnedContainerIds.value.splice(pinnedContainerIds.value.indexOf(id), 1); + + const isPinned = ({ id }: { id: string }) => pinnedContainerIds.value.includes(id); + + return { + pinnedLogs, + isPinned, + pinContainer, + unPinContainer, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(usePinnedLogsStore, import.meta.hot)); +} diff --git a/assets/stores/swarm.ts b/assets/stores/swarm.ts new file mode 100644 index 00000000..38b0bebe --- /dev/null +++ b/assets/stores/swarm.ts @@ -0,0 +1,87 @@ +import { acceptHMRUpdate, defineStore } from "pinia"; + +import { Container, GroupedContainers } from "@/models/Container"; +import { Service, Stack } from "@/models/Stack"; + +export const useSwarmStore = defineStore("swarm", () => { + const containerStore = useContainerStore(); + const { containers } = storeToRefs(containerStore) as unknown as { containers: Ref }; + + const runningContainers = computed(() => containers.value.filter((c) => c.state === "running")); + + const stacks = computed(() => { + const namespaced: Record = {}; + for (const item of runningContainers.value) { + const namespace = item.namespace; + if (namespace === undefined) continue; + namespaced[namespace] ||= []; + namespaced[namespace].push(item); + } + + const newStacks: Stack[] = []; + + for (const [name, containers] of Object.entries(namespaced)) { + const services: Record = {}; + + for (const container of containers) { + const service = container.labels["com.docker.swarm.service.name"]; + + if (service === undefined) continue; + services[service] ||= []; + services[service].push(container); + } + + const newServices: Service[] = []; + + for (const [name, containers] of Object.entries(services)) { + newServices.push(new Service(name, containers)); + } + + newStacks.push(new Stack(name, containers, newServices)); + } + return newStacks; + }); + + const services = computed(() => { + const services: Record = {}; + + for (const container of runningContainers.value) { + const service = container.labels["com.docker.swarm.service.name"]; + const namespace = container.namespace; + + if (service === undefined) continue; + if (namespace) continue; // skip containers that already have a stack + services[service] ||= []; + services[service].push(container); + } + + const serviceWithStack = stacks.value.flatMap((stack) => stack.services); + + const servicesWithoutStack = Object.entries(services).map(([name, containers]) => new Service(name, containers)); + + return [...serviceWithStack, ...servicesWithoutStack]; + }); + + const customGroups = computed(() => { + const grouped: Record = {}; + + for (const container of runningContainers.value) { + const group = container.customGroup; + if (group === undefined) continue; + grouped[group] ||= []; + grouped[group].push(container); + } + + return Object.entries(grouped).map(([name, containers]) => new GroupedContainers(name, containers)); + }); + + return { + stacks, + services, + customGroups, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useSwarmStore, import.meta.hot)); +} diff --git a/assets/types/Container.d.ts b/assets/types/Container.d.ts index 61103a6c..b1011cc5 100644 --- a/assets/types/Container.d.ts +++ b/assets/types/Container.d.ts @@ -17,6 +17,7 @@ export type ContainerJson = { readonly labels: Record; readonly stats: ContainerStat[]; readonly health?: ContainerHealth; + readonly group?: string; }; export type ContainerState = "created" | "running" | "exited" | "dead" | "paused" | "restarting"; diff --git a/assets/utils/index.ts b/assets/utils/index.ts index 7a2f8fae..2dddcbd1 100644 --- a/assets/utils/index.ts +++ b/assets/utils/index.ts @@ -75,3 +75,12 @@ export function useSimpleRefHistory(source: Ref, options: UseSimpleRefHist return history; } + +export function hashCode(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; + } + return hash; +} diff --git a/docker-compose.yml b/docker-compose.yml index 5ded3c95..837b8220 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.4" services: custom_base: container_name: custom_base diff --git a/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png b/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png index 610ee47f..79a9e5b9 100644 Binary files a/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png and b/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png differ diff --git a/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png b/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png index 3c842002..b5da71db 100644 Binary files a/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png and b/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png differ diff --git a/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png b/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png index 893d3f64..74b9efb6 100644 Binary files a/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png and b/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png differ diff --git a/e2e/visual.spec.ts-snapshots/default-homepage-1-chromium-linux.png b/e2e/visual.spec.ts-snapshots/default-homepage-1-chromium-linux.png index 1b771a4b..d246c047 100644 Binary files a/e2e/visual.spec.ts-snapshots/default-homepage-1-chromium-linux.png and b/e2e/visual.spec.ts-snapshots/default-homepage-1-chromium-linux.png differ diff --git a/internal/docker/client.go b/internal/docker/client.go index 167630bc..87c50c6f 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -213,6 +213,12 @@ func (d *httpClient) ListContainers() ([]Container, error) { if len(c.Names) > 0 { name = strings.TrimPrefix(c.Names[0], "/") } + + group := "" + if c.Labels["dev.dozzle.group"] != "" { + group = c.Labels["dev.dozzle.group"] + } + container := Container{ ID: c.ID[:12], Names: c.Names, @@ -227,6 +233,7 @@ func (d *httpClient) ListContainers() ([]Container, error) { Health: findBetweenParentheses(c.Status), Labels: c.Labels, Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats + Group: group, } containers = append(containers, container) } @@ -303,7 +310,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since string, ShowStdout: stdType&STDOUT != 0, ShowStderr: stdType&STDERR != 0, Follow: true, - Tail: "300", + Tail: strconv.Itoa(100), Timestamps: true, Since: since, } diff --git a/internal/docker/client_test.go b/internal/docker/client_test.go index dbf1e1ba..4ea288cc 100644 --- a/internal/docker/client_test.go +++ b/internal/docker/client_test.go @@ -149,7 +149,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) { b = append(b, []byte(expected)...) reader := io.NopCloser(bytes.NewReader(b)) - options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"} + options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "100", Timestamps: true, Since: "since"} proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil) client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} diff --git a/internal/docker/container_store.go b/internal/docker/container_store.go index 245d1f84..72156132 100644 --- a/internal/docker/container_store.go +++ b/internal/docker/container_store.go @@ -11,25 +11,27 @@ import ( ) type ContainerStore struct { - containers *xsync.MapOf[string, *Container] - subscribers *xsync.MapOf[context.Context, chan ContainerEvent] - client Client - statsCollector *StatsCollector - wg sync.WaitGroup - connected atomic.Bool - events chan ContainerEvent - ctx context.Context + containers *xsync.MapOf[string, *Container] + subscribers *xsync.MapOf[context.Context, chan ContainerEvent] + newContainerSubscribers *xsync.MapOf[context.Context, chan Container] + client Client + statsCollector *StatsCollector + wg sync.WaitGroup + connected atomic.Bool + events chan ContainerEvent + ctx context.Context } func NewContainerStore(ctx context.Context, client Client) *ContainerStore { s := &ContainerStore{ - containers: xsync.NewMapOf[string, *Container](), - client: client, - subscribers: xsync.NewMapOf[context.Context, chan ContainerEvent](), - statsCollector: NewStatsCollector(client), - wg: sync.WaitGroup{}, - events: make(chan ContainerEvent), - ctx: ctx, + containers: xsync.NewMapOf[string, *Container](), + client: client, + subscribers: xsync.NewMapOf[context.Context, chan ContainerEvent](), + newContainerSubscribers: xsync.NewMapOf[context.Context, chan Container](), + statsCollector: NewStatsCollector(client), + wg: sync.WaitGroup{}, + events: make(chan ContainerEvent), + ctx: ctx, } s.wg.Add(1) @@ -105,6 +107,10 @@ func (s *ContainerStore) SubscribeStats(ctx context.Context, stats chan Containe s.statsCollector.Subscribe(ctx, stats) } +func (s *ContainerStore) SubscribeNewContainers(ctx context.Context, containers chan Container) { + s.newContainerSubscribers.Store(ctx, containers) +} + func (s *ContainerStore) init() { stats := make(chan ContainerStat) s.statsCollector.Subscribe(s.ctx, stats) @@ -122,6 +128,14 @@ func (s *ContainerStore) init() { if container, err := s.client.FindContainer(event.ActorID); err == nil { log.Debugf("container %s started", container.ID) s.containers.Store(container.ID, &container) + s.newContainerSubscribers.Range(func(c context.Context, containers chan Container) bool { + select { + case containers <- container: + case <-c.Done(): + s.newContainerSubscribers.Delete(c) + } + return true + }) } case "destroy": log.Debugf("container %s destroyed", event.ActorID) diff --git a/internal/docker/event_generator.go b/internal/docker/event_generator.go index 6728004a..eb780c9d 100644 --- a/internal/docker/event_generator.go +++ b/internal/docker/event_generator.go @@ -22,13 +22,14 @@ import ( ) type EventGenerator struct { - Events chan *LogEvent - Errors chan error - reader *bufio.Reader - next *LogEvent - buffer chan *LogEvent - tty bool - wg sync.WaitGroup + Events chan *LogEvent + Errors chan error + reader *bufio.Reader + next *LogEvent + buffer chan *LogEvent + tty bool + wg sync.WaitGroup + containerID string } var bufPool = sync.Pool{ @@ -39,13 +40,14 @@ var bufPool = sync.Pool{ var ErrBadHeader = fmt.Errorf("dozzle/docker: unable to read header") -func NewEventGenerator(reader io.Reader, tty bool) *EventGenerator { +func NewEventGenerator(reader io.Reader, container Container) *EventGenerator { generator := &EventGenerator{ - reader: bufio.NewReader(reader), - buffer: make(chan *LogEvent, 100), - Errors: make(chan error, 1), - Events: make(chan *LogEvent), - tty: tty, + reader: bufio.NewReader(reader), + buffer: make(chan *LogEvent, 100), + Errors: make(chan error, 1), + Events: make(chan *LogEvent), + tty: container.Tty, + containerID: container.ID, } generator.wg.Add(2) go generator.consumeReader() @@ -84,7 +86,7 @@ func (g *EventGenerator) consumeReader() { message, streamType, readerError := readEvent(g.reader, g.tty) if message != "" { logEvent := createEvent(message, streamType) - + logEvent.ContainerID = g.containerID logEvent.Level = guessLogLevel(logEvent) g.buffer <- logEvent } diff --git a/internal/docker/event_generator_test.go b/internal/docker/event_generator_test.go index 2fc001a9..7c4f4e62 100644 --- a/internal/docker/event_generator_test.go +++ b/internal/docker/event_generator_test.go @@ -19,7 +19,7 @@ func TestEventGenerator_Events_tty(t *testing.T) { input := "example input" reader := bufio.NewReader(strings.NewReader(input)) - g := NewEventGenerator(reader, true) + g := NewEventGenerator(reader, Container{Tty: true}) event := <-g.Events require.NotNil(t, event, "Expected event to not be nil, but got nil") @@ -30,7 +30,7 @@ func TestEventGenerator_Events_non_tty(t *testing.T) { input := "example input" reader := bytes.NewReader(makeMessage(input, STDOUT)) - g := NewEventGenerator(reader, false) + g := NewEventGenerator(reader, Container{Tty: false}) event := <-g.Events require.NotNil(t, event, "Expected event to not be nil, but got nil") @@ -41,7 +41,7 @@ func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) { input := "example input" reader := bytes.NewReader(makeMessage(input, STDOUT)) - g := NewEventGenerator(reader, false) + g := NewEventGenerator(reader, Container{Tty: false}) <-g.Events _, ok := <-g.Events @@ -52,7 +52,7 @@ func TestEventGenerator_Events_routines_done(t *testing.T) { input := "example input" reader := bytes.NewReader(makeMessage(input, STDOUT)) - g := NewEventGenerator(reader, false) + g := NewEventGenerator(reader, Container{Tty: false}) <-g.Events assert.False(t, waitTimeout(&g.wg, 1*time.Second), "Expected routines to be done") } diff --git a/internal/docker/types.go b/internal/docker/types.go index f17250e4..e13b69df 100644 --- a/internal/docker/types.go +++ b/internal/docker/types.go @@ -22,6 +22,7 @@ type Container struct { Tty bool `json:"-"` Labels map[string]string `json:"labels,omitempty"` Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"` + Group string `json:"group,omitempty"` } // ContainerStat represent stats instant for a container @@ -48,12 +49,13 @@ const ( ) type LogEvent struct { - Message any `json:"m,omitempty"` - Timestamp int64 `json:"ts"` - Id uint32 `json:"id,omitempty"` - Level string `json:"l,omitempty"` - Position LogPosition `json:"p,omitempty"` - Stream string `json:"s,omitempty"` + Message any `json:"m,omitempty"` + Timestamp int64 `json:"ts"` + Id uint32 `json:"id,omitempty"` + Level string `json:"l,omitempty"` + Position LogPosition `json:"p,omitempty"` + Stream string `json:"s,omitempty"` + ContainerID string `json:"c,omitempty"` } func (l *LogEvent) HasLevel() bool { diff --git a/internal/web/__snapshots__/web.snapshot b/internal/web/__snapshots__/web.snapshot index d1a20b47..aac1bec9 100644 --- a/internal/web/__snapshots__/web.snapshot +++ b/internal/web/__snapshots__/web.snapshot @@ -24,12 +24,12 @@ Location: /foobar/ Moved Permanently. /* snapshot: Test_createRoutes_redirect_with_auth */ -HTTP/1.1 307 Temporary Redirect -Connection: close -Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/html; charset=utf-8 -Location: /foobar/login - +HTTP/1.1 307 Temporary Redirect +Connection: close +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: text/html; charset=utf-8 +Location: /foobar/login + Temporary Redirect. /* snapshot: Test_createRoutes_simple_redirect */ @@ -42,33 +42,33 @@ Location: /login?redirectUrl=/ Temporary Redirect. /* snapshot: Test_createRoutes_username_password */ -HTTP/1.1 307 Temporary Redirect -Connection: close -Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/html; charset=utf-8 -Location: /login - +HTTP/1.1 307 Temporary Redirect +Connection: close +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: text/html; charset=utf-8 +Location: /login + Temporary Redirect. /* snapshot: Test_createRoutes_username_password_invalid */ -HTTP/1.1 401 Unauthorized -Connection: close -Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/plain; charset=utf-8 -X-Content-Type-Options: nosniff - +HTTP/1.1 401 Unauthorized +Connection: close +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: text/plain; charset=utf-8 +X-Content-Type-Options: nosniff + Unauthorized /* snapshot: Test_createRoutes_username_password_valid_session */ -HTTP/1.1 200 OK -Connection: close -Cache-Control: no-transform -Cache-Control: no-cache -Connection: keep-alive -Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/event-stream -X-Accel-Buffering: no - +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-transform +Cache-Control: no-cache +Connection: keep-alive +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: text/event-stream +X-Accel-Buffering: no + event: container-stopped data: end of stream @@ -86,8 +86,8 @@ Connection: close Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; Content-Type: application/x-jsonl; charset=UTF-8 -{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout"} -{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr"} +{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"} +{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"} /* snapshot: Test_handler_download_logs */ INFO Testing logs... @@ -105,15 +105,15 @@ event: containers-changed data: [] /* snapshot: Test_handler_streamEvents_error_request */ -HTTP/1.1 200 OK -Connection: close -Cache-Control: no-transform -Cache-Control: no-cache -Connection: keep-alive -Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/event-stream -X-Accel-Buffering: no - +HTTP/1.1 200 OK +Connection: close +Cache-Control: no-transform +Cache-Control: no-cache +Connection: keep-alive +Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; +Content-Type: text/event-stream +X-Accel-Buffering: no + event: containers-changed data: [] @@ -148,17 +148,14 @@ X-Content-Type-Options: nosniff error finding container /* snapshot: Test_handler_streamLogs_error_reading */ -HTTP/1.1 500 Internal Server Error +HTTP/1.1 200 OK Connection: close Cache-Control: no-transform Cache-Control: no-cache Connection: keep-alive Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; -Content-Type: text/plain; charset=utf-8 -X-Accel-Buffering: no -X-Content-Type-Options: nosniff - -test error +Content-Type: text/event-stream +X-Accel-Buffering: no /* snapshot: Test_handler_streamLogs_error_std */ HTTP/1.1 400 Bad Request @@ -179,10 +176,10 @@ Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; i Content-Type: text/event-stream X-Accel-Buffering: no -data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info","s":"stdout"} +data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info","s":"stdout","c":"123456"} event: container-stopped -data: end of stream +data: {"actorId":"123456","name":"container-stopped","host":"localhost"} /* snapshot: Test_handler_streamLogs_happy_container_stopped */ HTTP/1.1 200 OK @@ -192,10 +189,7 @@ Cache-Control: no-cache Connection: keep-alive Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; Content-Type: text/event-stream -X-Accel-Buffering: no - -event: container-stopped -data: end of stream +X-Accel-Buffering: no /* snapshot: Test_handler_streamLogs_happy_with_id */ HTTP/1.1 200 OK @@ -207,8 +201,8 @@ Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; i Content-Type: text/event-stream X-Accel-Buffering: no -data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout"} +data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout","c":"123456"} id: 1589396137772 event: container-stopped -data: end of stream \ No newline at end of file +data: {"actorId":"123456","name":"container-stopped","host":"localhost"} \ No newline at end of file diff --git a/internal/web/events.go b/internal/web/events.go index 146c77ce..c428b79f 100644 --- a/internal/web/events.go +++ b/internal/web/events.go @@ -43,7 +43,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { } store.SubscribeStats(ctx, stats) store.Subscribe(ctx, events) - } defer func() { diff --git a/internal/web/logs.go b/internal/web/logs.go index 18cb7f7b..e85315fb 100644 --- a/internal/web/logs.go +++ b/internal/web/logs.go @@ -107,7 +107,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) return } - g := docker.NewEventGenerator(reader, container.Tty) + g := docker.NewEventGenerator(reader, container) encoder := json.NewEncoder(w) for event := range g.Events { @@ -117,9 +117,206 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) } } -func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") +func (h *handler) newContainers(ctx context.Context) chan docker.Container { + containers := make(chan docker.Container) + for _, store := range h.stores { + store.SubscribeNewContainers(ctx, containers) + } + return containers +} + +func (h *handler) streamContainerLogs(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + container, err := h.clientFromRequest(r).FindContainer(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + containers := make(chan docker.Container, 1) + containers <- container + + go func() { + newContainers := h.newContainers(r.Context()) + for { + select { + case container := <-newContainers: + if container.ID == id { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamContainerLogs") + return + } + } + case <-r.Context().Done(): + log.Debugf("closing container channel streamContainerLogs") + return + } + } + }() + + streamLogsForContainers(w, r, h.clients, containers) +} + +func (h *handler) streamLogsMerged(w http.ResponseWriter, r *http.Request) { + if !r.URL.Query().Has("id") { + http.Error(w, "ids query parameter is required", http.StatusBadRequest) + return + } + + containers := make(chan docker.Container, len(r.URL.Query()["id"])) + + for _, id := range r.URL.Query()["id"] { + container, err := h.clientFromRequest(r).FindContainer(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + containers <- container + } + + streamLogsForContainers(w, r, h.clients, containers) +} + +func (h *handler) streamServiceLogs(w http.ResponseWriter, r *http.Request) { + service := chi.URLParam(r, "service") + containers := make(chan docker.Container, 10) + + go func() { + for _, store := range h.stores { + list, err := store.List() + if err != nil { + log.Errorf("error while listing containers %v", err.Error()) + return + } + + for _, container := range list { + if container.State == "running" && (container.Labels["com.docker.swarm.service.name"] == service) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + } + + } + newContainers := h.newContainers(r.Context()) + for { + select { + case container := <-newContainers: + if container.State == "running" && (container.Labels["com.docker.swarm.service.name"] == service) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + }() + + streamLogsForContainers(w, r, h.clients, containers) +} + +func (h *handler) streamGroupedLogs(w http.ResponseWriter, r *http.Request) { + group := chi.URLParam(r, "group") + containers := make(chan docker.Container, 10) + + go func() { + for _, store := range h.stores { + list, err := store.List() + if err != nil { + log.Errorf("error while listing containers %v", err.Error()) + return + } + + for _, container := range list { + if container.State == "running" && (container.Group == group) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + } + } + newContainers := h.newContainers(r.Context()) + for { + select { + case container := <-newContainers: + if container.State == "running" && (container.Group == group) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + case <-r.Context().Done(): + log.Debugf("closing container channel streamServiceLogs") + return + } + } + }() + + streamLogsForContainers(w, r, h.clients, containers) +} + +func (h *handler) streamStackLogs(w http.ResponseWriter, r *http.Request) { + stack := chi.URLParam(r, "stack") + containers := make(chan docker.Container, 10) + + go func() { + for _, store := range h.stores { + list, err := store.List() + if err != nil { + log.Errorf("error while listing containers %v", err.Error()) + return + } + + for _, container := range list { + if container.State == "running" && (container.Labels["com.docker.stack.namespace"] == stack) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamStackLogs") + return + } + } + } + } + newContainers := h.newContainers(r.Context()) + for { + select { + case container := <-newContainers: + if container.State == "running" && (container.Labels["com.docker.stack.namespace"] == stack) { + select { + case containers <- container: + case <-r.Context().Done(): + log.Debugf("closing container channel streamStackLogs") + return + } + } + case <-r.Context().Done(): + log.Debugf("closing container channel streamStackLogs") + return + } + } + }() + + streamLogsForContainers(w, r, h.clients, containers) +} + +func streamLogsForContainers(w http.ResponseWriter, r *http.Request, clients map[string]docker.Client, containers chan docker.Container) { var stdTypes docker.StdType if r.URL.Query().Has("stdout") { stdTypes |= docker.STDOUT @@ -139,47 +336,22 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) { return } - container, err := h.clientFromRequest(r).FindContainer(id) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-transform") w.Header().Add("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") - lastEventId := r.Header.Get("Last-Event-ID") - if len(r.URL.Query().Get("lastEventId")) > 0 { - lastEventId = r.URL.Query().Get("lastEventId") - } - - reader, err := h.clientFromRequest(r).ContainerLogs(r.Context(), container.ID, lastEventId, stdTypes) - if err != nil { - if err == io.EOF { - fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n") - f.Flush() - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } + logs := make(chan *docker.LogEvent) + events := make(chan *docker.ContainerEvent) ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() - g := docker.NewEventGenerator(reader, container.Tty) - loop: for { select { - case event, ok := <-g.Events: - if !ok { - log.WithFields(log.Fields{"id": id}).Debug("stream closed") - break loop - } + case event := <-logs: if buf, err := json.Marshal(event); err != nil { log.Errorf("json encoding error while streaming %v", err.Error()) } else { @@ -193,27 +365,49 @@ loop: case <-ticker.C: fmt.Fprintf(w, ":ping \n\n") f.Flush() - } - } + case container := <-containers: + go func(container docker.Container) { + reader, err := clients[container.Host].ContainerLogs(r.Context(), container.ID, "", stdTypes) + if err != nil { + return + } + g := docker.NewEventGenerator(reader, container) + for event := range g.Events { + logs <- event + } + select { + case err := <-g.Errors: + if err != nil { + if err == io.EOF { + log.WithError(err).Debugf("stream closed for container %v", container.Name) + events <- &docker.ContainerEvent{ActorID: container.ID, Name: "container-stopped", Host: container.Host} + } else if err != r.Context().Err() { + log.Errorf("unknown error while streaming %v", err.Error()) + } + } + default: + // do nothing + } + }(container) - select { - case err := <-g.Errors: - if err != nil { - if err == io.EOF { - log.Debugf("container stopped: %v", container.ID) - fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n") + case event := <-events: + log.Debugf("received container event %v", event) + if buf, err := json.Marshal(event); err != nil { + log.Errorf("json encoding error while streaming %v", err.Error()) + } else { + fmt.Fprintf(w, "event: container-stopped\ndata: %s\n\n", buf) f.Flush() - } else if err != context.Canceled { - log.Errorf("unknown error while streaming %v", err.Error()) } + + case <-r.Context().Done(): + log.Debugf("context cancelled") + break loop } - default: } if log.IsLevelEnabled(log.DebugLevel) { var m runtime.MemStats runtime.ReadMemStats(&m) - // For info on each, see: https://golang.org/pkg/runtime/#MemStats log.WithFields(log.Fields{ "allocated": humanize.Bytes(m.Alloc), "totalAllocated": humanize.Bytes(m.TotalAlloc), diff --git a/internal/web/logs_test.go b/internal/web/logs_test.go index 91490d9c..ce8bd97c 100644 --- a/internal/web/logs_test.go +++ b/internal/web/logs_test.go @@ -2,6 +2,7 @@ package web import ( "bytes" + "context" "encoding/binary" "errors" "io" @@ -19,8 +20,11 @@ import ( ) func Test_handler_streamLogs_happy(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + id := "123456" - req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + q := req.URL.Query() q.Add("stdout", "true") q.Add("stderr", "true") @@ -32,8 +36,14 @@ func Test_handler_streamLogs_happy(t *testing.T) { data := makeMessage("INFO Testing logs...", docker.STDOUT) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false}, nil) - mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false, Host: "localhost"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil). + Run(func(args mock.Arguments) { + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + }) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() @@ -44,7 +54,8 @@ func Test_handler_streamLogs_happy(t *testing.T) { func Test_handler_streamLogs_happy_with_id(t *testing.T) { id := "123456" - req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) q := req.URL.Query() q.Add("stdout", "true") q.Add("stderr", "true") @@ -56,8 +67,14 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) { data := makeMessage("2020-05-13T18:55:37.772853839Z INFO Testing logs...", docker.STDOUT) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil). + Run(func(args mock.Arguments) { + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + }) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() @@ -68,7 +85,8 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) { func Test_handler_streamLogs_happy_container_stopped(t *testing.T) { id := "123456" - req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) q := req.URL.Query() q.Add("stdout", "true") q.Add("stderr", "true") @@ -77,8 +95,14 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) { require.NoError(t, err, "NewRequest should not return an error.") mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF). + Run(func(args mock.Arguments) { + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + }) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() @@ -89,7 +113,8 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) { func Test_handler_streamLogs_error_finding_container(t *testing.T) { id := "123456" - req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) q := req.URL.Query() q.Add("stdout", "true") q.Add("stderr", "true") @@ -98,7 +123,13 @@ func Test_handler_streamLogs_error_finding_container(t *testing.T) { require.NoError(t, err, "NewRequest should not return an error.") mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container")) + mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container")). + Run(func(args mock.Arguments) { + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + }) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() @@ -108,8 +139,10 @@ func Test_handler_streamLogs_error_finding_container(t *testing.T) { } func Test_handler_streamLogs_error_reading(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + id := "123456" - req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) + req, err := http.NewRequestWithContext(ctx, "GET", "/api/hosts/localhost/containers/"+id+"/logs/stream", nil) q := req.URL.Query() q.Add("stdout", "true") q.Add("stderr", "true") @@ -118,8 +151,14 @@ func Test_handler_streamLogs_error_reading(t *testing.T) { require.NoError(t, err, "NewRequest should not return an error.") mockedClient := new(MockedClient) - mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) - mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error")) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil) + mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error")). + Run(func(args mock.Arguments) { + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + }) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() @@ -135,6 +174,7 @@ func Test_handler_streamLogs_error_std(t *testing.T) { require.NoError(t, err, "NewRequest should not return an error.") mockedClient := new(MockedClient) + mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil) handler := createDefaultHandler(mockedClient) rr := httptest.NewRecorder() diff --git a/internal/web/routes.go b/internal/web/routes.go index f1c2ba3c..bd0bb06e 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -90,9 +90,13 @@ func createRouter(h *handler) *chi.Mux { if h.config.Authorization.Provider != NONE { r.Use(auth.RequireAuthentication) } - r.Get("/api/hosts/{host}/containers/{id}/logs/stream", h.streamLogs) + r.Get("/api/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs) r.Get("/api/hosts/{host}/containers/{id}/logs/download", h.downloadLogs) r.Get("/api/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates) + r.Get("/api/hosts/{host}/logs/mergedStream", h.streamLogsMerged) + r.Get("/api/stacks/{stack}/logs/stream", h.streamStackLogs) + r.Get("/api/services/{service}/logs/stream", h.streamServiceLogs) + r.Get("/api/groups/{group}/logs/stream", h.streamGroupedLogs) r.Get("/api/events/stream", h.streamEvents) if h.config.EnableActions { r.Post("/api/hosts/{host}/containers/{id}/actions/{action}", h.containerActions) diff --git a/locales/en.yml b/locales/en.yml index 038f0e6f..bbd65e38 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -10,6 +10,7 @@ toolbar: label: containers: Containers container: No containers | 1 container | {count} containers + serivce: No services | 1 service | {count} services running-containers: Running Containers all-containers: All Containers host: Host @@ -22,7 +23,9 @@ label: avg-mem: Avg. MEM (%) pinned: Pinned per-page: Rows per page - + swarm-mode: Swam Mode + services: Services + custom-groups: Custom Groups tooltip: search: Search containers (⌘ + k, ⌃k) pin-column: Pin as column diff --git a/package.json b/package.json index 8e43837c..9c288137 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "typescript": "^5.4.5", "vitepress": "1.2.2", "vitest": "^1.6.0", + "vue-component-type-helpers": "^2.0.19", "vue-tsc": "^2.0.19" }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2889d95..fe65feec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.12.12)(jsdom@24.0.0) + vue-component-type-helpers: + specifier: ^2.0.19 + version: 2.0.19 vue-tsc: specifier: ^2.0.19 version: 2.0.19(typescript@5.4.5) @@ -3293,8 +3296,8 @@ packages: jsdom: optional: true - vue-component-type-helpers@2.0.11: - resolution: {integrity: sha512-8aluKz5oVC8PvVQAYgyIefOlqzKVmAOTCx2imbrFBVLbF7mnJvyMsE2A7rqX/4f4uT6ee9o8u3GcoRpUWc0xsw==} + vue-component-type-helpers@2.0.19: + resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} vue-demi@0.14.7: resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} @@ -4546,7 +4549,7 @@ snapshots: '@vue/test-utils@2.4.6': dependencies: js-beautify: 1.15.1 - vue-component-type-helpers: 2.0.11 + vue-component-type-helpers: 2.0.19 '@vueuse/components@10.9.0(vue@3.4.27(typescript@5.4.5))': dependencies: @@ -6693,7 +6696,7 @@ snapshots: - supports-color - terser - vue-component-type-helpers@2.0.11: {} + vue-component-type-helpers@2.0.19: {} vue-demi@0.14.7(vue@3.4.27(typescript@5.4.5)): dependencies: