Compare commits

...

45 Commits

Author SHA1 Message Date
Amir Raminfar 3c15ac225c chore: release v8.5.2 2024-09-26 10:32:24 -07:00
renovate[bot] 9aeb5164f2 chore(deps): update all non-major dependencies (#3292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Amir Raminfar <findamir@gmail.com>
2024-09-26 17:31:26 +00:00
Amir Raminfar 3497bf37d1 chore: fixes typescheck 2024-09-26 10:26:13 -07:00
Amir Raminfar f600d264bc chore: updates duckdb 2024-09-26 09:55:50 -07:00
Amir Raminfar e48aec12f5 feat: shows first 1000 records now 2024-09-26 09:54:51 -07:00
Huang Huan 412b599bcd chore: Update zh.yml to newest version (#3291) 2024-09-26 07:57:17 -07:00
renovate[bot] dc45c79402 chore(deps): update all non-major dependencies (#3290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 16:45:08 +00:00
Amir Raminfar 0f753d37fc chore: adds timeout to suspense 2024-09-24 14:11:59 -07:00
renovate[bot] a79b786faf chore(deps): update all non-major dependencies (#3289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 21:02:18 +00:00
Amir Raminfar 1fd7f683b2 chore: ignore duckdb 2024-09-22 08:45:09 -07:00
Amir Raminfar b666a842f6 chore: updates duckdb 2024-09-22 08:42:50 -07:00
Amir Raminfar 721d774f8e chore: adds duckdb-wasm to ignore 2024-09-22 08:42:30 -07:00
renovate[bot] 027b25cce5 fix(deps): update all non-major dependencies (#3286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 15:39:55 +00:00
Amir Raminfar d3a5324f0f chore: updates docs 2024-09-21 14:24:07 -07:00
Amir Raminfar 0f8bea46af chore: ignore errors 2024-09-21 14:19:26 -07:00
Amir Raminfar 0d783c26cb chore: cleans up async operations 2024-09-21 08:15:05 -07:00
Amir Raminfar 4b74944e52 chore: updates duckdb 2024-09-20 19:10:39 -07:00
Amir Raminfar 02ca609d16 chore: updates readme 2024-09-20 17:25:16 -07:00
Amir Raminfar 64b542131a chore: updates documentation 2024-09-20 17:14:13 -07:00
Amir Raminfar 6343629457 chore: adds timeout 2024-09-20 16:53:27 -07:00
Amir Raminfar dfaa52e2a6 chore: removes go-json (#3284) 2024-09-20 17:02:00 +00:00
Amir Raminfar 0f3585a3f4 chore: release v8.5.1 2024-09-20 09:45:23 -07:00
renovate[bot] ddddc6e512 chore(deps): update all non-major dependencies (#3283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 16:24:40 +00:00
Amir Raminfar 92614ea63c perf: introduces a timeout when fetching host info (#3276) 2024-09-20 16:19:08 +00:00
Amir Raminfar 03fdd82db6 chore: release v8.5.0 2024-09-19 08:08:29 -07:00
renovate[bot] 9421738fd4 chore(deps): update all non-major dependencies (#3281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 00:38:07 +00:00
Amir Raminfar 90b9d97685 feat: duckdb wasm capibility (#3272) 2024-09-18 17:27:46 -07:00
Amir Raminfar f1553e33e1 chore: release v8.4.4 2024-09-17 13:07:18 -07:00
renovate[bot] b7d376c45b chore(deps): update all non-major dependencies (#3280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 17:47:06 +00:00
Amir Raminfar 66d8064462 fix: fixes a css bug that was causing browser horizontal scrollbar. see #3278 (#3279) 2024-09-17 05:55:03 -07:00
renovate[bot] 2db41e044f fix(deps): update dependency date-fns to v4 (#3277) 2024-09-15 20:16:59 -07:00
renovate[bot] 70b4aca23b chore(deps): update all non-major dependencies (#3274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 16:06:31 +00:00
Amir Raminfar df35f57ccf chore: clean up 2024-09-14 08:59:30 -07:00
Amir Raminfar f8f57d4d9a chore: generate import 2024-09-14 08:58:40 -07:00
renovate[bot] b777136449 fix(deps): update all non-major dependencies (#3271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-13 15:33:06 +00:00
renovate[bot] a482be0c4c fix(deps): update all non-major dependencies (#3270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 15:23:41 +00:00
renovate[bot] 5cc0ba5388 fix(deps): update dependency @intlify/unplugin-vue-i18n to v5 (#3268) 2024-09-10 14:14:57 +00:00
renovate[bot] a53346a30d fix(deps): update dependency vue-i18n to v10 (#3269) 2024-09-10 14:06:20 +00:00
renovate[bot] 0f15a6af95 chore(deps): update all non-major dependencies (#3267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 13:29:32 +00:00
renovate[bot] 1246553e8d fix(deps): update all non-major dependencies (#3264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 18:05:49 +00:00
Amir Raminfar c528aa1d3d chore: release v8.4.3 2024-09-09 10:56:05 -07:00
Amir Raminfar e841b6cba7 perf: improves animation css to use translate instead 2024-09-09 10:39:54 -07:00
Amir Raminfar 8183b9e70f fix: cleans up login page (#3263) 2024-09-09 15:28:03 +00:00
renovate[bot] 141874b6a5 fix(deps): update dependency unplugin-vue-router to ^0.10.8 (#3262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 00:34:34 +00:00
Amir Raminfar d6477848d5 fix: cleans up on hover background color 2024-09-08 14:10:48 -07:00
77 changed files with 2244 additions and 1687 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"rangeStrategy": "bump"
"rangeStrategy": "bump",
"ignoreDeps": ["@duckdb/duckdb-wasm"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:22.8.0-alpine AS node
FROM --platform=$BUILDPLATFORM node:22.9.0-alpine AS node
RUN corepack enable
+4 -3
View File
@@ -13,12 +13,13 @@ https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
- Intelligent fuzzy search for container names 🤖
- Search logs using regex 🔦
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine) 📊
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Live stats with memory and CPU usage
- Multi-user authentication with support for proxy forward authorization 🚨
- Swarm mode support 🐳
- Agent mode for monitoring multiple Docker hosts 🕵️‍♂️
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for proxy forward authorization 🚨
- [Swarm](https://dozzle.dev/guide/swarm-mode) mode support 🐳
- [Agent](https://dozzle.dev/guide/agent) mode for monitoring multiple Docker hosts 🕵️‍♂️
- Dark mode 🌙
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
+19 -359
View File
@@ -3,6 +3,7 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const $: typeof import('vue/macros')['$']
@@ -30,6 +31,7 @@ declare global {
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createDrawer: typeof import('./composable/drawer')['createDrawer']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
@@ -46,6 +48,8 @@ declare global {
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const drawerContext: typeof import('./composable/drawer')['drawerContext']
const drwaer: typeof import('./composable/showLogDetails')['drwaer']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
@@ -61,6 +65,7 @@ declare global {
const hashCode: typeof import('./utils/index')['hashCode']
const hourStyle: typeof import('./stores/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const initDuckDB: typeof import('./composable/duckdb')['initDuckDB']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
@@ -101,6 +106,7 @@ declare global {
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
@@ -215,7 +221,9 @@ declare global {
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDrawer: typeof import('./composable/drawer')['useDrawer']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useDuckDB: typeof import('./composable/duckdb')['useDuckDB']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
@@ -240,6 +248,7 @@ declare global {
const useHead: typeof import('@vueuse/head')['useHead']
const useHosts: typeof import('./stores/hosts')['useHosts']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@@ -259,6 +268,7 @@ declare global {
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMergedStream: typeof import('./composable/eventStreams')['useMergedStream']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -316,6 +326,7 @@ declare global {
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwarmStore: typeof import('./stores/swarm')['useSwarmStore']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
@@ -396,6 +407,7 @@ declare module 'vue' {
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
@@ -412,6 +424,7 @@ declare module 'vue' {
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly drawerContext: UnwrapRef<typeof import('./composable/drawer')['drawerContext']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
@@ -467,12 +480,12 @@ declare module 'vue' {
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeysForContainer: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeysForContainer']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@@ -502,7 +515,6 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -579,7 +591,9 @@ declare module 'vue' {
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useDuckDB: UnwrapRef<typeof import('./composable/duckdb')['useDuckDB']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
@@ -604,6 +618,7 @@ declare module 'vue' {
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@@ -613,363 +628,6 @@ declare module 'vue' {
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMergedStream: UnwrapRef<typeof import('./composable/eventStreams')['useMergedStream']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePinnedLogsStore: UnwrapRef<typeof import('./stores/pinned')['usePinnedLogsStore']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useServiceStream: UnwrapRef<typeof import('./composable/eventStreams')['useServiceStream']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStackStream: UnwrapRef<typeof import('./composable/eventStreams')['useStackStream']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwarmStore: UnwrapRef<typeof import('./stores/swarm')['useSwarmStore']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composable/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
declare module '@vue/runtime-core' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly dateLocale: UnwrapRef<typeof import('./stores/settings')['dateLocale']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hashCode: UnwrapRef<typeof import('./utils/index')['hashCode']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composable/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly loggingContextKey: UnwrapRef<typeof import('./composable/logContext')['loggingContextKey']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeysForContainer: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeysForContainer']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
@@ -978,6 +636,7 @@ declare module '@vue/runtime-core' {
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMergedStream: UnwrapRef<typeof import('./composable/eventStreams')['useMergedStream']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -1035,6 +694,7 @@ declare module '@vue/runtime-core' {
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwarmStore: UnwrapRef<typeof import('./stores/swarm')['useSwarmStore']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
+4
View File
@@ -46,6 +46,7 @@ declare module 'vue' {
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default']
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
@@ -53,6 +54,7 @@ declare module 'vue' {
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
'Mdi:account': typeof import('~icons/mdi/account')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
@@ -65,6 +67,7 @@ declare module 'vue' {
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:key': typeof import('~icons/mdi/key')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
@@ -83,6 +86,7 @@ declare module 'vue' {
'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:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
@@ -20,6 +20,12 @@
<KeyShortcut char="f" />
</a>
</li>
<li v-if="hasComplexLogs">
<a @click.prevent="showDrawer(LogAnalytics, { container }, 'lg')">
<ph:file-sql /> SQL Analytics
<KeyShortcut char="f" :modifiers="['shift', 'meta']" />
</a>
</li>
<li class="line"></li>
<li>
<a
@@ -102,18 +108,26 @@
<script lang="ts" setup>
import { Container } from "@/models/Container";
import LogAnalytics from "../LogViewer/LogAnalytics.vue";
const { showSearch } = useSearchFilter();
const { enableActions } = config;
const clear = defineEmit();
const { streamConfig } = useLoggingContext();
const { streamConfig, hasComplexLogs } = useLoggingContext();
const showDrawer = useDrawer();
const { container } = defineProps<{ container: Container }>();
const clear = defineEmit();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
onKeyStroke("f", (e) => {
if (hasComplexLogs.value) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(LogAnalytics, { container }, "lg");
e.preventDefault();
}
}
});
const downloadParams = computed(() =>
Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
@@ -126,9 +140,7 @@ const downloadUrl = computed(() =>
),
);
const disableRestart = computed(() => {
return actionStates.stop || actionStates.start || actionStates.restart;
});
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
</script>
<style scoped lang="postcss">
@@ -26,7 +26,6 @@
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
import { useTemplateRef } from "vue";
const {
id,
@@ -1,8 +1,5 @@
<template>
<div
class="group/item relative flex w-full cursor-pointer gap-x-2 hover:bg-secondary/10"
@click="showLogDetails(logEntry)"
>
<div class="group/item clickable relative flex w-full gap-x-2" @click="showDrawer(LogDetails, { entry: logEntry })">
<div v-if="showContainerName">
<ContainerName :id="logEntry.containerID" />
</div>
@@ -31,6 +28,7 @@
</template>
<script lang="ts" setup>
import { type ComplexLogEntry } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
const { logEntry, showContainerName = false } = defineProps<{
logEntry: ComplexLogEntry;
@@ -41,7 +39,7 @@ const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
const showLogDetails = useLogDetails();
const showDrawer = useDrawer();
</script>
<style lang="postcss" scoped>
@@ -81,6 +81,7 @@ describe("<ContainerEventSource />", () => {
[loggingContextKey as symbol]: {
containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]),
streamConfig: reactive({ stdout: true, stderr: true }),
hasComplexLogs: ref(false),
},
},
},
+1 -1
View File
@@ -21,7 +21,7 @@ defineExpose({
});
const fetchMore = async () => {
if (!isLoadingMore()) {
if (!isLoadingMore.value) {
loadingMore.value = true;
enabled.value = false;
await loadOlderLogs();
@@ -0,0 +1,127 @@
<template>
<aside>
<header class="flex items-center gap-4">
<h1 class="mobile-hidden text-2xl">{{ container.name }}</h1>
<h2 class="text-sm"><DistanceTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
<section>
<label class="form-control">
<textarea
v-model="query"
class="textarea textarea-primary w-full font-mono text-lg"
:class="{ 'textarea-error': error }"
></textarea>
<div class="label">
<span class="label-text-alt text-error" v-if="error">{{ error }}</span>
<span class="label-text-alt" v-else>
Total {{ results.numRows }} records
<template v-if="results.numRows > pageLimit">. Showing first {{ page.numRows }}.</template></span
>
</div>
</label>
</section>
<table class="table table-zebra table-pin-rows table-md" v-if="!evaluating && isReady">
<thead>
<tr>
<th v-for="column in columns" :key="column">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in page" :key="row">
<td v-for="column in columns" :key="column">{{ row[column] }}</td>
</tr>
</tbody>
</table>
<table class="table table-md animate-pulse" v-else>
<thead>
<tr>
<th v-for="_ in 3">
<div class="h-4 w-20 animate-pulse bg-base-content/50 opacity-50"></div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="_ in 9">
<td v-for="_ in 3">
<div class="h-4 w-20 bg-base-content/50 opacity-20"></div>
</td>
</tr>
</tbody>
</table>
</div>
</aside>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
const { container } = defineProps<{ container: Container }>();
const query = ref("SELECT * FROM logs");
const error = ref<string | null>(null);
const debouncedQuery = debouncedRef(query, 500);
const evaluating = ref(false);
const pageLimit = 1000;
const url = withBase(
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
);
const [{ useDuckDB }, response] = await Promise.all([import(`@/composable/duckdb`), fetch(url)]);
if (!response.ok) {
console.log("error fetching logs from", url);
throw new Error(`Failed to fetch logs: ${response.statusText}`);
}
const { db, conn } = await useDuckDB();
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
const { isReady } = useAsyncState(
async () => {
await db.registerFileBuffer("logs.json", new Uint8Array(await response.arrayBuffer()));
await conn.query(
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
);
},
undefined,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
},
);
const results = computedAsync(
async () => {
if (isReady.value) {
return await conn.query<Record<string, any>>(debouncedQuery.value);
} else {
return empty;
}
},
empty,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
evaluating,
},
);
whenever(evaluating, () => {
error.value = null;
});
const columns = computed(() =>
results.value.numRows > 0 ? Object.keys(results.value.get(0) as Record<string, any>) : [],
);
const page = computed(() => (results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value));
</script>
<style lang="postcss" scoped></style>
+1 -1
View File
@@ -71,7 +71,7 @@ useIntersectionObserver(
monospace;
> li {
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] md:px-4;
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] has-[.clickable]:cursor-pointer has-[.clickable]:hover:bg-primary/10 md:px-4;
&:last-child {
scroll-margin-block-end: 5rem;
}
+1 -10
View File
@@ -1,13 +1,9 @@
<template>
<SideDrawer ref="drawer">
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
</SideDrawer>
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
</template>
<script lang="ts" setup>
import SideDrawer from "@/components/common/SideDrawer.vue";
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
@@ -21,12 +17,7 @@ const { filteredPayload } = useVisibleFilter(visibleKeys);
const { debouncedSearchFilter } = useSearchFilter();
const { streamConfig } = useLoggingContext();
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
const { entry } = provideLogDetails(drawer);
const visibleMessages = filteredPayload(messages);
const router = useRouter();
watchEffect(() => {
@@ -5,8 +5,9 @@
</template>
<script lang="ts" setup generic="T">
import LogEventSource from "@/components/ContainerViewer/LogEventSource.vue";
import EventSource from "@/components/LogViewer/EventSource.vue";
import { LogStreamSource } from "@/composable/eventStreams";
import { ComponentExposed } from "vue-component-type-helpers";
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
@@ -15,15 +16,15 @@ const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
entity: T;
}>();
const source = $ref<InstanceType<typeof LogEventSource>>();
const source = useTemplateRef<ComponentExposed<typeof EventSource>>("source");
defineExpose({
clear: () => source?.clear(),
clear: () => source.value?.clear(),
});
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
source?.clear();
source.value?.clear();
e.preventDefault();
}
});
@@ -59,16 +59,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<dialog data-v-43493931="" class="modal-right modal items-start outline-none backdrop:bg-none">
<div data-v-43493931="" class="modal-box">
<form data-v-43493931="" method="dialog"><button data-v-43493931="" class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active"><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-off">
<path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"></path>
</svg><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-on">
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"></path>
</svg></button></form>
</div>
<form data-v-43493931="" method="dialog" class="modal-backdrop"><button data-v-43493931="">close</button></form>
</dialog>
<!--v-if-->"
`;
+4 -5
View File
@@ -25,7 +25,7 @@
</div>
</div>
<div
class="animate-background h-1 bg-gradient-radial from-primary to-transparent to-70%"
class="animate-background h-1 w-1/2 bg-gradient-radial from-primary to-transparent to-75%"
v-show="!scrollContext.paused && !scrollContext.loading"
></div>
<div ref="scrollObserver" class="h-px"></div>
@@ -95,17 +95,16 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
}
.animate-background {
animation: gradient-animation 2s ease infinite;
background-position: center;
animation: gradient-animation 3s ease-out infinite;
}
@keyframes gradient-animation {
0%,
100% {
background-size: 100% 100%;
transform: translateX(0%);
}
50% {
background-size: 140% 120%;
transform: translateX(100%);
}
}
</style>
+1 -1
View File
@@ -34,7 +34,7 @@ const { searchQueryFilter, showSearch, resetSearch, isValidQuery } = useSearchFi
const { style } = useDraggable(container);
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
showSearch.value = true;
nextTick(() => input.value?.focus() || input.value?.select());
e.preventDefault();
+21 -4
View File
@@ -1,13 +1,13 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-none backdrop:bg-none">
<div class="modal-box">
<div class="modal-box" :width="width">
<form method="dialog">
<button class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot></slot>
<slot v-if="open"></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
@@ -15,17 +15,34 @@
</dialog>
</template>
<script setup lang="ts">
const panel = ref<HTMLDialogElement>();
import { type DrawerWidth } from "@/composable/drawer";
const panel = useTemplateRef<HTMLDialogElement>("panel");
const open = ref(false);
const { width } = defineProps<{
width: DrawerWidth;
}>();
defineExpose({
open: () => {
open.value = true;
panel.value?.showModal();
},
});
useEventListener(panel, "close", () => (open.value = false));
</script>
<style scoped lang="postcss">
.modal-right :where(.modal-box) {
@apply fixed right-0 h-lvh max-h-screen max-w-3xl translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
@apply fixed right-0 h-lvh max-h-screen translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
&[width="md"] {
@apply max-w-3xl;
}
&[width="lg"] {
@apply max-w-5xl;
}
}
.modal-right[open] .modal-box {
+29
View File
@@ -0,0 +1,29 @@
import SideDrawer from "@/components/common/SideDrawer.vue";
import { Component } from "vue";
export type DrawerWidth = "md" | "xl" | "lg";
export const drawerContext = Symbol("drawer") as InjectionKey<
(c: Component, p: Record<string, any>, s?: DrawerWidth) => void
>;
export const createDrawer = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const component = shallowRef<Component | null>(null);
const properties = shallowRef<Record<string, any>>({});
const width = ref<DrawerWidth>("md");
const showDrawer = (c: Component, p: Record<string, any>, w: DrawerWidth = "md") => {
component.value = c;
properties.value = p;
width.value = w;
drawer.value?.open();
};
provide(drawerContext, showDrawer);
return { component, properties, showDrawer, width };
};
export const useDrawer = () =>
inject(drawerContext, () => {
console.error("No drawer context provided");
});
+30
View File
@@ -0,0 +1,30 @@
import * as duckdb from "@duckdb/duckdb-wasm";
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
export async function useDuckDB() {
let cleanup: (() => void) | undefined;
onUnmounted(() => cleanup?.());
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker!}");`], { type: "text/javascript" }),
);
// Instantiate the asynchronus version of DuckDB-Wasm
const worker = new Worker(worker_url);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(worker_url);
const conn = await db.connect();
cleanup = async () => {
console.log("Cleaning up DuckDB");
await conn.close();
await db.terminate();
worker.terminate();
};
return { db, conn };
}
+8 -7
View File
@@ -7,6 +7,7 @@ import {
LogEntry,
asLogEntry,
ContainerEventLogEntry,
ComplexLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Service, Stack } from "@/models/Stack";
@@ -103,7 +104,7 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
buffer.value = [];
}
const { streamConfig } = useLoggingContext();
const { streamConfig, hasComplexLogs } = useLoggingContext();
const params = computed(() => {
const params = Object.entries(toValue(streamConfig))
@@ -154,11 +155,11 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
watch(urlWithParams, () => connect(), { immediate: true });
let fetchingInProgress = false;
const isLoadingMore = ref(false);
async function loadOlderLogs() {
if (!loadMoreUrl) return;
if (fetchingInProgress) return;
if (isLoadingMore.value) return;
const to = messages.value[0].date;
const last = messages.value[Math.min(messages.value.length - 1, 300)].date;
@@ -167,7 +168,7 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const abortController = new AbortController();
const signal = abortController.signal;
fetchingInProgress = true;
isLoadingMore.value = true;
try {
const moreParams = { ...params.value, from: from.toISOString(), to: to.toISOString(), minimum: "100" };
const urlWithMoreParams = computed(() =>
@@ -187,13 +188,13 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
} catch (e) {
console.error("Error loading older logs", e);
} finally {
fetchingInProgress = false;
isLoadingMore.value = false;
}
}
onScopeDispose(() => close());
const isLoadingMore = () => fetchingInProgress;
watch(messages, () => (hasComplexLogs.value = messages.value.some((m) => m instanceof ComplexLogEntry)));
return { messages, loadOlderLogs, isLoadingMore };
return { messages, loadOlderLogs, isLoadingMore, hasComplexLogs };
}
+3
View File
@@ -4,6 +4,7 @@ type LogContext = {
streamConfig: { stdout: boolean; stderr: boolean };
containers: Container[];
loadingMore: boolean;
hasComplexLogs: boolean;
};
// export for testing
@@ -19,6 +20,7 @@ export const provideLoggingContext = (containers: Ref<Container[]>) => {
streamConfig: { stdout, stderr },
containers,
loadingMore: false,
hasComplexLogs: false,
}),
);
};
@@ -30,6 +32,7 @@ export const useLoggingContext = () => {
streamConfig: { stdout: true, stderr: true },
containers: [],
loadingMore: false,
hasComplexLogs: false,
}),
);
-22
View File
@@ -1,22 +0,0 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
import SideDrawer from "@/components/common/SideDrawer.vue";
export const showLogDetails = Symbol("showLogDetails") as InjectionKey<
(logEntry: LogEntry<string | JSONObject>) => void
>;
export const provideLogDetails = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const entry = ref<LogEntry<string | JSONObject>>();
provide(showLogDetails, (logEntry: LogEntry<string | JSONObject>) => {
entry.value = logEntry;
drawer.value?.open();
});
return { entry };
};
export const useLogDetails = () => {
const showDetails = inject(showLogDetails, () => {});
return showDetails;
};
+9
View File
@@ -42,6 +42,12 @@
<button>close</button>
</form>
</dialog>
<SideDrawer ref="drawer" :width="drawerWidth">
<Suspense :timeout="0">
<component :is="drawerComponent" v-bind="drawerProperties" />
<template #fallback> Loading dependencies... </template>
</Suspense>
</SideDrawer>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
@@ -71,11 +77,14 @@
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/stores/settings";
import SideDrawer from "@/components/common/SideDrawer.vue";
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { toasts, removeToast } = useToast();
const drawer = useTemplateRef<InstanceType<typeof SideDrawer>>("drawer") as Ref<InstanceType<typeof SideDrawer>>;
const { component: drawerComponent, properties: drawerProperties, width: drawerWidth } = createDrawer(drawer);
const modal = ref<HTMLDialogElement>();
const open = ref(false);
+21 -25
View File
@@ -1,38 +1,36 @@
<template>
<div class="card w-96 flex-shrink-0 bg-base-lighter shadow-2xl">
<div class="card-body">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="form-control">
<label class="label">
<span class="label-text"> {{ $t("label.username") }} </span>
</label>
<form action="" method="post" @submit.prevent="onLogin" ref="form" class="flex flex-col gap-8">
<label class="input input-bordered flex items-center gap-2 border-2 has-[:focus]:input-primary">
<mdi:account class="has-[+:focus]:text-primary" />
<input
class="input input-bordered"
type="text"
class="grow"
:placeholder="$t('label.username')"
name="username"
autocomplete="username"
v-model="username"
autofocus
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{{ $t("label.password") }}</span>
</label>
</label>
<label class="input input-bordered flex items-center gap-2 border-2 has-[:focus]:input-primary">
<mdi:key class="has-[+:focus]:text-primary" />
<input
class="input input-bordered"
type="password"
class="grow"
:placeholder="$t('label.password')"
name="password"
autocomplete="current-password"
v-model="password"
autofocus
required
/>
</div>
</label>
<label class="label text-red" v-if="error">
{{ $t("error.invalid-auth") }}
</label>
<div class="form-control mt-6">
<button class="btn btn-primary" type="submit">{{ $t("button.login") }}</button>
</div>
<button class="btn btn-primary uppercase" type="submit">{{ $t("button.login") }}</button>
</form>
</div>
</div>
@@ -43,27 +41,25 @@ const { t } = useI18n();
setTitle(t("title.login"));
let error = $ref(false);
let username = $ref("");
let password = $ref("");
let form: HTMLFormElement | undefined = $ref();
const error = ref(false);
const form = ref<HTMLFormElement>();
const params = new URLSearchParams(window.location.search);
async function onLogin() {
const response = await fetch(withBase("/api/token"), {
body: new FormData(form),
body: new FormData(form.value),
method: "POST",
});
if (response.status == 200) {
error = false;
error.value = false;
if (params.has("redirectUrl")) {
window.location.href = withBase(params.get("redirectUrl")!);
} else {
window.location.href = withBase("/");
}
} else {
error = true;
error.value = true;
}
}
</script>
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -94,7 +94,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.47.0-jammy
image: mcr.microsoft.com/playwright:v1.47.2-jammy
working_dir: /app
volumes:
- .:/app
+1
View File
@@ -75,6 +75,7 @@ export default defineConfig({
{ text: "Swarm Mode", link: "/guide/swarm-mode" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
{ text: "SQL Engine", link: "/guide/sql-engine" },
],
},
{
+57
View File
@@ -0,0 +1,57 @@
---
title: SQL Engine
---
# SQL Engine <Badge type="warning" text="beta" /> <Badge type="tip" text="v8.5x" />
The SQL Engine is a powerful tool that allows you to run SQL queries against your data. It is designed to provide a seamless experience for users who are familiar with SQL and want to interact with their data using a familiar language.
This feature is currently in beta and is available to all users. If you have any feedback or suggestions, please let us know!
## Getting Started
To get started with the SQL Engine, you will need to have a dataset that you can query. Only JSON logs can be queried using SQL. Dozzle leveages the power of WebAssembly to run SQL queries in the browser, which means that your data never leaves your machine.
To start using the SQL Engine, make sure you have JSON logs and naviage to the drop down and choose `SQL Analytics`. There is also a keyboard shortcut `^+⇧+f` or `⌘+⇧+f` to quickly open the SQL Engine.
## How does it work?
The SQL Engine uses WebAssembly to run SQL queries in the browser with DuckDB. When the SQL Engine is first opened, DuckDB WASM is downloaded and initialized in the browser. This could take a while if you are on a slow connection. The SQL Engine then reads _only_ the JSON logs and creates a virtual table in DuckDB. This allows you to run SQL queries against your data in real-time.
The query that Dozzle runs initially is similar to:
```sql
CREATE TABLE logs AS SELECT unnest(m) FROM 'logs.json'
```
This query creates a table called `logs` and unnests the JSON logs into rows. You can then run SQL queries against this table to analyze your data.
## Example Queries
Here are some example queries that you can run using the SQL Engine:
### Count the number of logs
```sql
SELECT COUNT(*) FROM logs
```
### Filter logs by a specific field
```sql
SELECT * FROM logs WHERE level = 'error'
```
### Group logs by a specific field
```sql
SELECT level, COUNT(*) FROM logs GROUP BY level
```
## Limitations
WebAssembly has some limitations that you should be aware of when using the SQL Engine:
- The SQL Engine only supports structured data such as JSON.
- The SQL Engine is limited to running queries in the browser. This means that you cannot run queries that require access to external resources or databases.
- There is a maximum of 4GB of memory that can be used by the SQL Engine. If you run out of memory, you will need to refresh the page to clear the memory.
+9 -8
View File
@@ -16,12 +16,13 @@ Configurations can be done with flags or environment variables. The table below
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
| `--timeout` | `DOZZLE_TIMEOUT` | `3s` |
> [!TIP] Repeated Flags
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent tcp://167.99.1.1:7007 --remote-agent tcp://167.99.1.2:7007` or comma separated `DOZZLE_REMOTE_AGENT=tcp://167.99.1.1:7007,tcp://167.99.1.2:7007`.
@@ -36,11 +37,11 @@ docker run amir20/dozzle generate admin --password password --email test@email.n
In this example, `admin` is the username. Email and name are optional but recommended to display accurate avatars. `docker run amir20/dozzle generate --help` displays all options.
| Flag | Description | Default |
| ------------ | --------------- | ------- |
| `--password` | User's password | |
| `--email` | User's email | |
| `--name` | User's fullname | |
| Flag | Description | Default |
| ------------ | ---------------- | ------- |
| `--password` | User's password | |
| `--email` | User's email | |
| `--name` | User's full name | |
See [authentication](/guide/authentication) for more information.
@@ -58,7 +59,7 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
See [agent](/guide/agent) for more information.
## Healtcheck
## Healthcheck
Dozzle supports healthcheck using `dozzle healthcheck` command. It is not enabled by default as it adds extra CPU usage. To use `healthcheck` you need to configure it.
+3 -3
View File
@@ -4,8 +4,8 @@ title: What is Dozzle?
# What is Dozzle?
Dozzle is open-sourced project sponsored by Docker OSS. It is a log viewer designed to simplify the process of monitoring and debugging containers. It is a lightweight, web-based application that provides real-time log streaming, filtering, and searching capabilities through an intuitive user interface.
Dozzle is an open-source project sponsored by Docker OSS. It is a log viewer designed to simplify monitoring and debugging containers. This lightweight, web-based application offers real-time log streaming, filtering, and searching capabilities through an intuitive user interface.
Users can quickly and easily access logs generated by their Docker containers, making it an essential tool for debugging and troubleshooting applications running in a Docker environment. Out of the box, Dozzle supports JSON logs with intelligent color coding.
Users can quickly access logs generated by their Docker containers, making it an essential tool for debugging and troubleshooting applications in a Docker environment. By default, Dozzle supports JSON logs with intelligent color coding.
Dozzle is easy to install and can be configured with minimal effort, making it an ideal solution for developers and system administrators looking for an efficient and user-friendly log viewer for their Docker environment. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
Dozzle is easy to install and configure, making it an ideal solution for developers and system administrators seeking an efficient, user-friendly log viewer for their Docker environment. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
+5 -3
View File
@@ -32,9 +32,11 @@ features:
link: /guide/remote-hosts
linkText: Learn More
icon: 🌐
- title: No Database Required
details: Streams logs directly from Docker, remaining lightweight without extra overhead or complexity.
icon: 📦
- title: SQL Engine
details: Use SQL queries to analyze logs inside your browser with WebAssembly and DuckDB.
icon: 📊
linkText: Learn More
link: /guide/sql-engine
- title: Secured Agents
details: Connect to remote hosts securely with agents, providing a more secure way to connect to remote hosts.
icon: 🔒
+1 -1
View File
@@ -6,6 +6,6 @@
"dozzle": "workspace:*"
},
"dependencies": {
"@fontsource-variable/playfair-display": "^5.0.27"
"@fontsource-variable/playfair-display": "^5.1.0"
}
}
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@fontsource-variable/playfair-display':
specifier: ^5.0.27
version: 5.0.27
specifier: ^5.1.0
version: 5.1.0
devDependencies:
dozzle:
specifier: workspace:*
@@ -18,9 +18,9 @@ importers:
packages:
'@fontsource-variable/playfair-display@5.0.27':
resolution: {integrity: sha512-VQ7YOmWSvE+ZPg6tP7KtPUIM5in25/fnH+0Y74Z126hGQoan4gIhr557grh5RyIeakIptYizzcj36RJIksrQ9w==}
'@fontsource-variable/playfair-display@5.1.0':
resolution: {integrity: sha512-51UJAHznXiGkchEOT5AYFuIy9N9m1JE7YxcdOtY0H4SKgzK9YYGKpeCXv3dz2e1gAd+BP2TyR0QXNnVjgeCbgw==}
snapshots:
'@fontsource-variable/playfair-display@5.0.27': {}
'@fontsource-variable/playfair-display@5.1.0': {}
+4 -4
View File
@@ -4,7 +4,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alexflint/go-arg v1.5.1
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/docker v27.2.0+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
@@ -27,14 +27,13 @@ require (
github.com/PuerkitoBio/goquery v1.10.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/goccy/go-json v0.10.3
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.4
golang.org/x/sync v0.8.0
google.golang.org/grpc v1.66.0
google.golang.org/grpc v1.67.0
google.golang.org/protobuf v1.34.2
)
@@ -51,6 +50,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -73,7 +73,7 @@ require (
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gotest.tools/v3 v3.0.3 // indirect
)
+10 -18
View File
@@ -2,8 +2,6 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
@@ -31,8 +29,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.0+incompatible h1:BNb1QY6o4JdKpqwi9IB+HUYcRRrVN4aGFUTvDmWYK1A=
github.com/docker/docker v27.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -159,8 +159,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -176,8 +174,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -200,8 +196,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -213,8 +207,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
@@ -231,12 +223,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+10 -9
View File
@@ -6,10 +6,11 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/goccy/go-json"
"io"
"time"
"encoding/json"
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/utils"
@@ -260,8 +261,8 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
}
}
func (c *Client) FindContainer(containerID string) (docker.Container, error) {
response, err := c.client.FindContainer(context.Background(), &pb.FindContainerRequest{ContainerId: containerID})
func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.Container, error) {
response, err := c.client.FindContainer(ctx, &pb.FindContainerRequest{ContainerId: containerID})
if err != nil {
return docker.Container{}, err
}
@@ -294,8 +295,8 @@ func (c *Client) FindContainer(containerID string) (docker.Container, error) {
}, nil
}
func (c *Client) ListContainers() ([]docker.Container, error) {
response, err := c.client.ListContainers(context.Background(), &pb.ListContainersRequest{})
func (c *Client) ListContainers(ctx context.Context) ([]docker.Container, error) {
response, err := c.client.ListContainers(ctx, &pb.ListContainersRequest{})
if err != nil {
return nil, err
}
@@ -332,8 +333,8 @@ func (c *Client) ListContainers() ([]docker.Container, error) {
return containers, nil
}
func (c *Client) Host() (docker.Host, error) {
info, err := c.client.HostInfo(context.Background(), &pb.HostInfoRequest{})
func (c *Client) Host(ctx context.Context) (docker.Host, error) {
info, err := c.client.HostInfo(ctx, &pb.HostInfoRequest{})
if err != nil {
return docker.Host{
Endpoint: c.endpoint,
@@ -354,7 +355,7 @@ func (c *Client) Host() (docker.Host, error) {
}, nil
}
func (c *Client) ContainerAction(containerId string, action docker.ContainerAction) error {
func (c *Client) ContainerAction(ctx context.Context, containerId string, action docker.ContainerAction) error {
var containerAction pb.ContainerAction
switch action {
case docker.Start:
@@ -368,7 +369,7 @@ func (c *Client) ContainerAction(containerId string, action docker.ContainerActi
}
_, err := c.client.ContainerAction(context.Background(), &pb.ContainerActionRequest{ContainerId: containerId, Action: containerAction})
_, err := c.client.ContainerAction(ctx, &pb.ContainerActionRequest{ContainerId: containerId, Action: containerAction})
return err
}
+10 -10
View File
@@ -31,13 +31,13 @@ type MockedClient struct {
docker.Client
}
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
args := m.Called(id)
func (m *MockedClient) FindContainer(ctx context.Context, id string) (docker.Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(docker.Container), args.Error(1)
}
func (m *MockedClient) ContainerActions(action docker.ContainerAction, containerID string) error {
args := m.Called(action, containerID)
func (m *MockedClient) ContainerActions(ctx context.Context, action docker.ContainerAction, containerID string) error {
args := m.Called(ctx, action, containerID)
return args.Error(0)
}
@@ -46,8 +46,8 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- docker
return args.Error(0)
}
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
args := m.Called()
func (m *MockedClient) ListContainers(ctx context.Context) ([]docker.Container, error) {
args := m.Called(ctx)
return args.Get(0).([]docker.Container), args.Error(1)
}
@@ -92,7 +92,7 @@ func init() {
}
client = &MockedClient{}
client.On("ListContainers").Return([]docker.Container{
client.On("ListContainers", mock.Anything).Return([]docker.Container{
{
ID: "123456",
Name: "test",
@@ -111,7 +111,7 @@ func init() {
time.Sleep(5 * time.Second)
})
client.On("FindContainer", "123456").Return(docker.Container{
client.On("FindContainer", mock.Anything, "123456").Return(docker.Container{
ID: "123456",
Name: "test",
Host: "localhost",
@@ -142,7 +142,7 @@ func TestFindContainer(t *testing.T) {
t.Fatal(err)
}
container, _ := rpc.FindContainer("123456")
container, _ := rpc.FindContainer(context.Background(), "123456")
assert.Equal(t, container, docker.Container{
ID: "123456",
@@ -167,7 +167,7 @@ func TestListContainers(t *testing.T) {
t.Fatal(err)
}
containers, _ := rpc.ListContainers()
containers, _ := rpc.ListContainers(context.Background())
assert.Equal(t, containers, []docker.Container{
{
+3 -3
View File
@@ -7,7 +7,7 @@ import (
"crypto/x509"
"fmt"
"github.com/goccy/go-json"
"encoding/json"
"time"
@@ -79,7 +79,7 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
return err
}
container, err := s.client.FindContainer(in.ContainerId)
container, err := s.client.FindContainer(out.Context(), in.ContainerId)
if err != nil {
return err
}
@@ -295,7 +295,7 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
return nil, status.Error(codes.InvalidArgument, "invalid action")
}
err := s.client.ContainerActions(action, in.ContainerId)
err := s.client.ContainerActions(ctx, action, in.ContainerId)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
+1 -1
View File
@@ -2,8 +2,8 @@ package analytics
import (
"bytes"
"encoding/json"
"fmt"
"github.com/goccy/go-json"
"net/http"
"net/http/httputil"
+12 -12
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/goccy/go-json"
"encoding/json"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types"
@@ -59,15 +59,15 @@ type DockerCLI interface {
}
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ListContainers(context.Context) ([]Container, error)
FindContainer(context.Context, string) (Container, error)
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
ContainerEvents(context.Context, chan<- ContainerEvent) error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() Host
ContainerActions(action ContainerAction, containerID string) error
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
IsSwarmMode() bool
SystemInfo() system.Info
}
@@ -179,9 +179,9 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
}
// Finds a container by id, skipping the filters
func (d *httpClient) FindContainer(id string) (Container, error) {
func (d *httpClient) FindContainer(ctx context.Context, id string) (Container, error) {
log.Debug().Str("id", id).Msg("Finding container")
if json, err := d.cli.ContainerInspect(context.Background(), id); err == nil {
if json, err := d.cli.ContainerInspect(ctx, id); err == nil {
return newContainerFromJSON(json, d.host.ID), nil
} else {
return Container{}, err
@@ -189,26 +189,26 @@ func (d *httpClient) FindContainer(id string) (Container, error) {
}
func (d *httpClient) ContainerActions(action ContainerAction, containerID string) error {
func (d *httpClient) ContainerActions(ctx context.Context, action ContainerAction, containerID string) error {
switch action {
case Start:
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
return d.cli.ContainerStart(ctx, containerID, container.StartOptions{})
case Stop:
return d.cli.ContainerStop(context.Background(), containerID, container.StopOptions{})
return d.cli.ContainerStop(ctx, containerID, container.StopOptions{})
case Restart:
return d.cli.ContainerRestart(context.Background(), containerID, container.StopOptions{})
return d.cli.ContainerRestart(ctx, containerID, container.StopOptions{})
default:
return fmt.Errorf("unknown action: %s", action)
}
}
func (d *httpClient) ListContainers() ([]Container, error) {
func (d *httpClient) ListContainers(ctx context.Context) ([]Container, error) {
log.Debug().Interface("filter", d.filters).Str("host", d.host.Name).Msg("Listing containers")
containerListOptions := container.ListOptions{
Filters: d.filters,
All: true,
}
list, err := d.cli.ContainerList(context.Background(), containerListOptions)
list, err := d.cli.ContainerList(ctx, containerListOptions)
if err != nil {
return nil, err
}
+9 -9
View File
@@ -92,7 +92,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers()
list, err := client.ListContainers(context.Background())
assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.")
@@ -104,7 +104,7 @@ func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers()
list, err := client.ListContainers(context.Background())
assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.")
@@ -127,7 +127,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers()
list, err := client.ListContainers(context.Background())
require.NoError(t, err, "error should not return an error.")
Ids := []string{"1234567890_a", "abcdefghijkl"}
@@ -191,7 +191,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
container, err := client.FindContainer("abcdefghijkl")
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container.ID, "abcdefghijkl")
@@ -204,7 +204,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
_, err := client.FindContainer("not_valid")
_, err := client.FindContainer(context.Background(), "not_valid")
require.Error(t, err, "error should be thrown")
proxy.AssertExpectations(t)
@@ -222,14 +222,14 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerRestart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
container, err := client.FindContainer("abcdefghijkl")
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container.ID, "abcdefghijkl")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(ContainerAction(action), container.ID)
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, err, nil)
}
@@ -246,12 +246,12 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
container, err := client.FindContainer("random-id")
container, err := client.FindContainer(context.Background(), "random-id")
require.Error(t, err, "error should be thrown")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(ContainerAction(action), container.ID)
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
require.Error(t, err, "error should be thrown")
assert.Error(t, err, "error should have been returned")
}
+12 -4
View File
@@ -5,6 +5,7 @@ import (
"errors"
"sync"
"sync/atomic"
"time"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog/log"
@@ -59,7 +60,9 @@ func (s *ContainerStore) checkConnectivity() error {
s.connected.Store(false)
}()
if containers, err := s.client.ListContainers(); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s is enough to fetch all containers
defer cancel()
if containers, err := s.client.ListContainers(ctx); err != nil {
return err
} else {
s.containers.Clear()
@@ -81,7 +84,9 @@ func (s *ContainerStore) checkConnectivity() error {
}
go func(c Container, i int) {
defer sem.Release(1)
if container, err := s.client.FindContainer(c.ID); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 2s is hardcoded timeout for fetching container
defer cancel()
if container, err := s.client.FindContainer(ctx, c.ID); err == nil {
s.containers.Store(c.ID, &container)
}
}(c, i)
@@ -173,8 +178,10 @@ func (s *ContainerStore) init() {
log.Trace().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
switch event.Name {
case "start":
if container, err := s.client.FindContainer(event.ActorID); err == nil {
list, _ := s.client.ListContainers()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
@@ -193,6 +200,7 @@ func (s *ContainerStore) init() {
})
}
}
cancel()
case "destroy":
log.Debug().Str("id", event.ActorID).Msg("container destroyed")
s.containers.Delete(event.ActorID)
+8 -8
View File
@@ -14,13 +14,13 @@ type mockedClient struct {
Client
}
func (m *mockedClient) ListContainers() ([]Container, error) {
args := m.Called()
func (m *mockedClient) ListContainers(ctx context.Context) ([]Container, error) {
args := m.Called(ctx)
return args.Get(0).([]Container), args.Error(1)
}
func (m *mockedClient) FindContainer(id string) (Container, error) {
args := m.Called(id)
func (m *mockedClient) FindContainer(ctx context.Context, id string) (Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(Container), args.Error(1)
}
@@ -42,7 +42,7 @@ func (m *mockedClient) Host() Host {
func TestContainerStore_List(t *testing.T) {
client := new(mockedClient)
client.On("ListContainers").Return([]Container{
client.On("ListContainers", mock.Anything).Return([]Container{
{
ID: "1234",
Name: "test",
@@ -56,7 +56,7 @@ func TestContainerStore_List(t *testing.T) {
ID: "localhost",
})
client.On("FindContainer", "1234").Return(Container{
client.On("FindContainer", mock.Anything, "1234").Return(Container{
ID: "1234",
Name: "test",
Image: "test",
@@ -74,7 +74,7 @@ func TestContainerStore_List(t *testing.T) {
func TestContainerStore_die(t *testing.T) {
client := new(mockedClient)
client.On("ListContainers").Return([]Container{
client.On("ListContainers", mock.Anything).Return([]Container{
{
ID: "1234",
Name: "test",
@@ -100,7 +100,7 @@ func TestContainerStore_die(t *testing.T) {
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.ContainerStat")).Return(nil)
client.On("FindContainer", "1234").Return(Container{
client.On("FindContainer", mock.Anything, "1234").Return(Container{
ID: "1234",
Name: "test",
Image: "test",
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"sync"
"time"
"github.com/goccy/go-json"
"encoding/json"
orderedmap "github.com/wk8/go-ordered-map/v2"
+1 -1
View File
@@ -1,7 +1,7 @@
package docker
import (
"github.com/goccy/go-json"
"encoding/json"
"testing"
orderedmap "github.com/wk8/go-ordered-map/v2"
+1 -1
View File
@@ -1,7 +1,7 @@
package docker
import (
"github.com/goccy/go-json"
"encoding/json"
"reflect"
"testing"
+4 -2
View File
@@ -88,16 +88,17 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
sc.reset()
sc.totalStarted.Add(1)
var ctx context.Context
sc.mu.Lock()
if sc.stopper != nil {
sc.mu.Unlock()
return false
}
var ctx context.Context
ctx, sc.stopper = context.WithCancel(parentCtx)
sc.mu.Unlock()
if containers, err := sc.client.ListContainers(); err == nil {
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
if containers, err := sc.client.ListContainers(timeoutCtx); err == nil {
for _, c := range containers {
if c.State == "running" {
go streamStats(ctx, sc, c.ID)
@@ -106,6 +107,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
} else {
log.Error().Str("host", sc.client.Host().Name).Err(err).Msg("failed to list containers")
}
cancel()
events := make(chan ContainerEvent)
+2 -1
View File
@@ -10,7 +10,7 @@ import (
func startedCollector(ctx context.Context) *StatsCollector {
client := new(mockedClient)
client.On("ListContainers").Return([]Container{
client.On("ListContainers", mock.Anything).Return([]Container{
{
ID: "1234",
Name: "test",
@@ -46,6 +46,7 @@ func startedCollector(ctx context.Context) *StatsCollector {
return collector
}
func TestCancelers(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
+3 -2
View File
@@ -1,18 +1,19 @@
package healthcheck
import (
"context"
"crypto/tls"
"github.com/amir20/dozzle/internal/agent"
"github.com/rs/zerolog/log"
)
func RPCRequest(addr string, certs tls.Certificate) error {
func RPCRequest(ctx context.Context, addr string, certs tls.Certificate) error {
client, err := agent.NewClient(addr, certs)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create agent client")
}
containers, err := client.ListContainers()
containers, err := client.ListContainers(ctx)
log.Trace().Int("containers", len(containers)).Msg("Healtcheck RPC request completed")
return err
}
+1 -1
View File
@@ -1,8 +1,8 @@
package profile
import (
"encoding/json"
"errors"
"github.com/goccy/go-json"
"io"
"sync"
+1 -1
View File
@@ -2,7 +2,7 @@ package releases
import (
"bytes"
"github.com/goccy/go-json"
"encoding/json"
"net/http"
"strings"
"time"
+11
View File
@@ -2,6 +2,7 @@ package cli
import (
"strings"
"time"
"github.com/alexflint/go-arg"
)
@@ -24,6 +25,8 @@ type Args struct {
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
TimeoutString string `arg:"env:DOZZLE_TIMEOUT" default:"3s" help:"sets the timeout for docker client"`
Timeout time.Duration `arg:"-"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
@@ -65,5 +68,13 @@ func ParseArgs() (Args, interface{}) {
args.Filter[key] = append(args.Filter[key], val)
}
if args.TimeoutString != "" {
timeout, err := time.ParseDuration(args.TimeoutString)
if err != nil {
parser.Fail("timeout should be a valid duration")
}
args.Timeout = timeout
}
return args, parser.Subcommand()
}
+9 -4
View File
@@ -1,6 +1,7 @@
package cli
import (
"context"
"embed"
"github.com/amir20/dozzle/internal/docker"
@@ -22,7 +23,9 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *
log.Info().Interface("host", host).Msg("Adding remote host")
if client, err := docker.NewRemoteClient(args.Filter, host); err == nil {
if _, err := client.ListContainers(); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
if _, err := client.ListContainers(ctx); err == nil {
clients = append(clients, docker_support.NewDockerClientService(client))
} else {
log.Warn().Err(err).Interface("host", host).Msg("Could not connect to remote host")
@@ -34,7 +37,9 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *
localClient, err := docker.NewLocalClient(args.Filter, args.Hostname)
if err == nil {
_, err := localClient.ListContainers()
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
_, err := localClient.ListContainers(ctx)
if err != nil {
log.Debug().Err(err).Msg("Could not connect to local Docker Engine")
} else {
@@ -48,6 +53,6 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *
log.Fatal().Err(err).Msg("Could not read certificates")
}
clientManager := docker_support.NewRetriableClientManager(args.RemoteAgent, certs, clients...)
return localClient, docker_support.NewMultiHostService(clientManager)
clientManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs, clients...)
return localClient, docker_support.NewMultiHostService(clientManager, args.Timeout)
}
+8 -8
View File
@@ -20,8 +20,8 @@ func NewAgentService(client *agent.Client) ClientService {
}
}
func (a *agentService) FindContainer(id string) (docker.Container, error) {
return a.client.FindContainer(id)
func (a *agentService) FindContainer(ctx context.Context, id string) (docker.Container, error) {
return a.client.FindContainer(ctx, id)
}
func (a *agentService) RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
@@ -36,12 +36,12 @@ func (a *agentService) StreamLogs(ctx context.Context, container docker.Containe
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
}
func (a *agentService) ListContainers() ([]docker.Container, error) {
return a.client.ListContainers()
func (a *agentService) ListContainers(ctx context.Context) ([]docker.Container, error) {
return a.client.ListContainers(ctx)
}
func (a *agentService) Host() (docker.Host, error) {
host, err := a.client.Host()
func (a *agentService) Host(ctx context.Context) (docker.Host, error) {
host, err := a.client.Host(ctx)
if err != nil {
host := a.host
host.Available = false
@@ -64,6 +64,6 @@ func (d *agentService) SubscribeContainersStarted(ctx context.Context, container
go d.client.StreamNewContainers(ctx, containers)
}
func (a *agentService) ContainerAction(container docker.Container, action docker.ContainerAction) error {
return a.client.ContainerAction(container.ID, action)
func (a *agentService) ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error {
return a.client.ContainerAction(ctx, container.ID, action)
}
+9 -9
View File
@@ -9,10 +9,10 @@ import (
)
type ClientService interface {
FindContainer(id string) (docker.Container, error)
ListContainers() ([]docker.Container, error)
Host() (docker.Host, error)
ContainerAction(container docker.Container, action docker.ContainerAction) error
FindContainer(ctx context.Context, id string) (docker.Container, error)
ListContainers(ctx context.Context) ([]docker.Container, error)
Host(ctx context.Context) (docker.Host, error)
ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error
LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error)
RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error)
@@ -70,19 +70,19 @@ func (d *dockerClientService) StreamLogs(ctx context.Context, container docker.C
}
}
func (d *dockerClientService) FindContainer(id string) (docker.Container, error) {
func (d *dockerClientService) FindContainer(ctx context.Context, id string) (docker.Container, error) {
return d.store.FindContainer(id)
}
func (d *dockerClientService) ContainerAction(container docker.Container, action docker.ContainerAction) error {
return d.client.ContainerActions(action, container.ID)
func (d *dockerClientService) ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error {
return d.client.ContainerActions(ctx, action, container.ID)
}
func (d *dockerClientService) ListContainers() ([]docker.Container, error) {
func (d *dockerClientService) ListContainers(ctx context.Context) ([]docker.Container, error) {
return d.store.ListContainers()
}
func (d *dockerClientService) Host() (docker.Host, error) {
func (d *dockerClientService) Host(ctx context.Context) (docker.Host, error) {
return d.client.Host(), nil
}
+2 -2
View File
@@ -25,6 +25,6 @@ func (c *containerService) StreamLogs(ctx context.Context, from time.Time, stdTy
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
}
func (c *containerService) Action(action docker.ContainerAction) error {
return c.clientService.ContainerAction(c.Container, action)
func (c *containerService) Action(ctx context.Context, action docker.ContainerAction) error {
return c.clientService.ContainerAction(ctx, c.Container, action)
}
+18 -8
View File
@@ -3,6 +3,7 @@ package docker_support
import (
"context"
"fmt"
"time"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
@@ -24,16 +25,18 @@ type ClientManager interface {
List() []ClientService
RetryAndList() ([]ClientService, []error)
Subscribe(ctx context.Context, channel chan<- docker.Host)
Hosts() []docker.Host
Hosts(ctx context.Context) []docker.Host
}
type MultiHostService struct {
manager ClientManager
timeout time.Duration
}
func NewMultiHostService(manager ClientManager) *MultiHostService {
func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHostService {
m := &MultiHostService{
manager: manager,
timeout: timeout,
}
return m
@@ -44,8 +47,9 @@ func (m *MultiHostService) FindContainer(host string, id string) (*containerServ
if !ok {
return nil, fmt.Errorf("host %s not found", host)
}
container, err := client.FindContainer(id)
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
container, err := client.FindContainer(ctx, id)
if err != nil {
return nil, err
}
@@ -61,8 +65,10 @@ func (m *MultiHostService) ListContainersForHost(host string) ([]docker.Containe
if !ok {
return nil, fmt.Errorf("host %s not found", host)
}
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
return client.ListContainers()
return client.ListContainers(ctx)
}
func (m *MultiHostService) ListAllContainers() ([]docker.Container, []error) {
@@ -70,9 +76,11 @@ func (m *MultiHostService) ListAllContainers() ([]docker.Container, []error) {
clients, errors := m.manager.RetryAndList()
for _, client := range clients {
list, err := client.ListContainers()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
list, err := client.ListContainers(ctx)
if err != nil {
host, _ := client.Host()
host, _ := client.Host(ctx)
log.Debug().Err(err).Str("host", host.Name).Msg("error listing containers")
host.Available = false
errors = append(errors, &HostUnavailableError{Host: host, Err: err})
@@ -131,7 +139,9 @@ func (m *MultiHostService) TotalClients() int {
}
func (m *MultiHostService) Hosts() []docker.Host {
return m.manager.Hosts()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
return m.manager.Hosts(ctx)
}
func (m *MultiHostService) LocalHost() (docker.Host, error) {
@@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"sync"
"time"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
@@ -20,12 +21,15 @@ type RetriableClientManager struct {
certs tls.Certificate
mu sync.RWMutex
subscribers *xsync.MapOf[context.Context, chan<- docker.Host]
timeout time.Duration
}
func NewRetriableClientManager(agents []string, certs tls.Certificate, clients ...ClientService) *RetriableClientManager {
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...ClientService) *RetriableClientManager {
clientMap := make(map[string]ClientService)
for _, client := range clients {
host, err := client.Host()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("host", host.Name).Msg("error fetching host info for client")
continue
@@ -47,7 +51,9 @@ func NewRetriableClientManager(agents []string, certs tls.Certificate, clients .
continue
}
host, err := agent.Host()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
host, err := agent.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("endpoint", endpoint).Msg("error fetching host info for agent")
failed = append(failed, endpoint)
@@ -66,6 +72,7 @@ func NewRetriableClientManager(agents []string, certs tls.Certificate, clients .
failedAgents: failed,
certs: certs,
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
timeout: timeout,
}
}
@@ -92,7 +99,9 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
host, err := agent.Host()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
host, err := agent.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("endpoint", endpoint).Msg("error fetching host info for agent")
errors = append(errors, err)
@@ -146,12 +155,13 @@ func (m *RetriableClientManager) String() string {
return fmt.Sprintf("RetriableClientManager{clients: %d, failedAgents: %d}", len(m.clients), len(m.failedAgents))
}
func (m *RetriableClientManager) Hosts() []docker.Host {
func (m *RetriableClientManager) Hosts(ctx context.Context) []docker.Host {
clients := m.List()
hosts := lop.Map(clients, func(client ClientService, _ int) docker.Host {
host, err := client.Host()
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("host", host.Name).Msg("error fetching host info for client")
host.Available = false
} else {
host.Available = true
@@ -7,6 +7,7 @@ import (
"net"
"os"
"sync"
"time"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
@@ -25,6 +26,7 @@ type SwarmClientManager struct {
localClient docker.Client
localIPs []string
name string
timeout time.Duration
}
func localIPs() []string {
@@ -44,7 +46,7 @@ func localIPs() []string {
return ips
}
func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate) *SwarmClientManager {
func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, timeout time.Duration) *SwarmClientManager {
clientMap := make(map[string]ClientService)
localService := NewDockerClientService(localClient)
clientMap[localClient.Host().ID] = localService
@@ -54,7 +56,9 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate) *Sw
log.Fatal().Msg("HOSTNAME environment variable not set when looking for swarm service name")
}
container, err := localClient.FindContainer(id)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
container, err := localClient.FindContainer(ctx, id)
if err != nil {
log.Fatal().Err(err).Msg("error finding own container when looking for swarm service name")
}
@@ -70,6 +74,7 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate) *Sw
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
localIPs: localIPs(),
name: serviceName,
timeout: timeout,
}
}
@@ -97,7 +102,9 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
clients := lo.Values(m.clients)
endpoints := lo.KeyBy(clients, func(client ClientService) string {
host, _ := client.Host()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
host, _ := client.Host(ctx)
return host.Endpoint
})
@@ -125,7 +132,9 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
host, err := agent.Host()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
host, err := agent.Host(ctx)
if err != nil {
log.Warn().Err(err).Stringer("ip", ip).Msg("error getting host from agent client")
errors = append(errors, err)
@@ -183,12 +192,13 @@ func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
return client, ok
}
func (m *SwarmClientManager) Hosts() []docker.Host {
func (m *SwarmClientManager) Hosts(ctx context.Context) []docker.Host {
clients := m.List()
return lop.Map(clients, func(client ClientService, _ int) docker.Host {
host, err := client.Host()
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client")
host.Available = false
} else {
host.Available = true
+1 -1
View File
@@ -3,7 +3,7 @@ package utils
import (
"sync"
"github.com/goccy/go-json"
"encoding/json"
)
type RingBuffer[T any] struct {
+13 -13
View File
@@ -1,7 +1,7 @@
/* snapshot: Test_createRoutes_foobar */
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/plain; charset=utf-8
foo page
@@ -9,7 +9,7 @@ foo page
/* snapshot: Test_createRoutes_index */
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/plain; charset=utf-8
index page
@@ -17,7 +17,7 @@ index page
/* snapshot: Test_createRoutes_redirect */
HTTP/1.1 301 Moved Permanently
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/html; charset=utf-8
Location: /foobar/
@@ -35,7 +35,7 @@ Location: /foobar/login
/* snapshot: Test_createRoutes_simple_redirect */
HTTP/1.1 307 Temporary Redirect
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/html; charset=utf-8
Location: /login?redirectUrl=/
@@ -75,7 +75,7 @@ data: end of stream
/* snapshot: Test_createRoutes_version */
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/html
<pre>dev</pre>
@@ -83,7 +83,7 @@ Content-Type: text/html
/* snapshot: Test_handler_between_dates */
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; 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","c":"123456"}
@@ -92,7 +92,7 @@ Content-Type: application/x-jsonl; charset=UTF-8
/* snapshot: Test_handler_between_dates_with_fill */
HTTP/1.1 200 OK
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; 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","c":"123456"}
@@ -132,7 +132,7 @@ 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-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/event-stream
X-Accel-Buffering: no
@@ -162,14 +162,14 @@ 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-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/event-stream
X-Accel-Buffering: no
/* snapshot: Test_handler_streamLogs_error_std */
HTTP/1.1 400 Bad Request
Connection: close
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
@@ -181,7 +181,7 @@ 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-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/event-stream
X-Accel-Buffering: no
@@ -197,7 +197,7 @@ 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-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/event-stream
X-Accel-Buffering: no
@@ -210,7 +210,7 @@ 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-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
Content-Type: text/event-stream
X-Accel-Buffering: no
+5 -1
View File
@@ -1,7 +1,9 @@
package web
import (
"context"
"net/http"
"time"
"github.com/amir20/dozzle/internal/docker"
"github.com/go-chi/chi/v5"
@@ -26,7 +28,9 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
return
}
if err := containerService.Action(parsedAction); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := containerService.Action(ctx, parsedAction); err != nil {
log.Error().Err(err).Msg("error while trying to perform container action")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+8 -8
View File
@@ -17,15 +17,15 @@ func mockedClient() *MockedClient {
mockedClient := new(MockedClient)
container := docker.Container{ID: "123"}
mockedClient.On("FindContainer", "123").Return(container, nil)
mockedClient.On("FindContainer", "456").Return(docker.Container{}, errors.New("container not found"))
mockedClient.On("ContainerActions", docker.Start, container.ID).Return(nil)
mockedClient.On("ContainerActions", docker.Stop, container.ID).Return(nil)
mockedClient.On("ContainerActions", docker.Restart, container.ID).Return(nil)
mockedClient.On("ContainerActions", docker.Start, mock.Anything).Return(errors.New("container not found"))
mockedClient.On("ContainerActions", docker.ContainerAction("something-else"), container.ID).Return(errors.New("unknown action"))
mockedClient.On("FindContainer", mock.Anything, "123").Return(container, nil)
mockedClient.On("FindContainer", mock.Anything, "456").Return(docker.Container{}, errors.New("container not found"))
mockedClient.On("ContainerActions", mock.Anything, docker.Start, container.ID).Return(nil)
mockedClient.On("ContainerActions", mock.Anything, docker.Stop, container.ID).Return(nil)
mockedClient.On("ContainerActions", mock.Anything, docker.Restart, container.ID).Return(nil)
mockedClient.On("ContainerActions", mock.Anything, docker.Start, mock.Anything).Return(errors.New("container not found"))
mockedClient.On("ContainerActions", mock.Anything, docker.ContainerAction("something-else"), container.ID).Return(errors.New("unknown action"))
mockedClient.On("Host").Return(docker.Host{ID: "localhost"})
mockedClient.On("ListContainers").Return([]docker.Container{container}, nil)
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{container}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.Anything).Return(nil)
return mockedClient
+4 -1
View File
@@ -6,7 +6,10 @@ import (
func cspHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;")
w.Header().Set(
"Content-Security-Policy",
"default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;",
)
next.ServeHTTP(w, r)
})
}
+2 -2
View File
@@ -25,7 +25,7 @@ func Test_handler_download_logs(t *testing.T) {
data := makeMessage("INFO Testing logs...", docker.STDOUT)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Tty: false}, nil)
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, mock.Anything, mock.Anything, docker.STDOUT).Return(io.NopCloser(bytes.NewReader(data)), nil)
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
@@ -33,7 +33,7 @@ func Test_handler_download_logs(t *testing.T) {
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
time.Sleep(1 * time.Second)
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", State: "running"},
}, nil)
+4 -4
View File
@@ -18,13 +18,12 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
return
}
ctx := r.Context()
events := make(chan docker.ContainerEvent)
stats := make(chan docker.ContainerStat)
availableHosts := make(chan docker.Host)
h.multiHostService.SubscribeEventsAndStats(ctx, events, stats)
h.multiHostService.SubscribeAvailableHosts(ctx, availableHosts)
h.multiHostService.SubscribeEventsAndStats(r.Context(), events, stats)
h.multiHostService.SubscribeAvailableHosts(r.Context(), availableHosts)
allContainers, errors := h.multiHostService.ListAllContainers()
@@ -63,6 +62,7 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
case "start", "die", "destroy":
if event.Name == "start" {
log.Debug().Str("container", event.ActorID).Msg("container started")
if containers, err := h.multiHostService.ListContainersForHost(event.Host); err == nil {
if err := sseWriter.Event("containers-changed", containers); err != nil {
log.Error().Err(err).Msg("error writing containers to event stream")
@@ -92,7 +92,7 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
return
}
}
case <-ctx.Done():
case <-r.Context().Done():
return
}
}
+4 -4
View File
@@ -24,7 +24,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
messages := args.Get(1).(chan<- docker.ContainerEvent)
@@ -42,7 +42,7 @@ func Test_handler_streamEvents_happy(t *testing.T) {
time.Sleep(50 * time.Millisecond)
cancel()
})
mockedClient.On("FindContainer", "1234").Return(docker.Container{
mockedClient.On("FindContainer", mock.Anything, "1234").Return(docker.Container{
ID: "1234",
Name: "test",
Image: "test",
@@ -54,8 +54,8 @@ func Test_handler_streamEvents_happy(t *testing.T) {
})
// This is needed so that the server is initialized for store
manager := docker_support.NewRetriableClientManager(nil, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient))
multiHostService := docker_support.NewMultiHostService(manager)
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(mockedClient))
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
server := CreateServer(multiHostService, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sort"
"strings"
"github.com/goccy/go-json"
"encoding/json"
"net/http"
"path"
+25 -5
View File
@@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/goccy/go-json"
"encoding/json"
"fmt"
"io"
@@ -120,6 +120,13 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
}
}
onlyComplex := r.URL.Query().Has("jsonOnly")
everything := r.URL.Query().Has("everything")
if everything {
from = time.Time{}
to = time.Now()
}
minimum := 0
if r.URL.Query().Has("minimum") {
minimum, err = strconv.Atoi(r.URL.Query().Get("minimum"))
@@ -134,6 +141,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
}
}
encoder := json.NewEncoder(w)
for {
if buffer.Len() > minimum {
break
@@ -154,12 +162,23 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
buffer.Push(event)
}
} else {
buffer.Push(event)
if onlyComplex {
if _, ok := event.Message.(string); ok {
continue
}
}
if everything {
if err := encoder.Encode(event); err != nil {
log.Error().Err(err).Msg("error encoding log event")
}
} else {
buffer.Push(event)
}
}
}
if from.Before(containerService.Container.Created) {
log.Debug().Msg("reached beginning of logs")
if everything || from.Before(containerService.Container.Created) {
break
}
@@ -169,7 +188,6 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
log.Debug().Int("buffer_size", buffer.Len()).Msg("sending logs to client")
encoder := json.NewEncoder(w)
for _, event := range buffer.Data() {
if err := encoder.Encode(event); err != nil {
log.Error().Err(err).Msg("error encoding log event")
@@ -272,7 +290,9 @@ func streamLogsForContainers(w http.ResponseWriter, r *http.Request, multiHostCl
events := make([]*docker.LogEvent, 0)
stillRunning := false
for _, container := range existingContainers {
containerService, err := multiHostClient.FindContainer(container.Host, container.ID)
if err != nil {
log.Error().Err(err).Msg("error while finding container")
return
+14 -46
View File
@@ -38,7 +38,7 @@ func Test_handler_streamLogs_happy(t *testing.T) {
now := time.Now()
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false, Host: "localhost", StartedAt: now}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Tty: false, Host: "localhost", StartedAt: now}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, now, docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
Run(func(args mock.Arguments) {
go func() {
@@ -49,7 +49,7 @@ func Test_handler_streamLogs_happy(t *testing.T) {
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
@@ -80,7 +80,7 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, started, docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
Run(func(args mock.Arguments) {
go func() {
@@ -92,7 +92,7 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
@@ -120,7 +120,7 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, started, docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF).
Run(func(args mock.Arguments) {
go func() {
@@ -131,7 +131,7 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil)
@@ -143,38 +143,6 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
mockedClient.AssertExpectations(t)
}
// func Test_handler_streamLogs_error_finding_container(t *testing.T) {
// id := "123456"
// 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")
// req.URL.RawQuery = q.Encode()
// 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")).
// Run(func(args mock.Arguments) {
// go func() {
// time.Sleep(50 * time.Millisecond)
// cancel()
// }()
// })
// mockedClient.On("Host").Return(docker.Host{
// ID: "localhost",
// })
// mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
// mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil)
// handler := createDefaultHandler(mockedClient)
// rr := httptest.NewRecorder()
// handler.ServeHTTP(rr, req)
// abide.AssertHTTPResponse(t, t.Name(), rr.Result())
// mockedClient.AssertExpectations(t)
// }
func Test_handler_streamLogs_error_reading(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
@@ -189,7 +157,7 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, started, docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error")).
Run(func(args mock.Arguments) {
go func() {
@@ -200,7 +168,7 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil)
@@ -219,11 +187,11 @@ 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)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id, Host: "localhost"}, nil)
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).
@@ -260,11 +228,11 @@ func Test_handler_between_dates(t *testing.T) {
data := append(first, second...)
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, from, to, docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id}, nil)
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil)
@@ -305,11 +273,11 @@ func Test_handler_between_dates_with_fill(t *testing.T) {
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, id, time.Date(2017, time.December, 31, 14, 0, 0, 0, time.UTC), to, docker.STDALL).
Return(io.NopCloser(bytes.NewReader(data)), nil).
Once()
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("FindContainer", mock.Anything, id).Return(docker.Container{ID: id}, nil)
mockedClient.On("Host").Return(docker.Host{
ID: "localhost",
})
mockedClient.On("ListContainers").Return([]docker.Container{
mockedClient.On("ListContainers", mock.Anything).Return([]docker.Container{
{ID: id, Name: "test", Host: "localhost", State: "running"},
}, nil)
mockedClient.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil)
+1 -1
View File
@@ -1,7 +1,7 @@
package web
import (
"github.com/goccy/go-json"
"encoding/json"
"net/http"
"time"
+9 -9
View File
@@ -23,13 +23,13 @@ type MockedClient struct {
docker.Client
}
func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
args := m.Called(id)
func (m *MockedClient) FindContainer(ctx context.Context, id string) (docker.Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(docker.Container), args.Error(1)
}
func (m *MockedClient) ContainerActions(action docker.ContainerAction, containerID string) error {
args := m.Called(action, containerID)
func (m *MockedClient) ContainerActions(ctx context.Context, action docker.ContainerAction, containerID string) error {
args := m.Called(ctx, action, containerID)
return args.Error(0)
}
@@ -38,8 +38,8 @@ func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- docker
return args.Error(0)
}
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
args := m.Called()
func (m *MockedClient) ListContainers(ctx context.Context) ([]docker.Container, error) {
args := m.Called(ctx)
return args.Get(0).([]docker.Container), args.Error(1)
}
@@ -73,7 +73,7 @@ func (m *MockedClient) SystemInfo() system.Info {
func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux {
if client == nil {
client = new(MockedClient)
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
client.(*MockedClient).On("ListContainers", mock.Anything).Return([]docker.Container{}, nil)
client.(*MockedClient).On("Host").Return(docker.Host{
ID: "localhost",
})
@@ -86,8 +86,8 @@ func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux
content = afero.NewIOFS(fs)
}
manager := docker_support.NewRetriableClientManager(nil, tls.Certificate{}, docker_support.NewDockerClientService(client))
multiHostService := docker_support.NewMultiHostService(manager)
manager := docker_support.NewRetriableClientManager(nil, 3*time.Second, tls.Certificate{}, docker_support.NewDockerClientService(client))
multiHostService := docker_support.NewMultiHostService(manager, 3*time.Second)
return createRouter(&handler{
multiHostService: multiHostService,
content: content,
+15 -17
View File
@@ -4,16 +4,14 @@ toolbar:
search: 搜索
show: 显示 {std}
show-all: 显示全部
stop: 停止
start: 启动
restart: 重启
label:
containers: 容器
container: 无容器 | 1 容器 | {count} 容器
running-containers: 运行中的容器
all-containers: 所有容器
total-containers: 所有容器
running: 运行中
total-cpu-usage: 所有CPU使用率
total-mem-usage: 所有内存使用率
dozzle-version: Dozzle版本
all: 全部
host: 主机
hosts: 主机
@@ -72,15 +70,13 @@ placeholder:
search: 搜索
settings:
display: 显示
locale: 覆盖语言
locale: 显示语言
small-scrollbars: 使用较小的滚动条
show-timesamps: 显示时间戳
soft-wrap: 断行
12-24-format: >-
默认情况下,Dozzle将使用您的浏览器区域设置来格式化时间。系统支持12或24小时制。
datetime-format: 自定义日期和时间格式
font-size: 字体大小
color-scheme: 颜色方案
datetime-format: 覆盖日期和时间格式
options: 选项
show-stopped-containers: 显示已停止的容器
about: 关于
@@ -91,6 +87,15 @@ settings:
show-std: 显示stdout和stderr标签
automatic-redirect: 自动重定向到同名的新容器
compact: 紧凑模式
log:
preview: 这是默认日志样式
warning: 警告日志看起来像这样
complex: 这是一个复杂的JSON格式日志
simple: 这是一个非常非常长的消息,默认情况下会自动换行。禁用软换行将禁用此功能。Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
multi-line-error:
start-line: 这是一个多行错误消息
middle-line: 带有第二行
end-line: 最后是第三行
releases:
features: 一个新功能 | {count} 个新功能
bugFixes: 一个错误修复 | {count} 个修复
@@ -100,15 +105,8 @@ releases:
latest: 最新
no_releases: 您拥有最新版本
log_actions:
copy_log: 复制日志
warning: 警告日志看起来像这样
complex: 这是一个复杂的JSON格式日志
simple: 这是一个非常非常长的消息,默认情况下会自动换行。禁用软换行将禁用此功能。Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
copy_log: 复制日志
jump_to_context: 跳转到上下文
multi-line-error:
start-line: 这是一个多行错误消息
middle-line: 带有第二行
end-line: 最后是第三行
toasts:
copied:
title: 已复制
+5 -3
View File
@@ -103,7 +103,9 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("Could not read certificates")
}
if err := healthcheck.RPCRequest(agentAddress, certs); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), args.Timeout)
defer cancel()
if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil {
log.Fatal().Err(err).Msg("Failed to make request")
}
}
@@ -155,8 +157,8 @@ func main() {
if err != nil {
log.Fatal().Err(err).Msg("Could not read certificates")
}
manager := docker_support.NewSwarmClientManager(localClient, certs)
multiHostService = docker_support.NewMultiHostService(manager)
manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout)
multiHostService = docker_support.NewMultiHostService(manager, args.Timeout)
log.Info().Msg("Starting in swarm mode")
listener, err := net.Listen("tcp", ":7007")
if err != nil {
+27 -26
View File
@@ -1,12 +1,12 @@
{
"name": "dozzle",
"version": "8.4.2",
"version": "8.5.2",
"description": "Realtime log viewer for docker containers.",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
},
"packageManager": "pnpm@9.9.0",
"packageManager": "pnpm@9.11.0",
"type": "module",
"repository": {
"type": "git",
@@ -29,6 +29,7 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@duckdb/duckdb-wasm": "1.28.1-dev287.0",
"@iconify-json/carbon": "^1.2.1",
"@iconify-json/cil": "^1.2.0",
"@iconify-json/ic": "^1.2.0",
@@ -37,13 +38,13 @@
"@iconify-json/mdi-light": "^1.2.0",
"@iconify-json/octicon": "^1.2.0",
"@iconify-json/ph": "^1.2.0",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@intlify/unplugin-vue-i18n": "^5.2.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@vueuse/components": "^11.0.3",
"@vueuse/core": "^11.0.3",
"@vueuse/integrations": "^11.0.3",
"@vueuse/router": "^11.0.3",
"@vueuse/components": "^11.1.0",
"@vueuse/core": "^11.1.0",
"@vueuse/integrations": "^11.1.0",
"@vueuse/router": "^11.1.0",
"ansi-to-html": "^0.7.2",
"autoprefixer": "^10.4.20",
"d3-array": "^3.2.4",
@@ -53,32 +54,32 @@
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "^4.12.10",
"date-fns": "^3.6.0",
"date-fns": "^4.1.0",
"entities": "^5.0.0",
"fuse.js": "^7.0.0",
"lodash.debounce": "^4.0.8",
"pinia": "^2.2.2",
"postcss": "^8.4.45",
"postcss": "^8.4.47",
"sortablejs": "^1.15.3",
"splitpanes": "^3.1.5",
"strip-ansi": "^7.1.0",
"tailwindcss": "^3.4.10",
"unplugin-auto-import": "^0.18.2",
"tailwindcss": "^3.4.13",
"unplugin-auto-import": "^0.18.3",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"unplugin-vue-macros": "^2.11.9",
"unplugin-vue-router": "^0.10.7",
"vite": "5.4.3",
"unplugin-vue-macros": "^2.12.2",
"unplugin-vue-router": "^0.10.8",
"vite": "5.4.8",
"vite-plugin-compression2": "^1.3.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vitepress": "1.3.4",
"vue": "^3.5.3",
"vue-i18n": "^9.14.0",
"vue-router": "^4.4.3"
"vue": "^3.5.9",
"vue-i18n": "^10.0.3",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.47.0",
"@playwright/test": "^1.47.2",
"@types/d3-array": "^3.2.1",
"@types/d3-ease": "^3.0.2",
"@types/d3-scale": "^4.0.8",
@@ -86,22 +87,22 @@
"@types/d3-shape": "^3.1.6",
"@types/d3-transition": "^3.0.8",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.5.4",
"@vitejs/plugin-vue": "5.1.3",
"@vue/compiler-sfc": "^3.5.3",
"@types/node": "^22.7.2",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "^3.5.9",
"@vue/test-utils": "^2.4.6",
"bumpp": "^9.5.2",
"c8": "^10.1.2",
"concurrently": "^9.0.0",
"concurrently": "^9.0.1",
"eventsourcemock": "^2.0.0",
"jsdom": "^25.0.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"prettier-plugin-tailwindcss": "^0.6.8",
"simple-git-hooks": "^2.11.1",
"ts-node": "^10.9.2",
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"typescript": "^5.6.2",
"vitest": "^2.1.1",
"vue-component-type-helpers": "^2.1.6",
"vue-tsc": "^2.1.6"
},
+1527 -925
View File
File diff suppressed because it is too large Load Diff