diff --git a/assets/auto-imports.d.ts b/assets/auto-imports.d.ts index 01c953e5..aebd2d4c 100644 --- a/assets/auto-imports.d.ts +++ b/assets/auto-imports.d.ts @@ -234,6 +234,7 @@ declare global { const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream'] const useHead: typeof import('@vueuse/head')['useHead'] + const useHostStream: typeof import('./composable/eventStreams')['useHostStream'] const useHosts: typeof import('./stores/hosts')['useHosts'] const useI18n: typeof import('vue-i18n')['useI18n'] const useId: typeof import('vue')['useId'] @@ -621,6 +622,7 @@ declare module 'vue' { readonly useGeolocation: UnwrapRef readonly useGroupedStream: UnwrapRef readonly useHead: UnwrapRef + readonly useHostStream: UnwrapRef readonly useHosts: UnwrapRef readonly useI18n: UnwrapRef readonly useId: UnwrapRef diff --git a/assets/components.d.ts b/assets/components.d.ts index d2e59153..2a937dce 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -39,6 +39,7 @@ declare module 'vue' { GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default'] HostIcon: typeof import('./components/common/HostIcon.vue')['default'] HostList: typeof import('./components/HostList.vue')['default'] + HostLog: typeof import('./components/HostViewer/HostLog.vue')['default'] HostMenu: typeof import('./components/HostMenu.vue')['default'] 'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default'] IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default'] @@ -91,6 +92,7 @@ declare module 'vue' { 'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default'] 'Ph:fileSql': typeof import('~icons/ph/file-sql')['default'] 'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default'] + 'Ph:listBulletsFill': typeof import('~icons/ph/list-bullets-fill')['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'] diff --git a/assets/components/HostMenu.vue b/assets/components/HostMenu.vue index 151d30dc..161e7b76 100644 --- a/assets/components/HostMenu.vue +++ b/assets/components/HostMenu.vue @@ -6,7 +6,18 @@ {{ $t("label.hosts") }}
  • - {{ hosts[sessionHost].name }} + + + {{ hosts[sessionHost].name }} +
  • diff --git a/assets/components/HostViewer/HostLog.vue b/assets/components/HostViewer/HostLog.vue new file mode 100644 index 00000000..3a5e9c70 --- /dev/null +++ b/assets/components/HostViewer/HostLog.vue @@ -0,0 +1,43 @@ + + + diff --git a/assets/components/ServiceViewer/ServiceLog.vue b/assets/components/ServiceViewer/ServiceLog.vue index bafb328e..69764e07 100644 --- a/assets/components/ServiceViewer/ServiceLog.vue +++ b/assets/components/ServiceViewer/ServiceLog.vue @@ -3,6 +3,7 @@ diff --git a/assets/composable/eventStreams.ts b/assets/composable/eventStreams.ts index ab336c6c..c7555edb 100644 --- a/assets/composable/eventStreams.ts +++ b/assets/composable/eventStreams.ts @@ -26,6 +26,10 @@ export function useContainerStream(container: Ref): LogStreamSource { return useLogStream(url, loadMoreUrl); } +export function useHostStream(host: Ref): LogStreamSource { + return useLogStream(computed(() => `/api/hosts/${host.value.id}/logs/stream`)); +} + export function useStackStream(stack: Ref): LogStreamSource { return useLogStream(computed(() => `/api/stacks/${stack.value.name}/logs/stream`)); } diff --git a/assets/pages/host/[id].vue b/assets/pages/host/[id].vue new file mode 100644 index 00000000..4c46c594 --- /dev/null +++ b/assets/pages/host/[id].vue @@ -0,0 +1,30 @@ + + + + +meta: + containerMode: true + diff --git a/assets/stores/container.ts b/assets/stores/container.ts index d53a36e7..e02913db 100644 --- a/assets/stores/container.ts +++ b/assets/stores/container.ts @@ -161,9 +161,23 @@ export const useContainerStore = defineStore("container", () => { const findContainerById = (id: string) => allContainersById.value[id]; + const containersByHost = computed(() => + containers.value.reduce( + (acc, container) => { + if (!acc[container.host]) { + acc[container.host] = []; + } + acc[container.host].push(container); + return acc; + }, + {} as Record, + ), + ); + return { containers, allContainersById, + containersByHost, visibleContainers, currentContainer, findContainerById, diff --git a/assets/typed-router.d.ts b/assets/typed-router.d.ts index b094d696..36be6b56 100644 --- a/assets/typed-router.d.ts +++ b/assets/typed-router.d.ts @@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' { '/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue }, { all: ParamValue }>, '/container/[id]': RouteRecordInfo<'/container/[id]', '/container/:id', { id: ParamValue }, { id: ParamValue }>, '/group/[name]': RouteRecordInfo<'/group/[name]', '/group/:name', { name: ParamValue }, { name: ParamValue }>, + '/host/[id]': RouteRecordInfo<'/host/[id]', '/host/:id', { id: ParamValue }, { id: ParamValue }>, '/login': RouteRecordInfo<'/login', '/login', Record, Record>, '/merged/[ids]': RouteRecordInfo<'/merged/[ids]', '/merged/:ids', { ids: ParamValue }, { ids: ParamValue }>, '/service/[name]': RouteRecordInfo<'/service/[name]', '/service/:name', { name: ParamValue }, { name: ParamValue }>, 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 65e03153..4276c4cd 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 ad35dc92..e9c30e07 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 3e776bc5..e87b8cdd 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 cadb21ec..8f783ea6 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/web/logs.go b/internal/web/logs.go index e28da672..d9b77400 100644 --- a/internal/web/logs.go +++ b/internal/web/logs.go @@ -220,6 +220,13 @@ func (h *handler) streamStackLogs(w http.ResponseWriter, r *http.Request) { }) } +func (h *handler) streamHostLogs(w http.ResponseWriter, r *http.Request) { + host := hostKey(r) + h.streamLogsForContainers(w, r, func(container *docker.Container) bool { + return container.State == "running" && container.Host == host + }) +} + func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter ContainerFilter) { var stdTypes docker.StdType if r.URL.Query().Has("stdout") { diff --git a/internal/web/routes.go b/internal/web/routes.go index f2b859d7..6e27e46d 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -94,6 +94,7 @@ func createRouter(h *handler) *chi.Mux { r.Use(auth.RequireAuthentication) } r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs) + r.Get("/hosts/{host}/logs/stream", h.streamHostLogs) r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates) r.Get("/hosts/{host}/logs/mergedStream/{ids}", h.streamLogsMerged) r.Get("/containers/{hostIds}/download", h.downloadLogs) // formatted as host:container,host:container diff --git a/locales/en.yml b/locales/en.yml index a941ee91..cddcaa6b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -34,6 +34,7 @@ tooltip: pin-column: Pin as column merge-services: Merge all services into one view merge-containers: Merge all containers into one view + merge-hosts: Merge all containers on this host into one view error: page-not-found: This page does not exist invalid-auth: Username or password are not valid