Compare commits

...

60 Commits

Author SHA1 Message Date
Amir Raminfar d3814455cb chore: release v5.0.2 2023-09-27 16:42:53 -07:00
Amir Raminfar 67dbbfce97 fix: improves event stream error and only shows when closed 2023-09-27 16:33:56 -07:00
Amir Raminfar a9afe7a299 fix: fixes bad translation 2023-09-27 16:21:59 -07:00
Amir Raminfar 00cac97114 chore: updates node modules 2023-09-27 16:02:35 -07:00
Amir Raminfar 58edb4f602 feat: adds automatic redirect when a new container is found (#2396)
* feat: implements a toast for alerting errors and other useful information

* removes unused code

* feat: adds automatic redirect when a new container is found

* complete the flow with alerts and settings page

* adds more langs and option for once

* removes files
2023-09-27 16:00:44 -07:00
Amir Raminfar c3b5991dc7 feat: implements a toast for alerting errors and other useful information (#2395)
* feat: implements a toast for alerting errors and other useful information

* removes unused code
2023-09-27 13:29:33 -07:00
Amir Raminfar 9751ce3cf5 chore: code clean up 2023-09-25 11:40:56 -07:00
Amir Raminfar 40d02d993b chore: code clean up 2023-09-25 11:37:07 -07:00
Amir Raminfar 889ebf3acb chore: release v5.0.1 2023-09-25 09:56:14 -07:00
Amir Raminfar a051245805 fix: UI improvements to colors 2023-09-25 09:54:16 -07:00
Amir Raminfar 66738313c8 fix: improves styles for redirect alert 2023-09-25 09:44:48 -07:00
renovate[bot] e8aa6adbda chore(deps): update docker/login-action action to v3 (#2392) 2023-09-24 18:53:33 -07:00
renovate[bot] 45ec71a6a1 chore(deps): update docker/build-push-action action to v5 (#2391) 2023-09-24 18:52:51 -07:00
Amir Raminfar 072a6c09bb feat: finds same name containers and shows option redirect to new containers (#2390) 2023-09-24 15:22:27 -07:00
Amir Raminfar c233549b4b chore: removes unused file 2023-09-23 14:07:52 -07:00
Amir Raminfar 4a30a2ba5e chore: release v5.0.0 2023-09-23 13:34:24 -07:00
Amir Raminfar 538a1e5dcd fix: fixes padding for settings page 2023-09-23 13:29:47 -07:00
Amir Raminfar 46ba01e3a8 chore: updates all other modules 2023-09-23 13:27:16 -07:00
Amir Raminfar 9074895653 chore: fixes tests 2023-09-23 13:22:52 -07:00
Amir Raminfar 9e30045e6f fix: Improves UI with padding and color improvements 2023-09-23 13:16:40 -07:00
Amir Raminfar ad461c9dbc chore: fixes tests 2023-09-23 13:06:01 -07:00
Amir Raminfar b150a6b772 fix: fixes no space between date and time 2023-09-23 12:14:49 -07:00
Amir Raminfar c123c62bf7 chore: release v5.0.0-beta.0 2023-09-22 13:54:00 -07:00
Amir Raminfar ccc42443c8 fix: fixes sorting on search dialog bug 2023-09-22 13:41:08 -07:00
Amir Raminfar 907360a334 chore: fixes tests 2023-09-22 13:17:33 -07:00
Amir Raminfar 3c4442a8dc feat: alt+enter in search opens a new pane 2023-09-22 13:15:22 -07:00
Amir Raminfar 9e98640731 chore: css clean up 2023-09-22 12:46:28 -07:00
Amir Raminfar fa47512771 fix: fixes broken links on settings page 2023-09-22 12:35:44 -07:00
Amir Raminfar e31e3138fb chore: creates a container context (#2388)
* chore: creates a container context

* fixes tests

* fixes tests
2023-09-22 12:24:05 -07:00
Amir Raminfar e2bb23b2ed fix: adds style for links in settings page 2023-09-22 11:37:14 -07:00
Amir Raminfar 60beb85bb1 fix: improves colors in light mode 2023-09-22 11:30:38 -07:00
Amir Raminfar b437850063 fix: adds custom scrollbars back 2023-09-22 11:03:34 -07:00
Amir Raminfar 9f3a256334 feat!: refactors UI using faster components and clean up visually (#2381)
* feat: moves to tailwindcss and better component library

* update styles

* creates toggle component

* adds drop down component

* cleans up components

* removes unused components

* uses tailwind for scroll view

* removes table component

* improves animation

* cleans up more styles

* uses more tailwind

* cleans up more styles with flex

* more styles

* removes bulma

* adds colors

* updates modules

* fixes bugs

* stops importing styles.scss

* more clean up

* cleans up headers

* cleans up title

* fixes title

* fixes mobile-hidden

* fixes shadow

* fixes colors

* add tailwindcss/nesting

* adds more colors

* fixes more colors

* updates colors

* fixes colors

* colors

* fixes menu on left

* menu and modal

* menu and modal

* fuzzy search

* fixes menu on left

* remove logs

* cleans up search

* adds host to search

* remove outline from inputs

* cleans up left search icon

* removes unused styles

* fixes docker

* removes sass!

* cleans up styles

* Fixe smobile menu

* fixes mobile menu

* fixes typecheck

* fixes seconday color

* adds drop down for container

* cleans header css

* updates css

* fixes other layouts

* updates some tests

* fixes border

* fixes home screen font

* fixes top header

* fixes tests

* fixes fieldlist

* fixes complex

* cleans up more

* removes index

* fixes tests

* fixes tests

* resolves conflicts
2023-09-22 10:59:29 -07:00
Amir Raminfar 2d30c8c529 chore: release v4.11.5 2023-09-22 10:24:19 -07:00
Amir Raminfar 8acd5794e8 fix: fixes crash when names are not found. see #2386 (#2387)
* fix: fixes crash when names are not found. see #2386

* adds snapshots
2023-09-22 10:24:03 -07:00
Amir Raminfar 2e01f3acb4 chore: adds keep label for issues 2023-09-20 14:23:46 -07:00
renovate[bot] 87951b0027 fix(deps): update all non-major dependencies (#2382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 09:28:07 -07:00
renovate[bot] 552a9cf78f chore(deps): update docker/bake-action action to v4 (#2383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 09:15:19 -07:00
Amir Raminfar 5b0f18dd28 fix(dep): rolling back dep causing breaking changes 2023-09-11 09:55:29 -07:00
renovate[bot] 0ac2ba4873 fix(deps): update dependency @intlify/unplugin-vue-i18n to v1 (#2379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 09:02:56 -07:00
renovate[bot] cfba470c0e chore(deps): update actions/checkout action to v4 (#2378) 2023-09-10 19:10:47 -07:00
renovate[bot] 9e1de4f226 chore(deps): update all non-major dependencies 2023-09-11 01:53:23 +00:00
Amir Raminfar 0812bfaca4 chore: removes unused component 2023-09-09 16:47:19 -07:00
Amir Raminfar 45d8991c5f chore: cleans up deps 2023-09-05 09:31:07 -07:00
renovate[bot] 0f46d7ffd7 fix(deps): update all non-major dependencies 2023-09-04 01:15:17 +00:00
Amir Raminfar fcf60daafa chore: clean up types 2023-08-28 11:11:44 -07:00
Amir Raminfar 617bf71cad fix: fixes flicker on scroll progress bar (#2371) 2023-08-28 16:06:31 +00:00
renovate[bot] 06c6a605fd chore(deps): update all non-major dependencies 2023-08-28 00:20:18 +00:00
Amir Raminfar d41508d4e7 chore: release v4.11.4 2023-08-25 15:18:35 -07:00
Amir Raminfar a77db667ab fix: quick fix to improve the slash with single host 2023-08-25 14:21:20 -07:00
Amir Raminfar 34ad45c64e feat: improves host labels and respects configurations for labels (#2369)
* feat: improves host labels and respects configurations for labels

* fixes tests
2023-08-25 13:42:00 -07:00
Amir Raminfar 512b181b48 fix: adds ellipses for titles that have swarm id (#2368) 2023-08-25 13:33:03 -07:00
Amir Raminfar be5e496380 chore: updates node modules 2023-08-25 11:00:27 -07:00
Amir Raminfar 887670ebfe chore: release v4.11.3 2023-08-25 10:47:56 -07:00
Amir Raminfar 9cdc8ad8f5 feat: adds host to container table on homepage (#2367)
* feat: adds host to container table on homepage

* more translations
2023-08-25 17:26:39 +00:00
renovate[bot] 0dd68b22ca fix(deps): update all non-major dependencies 2023-08-25 17:18:21 +00:00
Amir Raminfar 60ac81ff94 chore: code clean up on index 2023-08-25 10:14:37 -07:00
Amir Raminfar a79495cdfc chore: code clean up 2023-08-25 10:06:47 -07:00
Amir Raminfar 409363b26b chore: cleans up reflex 2023-08-25 10:00:41 -07:00
codeman1o1 2aaaf2ba1f Improve page title handling (#2365)
I tried my best with the translations.
Google Translate gives me the right English translation back for each language
2023-08-25 09:58:02 -07:00
98 changed files with 3215 additions and 3263 deletions
+1
View File
@@ -6,6 +6,7 @@ daysUntilClose: 3
exemptLabels:
- pinned
- security
- keep
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
+8 -8
View File
@@ -8,7 +8,7 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
name: Install Node
with:
@@ -33,7 +33,7 @@ jobs:
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -41,7 +41,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
name: Install Node
with:
@@ -58,7 +58,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
@@ -83,14 +83,14 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -105,7 +105,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node
+3 -3
View File
@@ -18,14 +18,14 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
+5 -5
View File
@@ -11,7 +11,7 @@ jobs:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
name: Install Node
with:
@@ -31,7 +31,7 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
name: Install Node
with:
@@ -56,7 +56,7 @@ jobs:
with:
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -64,7 +64,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
name: Install Node
with:
@@ -81,7 +81,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
+1
View File
@@ -1,3 +1,4 @@
module.exports = {
printWidth: 120,
plugins: ["prettier-plugin-tailwindcss"],
};
+1 -1
View File
@@ -1 +1 @@
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug --remote-host tcp://64.225.88.189:2376|clashleaders.com
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug
+2 -2
View File
@@ -14,7 +14,7 @@ COPY package.json ./
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* vite.config.ts ./
COPY .* *.config.ts *.config.js ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.21.0-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.21.1-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
+30 -1
View File
@@ -22,5 +22,34 @@ watchEffect(() => {
}
});
</script>
<style lang="postcss">
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
<style scoped lang="scss"></style>
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
}
</style>
+33 -4
View File
@@ -18,6 +18,7 @@ declare global {
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const automaticRedirect: typeof import('./composables/settings')['automaticRedirect']
const collapseNav: typeof import('./composables/settings')['collapseNav']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
@@ -25,13 +26,16 @@ declare global {
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const config: typeof import('./stores/config')['default']
const containerContext: typeof import('./composables/containerContext')['containerContext']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
@@ -60,12 +64,14 @@ declare global {
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composables/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isPinnedContainer: typeof import('./composables/storage')['isPinnedContainer']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./composables/settings')['lightTheme']
const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
@@ -98,6 +104,7 @@ declare global {
const persistentVisibleKeys: typeof import('./composables/storage')['persistentVisibleKeys']
const pinnedContainers: typeof import('./composables/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const provideContainerContext: typeof import('./composables/containerContext')['provideContainerContext']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -141,7 +148,6 @@ declare global {
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const togglePinnedContainer: typeof import('./composables/storage')['togglePinnedContainer']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -151,6 +157,7 @@ declare global {
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useAbs: typeof import('@vueuse/math')['useAbs']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
@@ -168,6 +175,7 @@ declare global {
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAverage: typeof import('@vueuse/math')['useAverage']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
@@ -175,10 +183,13 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerContext: typeof import('./composables/containerContext')['useContainerContext']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
@@ -213,6 +224,7 @@ declare global {
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFloor: typeof import('@vueuse/math')['useFloor']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
@@ -234,10 +246,13 @@ declare global {
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMath: typeof import('@vueuse/math')['useMath']
const useMax: typeof import('@vueuse/math')['useMax']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -257,15 +272,18 @@ declare global {
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePrecision: typeof import('@vueuse/math')['usePrecision']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
@@ -285,6 +303,7 @@ declare global {
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
@@ -302,8 +321,10 @@ declare global {
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('./composables/toast')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTrunc: typeof import('@vueuse/math')['useTrunc']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
@@ -358,6 +379,7 @@ declare module 'vue' {
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('./composables/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
@@ -365,6 +387,7 @@ declare module 'vue' {
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 containerContext: UnwrapRef<typeof import('./composables/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -437,6 +460,7 @@ declare module 'vue' {
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composables/containerContext')['provideContainerContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -480,7 +504,6 @@ declare module 'vue' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -518,6 +541,7 @@ declare module 'vue' {
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 useContainerContext: UnwrapRef<typeof import('./composables/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -641,6 +665,7 @@ declare module 'vue' {
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('./composables/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']>
@@ -691,6 +716,7 @@ declare module '@vue/runtime-core' {
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('./composables/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
@@ -698,6 +724,7 @@ declare module '@vue/runtime-core' {
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 containerContext: UnwrapRef<typeof import('./composables/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -770,6 +797,7 @@ declare module '@vue/runtime-core' {
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composables/containerContext')['provideContainerContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -813,7 +841,6 @@ declare module '@vue/runtime-core' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -851,6 +878,7 @@ declare module '@vue/runtime-core' {
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 useContainerContext: UnwrapRef<typeof import('./composables/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -974,6 +1002,7 @@ declare module '@vue/runtime-core' {
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('./composables/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']>
+13 -4
View File
@@ -10,13 +10,14 @@ declare module 'vue' {
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
'Cil:circle': typeof import('~icons/cil/circle')['default']
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:findInPage': typeof import('~icons/cil/find-in-page')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
@@ -27,11 +28,13 @@ declare module 'vue' {
DateTime: typeof import('./components/common/DateTime.vue')['default']
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
'Ic:sharpFindInPage': typeof import('~icons/ic/sharp-find-in-page')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
KeyShortcut: typeof import('./components/KeyShortcut.vue')['default']
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
@@ -41,18 +44,22 @@ declare module 'vue' {
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
'MdiLight:check': typeof import('~icons/mdi-light/check')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
Popup: typeof import('./components/Popup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@@ -67,6 +74,8 @@ declare module 'vue' {
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
+3 -14
View File
@@ -1,7 +1,7 @@
<template>
<div class="is-relative">
<div class="bar"></div>
<div class="is-overlay">
<div class="relative">
<div class="bar h-7 origin-left rounded-br rounded-tr bg-primary transition-transform"></div>
<div class="absolute inset-0 flex flex-col justify-center px-2 text-sm">
<slot></slot>
</div>
</div>
@@ -15,17 +15,6 @@ const minValue = computed(() => Math.min(value, 1));
<style scoped>
.bar {
height: 1.5em;
background-color: var(--primary-color);
transform-origin: left;
transform: scaleX(v-bind(minValue));
transition: transform 0.2s ease-out;
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
.is-overlay {
font-size: 0.9em;
padding: 0 0.5em;
}
</style>
+38 -23
View File
@@ -1,6 +1,6 @@
!
<template>
<table class="table is-fullwidth">
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
@@ -10,12 +10,10 @@
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a>
<span class="icon-text">
<span>{{ $t(value.label) }}</span>
<span class="icon">
<mdi:arrow-up />
</span>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
@@ -28,6 +26,7 @@
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
@@ -45,15 +44,18 @@
</tr>
</tbody>
</table>
<nav class="pagination is-right" role="navigation" aria-label="pagination" v-if="isPaginated">
<ul class="pagination-list">
<li v-for="i in totalPages">
<a class="pagination-link" :class="{ 'is-current': i === currentPage }" @click.prevent="currentPage = i">{{
i
}}</a>
</li>
</ul>
</nav>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<button
v-for="i in totalPages"
class="btn join-item"
:class="{ 'btn-primary': i === currentPage }"
@click="currentPage = i"
>
{{ i }}
</button>
</nav>
</div>
</template>
<script setup lang="ts">
@@ -66,6 +68,11 @@ const fields = {
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
mobileVisible: true,
},
host: {
label: "label.host",
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
mobileVisible: false,
},
state: {
label: "label.status",
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
@@ -131,19 +138,23 @@ function isVisible(field: keys) {
}
</script>
<style lang="scss" scoped>
.icon {
<style lang="postcss" scoped>
[data-icon] {
display: none;
transition: transform 0.2s ease-in-out;
[data-direction="desc"] & {
transform: rotate(180deg);
}
}
.selected-sort {
font-weight: bold;
border-color: var(--primary-color);
.icon {
display: inline-block;
th {
@apply border-b-2 border-base-lighter;
&.selected-sort {
font-weight: bold;
@apply border-primary;
[data-icon] {
display: inline-block;
}
}
}
@@ -153,4 +164,8 @@ tbody td {
text-overflow: ellipsis;
white-space: nowrap;
}
a {
@apply hover:text-primary;
}
</style>
-48
View File
@@ -1,48 +0,0 @@
<template>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<slot name="trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi:dots-vertical />
</span>
</button>
</slot>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.minimal .button {
background-color: rgba(0, 0, 0, 0);
border: none;
padding: 0.1em;
height: 100%;
& > .icon {
height: 100%;
& > svg {
align-self: flex-start;
height: 0.85em;
}
}
}
.is-top {
& .dropdown-menu {
top: 0;
}
&.is-last .dropdown-menu {
top: -30px;
}
}
</style>
+58 -82
View File
@@ -1,56 +1,64 @@
<template>
<div class="panel">
<o-autocomplete
ref="autocomplete"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="data"
@select="selected"
>
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="item.state">
<octicon:container-24 />
</span>
<div class="dropdown dropdown-open w-full">
<div class="input input-primary flex h-auto items-center">
<mdi:light-magnify class="flex h-8 w-8" />
<input
tabindex="0"
class="input input-ghost input-lg flex-1 px-1"
ref="input"
@keyup.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keyup.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keyup.enter.exact="selected(data[selectedIndex])"
@keyup.alt.enter="addColumn(data[selectedIndex])"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<mdi:keyboard-esc class="flex" />
</div>
<ul tabindex="0" class="menu dropdown-content rounded-box !relative mt-2 w-full bg-base-lighter p-2">
<li v-for="(item, index) in data">
<a
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
@click.prevent="selected(item)"
@mouseenter="selectedIndex = index"
:class="index === selectedIndex ? 'focus' : ''"
>
<div :class="{ 'text-primary': item.state === 'running' }">
<octicon:container-24 />
</div>
<div class="media-content">{{ item.host }} / {{ item.name }}</div>
<div class="media-right">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
<div class="truncate">
<span class="font-light">{{ item.host }}</span> / {{ item.name }}
</div>
</div>
</template>
</o-autocomplete>
<distance-time :date="item.created" class="ml-auto text-xs font-light" />
<a @click.stop.prevent="addColumn(item)" :title="$t('tooltip.pin-column')" class="hover:text-secondary">
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
</a>
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
const { maxResults: resultLimit = 20 } = defineProps<{
const { maxResults: resultLimit = 5 } = defineProps<{
maxResults?: number;
}>();
const close = defineEmit();
const query = ref("");
const autocomplete = ref<HTMLElement>();
const input = ref<HTMLInputElement>();
const selectedIndex = ref(0);
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const list = computed(() => {
return containers.value.map(({ id, created, name, state, host }) => {
return containers.value.map(({ id, created, name, state, hostLabel: host }) => {
return {
id,
created,
@@ -62,7 +70,11 @@ const list = computed(() => {
});
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
fuseOptions: {
keys: ["name", "host"],
includeScore: true,
useExtendedSearch: true,
},
resultLimit,
matchAllWhenSearchEmpty: true,
});
@@ -82,62 +94,26 @@ const data = computed(() => {
return 0;
}
})
.map(({ item }) => item);
.map(({ item }) => item)
.slice(0, resultLimit);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
watch(query, (data) => {
if (data.length > 0) {
selectedIndex.value = 0;
}
});
onMounted(() => input.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
close();
}
function addColumn(container: Container) {
function addColumn(container: { id: string }) {
store.appendActiveContainer(container);
close();
}
</script>
<style lang="scss" scoped>
.panel {
min-height: 400px;
width: 580px;
}
@media screen and (max-width: 768px) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
}
.running {
color: var(--primary-color);
}
.exited {
color: var(--scheme-main-ter);
}
.column-icon {
&:hover {
color: var(--secondary-color);
}
}
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;
}
&:hover .media-right {
visibility: visible;
}
}
.icon {
vertical-align: middle;
}
</style>
<style scoped lang="postcss"></style>
+4 -46
View File
@@ -1,16 +1,12 @@
<template>
<div ref="root" class="infinte-loader">
<div class="spinner" v-show="isLoading">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<div ref="root" class="flex min-h-[1px] justify-center">
<span class="loading loading-bars loading-md mt-4 text-primary" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
const { onLoadMore = () => {}, enabled } = defineProps<{
onLoadMore: () => void;
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
onLoadMore: () => Promise<void>;
enabled: boolean;
}>();
@@ -33,41 +29,3 @@ const observer = new IntersectionObserver(async (entries) => {
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>
<style scoped lang="scss">
.infinte-loader {
min-height: 1px;
}
.spinner {
margin: 10px auto 0;
width: 70px;
text-align: center;
& > div {
width: 12px;
height: 12px;
background-color: var(--primary-color);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 0.8s infinite ease-in-out both;
}
& .bounce1 {
animation-delay: -0.32s;
}
& .bounce2 {
animation-delay: -0.16s;
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>
-25
View File
@@ -1,25 +0,0 @@
<template>
<span class="icon-text">
<span class="icon mx-0" v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</span>
<span class="icon mx-0" v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" />
<ph:control-bold v-else />
</span>
<kbd class="is-uppercase is-family-sans-serif ml-1">{{ char }}</kbd>
</span>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
<style scoped lang="scss">
.icon {
width: unset;
}
</style>
+16 -29
View File
@@ -1,25 +1,24 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<div class="flex gap-x-2">
<div v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<div v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<div class="flex">
<log-level :level="logEntry.level"></log-level>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues">
<span class="has-text-grey">{{ name }}=</span
><span class="has-text-weight-bold" v-if="value === null">&lt;null&gt;</span>
<div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues" class="inline-block">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="has-text-weight-bold" v-html="markSearch(JSON.stringify(value))"> </span>
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
</template>
<span class="has-text-weight-bold" v-html="markSearch(value)" v-else></span>
<span class="font-bold" v-html="markSearch(value)" v-else></span>
</li>
<li class="has-text-grey" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
@@ -42,19 +41,15 @@ const validValues = computed(() => {
});
</script>
<style lang="scss" scoped>
<style lang="postcss" scoped>
.text-light {
@apply text-base-content/70;
}
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
@apply ml-2 inline-block text-secondary;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
@@ -64,13 +59,5 @@ const validValues = computed(() => {
content: "collapse json";
}
}
li {
display: inline-block;
& + li {
margin-left: 1em;
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="icon is-small health" :health="health" v-if="health" :title="health">
<div class="inline-flex h-4 w-4" :health="health" v-if="health" :title="health">
<cil:check-circle v-if="health == 'healthy'" />
<cil:x-circle v-else-if="health == 'unhealthy'" />
<cil:circle v-else />
@@ -14,14 +14,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.health {
&[health="unhealthy"] {
color: var(--red-color);
}
<style lang="postcss" scoped>
[health="unhealthy"] {
@apply text-red;
}
&[health="healthy"] {
color: var(--green-color);
}
[health="healthy"] {
@apply text-green;
}
</style>
@@ -1,17 +1,17 @@
<template>
<div>
<span class="has-text-weight-light"> RUNNING </span>
<span class="has-text-weight-semibold">
<span class="font-light capitalize"> RUNNING </span>
<span class="font-semibold">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</span>
</div>
<div>
<span class="has-text-weight-light"> LOAD </span>
<span class="has-text-weight-semibold"> {{ container.stat.cpu }}% </span>
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu }}% </span>
</div>
<div>
<span class="has-text-weight-light"> MEM </span>
<span class="has-text-weight-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
</template>
@@ -22,5 +22,3 @@ const { container } = defineProps<{
container: Container;
}>();
</script>
<style lang="scss" scoped></style>
+6 -40
View File
@@ -1,25 +1,12 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<stat-monitor
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(unref(container.stat).memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="unref(container.stat).cpu + '%'"
></stat-monitor>
<div class="flex gap-4" v-if="container.stat">
<stat-monitor :data="memoryData" label="mem" :stat-value="formatBytes(unref(container.stat).memoryUsage)" />
<stat-monitor :data="cpuData" label="load" :stat-value="unref(container.stat).cpu + '%'" />
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const { container } = useContainerContext();
const cpuData = computedWithControl(
() => container.value.stat,
@@ -27,8 +14,8 @@ const cpuData = computedWithControl(
const history = container.value.statHistory;
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.cpu,
value: stat.snapshot.cpu + "%",
y: Math.max(0, stat.snapshot.cpu),
value: Math.max(0, stat.snapshot.cpu) + "%",
}));
return points;
},
@@ -47,24 +34,3 @@ const memoryData = computedWithControl(
},
);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>
+29 -40
View File
@@ -1,48 +1,37 @@
<template>
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
<div class="column is-ellipsis">
<container-health :health="container.health" v-if="container.health"></container-health>
<div class="name">
<span>{{ container.name }}</span
><span v-if="container.isSwarm" class="swarm-id">{{ container.swarmId }}</span>
<div class="flex flex-1 items-center gap-2 truncate">
<container-health :health="container.health" v-if="container.health"></container-health>
<div class="inline-flex font-mono text-sm">
<div v-if="config.hosts.length > 1" class="mobile-hidden font-thin">
{{ container.hostLabel }}<span class="mx-2">/</span>
</div>
<div class="font-semibold">{{ container.name }}</div>
<div
class="mobile-hidden max-w-[1.5em] truncate transition-[max-width] hover:max-w-[400px]"
v-if="container.isSwarm"
>
{{ container.swarmId }}
</div>
<tag class="is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</tag>
<span class="icon is-clickable" @click="togglePinnedContainer(container.storageKey)">
<carbon:star-filled v-if="pinned" />
<carbon:star v-else />
</span>
</div>
<tag class="mobile-hidden font-mono" size="small">{{ container.image.replace(/@sha.*/, "") }}</tag>
<label class="swap swap-rotate">
<input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" />
</label>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const pinned = computed(() => pinnedContainers.value.has(container.value.storageKey));
</script>
<style lang="scss" scoped>
.icon {
vertical-align: middle;
}
.name {
display: inline-flex;
.swarm-id {
max-width: 0;
display: inline-block;
overflow: hidden;
white-space: nowrap;
transition: max-width 0.2s ease-in-out;
will-change: max-width;
}
&:hover {
.swarm-id {
max-width: 400px;
const { container } = useContainerContext();
const pinned = computed({
get: () => pinnedContainers.value.has(container.value.storageKey),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.value.storageKey);
} else {
pinnedContainers.value.delete(container.value.storageKey);
}
}
}
</style>
},
});
</script>
@@ -1,24 +1,73 @@
<template>
<span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div class="flex-1 font-sans text-[0.9rem]">
<span class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div
class="alert alert-info mt-8 w-auto text-[1rem] md:mx-auto md:w-1/2"
v-if="nextContainer && logEntry.event === 'container-stopped'"
>
<carbon:information class="h-6 w-6 shrink-0 stroke-current" />
<div>
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
</div>
<div>
<TimedButton v-if="automaticRedirect" class="btn-primary btn-sm" @finished="redirectNow()">Cancel</TimedButton>
<router-link
:to="{ name: 'container-id', params: { id: nextContainer.id } }"
class="btn btn-primary btn-sm"
v-else
>
Redirect
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
defineProps<{
const { logEntry } = defineProps<{
logEntry: DockerEventLogEntry;
}>();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const { container } = useContainerContext();
const nextContainer = computed(
() =>
containers.value
.filter(
(c) =>
c.host === container.value.host &&
c.created > logEntry.date &&
c.name === container.value.name &&
c.state === "running",
)
.toSorted((a, b) => +a.created - +b.created)[0],
);
function redirectNow() {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.value.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "container-id", params: { id: nextContainer.value.id } });
}
</script>
<style lang="scss" scoped>
span {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.text {
white-space: pre-wrap;
}
<style lang="postcss" scoped>
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
+14 -10
View File
@@ -1,8 +1,8 @@
<template>
<ul v-if="expanded" ref="root">
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields">
<template v-if="isObject(value)">
<span class="has-text-grey">{{ name }}=</span>
<span class="text-light">{{ name }}=</span>
<field-list
:fields="value"
:parent-key="parentKey.concat(name)"
@@ -11,16 +11,20 @@
></field-list>
</template>
<template v-else-if="Array.isArray(value)">
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span>[
<span class="has-text-weight-bold" v-for="(item, index) in value">
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold" v-html="value"></span>
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
@@ -68,8 +72,8 @@ function fieldIndex(field: string) {
}
</script>
<style lang="scss" scoped>
ul {
margin-left: 2em;
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>
+67 -115
View File
@@ -1,137 +1,89 @@
<template>
<dropdown-menu class="is-right">
<template #trigger>
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr" />
<carbon:circle-solid class="is-blue is-small" v-if="streamConfig.stdout" />
</span>
</button>
</template>
<a class="dropdown-item" @click="clear()">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:trash-24 />
</div>
</div>
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.clear") }}</div>
<div class="level-item"><key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut></div>
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download/${container.host}/${container.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:download-24 />
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.download") }}</div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a class="dropdown-item" @click="showSearch = true">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi:light-magnify />
</div>
</div>
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.search") }}</div>
<div class="level-item"><key-shortcut char="f"></key-shortcut></div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box z-50 w-52 bg-base p-1 shadow">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut>
</a>
</li>
<li>
<a :href="`${base}/api/logs/download/${container.host}/${container.id}`" download>
<octicon:download-24 /> {{ $t("toolbar.download") }}
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:light-magnify /> {{ $t("toolbar.search") }}
<key-shortcut char="f"></key-shortcut>
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex h-4 w-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="is-red is-small" />
<carbon:circle-solid class="is-blue is-small" />
<carbon:circle-solid class="w-2 text-red" />
<carbon:circle-solid class="w-2 text-blue" />
</template>
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show-all") }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-blue is-small" v-if="!streamConfig.stderr && streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex h-4 w-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr && !streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex h-4 w-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
</div>
<div class="level-right">
<div class="level-item">
{{ $t("toolbar.show", { std: "STDERR" }) }}
</div>
</div>
</div>
</a>
</dropdown-menu>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type ComputedRef } from "vue";
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const clear = defineEmit();
const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { container, streamConfig } = useContainerContext();
</script>
<style lang="scss" scoped>
.level-left .level-item {
width: 2.2em;
align-items: center;
margin-right: 0.5em;
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
.is-small {
width: 0.6em;
a {
@apply whitespace-nowrap;
}
</style>
+8 -33
View File
@@ -1,20 +1,14 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-mobile is-vcentered is-marginless has-boxshadow">
<div class="column is-clipped is-paddingless">
<container-title @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat />
</div>
<div class="mx-2 flex items-center gap-2">
<container-title @close="$emit('close')" />
<container-stat class="ml-auto" />
<div class="mr-2 column is-narrow is-paddingless is-hidden-mobile">
<log-actions-toolbar @clear="onClearClicked()" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="close()"></button>
</div>
<log-actions-toolbar @clear="onClearClicked()" class="mobile-hidden" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default="{ setLoading }">
@@ -41,12 +35,8 @@ const {
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
const config = reactive({ stdout: true, stderr: true });
provide("container", container);
provide("stream-config", config);
provideContainerContext(container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
@@ -61,18 +51,3 @@ onKeyStroke("k", (e) => {
}
});
</script>
<style lang="scss" scoped>
button.delete {
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
}
&:hover {
opacity: 1;
}
}
</style>
+3 -12
View File
@@ -1,19 +1,10 @@
<template>
<date-time :date="date" class="date"></date-time>
<tag size="small">
<date-time :date="date" class="whitespace-nowrap text-blue"></date-time>
</tag>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
<style lang="scss" scoped>
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
white-space: nowrap;
background-color: var(--scheme-main-ter);
color: #258ccd;
}
</style>
@@ -4,15 +4,16 @@ import { createTestingPinia } from "@pinia/testing";
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../../composables/settings";
import { settings } from "@/composables/settings";
import { useSearchFilter } from "@/composables/search";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { computed, nextTick } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { containerContext } from "@/composables/containerContext";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: ["localhost"] },
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
}));
/**
@@ -71,8 +72,10 @@ describe("<LogEventSource />", () => {
LogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
"stream-config": reactive({ stdout: true, stderr: true }),
[containerContext as symbol]: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
streamConfig: reactive({ stdout: true, stderr: true }),
},
scrollingPaused: computed(() => false),
},
},
@@ -4,14 +4,10 @@
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const loadingMore = defineEmit<[value: boolean]>();
const container = inject("container") as ComputedRef<Container>;
const config = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { messages, loadOlderLogs } = useLogStream(container, config);
const { container, streamConfig } = useContainerContext();
const { messages, loadOlderLogs } = useLogStream(container, streamConfig);
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
+35 -44
View File
@@ -1,5 +1,5 @@
<template>
<div :class="level" :data-position="position"></div>
<div :data-level="level" :data-position="position" class="mt-1.5 h-2.5 w-2.5 flex-none rounded-lg"></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
@@ -10,54 +10,45 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
div {
display: inline-block;
width: 0.7em;
height: 0.7em;
border-radius: 0.5em;
align-self: auto;
margin-top: 0.4em;
<style lang="postcss" scoped>
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.4em;
margin-top: auto;
align-self: flex-end;
}
&[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.2em;
margin-top: auto;
align-self: flex-end;
}
[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.4em 0;
align-self: stretch;
}
&[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.2em 0;
align-self: auto;
}
[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.4em;
align-self: flex-start;
}
&[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.2em;
align-self: flex-start;
}
[data-level="debug"],
[data-level="trace"] {
@apply bg-purple;
}
&.debug,
&.trace {
background-color: var(--purple-color);
}
[data-level="info"] {
@apply bg-green;
}
&.info {
background-color: var(--green-color);
}
[data-level="error"],
[data-level="fatal"] {
@apply bg-red;
}
&.error,
&.fatal {
background-color: var(--red-color);
}
&.warn,
&.warning {
background-color: var(--orange-color);
}
[data-level="warn"],
[data-level="warning"] {
@apply bg-orange;
}
</style>
+6 -8
View File
@@ -12,14 +12,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.tag {
&[std="stdout"] {
color: var(--blue-color);
}
<style lang="postcss" scoped>
[std="stdout"] {
@apply text-blue;
}
&[std="stderr"] {
color: var(--red-color);
}
[std="stderr"] {
@apply text-red;
}
</style>
+26 -47
View File
@@ -1,43 +1,38 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
v-for="item in filtered"
:key="item.id"
:data-key="item.id"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil:find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
<a
class="jump-context tooltip-primary tooltip tooltip-right"
v-if="isSearching()"
data-tip="Jump to Context"
@click="handleJumpLineSelected($event, item)"
:href="`#${item.id}`"
>
<ic:sharp-find-in-page />
</a>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value" />
</li>
</ul>
</template>
<script lang="ts" setup>
import { type ComputedRef, toRaw } from "vue";
import { toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { Container } from "@/models/Container";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
}>();
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { container } = useContainerContext();
let visibleKeys = persistentVisibleKeys(container);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
@@ -46,7 +41,6 @@ const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry<string | JSONObject> | undefined = $ref(undefined);
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
@@ -65,9 +59,8 @@ watch(
{ immediate: true, flush: "post" },
);
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
.events {
padding: 1em 0;
font-family:
ui-monospace,
SFMono-Regular,
@@ -78,41 +71,27 @@ watch(
Menlo,
monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
> li {
@apply flex break-words px-4 py-1 last:snap-end odd:bg-gray-400/[0.07];
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&.selected {
border: 1px var(--secondary-color) solid;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
.jump-context {
@apply mr-2 flex items-center font-sans text-secondary hover:text-secondary-focus;
}
}
&.small {
font-size: 60%;
@apply text-[0.7em];
}
&.medium {
font-size: 80%;
@apply text-[0.8em];
}
&.large {
font-size: 120%;
@apply text-lg;
}
}
</style>
+5 -23
View File
@@ -1,15 +1,9 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
<div class="flex items-start gap-x-2">
<log-std :std="logEntry.std" v-if="showStd" />
<log-date :date="logEntry.date" v-if="showTimestamp" />
<log-level class="flex" :level="logEntry.level" :position="logEntry.position" />
<div class="whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap" v-html="colorize(logEntry.message)"></div>
</div>
</template>
<script lang="ts" setup>
@@ -24,15 +18,3 @@ defineProps<{
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
</script>
<style lang="scss" scoped>
.disable-wrap {
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
}
</style>
@@ -1,8 +1,10 @@
<template>
<div class="is-flex-grow-1 has-text-centered my-4">
<div class="is-relative">
<zig-zag class="is-overlay mt-2"></zig-zag>
<span class="text is-relative py-2 px-4">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
<div class="my-4 flex-1 text-center">
<div class="relative">
<zig-zag class="absolute inset-0 mt-2"></zig-zag>
<span class="relative whitespace-pre-wrap bg-base px-4 py-2 font-bold">{{
$t("error.logs-skipped", { total: logEntry.totalSkipped })
}}</span>
</div>
</div>
</template>
@@ -13,11 +15,3 @@ defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
<style lang="scss" scoped>
.text {
font-weight: bold;
white-space: pre-wrap;
background-color: var(--body-background-color);
}
</style>
+6 -33
View File
@@ -1,13 +1,13 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow is-hidden-mobile">
<div class="relative hover:text-secondary" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="mobile-hidden flex overflow-hidden rounded-sm border border-primary px-px pb-px pt-1">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light">{{ label }}</span>
<span class="has-text-weight-bold">
<div class="inline-flex gap-1 rounded bg-base p-px text-xs lg:absolute lg:-left-0.5 lg:-top-2">
<div class="font-light uppercase">{{ label }}</div>
<div class="select-none font-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
</div>
</div>
</div>
</template>
@@ -23,30 +23,3 @@ function onSelectedPoint(point: Point<unknown>) {
let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.host:hover span {
color: var(--secondary-color);
}
@media screen and (min-width: 768px) {
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
}
</style>
+3 -19
View File
@@ -1,7 +1,7 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove">
<path :d="path" class="area" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
<svg :width="width" :height="height" @mousemove="onMove" class="group">
<path :d="path" class="fill-primary" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="invisible stroke-secondary stroke-2 group-hover:visible" />
</svg>
</template>
@@ -42,19 +42,3 @@ function onMove(e: MouseEvent) {
selectedPoint(point);
}
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
:deep(.line) {
stroke: var(--secondary-color);
stroke-width: 2;
display: none;
}
svg:hover :deep(.line) {
display: unset;
}
</style>
+2 -10
View File
@@ -2,18 +2,10 @@
<svg width="100%" height="8" class="zigzag">
<defs>
<pattern id="zigzag" x="0" y="0" width="30" height="8" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="15" y2="8" class="line" />
<line x1="15" y1="8" x2="30" y2="0" class="line" />
<line x1="0" y1="0" x2="15" y2="8" class="stroke-primary stroke-1" />
<line x1="15" y1="8" x2="30" y2="0" class="stroke-primary stroke-1" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.line {
stroke: var(--primary-color);
stroke-width: 1;
stroke-linecap: round;
}
</style>
@@ -1,237 +1,105 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\"><a data-v-2e92daca=\\"\\" class=\\"jump-context tooltip-primary tooltip tooltip-right\\" data-tip=\\"Jump to Context\\" href=\\"#2\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\">
<path fill=\\"currentColor\\" d=\\"M20 19.59V8l-6-6H4v20l15.57-.02l-4.81-4.81c-.8.52-1.74.83-2.76.83c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5c0 1.02-.31 1.96-.83 2.75L20 19.59zM9 13c0 1.66 1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3z\\"></path>
</svg></a>
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
</div>
</div>
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
"<div class=\\"flex min-h-[1px] justify-center\\"><span class=\\"loading loading-bars loading-md mt-4 text-primary\\" style=\\"display: none;\\"></span></div>
<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
+27 -20
View File
@@ -1,12 +1,16 @@
<template>
<div @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" ref="trigger"><slot></slot></div>
<Teleport to="body">
<Transition name="fade">
<div v-show="show && (delayedShow || glopbalShow)" class="content" ref="content">
<slot></slot>
<teleport to="body">
<transition name="fade">
<div
v-show="show && (delayedShow || glopbalShow)"
class="fixed z-50 rounded border border-secondary/50 bg-base-lighter p-4 shadow"
ref="content"
>
<slot name="content"></slot>
</div>
</Transition>
</Teleport>
</transition>
</teleport>
</template>
<script lang="ts" setup>
@@ -17,9 +21,8 @@ let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content: HTMLElement | null = $ref(null);
let trigger: HTMLElement | null = $ref(null);
function onMouseEnter(e: MouseEvent) {
const onMouseEnter = (e: Event) => {
show.value = true;
glopbalShow.value = true;
if (e.target && content && e.target instanceof Element) {
@@ -30,22 +33,26 @@ function onMouseEnter(e: MouseEvent) {
content.style.left = `${x}px`;
content.style.top = `${y}px`;
}
}
};
function onMouseLeave() {
const onMouseLeave = () => {
show.value = false;
glopbalShow.value = false;
}
};
const el = useCurrentElement();
useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter);
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
</script>
<style scoped>
.content {
position: fixed;
z-index: 9999;
background: var(--scheme-main-ter);
border-radius: 0.5em;
padding: 1em;
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
+28 -36
View File
@@ -1,18 +1,18 @@
<template>
<transition name="fade">
<div class="scroll-progress" ref="root" v-show="!autoHide || show">
<transition name="fadeout">
<div class="pointer-events-none relative inline-block" ref="root" v-show="!autoHide || show">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" />
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="column is-narrow is-paddingless is-size-2">&#8734;</div>
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<span class="column is-narrow is-paddingless is-size-2">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
</span>
<span class="column is-narrow is-paddingless"> % </span>
<span> % </span>
</template>
</div>
</div>
@@ -54,36 +54,28 @@ watchPostEffect(() => {
}
});
</script>
<style scoped lang="scss">
.scroll-progress {
display: inline-block;
position: relative;
pointer-events: none;
<style scoped lang="postcss">
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
circle {
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill: var(--scheme-main-ter);
fill-opacity: 0.8;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: var(--primary-color);
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill-opacity: 0.75;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
}
}
@keyframes svg-animation {
@@ -113,11 +105,11 @@ watchPostEffect(() => {
}
}
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
.fadeout-leave-active {
@apply transition-opacity;
}
.fade-leave-to {
.fadeout-leave-to {
opacity: 0;
}
</style>
+22 -105
View File
@@ -1,24 +1,27 @@
<template>
<section :class="{ 'is-full-height-scrollable': scrollable }">
<header v-if="$slots.header">
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
<header
v-if="$slots.header"
class="sticky top-[70px] z-[2] border-b border-base-content/10 bg-base py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
>
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined">
<div class="is-scrollbar-progress is-hidden-mobile">
<scroll-progress v-if="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<div class="invisible mr-28 text-right md:visible" v-show="paused">
<scroll-progress :indeterminate="loading" :auto-hide="!loading" class="z-2 !fixed top-16" />
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
</div>
<div ref="scrollObserver" class="is-scroll-observer"></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="is-scrollbar-notification">
<div class="mr-16 text-right">
<transition name="fade">
<button
class="button has-boxshadow"
:class="hasMore ? 'has-more' : ''"
class="fixed bottom-8 rounded bg-primary p-3 text-primary-content shadow transition-colors hover:bg-primary-focus"
:class="hasMore ? 'animate-bounce-fast bg-secondary text-secondary-content hover:bg-secondary-focus' : ''"
@click="scrollToBottom()"
v-show="paused"
>
@@ -71,106 +74,20 @@ function setLoading(value: boolean) {
loading = value;
}
</script>
<style scoped lang="scss">
section {
display: flex;
flex-direction: column;
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
header {
position: sticky;
top: 0;
background: var(--body-background-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 1;
@media screen and (max-width: 768px) {
top: 70px;
}
}
&.is-full-height-scrollable {
height: 100vh;
min-height: 0;
}
main {
flex: 1;
overflow: auto;
scroll-snap-type: y proximity;
}
.is-scrollbar-progress {
text-align: right;
margin-right: 110px;
.scroll-progress {
position: fixed;
top: 60px;
z-index: 2;
}
}
.is-scroll-observer {
height: 1px;
}
.is-scrollbar-notification {
text-align: right;
margin-right: 65px;
button {
position: fixed;
bottom: 30px;
background-color: var(--primary-color);
transition: background-color 0.24s ease-out;
border: none !important;
color: #eee;
&.has-more {
background-color: var(--secondary-color);
animation-name: bounce;
animation-duration: 1000ms;
animation-fill-mode: both;
color: #222;
}
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-out !important;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
<style>
@media screen and (max-width: 768px) {
.splitpanes__pane {
overflow: unset !important;
}
.splitpanes__pane {
overflow: unset !important;
}
</style>
+17 -61
View File
@@ -1,23 +1,22 @@
<template>
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="search">
<div class="column">
<p class="control has-icons-left">
<input
class="input"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
<span class="icon is-left">
<mdi:light-magnify />
</span>
</p>
</div>
<div class="column is-1 has-text-centered">
<button class="delete is-medium" @click="resetSearch()"></button>
<div
class="fixed -right-px -top-px z-10 flex w-96 items-center gap-4 rounded-bl border border-secondary/20 bg-base-darker p-4 shadow"
v-show="showSearch"
v-if="search"
>
<div class="input input-primary flex h-auto items-center">
<mdi:light-magnify />
<input
class="input flex-1"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
</div>
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
</div>
</template>
@@ -35,46 +34,3 @@ onKeyStroke("f", (e) => {
onUnmounted(() => resetSearch());
</script>
<style lang="scss" scoped>
.search {
width: 350px;
position: fixed;
padding: 10px;
background: var(--scheme-main-ter);
top: 0;
right: 0;
border-radius: 0 0 0 5px;
z-index: 10;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
button.delete {
margin-left: 1em;
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
}
&:hover {
opacity: 1;
}
}
.icon {
padding: 10px 3px;
}
.input {
color: var(--body-color);
&::placeholder {
color: var(--border-color);
}
}
}
</style>
+53 -57
View File
@@ -1,50 +1,51 @@
<template>
<div v-if="ready">
<nav class="breadcrumb menu-label" aria-label="breadcrumbs">
<ul v-if="sessionHost">
<li>
<a href="#" @click.prevent="setHost(null)">{{ hosts[sessionHost].name }}</a>
<div v-if="ready" data-testid="side-menu">
<div class="breadcrumbs">
<ul>
<li><a @click.prevent="setHost(null)" class="link-primary">Hosts</a></li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
{{ hosts[sessionHost].name }}
</li>
</ul>
<ul v-else>
<li>Hosts</li>
</ul>
</nav>
</div>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu-list" v-if="!sessionHost">
<ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in config.hosts">
<a @click.prevent="setHost(host.id)">{{ host.name }}</a>
<a @click.prevent="setHost(host.id)">
<ph:computer-tower />
{{ host.name }}
</a>
</li>
</ul>
<transition-group tag="ul" name="list" class="menu-list" v-else>
<li v-for="item in menuItems" :key="item.id" :class="item.state" :data-label="item.id">
<div class="menu-label mt-4 mb-3" v-if="isLabel(item)">
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li
v-for="item in menuItems"
:key="item.id"
:class="isLabel(item) ? 'menu-title' : item.state"
:data-testid="item.id"
>
<template v-if="isLabel(item)">
{{ item.label }}
</div>
</template>
<popup v-else>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
active-class="active-primary"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
<span>{{ item.name }}</span
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<div class="is-flex-shrink-1 is-flex icons">
<div
class="icon is-small pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</div>
<container-health :health="item.health"></container-health>
</div>
<div class="truncate">
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<span
class="pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
<container-health :health="item.health"></container-health>
</router-link>
<template #content>
<container-popup :container="item"></container-popup>
@@ -54,9 +55,10 @@
</transition-group>
</transition>
</div>
<ul class="menu-list is-hidden-mobile has-light-opacity" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
<div role="status" class="flex animate-pulse flex-col gap-4" v-else>
<div class="h-3 w-full rounded-full bg-base-content/50 opacity-50" v-for="_ in 9"></div>
<span class="sr-only">Loading...</span>
</div>
</template>
<script lang="ts" setup>
@@ -104,7 +106,7 @@ const groupedContainers = computed(() =>
type MenuLabel = { label: string; id: string; state: string };
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel;
const allLabel = { label: t("label.containers"), id: "all", state: "label" } as MenuLabel;
const allLabel = { label: t("label.containers"), id: "containers", state: "label" } as MenuLabel;
function isLabel(item: Container | MenuLabel): item is MenuLabel {
return (item as MenuLabel).label !== undefined;
@@ -138,35 +140,29 @@ const activeContainersById = computed(() =>
),
);
</script>
<style scoped lang="scss">
.has-light-opacity {
opacity: 0.5;
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
li.exited a,
li.dead a {
color: #777;
}
.icons {
column-gap: 0.35em;
align-items: baseline;
}
a {
.containers a {
@apply auto-cols-[auto_max-content];
.pin {
display: none;
&:hover {
color: var(--secondary-color);
@apply text-secondary;
}
}
&:hover {
.pin {
display: block;
display: inline-block;
}
}
}
li.exited {
@apply opacity-50;
}
.slide-left-enter-active,
.slide-left-leave-active,
@@ -177,22 +173,22 @@ a {
.slide-left-enter-from {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.list-move,
+34 -61
View File
@@ -1,48 +1,40 @@
<template>
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<aside class="fixed h-screen w-[inherit] overflow-auto p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
</div>
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
</div>
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link
:to="{ name: 'settings' }"
active-class="is-active"
class="button is-rounded is-small"
:aria-label="$t('title.settings')"
>
<span class="icon">
<mdi:light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
</a>
</div>
<small class="mb-4 block text-xs font-light" v-if="hostname">
{{ hostname }}
</small>
</h1>
<div class="flex gap-4">
<router-link
:to="{ name: 'settings' }"
:aria-label="$t('title.settings')"
data-testid="settings"
class="btn btn-circle btn-sm"
>
<mdi:light-cog />
</router-link>
<a :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured" class="btn btn-circle btn-sm">
<mdi:light-logout />
</a>
</div>
<a
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
@click="$emit('search')"
:title="$t('tooltip.search')"
>
<mdi:light-magnify />
Search
<key-shortcut char="k"></key-shortcut>
</a>
<side-menu class="mt-4"></side-menu>
</aside>
</template>
@@ -50,22 +42,3 @@
<script lang="ts" setup>
const { base, secured, hostname } = config;
</script>
<style scoped lang="scss">
aside {
padding: 1em;
height: 100vh;
overflow: auto;
position: fixed;
width: inherit;
.is-hidden-mobile.is-active {
display: block !important;
}
}
.logo {
width: 122px;
height: 54px;
fill: var(--logo-color);
}
</style>
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div>
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
<div class="inline-flex gap-2">
<time :datetime="date.toISOString()" class="mobile-hidden">{{ dateStr }}</time>
<time :datetime="date.toISOString()">{{ timeStr }}</time>
</div>
</template>
+44
View File
@@ -0,0 +1,44 @@
<template>
<details class="dropdown" ref="details" v-on-click-outside="close">
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
<slot name="trigger"> {{ values[modelValue] ?? defaultLabel }} <carbon:caret-down /></slot>
</summary>
<ul class="menu dropdown-content rounded-box z-50 w-52 bg-base p-2 shadow">
<slot>
<li v-for="item in options">
<a @click="modelValue = item.value">
<mdi-light:check class="w-4" v-if="modelValue == item.value" />
<div v-else class="w-4"></div>
{{ item.label }}
</a>
</li>
</slot>
</ul>
</details>
</template>
<script lang="ts" setup>
import { vOnClickOutside } from "@vueuse/components";
type DropdownItem = {
label: string;
value: string;
};
const { options = [], defaultLabel = "" } = defineProps<{ options?: DropdownItem[]; defaultLabel?: string }>();
const { modelValue } = defineModels<{
modelValue: string;
}>();
const values = computed(() =>
options.reduce(
(acc, curr) => {
acc[curr.value] = curr.label;
return acc;
},
{} as Record<string, string>,
),
);
const details = ref<HTMLElement | null>(null);
const close = () => details.value?.removeAttribute("open");
watch(modelValue, () => close());
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="inline-flex items-center">
<template v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</template>
<template v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" class="h-4 w-4" />
<ph:control-bold v-else class="h-4 w-4" />
</template>
<kbd class="uppercase">{{ char }}</kbd>
</div>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
+64 -127
View File
@@ -1,88 +1,58 @@
<template>
<aside>
<div class="columns is-marginless is-gapless is-mobile is-vcentered">
<div class="column is-narrow">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
</div>
<nav class="fixed top-0 z-10 w-full border-b border-base-content/20 bg-base p-2" data-testid="navigation">
<div class="flex items-center">
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<div class="column is-narrow push-right">
<a
role="button"
class="navbar-burger burger is-hidden-tablet is-pulled-right"
@click="showNav = !showNav"
:class="{ 'is-active': showNav }"
>
<span></span> <span></span> <span></span>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:light-magnify class="h-5 w-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
</div>
</div>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div v-if="config.hosts.length > 1">
<o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button" size="small">
<span>{{ sessionHost ? hosts[sessionHost].name : "" }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value.id" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value.name }}</span>
</o-dropdown-item>
</o-dropdown>
</div>
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi:light-cog />
</span>
<transition name="fade">
<div v-show="show">
<div class="mt-4 flex items-center justify-center gap-2">
<dropdown
v-model="sessionHost"
:options="hosts"
defaultLabel="Hosts"
class="btn-sm"
v-if="config.hosts.length > 1"
/>
<router-link :to="{ name: 'settings' }" class="btn btn-outline btn-sm">
<mdi:light-cog /> {{ $t("button.settings") }}
</router-link>
</div>
</div>
<div class="level-item has-text-centered" v-if="secured">
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
<a class="btn btn-outline btn-sm" :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured">
<mdi:light-logout /> {{ $t("button.logout") }}
</a>
</div>
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="is-ellipsis">
{{ item.name }}
</div>
</router-link>
</li>
</ul>
</aside>
<ul class="menu">
<li class="menu-title">{{ $t("label.containers") }}</li>
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary"
class="truncate"
:title="item.name"
>
{{ item.name }}
</router-link>
</li>
</ul>
</div>
</transition>
</nav>
</template>
<script lang="ts" setup>
@@ -92,10 +62,10 @@ const store = useContainerStore();
const route = useRoute();
const { visibleContainers } = storeToRefs(store);
let showNav = $ref(false);
const show = ref(false);
watch(route, () => {
showNav = false;
show.value = false;
});
const sortedContainers = computed(() =>
@@ -112,59 +82,26 @@ const sortedContainers = computed(() =>
}),
);
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
const hosts = computed(() => config.hosts.map(({ id, name }) => ({ value: id, label: name })));
</script>
<style scoped lang="scss">
aside {
padding: 1em;
position: fixed;
left: 0;
right: 0;
background: var(--scheme-main-ter);
z-index: 10;
max-height: 100vh;
overflow: auto;
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.level.is-hidden-mobile.is-active {
display: flex !important;
}
.fade-enter-active .menu,
.fade-leave-active .menu {
@apply transition-transform;
}
.menu-label {
margin-top: 1em;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
.title {
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
}
.burger {
color: var(--body-color);
}
.is-hidden-mobile.is-active {
display: block !important;
}
.navbar-burger {
height: 2.35rem;
}
.logo {
width: 82px;
height: 36px;
fill: var(--logo-color);
}
.column.push-right {
margin-left: auto;
}
.fade-enter-from .menu,
.fade-leave-to .menu {
@apply -translate-y-2;
}
</style>
+4 -8
View File
@@ -1,5 +1,5 @@
<template>
<div class="tag" :size="size">
<div class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" :size="size">
<slot></slot>
</div>
</template>
@@ -8,12 +8,8 @@
const { size = undefined } = defineProps<{ size?: "small" | undefined }>();
</script>
<style scoped lang="scss">
.tag {
background-color: var(--scheme-main-ter);
border: 1px solid var(--border-color);
&[size="small"] {
font-size: 0.61rem;
}
<style scoped lang="postcss">
[size="small"] {
@apply text-xs;
}
</style>
+39
View File
@@ -0,0 +1,39 @@
<template>
<button class="btn relative overflow-hidden" @click="cancel()">
<div class="absolute inset-0 origin-left bg-white/30" ref="progress"></div>
<div class="z-10">
<slot></slot>
</div>
</button>
</template>
<script lang="ts" setup>
const progress = ref<HTMLElement>();
const finished = defineEmit();
const cancelled = defineEmit();
let animation: Animation | undefined;
onMounted(async () => {
animation = progress.value?.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], {
duration: 4000,
easing: "linear",
fill: "forwards",
});
try {
await animation?.finished;
finished();
} catch (e) {
progress.value?.animate([{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], {
duration: 0,
fill: "forwards",
});
cancelled();
}
});
const cancel = () => {
animation?.cancel();
};
</script>
<style scoped lang="postcss"></style>
+19
View File
@@ -0,0 +1,19 @@
<template>
<label class="label inline-flex cursor-pointer gap-4 font-normal">
<input
type="checkbox"
class="toggle toggle-primary hover:bg-primary-focus"
:checked="modelValue"
@click="toggle()"
/>
<slot />
</label>
</template>
<script lang="ts" setup>
const { modelValue } = defineModels<{
modelValue: boolean;
}>();
const toggle = useToggle(modelValue);
</script>
+23
View File
@@ -0,0 +1,23 @@
import { Container } from "@/models/Container";
type ContainerContext = {
container: Ref<Container>;
streamConfig: { stdout: boolean; stderr: boolean };
};
export const containerContext = Symbol("containerContext") as InjectionKey<ContainerContext>;
export const provideContainerContext = (container: Ref<Container>) => {
provide(containerContext, {
container,
streamConfig: reactive({ stdout: true, stderr: true }),
});
};
export const useContainerContext = () => {
const context = inject(containerContext);
if (!context) {
throw new Error("No container context provided");
}
return context;
};
+2 -2
View File
@@ -1,4 +1,4 @@
import { type ComputedRef, type Ref } from "vue";
import { type Ref } from "vue";
import { encodeXML } from "entities";
import debounce from "lodash.debounce";
import {
@@ -26,7 +26,7 @@ type LogStreamConfig = {
stderr: boolean;
};
export function useLogStream(container: ComputedRef<Container>, streamConfig: LogStreamConfig) {
export function useLogStream(container: Ref<Container>, streamConfig: LogStreamConfig) {
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
+3
View File
@@ -13,6 +13,7 @@ export const DEFAULT_SETTINGS: {
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
automaticRedirect: boolean;
} = {
search: true,
size: "medium",
@@ -25,6 +26,7 @@ export const DEFAULT_SETTINGS: {
hourStyle: "auto",
softWrap: true,
collapseNav: false,
automaticRedirect: true,
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
@@ -42,4 +44,5 @@ export const {
menuWidth,
size,
search,
automaticRedirect,
} = toRefs(settings);
+1 -9
View File
@@ -7,17 +7,9 @@ if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
export function persistentVisibleKeys(container: Ref<Container>) {
return computed(() => useStorage(container.value.storageKey, []));
}
const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
export function togglePinnedContainer(id: string) {
if (pinnedContainers.value.has(id)) {
pinnedContainers.value.delete(id);
} else {
pinnedContainers.value.add(id);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
const { hostname } = config;
let subtitle = $ref("");
const title = $computed(() => `${subtitle} - Dozzle` + (hostname ? ` @ ${hostname}` : ""));
const title = $computed(() => (subtitle ? `${subtitle} - ` : "") + "Dozzle" + (hostname ? ` @ ${hostname}` : ""));
useTitle($$(title));
+45
View File
@@ -0,0 +1,45 @@
type Toast = {
id: string;
createdAt: Date;
title?: string;
message: string;
type: "success" | "error" | "warning" | "info";
};
type ToastOptions = {
expire?: number;
once?: boolean;
};
const toasts = ref<Toast[]>([]);
const showToast = (
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
) => {
if (once && toasts.value.some((t) => t.id === toast.id)) {
return;
}
toasts.value.push({
id: Date.now().toString(),
createdAt: new Date(),
...toast,
});
if (expire > 0) {
setTimeout(() => {
removeToast(toasts.value[0].id);
}, expire);
}
};
const removeToast = (id: Toast["id"]) => {
toasts.value = toasts.value.filter((toast) => toast.id !== id);
};
export const useToast = () => {
return {
toasts,
showToast,
removeToast,
};
};
+53 -65
View File
@@ -1,5 +1,5 @@
<template>
<main v-if="!authorizationNeeded">
<div v-if="!authorizationNeeded">
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
@@ -7,7 +7,7 @@
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<pane class="router-view min-h-screen">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
@@ -24,37 +24,64 @@
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapse"
class="button is-small is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
<label
class="btn btn-circle swap swap-rotate fixed bottom-8 left-4"
:class="{ '!-left-3': collapseNav }"
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi:light-chevron-right />
</span>
<span class="icon" v-else>
<mdi:light-chevron-left />
</span>
</button>
</main>
<input type="checkbox" v-model="collapseNav" />
<mdi:light-chevron-right class="swap-on text-secondary" />
<mdi:light-chevron-left class="swap-off" />
</label>
</div>
<dialog ref="modal" class="modal items-start bg-white/20 backdrop:backdrop-blur-sm" @close="open = false">
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
<FuzzySearchModal @close="open = false" v-if="open" />
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
v-for="toast in toasts"
:key="toast.id"
:class="{ 'alert-error': toast.type === 'error', 'alert-info': toast.type === 'info' }"
>
<carbon:information class="h-6 w-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
<carbon:warning class="h-6 w-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
<div>
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
{{ toast.message }}
</div>
<div>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
const { oruga } = useProgrammatic();
import { collapseNav } from "@/composables/settings";
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
const { activeContainers } = storeToRefs(containerStore);
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
const { toasts, removeToast } = useToast();
const modal = ref<HTMLDialogElement>();
const open = ref(false);
watch(open, () => {
if (open.value) {
modal.value?.showModal();
} else {
modal.value?.close();
}
});
onKeyStroke("k", (e) => {
@@ -65,17 +92,9 @@ onKeyStroke("k", (e) => {
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function collapse() {
collapseNav.value = !collapseNav.value;
open.value = true;
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
@@ -83,14 +102,9 @@ function onResized(e: any) {
}
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
@apply min-w-[3px] bg-base-lighter hover:bg-secondary;
}
@media screen and (max-width: 768px) {
@@ -98,30 +112,4 @@ function onResized(e: any) {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
color: var(--text-strong-color);
background: var(--scheme-main);
&:hover {
left: -25px;
}
}
}
</style>
+5 -7
View File
@@ -1,9 +1,7 @@
<template>
<main>
<router-view></router-view>
</main>
<div class="hero min-h-screen">
<div class="hero-content">
<router-view></router-view>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>
+121
View File
@@ -0,0 +1,121 @@
@import "splitpanes/dist/splitpanes.css";
@define-mixin light {
--base-lighter-color: 0 0% 100%;
--base-color: 0 0% 97%;
--base-darker-color: 0 0% 90%;
--base-content-color: 0 0% 21%;
--primary-color: 171 100% 41%;
--primary-focus-color: 171 100% 31%;
--secondary-color: 34 95% 53%;
--secondary-focus-color: 34 95% 43%;
}
@define-mixin dark {
--base-lighter-color: 0 0% 14%;
--base-color: 0 0% 7%;
--base-darker-color: 0 0% 4%;
--base-content-color: 0 0% 86%;
--primary-color: 171 100% 41%;
--primary-focus-color: 171 100% 31%;
--secondary-color: 44 100% 77%;
--secondary-focus-color: 44 100% 67%;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--p: var(--primary-color);
--pf: var(--primary-focus-color);
--pc: 0 0% 100%;
--s: var(--secondary-color);
--sf: var(--secondary-focus-color);
--sc: 0 0% 4%;
--b1: var(--base-lighter-color);
--b2: var(--base-color);
--b3: var(--base-darker-color);
--bc: var(--base-content-color);
--in: 207 90% 54%;
--inc: 207 90% 94%;
--er: 4 90% 58%;
--erc: 4 90% 98%;
}
html[data-theme="dark"] {
@mixin dark;
}
@media (prefers-color-scheme: dark) {
html {
@mixin dark;
}
}
html[data-theme="light"] {
@mixin light;
}
@media (prefers-color-scheme: light) {
html {
@mixin light;
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-lg;
}
mark {
@apply inline-block rounded-sm bg-secondary;
animation: pops 200ms ease-out;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
}
@layer components {
.input {
@apply focus:outline-none;
}
.btn {
@apply font-normal normal-case;
}
.btn:not(.btn-primary):not(.btn-secondary) {
@apply hover:bg-base-lighter;
}
}
@layer utilities {
@media (max-width: 768px) {
.mobile-hidden {
display: none;
}
}
.active-primary {
--n: var(--p);
--nc: var(--pc);
@apply active;
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import "./styles.scss";
// import "./styles.scss";
import "./main.css";
import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
+7 -1
View File
@@ -1,5 +1,11 @@
import { describe, expect, test } from "vitest";
import { describe, expect, test, vi } from "vitest";
import { Container } from "./Container";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
}));
describe("Container", () => {
const names = [
[
+14
View File
@@ -7,6 +7,16 @@ type Stat = Omit<ContainerStat, "id">;
const SWARM_ID_REGEX = /(\.[a-z0-9]{25})+$/i;
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
export class Container {
private _stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
@@ -49,6 +59,10 @@ export class Container {
return unref(this._stat);
}
get hostLabel() {
return hosts.value[this.host]?.name;
}
get storageKey() {
return `${stripVersion(this.image)}:${this.command}`;
}
+2 -6
View File
@@ -5,10 +5,6 @@ import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export interface HasComponent {
getComponent(): Component;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
@@ -22,7 +18,7 @@ export interface LogEvent {
readonly s: "stdout" | "stderr" | "unknown";
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
export abstract class LogEntry<T extends string | JSONObject> {
protected readonly _message: T;
constructor(
message: T,
@@ -102,7 +98,7 @@ export class DockerEventLogEntry extends LogEntry<string> {
constructor(
message: string,
date: Date,
public readonly event: string,
public readonly event: "container-stopped" | "container-started",
) {
super(message, date.getTime(), date, "stderr", "info");
}
+14 -14
View File
@@ -1,20 +1,20 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
}),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
},
),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};
export default i18n;
-16
View File
@@ -1,16 +0,0 @@
import { type App } from "vue";
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Table, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
app
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Modal)
.use(Field)
.use(Skeleton)
.use(Table)
.use(Config, bulmaConfig);
};
+4 -7
View File
@@ -1,11 +1,8 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.page-not-found") }}</p>
</div>
</div>
</div>
+20 -11
View File
@@ -1,10 +1,17 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0" v-if="currentContainer"></log-container>
<div v-else-if="ready" class="notification is-warning is-light m-6">
<h1 class="title">
{{ $t("error.container-not-found") }}
</h1>
<log-container
:id="id"
:show-title="true"
:scrollable="activeContainers.length > 0"
v-if="currentContainer"
></log-container>
<div v-else-if="ready" class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.container-not-found") }}</p>
</div>
</div>
</div>
</template>
@@ -16,11 +23,13 @@ const { id } = defineProps<{ id: string }>();
const currentContainer = store.currentContainer($$(id));
const { activeContainers, ready } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
watchEffect(() => {
if (ready.value) {
if (currentContainer.value) {
setTitle(currentContainer.value.name);
} else {
setTitle("Not Found");
}
}
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>
+47 -92
View File
@@ -1,62 +1,45 @@
<template>
<div class="section tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }} / {{ containers.length }}</p>
<p class="heading">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</p>
</div>
<div class="flex flex-col gap-16 px-4 pt-8 md:px-8">
<section>
<div class="stats grid bg-base-lighter shadow">
<div class="stat">
<div class="stat-value">{{ runningContainers.length }} / {{ containers.length }}</div>
<div class="stat-title">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
<div class="stat">
<div class="stat-value">{{ totalCpu }}%</div>
<div class="stat-title">{{ $t("label.total-cpu-usage") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ formatBytes(totalMem) }}</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
<div class="stat">
<div class="stat-value">{{ formatBytes(totalMem) }}</div>
<div class="stat-title">{{ $t("label.total-mem-usage") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</div>
</div>
</div>
<section class="section table-container">
<div class="box">
<div class="stat">
<div class="stat-value">{{ version }}</div>
<div class="stat-title">{{ $t("label.dozzle-version") }}</div>
</div>
</div>
</section>
<section>
<container-table :containers="runningContainers"></container-table>
</div>
</section>
</section>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
const { t } = useI18n();
const { version } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore) as { containers: unknown } as { containers: Ref<Container[]> };
const { containers, ready } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
ready: Ref<boolean>;
};
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
const mostRecentContainers = $computed(() => containers.value.toSorted((a, b) => +b.created - +a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
let totalCpu = $ref(0);
@@ -76,59 +59,31 @@ useIntervalFn(
1000,
{ immediate: true },
);
watchEffect(() => {
if (ready.value) {
setTitle(t("title.dashboard", { count: runningContainers.length }));
}
});
</script>
<style lang="scss" scoped>
.panel {
border: 1px solid var(--border-color);
.panel-block,
.panel-tabs {
border-color: var(--border-color);
.is-active {
border-color: var(--border-hover-color);
}
.name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.status {
margin-left: auto;
white-space: nowrap;
}
}
}
@media screen and (max-width: 768px) {
.pb-0-is-mobile {
padding-bottom: 0 !important;
}
.pt-0-is-mobile {
padding-top: 0 !important;
}
}
.icon {
padding: 10px 3px;
}
.bar-chart {
height: 1.5em;
.bar-text {
font-size: 0.9em;
padding: 0 0.5em;
}
}
<style lang="postcss" scoped>
:deep(tr td) {
padding-top: 1em;
padding-bottom: 1em;
}
.stat > div {
@apply text-center;
}
.stat-value {
@apply font-light;
}
.stat-title {
@apply font-light;
}
.section + .section {
padding-top: 0;
}
+35 -46
View File
@@ -1,50 +1,39 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container">
<section class="columns is-centered section">
<div class="column is-4">
<div class="card">
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
<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>
<input
class="input input-bordered"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input
class="input input-bordered"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<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>
</form>
</div>
</div>
</template>
+59 -126
View File
@@ -1,8 +1,8 @@
<template>
<div>
<section class="section">
<div class="mt-10 flex flex-col gap-8 px-4 md:px-8">
<section>
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
<h2>{{ $t("settings.about") }}</h2>
</div>
<div>
@@ -14,120 +14,75 @@
</div>
</section>
<section class="section">
<section class="flex flex-col gap-4">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
<h2>{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
<div>
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
<div>
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
</div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
<div>
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
</div>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
<div>
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="hourStyle" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">
{{ $t("settings.12-24-format") }}
</div>
</div>
<div class="flex items-center gap-6">
<dropdown
v-model="hourStyle"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: '12', value: '12' },
{ label: '24', value: '24' },
]"
/>
{{ $t("settings.12-24-format") }}
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
<div class="flex items-center gap-6">
<dropdown
v-model="size"
:options="[
{ label: 'Small', value: 'small' },
{ label: 'Medium', value: 'medium' },
{ label: 'Large', value: 'large' },
]"
/>
{{ $t("settings.font-size") }}
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="lightTheme" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['auto', 'dark', 'light']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
<div class="flex items-center gap-6">
<dropdown
v-model="lightTheme"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
]"
/>
{{ $t("settings.color-scheme") }}
</div>
</section>
<section class="section">
<section class="flex flex-col gap-2">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
<h2>{{ $t("settings.options") }}</h2>
</div>
<div>
<toggle v-model="search">
<div>{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut></div>
</toggle>
</div>
<div class="item">
<o-switch v-model="search">
<span>{{ $t("settings.search") }} <key-shortcut char="f"></key-shortcut></span>
</o-switch>
<div>
<toggle v-model="showAllContainers">{{ $t("settings.show-stopped-containers") }}</toggle>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
<div>
<toggle v-model="automaticRedirect">{{ $t("settings.automatic-redirect") }}</toggle>
</div>
</section>
</div>
@@ -144,6 +99,7 @@ import {
showAllContainers,
size,
softWrap,
automaticRedirect,
} from "@/composables/settings";
const { t } = useI18n();
@@ -174,35 +130,12 @@ async function fetchNextRelease() {
fetchNextRelease();
</script>
<style lang="scss" scoped>
.title {
color: var(--title-color);
}
a.next-release {
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
.section {
padding: 1rem 1.5rem;
}
<style lang="postcss" scoped>
.has-underline {
border-bottom: 1px solid var(--border-color);
padding: 1em 0px;
margin-bottom: 1em;
@apply mb-4 border-b border-base-content/50 py-4;
}
.item {
padding: 1em 0;
}
code {
border-radius: 4px;
background-color: #444;
:deep(a:not(.menu a)) {
@apply text-primary underline-offset-4 hover:underline;
}
</style>
+36 -4
View File
@@ -2,6 +2,11 @@ import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";
const { showToast } = useToast();
// @ts-ignore
const { t } = i18n.global;
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
@@ -30,6 +35,19 @@ export const useContainerStore = defineStore("container", () => {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener("error", (e) => {
if (es?.readyState === EventSource.CLOSED) {
showToast(
{
id: "events-stream",
message: t("error.events-stream.message"),
title: t("error.events-stream.title"),
type: "error",
},
{ once: true },
);
}
});
es.addEventListener("containers-changed", (e: Event) =>
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[]),
@@ -63,6 +81,22 @@ export const useContainerStore = defineStore("container", () => {
connect();
(async function () {
try {
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
} catch (e) {
showToast(
{
id: "events-timeout",
message: t("error.events-timeout.message"),
title: t("error.events-timeout.title"),
type: "error",
},
{ once: true },
);
}
})();
const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
@@ -93,8 +127,8 @@ export const useContainerStore = defineStore("container", () => {
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: Container) =>
const appendActiveContainer = ({ id }: { id: string }) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: { id: string }) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
return {
@@ -110,8 +144,6 @@ export const useContainerStore = defineStore("container", () => {
};
});
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}
-250
View File
@@ -1,250 +0,0 @@
@charset "utf-8";
@import "bulma/sass/utilities/initial-variables.sass";
$body-background-color: var(--body-background-color);
$scheme-main: var(--scheme-main);
$scheme-main-bis: var(--scheme-main-bis);
$scheme-main-ter: var(--scheme-main-ter);
$border: var(--border-color);
$border-hover: var(--border-hover-color);
$menu-item-active-background-color: var(--menu-item-active-background-color);
$menu-item-color: var(--menu-item-color);
$menu-item-hover-background-color: var(--menu-item-hover-background-color);
$menu-item-hover-color: var(--menu-item-hover-color);
$text-strong: var(--text-strong-color);
$text: var(--text-color);
$text-light: var(--text-light-color);
$panel-heading-background-color: var(--panel-heading-background-color);
$panel-heading-color: var(--panel-heading-color);
$link: $turquoise;
$link-active: $grey-dark;
$link-hover: $yellow;
$dark-toolbar-color: rgba($black-bis, 0.7);
$light-toolbar-color: rgba($grey-darker, 0.7);
@import "bulma/bulma";
@import "@oruga-ui/theme-bulma/dist/scss/components/utils/all.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/autocomplete.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
@import "splitpanes/dist/splitpanes.css";
@mixin dark {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
--border-color: #{$grey-darker};
--border-hover-color: var(--secondary-color);
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: #{$yellow};
--body-background-color: #{$black-bis};
--action-toolbar-background-color: #{$dark-toolbar-color};
--body-color: #{$grey-lighter};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: hsl(0, 6%, 87%);
--menu-item-hover-background-color: #{$white-ter};
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--scheme-main-bis);
--text-strong-color: #{$grey-lightest};
--text-color: #{$grey-lighter};
--text-light-color: #{$grey};
}
@mixin light {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
--scheme-main-ter: #{$white-ter};
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: rgb(249 115 22);
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
--body-color: #{$grey-darker};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: #{$grey-dark};
--menu-item-hover-background-color: #eee8e7;
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--text-strong-color);
--text-strong-color: #{$grey-dark};
--text-color: #{$grey-darker};
--text-light-color: #{$grey};
}
[data-theme="dark"] {
@include dark;
}
[data-theme="light"] {
@include light;
}
@media (prefers-color-scheme: dark) {
html {
@include dark;
}
}
@media (prefers-color-scheme: light) {
html {
@include light;
}
}
:root {
--green-color: #00b5ad;
--red-color: #f44336;
--purple-color: #9c27b0;
--orange-color: #ff9800;
--blue-color: #2196f3;
}
.is-red {
color: var(--red-color);
}
.is-green {
color: var(--green-color);
}
.is-purple {
color: var(--purple-color);
}
.is-orange {
color: var(--orange-color);
}
.is-blue {
color: var(--blue-color);
}
html {
overflow-x: unset;
overflow-y: unset;
scroll-snap-type: y proximity;
}
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
}
@media screen and (min-width: 770px) {
.splitpanes__pane {
overflow: unset;
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
.splitpanes__splitter {
z-index: 99;
}
.is-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.modal {
z-index: 1000;
.modal-background {
backdrop-filter: blur(10px);
}
}
.button .button-wrapper > span {
display: contents;
}
.has-dropshadow {
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
}
.has-boxshadow {
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
}
mark {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
.button.is-rounded:hover {
color: var(--text-strong-color);
background: var(--scheme-main-ter);
}
a {
transition: color 0.2s ease-in-out;
}
+1 -1
View File
@@ -61,7 +61,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.37.1-jammy
image: mcr.microsoft.com/playwright:v1.38.0-jammy
working_dir: /app
volumes:
- .:/app
+5 -1
View File
@@ -157,10 +157,14 @@ func (d *Client) ListContainers() ([]Container, error) {
var containers = make([]Container, 0, len(list))
for _, c := range list {
name := "no name"
if len(c.Names) > 0 {
name = strings.TrimPrefix(c.Names[0], "/")
}
container := Container{
ID: c.ID[:12],
Names: c.Names,
Name: strings.TrimPrefix(c.Names[0], "/"),
Name: name,
Image: c.Image,
ImageID: c.ImageID,
Command: c.Command,
+6 -5
View File
@@ -1,13 +1,14 @@
{
"private": true,
"type": "module",
"name": "docs",
"devDependencies": {
"@netlify/functions": "^1.6.0",
"@unocss/preset-typography": "^0.55.2",
"@unocss/reset": "^0.55.2",
"@unocss/transformer-directives": "^0.55.2",
"@netlify/functions": "^2.1.0",
"@unocss/preset-typography": "^0.56.4",
"@unocss/reset": "^0.56.4",
"@unocss/transformer-directives": "^0.56.4",
"dozzle": "workspace:*",
"sitemap": "^7.1.1",
"unocss": "^0.55.2"
"unocss": "^0.56.4"
}
}
+269 -238
View File
@@ -6,17 +6,17 @@ settings:
devDependencies:
'@netlify/functions':
specifier: ^1.6.0
version: 1.6.0
specifier: ^2.1.0
version: 2.1.0
'@unocss/preset-typography':
specifier: ^0.55.2
version: 0.55.2
specifier: ^0.56.4
version: 0.56.4
'@unocss/reset':
specifier: ^0.55.2
version: 0.55.2
specifier: ^0.56.4
version: 0.56.4
'@unocss/transformer-directives':
specifier: ^0.55.2
version: 0.55.2
specifier: ^0.56.4
version: 0.56.4
dozzle:
specifier: workspace:*
version: link:..
@@ -24,8 +24,8 @@ devDependencies:
specifier: ^7.1.1
version: 7.1.1
unocss:
specifier: ^0.55.2
version: 0.55.2(postcss@8.4.27)(vite@4.4.9)
specifier: ^0.56.4
version: 0.56.4(postcss@8.4.30)(vite@4.4.9)
packages:
@@ -44,12 +44,12 @@ packages:
find-up: 5.0.0
dev: true
/@antfu/utils@0.7.5:
resolution: {integrity: sha512-dlR6LdS+0SzOAPx/TPRhnoi7hE251OVeT2Snw0RguNbBSbjUHdWr0l3vcUUDg26rEysT89kCbtw1lVorBXLLCg==}
/@antfu/utils@0.7.6:
resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==}
dev: true
/@esbuild/android-arm64@0.18.19:
resolution: {integrity: sha512-4+jkUFQxZkQfQOOxfGVZB38YUWHMJX2ihZwF+2nh8m7bHdWXpixiurgGRN3c/KMSwlltbYI0/i929jwBRMFzbA==}
/@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
@@ -57,8 +57,8 @@ packages:
dev: true
optional: true
/@esbuild/android-arm@0.18.19:
resolution: {integrity: sha512-1uOoDurJYh5MNqPqpj3l/TQCI1V25BXgChEldCB7D6iryBYqYKrbZIhYO5AI9fulf66sM8UJpc3UcCly2Tv28w==}
/@esbuild/android-arm@0.18.20:
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
@@ -66,8 +66,8 @@ packages:
dev: true
optional: true
/@esbuild/android-x64@0.18.19:
resolution: {integrity: sha512-ae5sHYiP/Ogj2YNrLZbWkBmyHIDOhPgpkGvFnke7XFGQldBDWvc/AyYwSLpNuKw9UNkgnLlB/jPpnBmlF3G9Bg==}
/@esbuild/android-x64@0.18.20:
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
@@ -75,8 +75,8 @@ packages:
dev: true
optional: true
/@esbuild/darwin-arm64@0.18.19:
resolution: {integrity: sha512-HIpQvNQWFYROmWDANMRL+jZvvTQGOiTuwWBIuAsMaQrnStedM+nEKJBzKQ6bfT9RFKH2wZ+ej+DY7+9xHBTFPg==}
/@esbuild/darwin-arm64@0.18.20:
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
@@ -84,8 +84,8 @@ packages:
dev: true
optional: true
/@esbuild/darwin-x64@0.18.19:
resolution: {integrity: sha512-m6JdvXJQt0thNLIcWOeG079h2ivhYH4B5sVCgqb/B29zTcFd7EE8/J1nIUHhdtwGeItdUeqKaqqb4towwxvglQ==}
/@esbuild/darwin-x64@0.18.20:
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
@@ -93,8 +93,8 @@ packages:
dev: true
optional: true
/@esbuild/freebsd-arm64@0.18.19:
resolution: {integrity: sha512-G0p4EFMPZhGn/xVNspUyMQbORH3nlKTV0bFNHPIwLraBuAkTeMyxNviTe0ZXUbIXQrR1lrwniFjNFU4s+x7veQ==}
/@esbuild/freebsd-arm64@0.18.20:
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
@@ -102,8 +102,8 @@ packages:
dev: true
optional: true
/@esbuild/freebsd-x64@0.18.19:
resolution: {integrity: sha512-hBxgRlG42+W+j/1/cvlnSa+3+OBKeDCyO7OG2ICya1YJaSCYfSpuG30KfOnQHI7Ytgu4bRqCgrYXxQEzy0zM5Q==}
/@esbuild/freebsd-x64@0.18.20:
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
@@ -111,8 +111,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-arm64@0.18.19:
resolution: {integrity: sha512-X8g33tczY0GsJq3lhyBrjnFtaKjWVpp1gMq5IlF9BQJ3TUfSK74nQnz9mRIEejmcV+OIYn6bkOJeUaU1Knrljg==}
/@esbuild/linux-arm64@0.18.20:
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
@@ -120,8 +120,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-arm@0.18.19:
resolution: {integrity: sha512-qtWyoQskfJlb9MD45mvzCEKeO4uCnDZ7lPFeNqbfaaJHqBiH9qA5Vu2EuckqYZuFMJWy1l4dxTf9NOulCVfUjg==}
/@esbuild/linux-arm@0.18.20:
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
@@ -129,8 +129,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-ia32@0.18.19:
resolution: {integrity: sha512-SAkRWJgb+KN+gOhmbiE6/wu23D6HRcGQi15cB13IVtBZZgXxygTV5GJlUAKLQ5Gcx0gtlmt+XIxEmSqA6sZTOw==}
/@esbuild/linux-ia32@0.18.20:
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
@@ -138,8 +138,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-loong64@0.18.19:
resolution: {integrity: sha512-YLAslaO8NsB9UOxBchos82AOMRDbIAWChwDKfjlGrHSzS3v1kxce7dGlSTsrb0PJwo1KYccypN3VNjQVLtz7LA==}
/@esbuild/linux-loong64@0.18.20:
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
@@ -147,8 +147,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-mips64el@0.18.19:
resolution: {integrity: sha512-vSYFtlYds/oTI8aflEP65xo3MXChMwBOG1eWPGGKs/ev9zkTeXVvciU+nifq8J1JYMz+eQ4J9JDN0O2RKF8+1Q==}
/@esbuild/linux-mips64el@0.18.20:
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
@@ -156,8 +156,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-ppc64@0.18.19:
resolution: {integrity: sha512-tgG41lRVwlzqO9tv9l7aXYVw35BxKXLtPam1qALScwSqPivI8hjkZLNH0deaaSCYCFT9cBIdB+hUjWFlFFLL9A==}
/@esbuild/linux-ppc64@0.18.20:
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
@@ -165,8 +165,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-riscv64@0.18.19:
resolution: {integrity: sha512-EgBZFLoN1S5RuB4cCJI31pBPsjE1nZ+3+fHRjguq9Ibrzo29bOLSBcH1KZJvRNh5qtd+fcYIGiIUia8Jw5r1lQ==}
/@esbuild/linux-riscv64@0.18.20:
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
@@ -174,8 +174,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-s390x@0.18.19:
resolution: {integrity: sha512-q1V1rtHRojAzjSigZEqrcLkpfh5K09ShCoIsdTakozVBnM5rgV58PLFticqDp5UJ9uE0HScov9QNbbl8HBo6QQ==}
/@esbuild/linux-s390x@0.18.20:
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
@@ -183,8 +183,8 @@ packages:
dev: true
optional: true
/@esbuild/linux-x64@0.18.19:
resolution: {integrity: sha512-D0IiYjpZRXxGZLQfsydeAD7ZWqdGyFLBj5f2UshJpy09WPs3qizDCsEr8zyzcym6Woj/UI9ZzMIXwvoXVtyt0A==}
/@esbuild/linux-x64@0.18.20:
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
@@ -192,8 +192,8 @@ packages:
dev: true
optional: true
/@esbuild/netbsd-x64@0.18.19:
resolution: {integrity: sha512-3tt3SOS8L3D54R8oER41UdDshlBIAjYhdWRPiZCTZ1E41+shIZBpTjaW5UaN/jD1ENE/Ok5lkeqhoNMbxstyxw==}
/@esbuild/netbsd-x64@0.18.20:
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
@@ -201,8 +201,8 @@ packages:
dev: true
optional: true
/@esbuild/openbsd-x64@0.18.19:
resolution: {integrity: sha512-MxbhcuAYQPlfln1EMc4T26OUoeg/YQc6wNoEV8xvktDKZhLtBxjkoeESSo9BbPaGKhAPzusXYj5n8n5A8iZSrA==}
/@esbuild/openbsd-x64@0.18.20:
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
@@ -210,8 +210,8 @@ packages:
dev: true
optional: true
/@esbuild/sunos-x64@0.18.19:
resolution: {integrity: sha512-m0/UOq1wj25JpWqOJxoWBRM9VWc3c32xiNzd+ERlYstUZ6uwx5SZsQUtkiFHaYmcaoj+f6+Tfcl7atuAz3idwQ==}
/@esbuild/sunos-x64@0.18.20:
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
@@ -219,8 +219,8 @@ packages:
dev: true
optional: true
/@esbuild/win32-arm64@0.18.19:
resolution: {integrity: sha512-L4vb6pcoB1cEcXUHU6EPnUhUc4+/tcz4OqlXTWPcSQWxegfmcOprhmIleKKwmMNQVc4wrx/+jB7tGkjjDmiupg==}
/@esbuild/win32-arm64@0.18.20:
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
@@ -228,8 +228,8 @@ packages:
dev: true
optional: true
/@esbuild/win32-ia32@0.18.19:
resolution: {integrity: sha512-rQng7LXSKdrDlNDb7/v0fujob6X0GAazoK/IPd9C3oShr642ri8uIBkgM37/l8B3Rd5sBQcqUXoDdEy75XC/jg==}
/@esbuild/win32-ia32@0.18.20:
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
@@ -237,8 +237,8 @@ packages:
dev: true
optional: true
/@esbuild/win32-x64@0.18.19:
resolution: {integrity: sha512-z69jhyG20Gq4QL5JKPLqUT+eREuqnDAFItLbza4JCmpvUnIlY73YNjd5djlO7kBiiZnvTnJuAbOjIoZIOa1GjA==}
/@esbuild/win32-x64@0.18.20:
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
@@ -250,11 +250,11 @@ packages:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true
/@iconify/utils@2.1.7:
resolution: {integrity: sha512-P8S3z/L1LcV4Qem9AoCfVAaTFGySEMzFEY4CHZLkfRj0Fv9LiR+AwjDgrDrzyI93U2L2mg9JHsbTJ52mF8suNw==}
/@iconify/utils@2.1.10:
resolution: {integrity: sha512-0/+5hxjzCZ9RoYpqxnOzbnpQyMdZRuHcMxPJeuX+x/aZkAAD/N4TajDjAPT7LpX+M0bfLExj/p0bbDkUfp0lrg==}
dependencies:
'@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.5
'@antfu/utils': 0.7.6
'@iconify/types': 2.0.0
debug: 4.3.4
kolorist: 1.8.0
@@ -293,13 +293,27 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@netlify/functions@1.6.0:
resolution: {integrity: sha512-6G92AlcpFrQG72XU8YH8pg94eDnq7+Q0YJhb8x4qNpdGsvuzvrfHWBmqFGp/Yshmv4wex9lpsTRZOocdrA2erQ==}
/@netlify/functions@2.1.0:
resolution: {integrity: sha512-QWuyj1Q6z6Cqcq3qCcOB75nxjYOxlNmv/d9/nnX4asSRQcWhM/ehXl/KDL6Sl9aufMRe6uQY10s6i9zCDNMRjg==}
engines: {node: '>=14.0.0'}
dependencies:
'@netlify/serverless-functions-api': 1.7.3
is-promise: 4.0.0
dev: true
/@netlify/node-cookies@0.1.0:
resolution: {integrity: sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==}
engines: {node: ^14.16.0 || >=16.0.0}
dev: true
/@netlify/serverless-functions-api@1.7.3:
resolution: {integrity: sha512-n6/7cJlSWvvbBlUOEAbkGyEld80S6KbG/ldQI9OhLfe1lTatgKmrTNIgqVNpaWpUdTgP2OHWFjmFBzkxxBWs5w==}
engines: {node: ^14.18.0 || >=16.0.0}
dependencies:
'@netlify/node-cookies': 0.1.0
urlpattern-polyfill: 8.0.2
dev: true
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -321,12 +335,12 @@ packages:
fastq: 1.15.0
dev: true
/@polka/url@1.0.0-next.21:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
/@polka/url@1.0.0-next.23:
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
dev: true
/@rollup/pluginutils@5.0.3:
resolution: {integrity: sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==}
/@rollup/pluginutils@5.0.4:
resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
@@ -334,13 +348,13 @@ packages:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
'@types/estree': 1.0.2
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
/@types/estree@1.0.2:
resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==}
dev: true
/@types/node@17.0.45:
@@ -357,195 +371,208 @@ packages:
'@types/node': 18.14.2
dev: true
/@unocss/astro@0.55.2(vite@4.4.9):
resolution: {integrity: sha512-cSzBKPEveZZQDZp5bq0UlL8CVvzB/1LsgZmZufxi9oMMjMJYqzfTkKg5z65GcP82Xp5c0N3KKkl/R6I+/7Iwvw==}
/@unocss/astro@0.56.4(vite@4.4.9):
resolution: {integrity: sha512-2op/0y5UjiE0PRzKwHSEcOFtjDTkeIoCJR9FAYFC+xTx95Sp7+S4yClwaURZtohPnxQmGug0eGpOgsFzoJwmOQ==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
peerDependenciesMeta:
vite:
optional: true
dependencies:
'@unocss/core': 0.55.2
'@unocss/reset': 0.55.2
'@unocss/vite': 0.55.2(vite@4.4.9)
'@unocss/core': 0.56.4
'@unocss/reset': 0.56.4
'@unocss/vite': 0.56.4(vite@4.4.9)
vite: 4.4.9
transitivePeerDependencies:
- rollup
dev: true
/@unocss/cli@0.55.2:
resolution: {integrity: sha512-ZJ8aBhm+3WjGCA5HcOQ4C3mbtJwkgMX2gpjjJ0MPh/iZOz3+/zmHlrXJCS3jIFouRYSwxxanWdrGUuLIQLqPhQ==}
/@unocss/cli@0.56.4:
resolution: {integrity: sha512-WyqqJWfOVb7hvWU5nxtSFAGiDp/Peeohj1f/Xt0KlmgOPS8RA7kmLjRYruuuRuK5yR677IB+0C4kJDFkRpRlcA==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.0.3
'@unocss/config': 0.55.2
'@unocss/core': 0.55.2
'@unocss/preset-uno': 0.55.2
'@rollup/pluginutils': 5.0.4
'@unocss/config': 0.56.4
'@unocss/core': 0.56.4
'@unocss/preset-uno': 0.56.4
cac: 6.7.14
chokidar: 3.5.3
colorette: 2.0.20
consola: 3.2.3
fast-glob: 3.3.1
magic-string: 0.30.2
magic-string: 0.30.3
pathe: 1.1.1
perfect-debounce: 1.0.0
transitivePeerDependencies:
- rollup
dev: true
/@unocss/config@0.55.2:
resolution: {integrity: sha512-RYDv9QzhUeBz9BY+Pty0xc9vk/m4LGBNMiBghcItW6zXN554JbSuoPD55DmnvO2iXrIYujBZdB/Kob6GLCZpqw==}
/@unocss/config@0.56.4:
resolution: {integrity: sha512-e8pdkoTkKxqkimAttZ5ELiDXn3oYteNDOxNANygWUhw3h6DA1sKh831dQkhTu3Kigsy0/gQXmFXbekrlywMkKw==}
engines: {node: '>=14'}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
unconfig: 0.3.10
dev: true
/@unocss/core@0.55.2:
resolution: {integrity: sha512-ZLEES8RDgWoK/vttUzl3PM2bZqL3HvhLgj8xdDa09Xw+JiTlR4c66s+hLn52oCoJTnT9lGsD2j7tTGN9ToSiTA==}
/@unocss/core@0.56.4:
resolution: {integrity: sha512-9DL+2adfjWWnFWp2QOJTq2OcfKKo++agaXnCX7CzeD8KgfWCBqxs2RDoxP3HrDbTflhDHdbjz23B+vJoVykhZw==}
dev: true
/@unocss/extractor-arbitrary-variants@0.55.2:
resolution: {integrity: sha512-mHEoFx+ITe3OgFoIUhkCQxRgUjvOJeHtI1Z3Sm8NDMy2vTqOlkSf7NLWEyFfQsSFYqpWGTkaW1XiMZujGMoB/g==}
/@unocss/extractor-arbitrary-variants@0.56.4:
resolution: {integrity: sha512-Q93Bu3wZld+Vj5dqfJYH6phcmjm7fy6mEul64RexLWF00As9WrRzWEXbLVYmmVwUm9e5xcxZO6s98gKq1nTH6Q==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/inspector@0.55.2:
resolution: {integrity: sha512-AMNZ7FsBFhQCMuAQugCk7d+3uoHDN2VFwCzSxk0ITgG51J90jfVgAo9mJf28W/AM4g0qVHScveJDPKzA+2o+Vg==}
/@unocss/inspector@0.56.4:
resolution: {integrity: sha512-DY3kmX5nGTb8QsbqcGYDvyXXTPhGw7PoVN48wA8o0c+KkcfjjfsoZRTFDpj6P5JcW8Z68gIBOLyTnql6tIMesg==}
dependencies:
'@unocss/rule-utils': 0.56.4
gzip-size: 6.0.0
sirv: 2.0.3
dev: true
/@unocss/postcss@0.55.2(postcss@8.4.27):
resolution: {integrity: sha512-HJLGINNlQ3DGL9zRGuctX+mOVW2w7o8Wj89v3/2qTcqXBDpwfn1+KlxSjU9rsEPdE4Ur3MIcVXcJC0wz4+EwEA==}
/@unocss/postcss@0.56.4(postcss@8.4.30):
resolution: {integrity: sha512-lc0+BjW1gY+86tvtUFppHg++Tw8I0V7hlldloUy7W4Fg2Mu8VAi3I/KaSSRvqMr5SNlfaOPv0XuB/BNkDE65Gw==}
engines: {node: '>=14'}
peerDependencies:
postcss: ^8.4.21
dependencies:
'@unocss/config': 0.55.2
'@unocss/core': 0.55.2
'@unocss/config': 0.56.4
'@unocss/core': 0.56.4
'@unocss/rule-utils': 0.56.4
css-tree: 2.3.1
fast-glob: 3.3.1
magic-string: 0.30.2
postcss: 8.4.27
magic-string: 0.30.3
postcss: 8.4.30
dev: true
/@unocss/preset-attributify@0.55.2:
resolution: {integrity: sha512-jn5ulsKpAipsX3Gf2/iSZydgI0eP1ENeoS6rrNBL8zl1mRihnZYFegS75rGYjO6sEfEHrhkBiSHOw7Uv5KtLbw==}
/@unocss/preset-attributify@0.56.4:
resolution: {integrity: sha512-mjs9NDniycAdipiLCHpGS75mItfFq3ruX6w4ypcvGxVAa8cGOhotWAAxqgHKSJobdDcoFTjnCaK1SA0svBplUQ==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/preset-icons@0.55.2:
resolution: {integrity: sha512-NK9LcTlBZv6zO8Qbu+VA9HblzYc5ebuFwaQMfQcYj2Z6dBOT27Ki41LY1qjEXzzMPXb44Q14Rlk0tJc8LtJIpQ==}
/@unocss/preset-icons@0.56.4:
resolution: {integrity: sha512-N02I6ZYt2cAThE4pINTRY5tUeHNhcUQYeoqtQhQUk7YFfwRMosWA5JbtH7LTzQpYbE2GsbjXkKdII4bkTWNwhA==}
dependencies:
'@iconify/utils': 2.1.7
'@unocss/core': 0.55.2
ofetch: 1.1.1
'@iconify/utils': 2.1.10
'@unocss/core': 0.56.4
ofetch: 1.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@unocss/preset-mini@0.55.2:
resolution: {integrity: sha512-jwUsrwtPwMvFVJUP+FVFjq+sp+xQPyFLRPSb89ZI34F1a3EwJ2wioDICLqWjOjY7zei9UgtSY0owBM9vwxw/kg==}
/@unocss/preset-mini@0.56.4:
resolution: {integrity: sha512-FCKs3sUUiHLXQ/ON/ZCQc2JZQox4tv0W8Du+8Y1mRZ6w1yazqqBhR2yDPaEZ51yKcHmDf6ndiTvcJ2tPIz/o8g==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/extractor-arbitrary-variants': 0.55.2
'@unocss/core': 0.56.4
'@unocss/extractor-arbitrary-variants': 0.56.4
'@unocss/rule-utils': 0.56.4
dev: true
/@unocss/preset-tagify@0.55.2:
resolution: {integrity: sha512-m8/9wBtUQSwnwsLANhUOc7sukF8ReHJ7ZC6fCfTozRMOhwu+bDcf9G7pguXdNC4DdZXI15cvbZzkYF2l733qUw==}
/@unocss/preset-tagify@0.56.4:
resolution: {integrity: sha512-CURDiJ+wzjzIARS2vF413hZ94u0EVK0+miY6l1eN6RrlW/YfU8LOaakbC/b0Cxl7hOhLrZuXFTgymcS9Ep4TMw==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/preset-typography@0.55.2:
resolution: {integrity: sha512-Y4JEihpKPDlXWXxnnMZbQclqZ4+DUD8RVFk46ERe9CLNEYkFObd4LG7yfSurr/C01zuU/GhEMyOWqSGsSyCxKg==}
/@unocss/preset-typography@0.56.4:
resolution: {integrity: sha512-/rwCZ0SHa8ht/Chb2rCoENL/S/CWspnTtvK4pPxALEcx/Mqi8Amk45YipU7gXmLsQ3eKwY/s0V1EgHF0DOccHg==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/preset-mini': 0.55.2
'@unocss/core': 0.56.4
'@unocss/preset-mini': 0.56.4
dev: true
/@unocss/preset-uno@0.55.2:
resolution: {integrity: sha512-8VJXC6+f5YBjUaTkf+EGAembDYMleb0zjkb4hwXxjPIsO+mXixdZC2icCiN/12DLlwH4FzEvObLKns3CGEAZZw==}
/@unocss/preset-uno@0.56.4:
resolution: {integrity: sha512-E4VbUe1nIeJ7D1en84osGzwP79IBJ1P9v37Pcx2PvaRAQTKf6WRF7EXzmzEm6HPF+skyGOX9RWuXs9QFODsW8A==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/preset-mini': 0.55.2
'@unocss/preset-wind': 0.55.2
'@unocss/core': 0.56.4
'@unocss/preset-mini': 0.56.4
'@unocss/preset-wind': 0.56.4
'@unocss/rule-utils': 0.56.4
dev: true
/@unocss/preset-web-fonts@0.55.2:
resolution: {integrity: sha512-kRnrfZPDkU2r9tp507rsh4kwhUzZ76XBTZLmElYm8tlP6HZzIHcFF8fdW15J4nh81b/IGw8ZOS7aQmqtHu3A8A==}
/@unocss/preset-web-fonts@0.56.4:
resolution: {integrity: sha512-H9Vy4nYpx1jpQvSBHBTSiB83zU85CtehoK03EbzUrz/i69EjBWiw52+mP/vrtMK0M/5QghcpNf64i8pObFId+w==}
dependencies:
'@unocss/core': 0.55.2
ofetch: 1.1.1
'@unocss/core': 0.56.4
ofetch: 1.3.3
dev: true
/@unocss/preset-wind@0.55.2:
resolution: {integrity: sha512-th/aOokb10ApaiVLNI093mvko4XryJ70oEhzz4tHdSuhnQWf5eY7+k7y9EEYFz8i1OOrKuer0HzUV27llZaufw==}
/@unocss/preset-wind@0.56.4:
resolution: {integrity: sha512-llQYQxrA531kl1juYMyaDEEl/IVdHoIPXaaJTdNHbGkSbMqrpZSRzLPsGE+CpaOBKTNqa93UyU1qlUGSr9ZB+A==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/preset-mini': 0.55.2
'@unocss/core': 0.56.4
'@unocss/preset-mini': 0.56.4
'@unocss/rule-utils': 0.56.4
dev: true
/@unocss/reset@0.55.2:
resolution: {integrity: sha512-paInTGIhtI96fcJGZWbkPLW/7qiTlHxSbEIs1HGHcbf3WbwNuKrJUvKlQAhUs2HILNKhvsTXQl05Os8gtinLEA==}
/@unocss/reset@0.56.4:
resolution: {integrity: sha512-7o3Jpog49ORWW+Rpz7bzYY+CStfLMyBXAeDOzhM2PQjftuQl4ZQOvIdXi6zrXDEN2SJQsi279s2t50ERZN8YcA==}
dev: true
/@unocss/scope@0.55.2:
resolution: {integrity: sha512-o1b86ejgaFDqfC712mUZqZDQNf6o1xDzm6+bgHySdiltR8Quo6l8RcoZjZrCvEogtPbko4/XJ374t1NQMUQf4g==}
dev: true
/@unocss/transformer-attributify-jsx-babel@0.55.2:
resolution: {integrity: sha512-pmfF546i8pKfMNeYZOJz2UzbuUwj0v7GqcoP5fClyRUzBMUfXdJwBSdFaYkdWR5Q/O1sv+pI0S8r/G9T7QuldA==}
/@unocss/rule-utils@0.56.4:
resolution: {integrity: sha512-pZmTPqi/Tb+vbwjNXu9+PwxEh7PVe+FwP+pzUbfNSUpE5Hg9CCbl8hpsXRE/r8kdaoKhmw2ryxvjocOgKXG/ag==}
engines: {node: '>=14'}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/transformer-attributify-jsx@0.55.2:
resolution: {integrity: sha512-WerdaNagorTtYDvbhlZEmeuBrQ5lmPE0vG9r20bPR/vLy9UmbIFPpzt6b/hSLqOUnZnaEfbrpNUlpBZgUXpvsg==}
dependencies:
'@unocss/core': 0.55.2
/@unocss/scope@0.56.4:
resolution: {integrity: sha512-fS8HEWFUeuNhE3fSBwgSfcgC8JS80sYY692uvpwWUN29gFawQDyEmAkGbqDZoBuvfFtKN2IA+evVJ5iED1vtuA==}
dev: true
/@unocss/transformer-compile-class@0.55.2:
resolution: {integrity: sha512-zKeJtAirFrgj8TheKplgdKrPV9hPN3i2gEy/aQ+CrHHImcQtxZ1FJzmJT1yV77MOXOdeRJOhiePNOe2TE1A4tw==}
/@unocss/transformer-attributify-jsx-babel@0.56.4:
resolution: {integrity: sha512-ftkW+HX8tgtSG8bL3Ek9NeSHJfLLdIhokyP/31Kx+DZoOXcwGFN2oBfOs/tkPRdLXUDeEh/9+xLZyXSfrq2Kjw==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/transformer-directives@0.55.2:
resolution: {integrity: sha512-IJKL5clOiv2RjvHYr4xumS4eFScPsi3Vg4vGugsmn43PZ1FsApp8UElHfhuhBsEEiffnsgTD+N5u/EiPpyI0Gw==}
/@unocss/transformer-attributify-jsx@0.56.4:
resolution: {integrity: sha512-GWiiCMI4W5MaF5y7GejnvIjRzgpTj6xouQYXurMVyq8GaXX34oSzin09Vh7F2/9lMD582UV5/ISu54A78RtswA==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/transformer-compile-class@0.56.4:
resolution: {integrity: sha512-/sPAamamhYBwg29ITrS9FOPF4Mt5r6H0ST0XdAaDVqt0JM3QmMHYUAn5YqvMS1z7c+A6FwsuZ5L0Sn55auXa+g==}
dependencies:
'@unocss/core': 0.56.4
dev: true
/@unocss/transformer-directives@0.56.4:
resolution: {integrity: sha512-L+h0pPgukk12TuqGaZjvGI0y6i3tFfl0dXJnJ2W6eeK2Y2CMirfhCo9Cb2uF81gfIdFKJqXGY6qujK+AS0ji/Q==}
dependencies:
'@unocss/core': 0.56.4
'@unocss/rule-utils': 0.56.4
css-tree: 2.3.1
dev: true
/@unocss/transformer-variant-group@0.55.2:
resolution: {integrity: sha512-BIAigftn+mfUeQT7sPzJNgvvbrmLj0gmYmeK4U7/8NxUuOuC0ROTNSw+MKU7yDiPYHqb1kxVZ47LZ3GdUcNPRA==}
/@unocss/transformer-variant-group@0.56.4:
resolution: {integrity: sha512-4ebNLWgspeoaHDLQ6qW8ZEWl7rzXURxBJLaw1hUqMwfbM/HEc5gPwUy7k7W+4TJyB426UpBb6kWLgO2WRrJXWw==}
dependencies:
'@unocss/core': 0.55.2
'@unocss/core': 0.56.4
dev: true
/@unocss/vite@0.55.2(vite@4.4.9):
resolution: {integrity: sha512-JEyEaJt8D+Ed3Z8GDQ0hMWqKsB47/DoS+aPzDoXSIVozgi8seHtfSChBOBUSgcCrozfBVp42YHbYYyloDkb2Yw==}
/@unocss/vite@0.56.4(vite@4.4.9):
resolution: {integrity: sha512-hswOJk2C7scwQioLIGDnT+H/bbC1EEkq+MhN+VDiVlRUYwaIgLITmSp0q6y6JK2RKD7VhDVxLNMYw4LxxcJuzA==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
dependencies:
'@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.0.3
'@unocss/config': 0.55.2
'@unocss/core': 0.55.2
'@unocss/inspector': 0.55.2
'@unocss/scope': 0.55.2
'@unocss/transformer-directives': 0.55.2
'@rollup/pluginutils': 5.0.4
'@unocss/config': 0.56.4
'@unocss/core': 0.56.4
'@unocss/inspector': 0.56.4
'@unocss/scope': 0.56.4
'@unocss/transformer-directives': 0.56.4
chokidar: 3.5.3
fast-glob: 3.3.1
magic-string: 0.30.2
magic-string: 0.30.3
vite: 4.4.9
transitivePeerDependencies:
- rollup
@@ -598,7 +625,7 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/colorette@2.0.20:
@@ -651,34 +678,34 @@ packages:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
dev: true
/esbuild@0.18.19:
resolution: {integrity: sha512-ra3CaIKCzJp5bU5BDfrCc0FRqKj71fQi+gbld0aj6lN0ifuX2fWJYPgLVLGwPfA+ruKna+OWwOvf/yHj6n+i0g==}
/esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/android-arm': 0.18.19
'@esbuild/android-arm64': 0.18.19
'@esbuild/android-x64': 0.18.19
'@esbuild/darwin-arm64': 0.18.19
'@esbuild/darwin-x64': 0.18.19
'@esbuild/freebsd-arm64': 0.18.19
'@esbuild/freebsd-x64': 0.18.19
'@esbuild/linux-arm': 0.18.19
'@esbuild/linux-arm64': 0.18.19
'@esbuild/linux-ia32': 0.18.19
'@esbuild/linux-loong64': 0.18.19
'@esbuild/linux-mips64el': 0.18.19
'@esbuild/linux-ppc64': 0.18.19
'@esbuild/linux-riscv64': 0.18.19
'@esbuild/linux-s390x': 0.18.19
'@esbuild/linux-x64': 0.18.19
'@esbuild/netbsd-x64': 0.18.19
'@esbuild/openbsd-x64': 0.18.19
'@esbuild/sunos-x64': 0.18.19
'@esbuild/win32-arm64': 0.18.19
'@esbuild/win32-ia32': 0.18.19
'@esbuild/win32-x64': 0.18.19
'@esbuild/android-arm': 0.18.20
'@esbuild/android-arm64': 0.18.20
'@esbuild/android-x64': 0.18.20
'@esbuild/darwin-arm64': 0.18.20
'@esbuild/darwin-x64': 0.18.20
'@esbuild/freebsd-arm64': 0.18.20
'@esbuild/freebsd-x64': 0.18.20
'@esbuild/linux-arm': 0.18.20
'@esbuild/linux-arm64': 0.18.20
'@esbuild/linux-ia32': 0.18.20
'@esbuild/linux-loong64': 0.18.20
'@esbuild/linux-mips64el': 0.18.20
'@esbuild/linux-ppc64': 0.18.20
'@esbuild/linux-riscv64': 0.18.20
'@esbuild/linux-s390x': 0.18.20
'@esbuild/linux-x64': 0.18.20
'@esbuild/netbsd-x64': 0.18.20
'@esbuild/openbsd-x64': 0.18.20
'@esbuild/sunos-x64': 0.18.20
'@esbuild/win32-arm64': 0.18.20
'@esbuild/win32-ia32': 0.18.20
'@esbuild/win32-x64': 0.18.20
dev: true
/estree-walker@2.0.2:
@@ -732,8 +759,8 @@ packages:
path-exists: 4.0.0
dev: true
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
@@ -801,8 +828,8 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/jiti@1.19.1:
resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==}
/jiti@1.20.0:
resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==}
hasBin: true
dev: true
@@ -826,8 +853,8 @@ packages:
p-locate: 5.0.0
dev: true
/magic-string@0.30.2:
resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==}
/magic-string@0.30.3:
resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
@@ -859,13 +886,13 @@ packages:
engines: {node: '>=6'}
dev: true
/mlly@1.4.0:
resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==}
/mlly@1.4.2:
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
dependencies:
acorn: 8.10.0
pathe: 1.1.1
pkg-types: 1.0.3
ufo: 1.2.0
ufo: 1.3.0
dev: true
/mrmime@1.0.1:
@@ -883,8 +910,8 @@ packages:
hasBin: true
dev: true
/node-fetch-native@1.2.0:
resolution: {integrity: sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==}
/node-fetch-native@1.4.0:
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
dev: true
/normalize-path@3.0.0:
@@ -899,12 +926,12 @@ packages:
path-key: 3.1.1
dev: true
/ofetch@1.1.1:
resolution: {integrity: sha512-SSMoktrp9SNLi20BWfB/BnnKcL0RDigXThD/mZBeQxkIRv1xrd9183MtLdsqRYLYSqW0eTr5t8w8MqjNhvoOQQ==}
/ofetch@1.3.3:
resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==}
dependencies:
destr: 2.0.1
node-fetch-native: 1.2.0
ufo: 1.2.0
node-fetch-native: 1.4.0
ufo: 1.3.0
dev: true
/onetime@5.1.2:
@@ -959,12 +986,12 @@ packages:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
dependencies:
jsonc-parser: 3.2.0
mlly: 1.4.0
mlly: 1.4.2
pathe: 1.1.1
dev: true
/postcss@8.4.27:
resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==}
/postcss@8.4.30:
resolution: {integrity: sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.6
@@ -988,12 +1015,12 @@ packages:
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: true
/rollup@3.27.2:
resolution: {integrity: sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ==}
/rollup@3.29.3:
resolution: {integrity: sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/run-parallel@1.2.0:
@@ -1026,7 +1053,7 @@ packages:
resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==}
engines: {node: '>= 10'}
dependencies:
'@polka/url': 1.0.0-next.21
'@polka/url': 1.0.0-next.23
mrmime: 1.0.1
totalist: 3.0.1
dev: true
@@ -1064,24 +1091,24 @@ packages:
engines: {node: '>=6'}
dev: true
/ufo@1.2.0:
resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==}
/ufo@1.3.0:
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
dev: true
/unconfig@0.3.10:
resolution: {integrity: sha512-tj317lhIq2iZF/NXrJnU1t2UaGUKKz1eL1sK2t63Oq66V9BxqvZV12m55fp/fpQJ+DDmVlLgo7cnLVOZkhlO/A==}
dependencies:
'@antfu/utils': 0.7.5
'@antfu/utils': 0.7.6
defu: 6.1.2
jiti: 1.19.1
mlly: 1.4.0
jiti: 1.20.0
mlly: 1.4.2
dev: true
/unocss@0.55.2(postcss@8.4.27)(vite@4.4.9):
resolution: {integrity: sha512-+C8tFUFIEv40DpEhjA/Yv+RB5HZumkWiON2OlPyrbzapQ8x60F9TUwUS3pw7MlpxI6GfTCYwXKEE6DTGCm1SLA==}
/unocss@0.56.4(postcss@8.4.30)(vite@4.4.9):
resolution: {integrity: sha512-RcaZHLSlj7BAupx6Y9jJwOSRJ+Lu1pnLwRZuppMfD4GgWWNymDRJ7HF1uNqCCYhxJa1LgU1irOEfO8pzK8NYkw==}
engines: {node: '>=14'}
peerDependencies:
'@unocss/webpack': 0.55.2
'@unocss/webpack': 0.56.4
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
peerDependenciesMeta:
'@unocss/webpack':
@@ -1089,26 +1116,26 @@ packages:
vite:
optional: true
dependencies:
'@unocss/astro': 0.55.2(vite@4.4.9)
'@unocss/cli': 0.55.2
'@unocss/core': 0.55.2
'@unocss/extractor-arbitrary-variants': 0.55.2
'@unocss/postcss': 0.55.2(postcss@8.4.27)
'@unocss/preset-attributify': 0.55.2
'@unocss/preset-icons': 0.55.2
'@unocss/preset-mini': 0.55.2
'@unocss/preset-tagify': 0.55.2
'@unocss/preset-typography': 0.55.2
'@unocss/preset-uno': 0.55.2
'@unocss/preset-web-fonts': 0.55.2
'@unocss/preset-wind': 0.55.2
'@unocss/reset': 0.55.2
'@unocss/transformer-attributify-jsx': 0.55.2
'@unocss/transformer-attributify-jsx-babel': 0.55.2
'@unocss/transformer-compile-class': 0.55.2
'@unocss/transformer-directives': 0.55.2
'@unocss/transformer-variant-group': 0.55.2
'@unocss/vite': 0.55.2(vite@4.4.9)
'@unocss/astro': 0.56.4(vite@4.4.9)
'@unocss/cli': 0.56.4
'@unocss/core': 0.56.4
'@unocss/extractor-arbitrary-variants': 0.56.4
'@unocss/postcss': 0.56.4(postcss@8.4.30)
'@unocss/preset-attributify': 0.56.4
'@unocss/preset-icons': 0.56.4
'@unocss/preset-mini': 0.56.4
'@unocss/preset-tagify': 0.56.4
'@unocss/preset-typography': 0.56.4
'@unocss/preset-uno': 0.56.4
'@unocss/preset-web-fonts': 0.56.4
'@unocss/preset-wind': 0.56.4
'@unocss/reset': 0.56.4
'@unocss/transformer-attributify-jsx': 0.56.4
'@unocss/transformer-attributify-jsx-babel': 0.56.4
'@unocss/transformer-compile-class': 0.56.4
'@unocss/transformer-directives': 0.56.4
'@unocss/transformer-variant-group': 0.56.4
'@unocss/vite': 0.56.4(vite@4.4.9)
vite: 4.4.9
transitivePeerDependencies:
- postcss
@@ -1116,6 +1143,10 @@ packages:
- supports-color
dev: true
/urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
dev: true
/vite@4.4.9:
resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -1144,11 +1175,11 @@ packages:
terser:
optional: true
dependencies:
esbuild: 0.18.19
postcss: 8.4.27
rollup: 3.27.2
esbuild: 0.18.20
postcss: 8.4.30
rollup: 3.29.3
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
dev: true
/which@2.0.2:
+2 -2
View File
@@ -4,6 +4,6 @@ test("authentication", async ({ page }) => {
await page.goto("http://auth:8080/");
await page.locator('input[name="username"]').fill("foo");
await page.locator('input[name="password"]').fill("bar");
await page.getByRole("button", { name: "Login" }).click();
await expect(page.locator("[data-label=all].label")).toHaveText("Containers");
await page.locator('button[type="submit"]').click();
await expect(page.getByTestId("containers")).toHaveText("Containers");
});
+2 -2
View File
@@ -16,7 +16,7 @@ test("has dashboard text", async ({ page }) => {
});
test("click on settings button", async ({ page }) => {
await page.getByRole("link", { name: "Settings" }).click();
await page.getByTestId("settings").click();
await expect(page.getByRole("heading", { name: "About" })).toBeVisible();
});
@@ -34,6 +34,6 @@ test.describe("es locale", () => {
test.use({ locale: "es" });
test("translated text", async ({ page }) => {
await expect(page.locator("[data-label=all].label")).toHaveText("Contenedores");
await expect(page.getByTestId("containers")).toHaveText("Contenedores");
});
});
+1 -1
View File
@@ -9,7 +9,7 @@ test("has right title", async ({ page }) => {
});
test("select running container", async ({ page }) => {
await page.locator("ul.menu-list").getByRole("link", { name: "dozzle" }).click();
await page.getByTestId("side-menu").getByRole("link", { name: "dozzle" }).click();
await expect(page).toHaveURL(/\/container/);
await expect(page.getByText("Accepting connections")).toBeVisible();
});
+10 -4
View File
@@ -5,14 +5,20 @@ test.beforeEach(async ({ page }) => {
});
test.describe("default", () => {
test("homepage", async ({ page }) => {
await expect(page.locator("aside")).toHaveScreenshot({});
test("homepage", async ({ page, isMobile }) => {
if (isMobile) {
await page.getByTestId("hamburger").click();
}
await expect(page.getByTestId("navigation")).toHaveScreenshot();
});
});
test.describe("dark", () => {
test.use({ colorScheme: "dark" });
test("homepage", async ({ page }) => {
await expect(page.locator("aside")).toHaveScreenshot({});
test("homepage", async ({ page, isMobile }) => {
if (isMobile) {
await page.getByTestId("hamburger").click();
}
await expect(page.getByTestId("navigation")).toHaveScreenshot();
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+1 -1
View File
@@ -5,7 +5,7 @@ require (
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.5+incompatible
github.com/docker/docker v24.0.6+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
+2
View File
@@ -65,6 +65,8 @@ github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m3
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+25 -2
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: Gesamte Speicher Auslastung
dozzle-version: Dozzle Version
all: Alle
host: Host
password: Passwort
username: Benutzername
container-name: Container Name
@@ -27,13 +28,35 @@ error:
invalid-auth: Benutzername und Passwort sind ungültig.
logs-skipped: \{total} Einträge übersprungen
container-not-found: Container nicht gefunden.
events-stream:
title: Unerwarteter Fehler
message: >-
Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie
Ihre Netzwerkeinstellungen. Wenn Sie einen Reverse-Proxy verwenden,
stellen Sie bitte sicher, dass er ordnungsgemäß konfiguriert ist.
events-timeout:
title: Etwas stimmt nicht
message: >-
Dozzle UI hat beim Verbinden mit der API ein Timeout. Bitte überprüfen Sie
die Netzwerkverbindung und versuchen Sie es erneut.
alert:
redirected:
title: Zu neuem Container umgeleitet
message: Dozzle hat Sie automatisch zu neuem Container {containerId} umgeleitet.
similar-container-found:
title: Ähnlicher Container gefunden
message: >-
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem
gleichen Host ausgeführt wird. Möchten Sie zu ihm wechseln?
title:
page-not-found: Seite nicht gefunden
login: Authentifizierung erforderlich
dashboard: 1 Container | {count} Container
settings: Einstellungen
button:
logout: Ausloggen
login: Anmeldung
settings: Einstellungen
placeholder:
search-containers: Suche Container (⌘ + k, ⌃k)
settings:
@@ -52,6 +75,6 @@ settings:
search: Aktiviere die Suche mit Dozzle mit
using-version: Du verwendest Dozzle {version}.
update-available: >-
Eine neue Version ist verfügbar! Aktualisiere auf <a href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
Eine neue Version ist verfügbar! Aktualisiere auf <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Zeige stdout und stderr Labels
automatic-redirect: Automatische Weiterleitung zu neuen Containern mit demselben Namen
+29 -5
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: Total Mem Usage
dozzle-version: Dozzle Version
all: All
host: Host
password: Password
username: Username
container-name: Container Name
@@ -24,17 +25,39 @@ tooltip:
search: Search containers (⌘ + k, ⌃k)
pin-column: Pin as column
error:
page-not-found: This page does not exist.
invalid-auth: Username and password are not valid.
page-not-found: This page does not exist
invalid-auth: Username or password are not valid
logs-skipped: Skipped {total} entries
container-not-found: Container not found.
container-not-found: Container not found
events-stream:
title: Unexpected Error
message: >-
Dozzle UI wasn't able to connect API. Please check your network settings.
If you are using a reverse proxy, please make sure it is configured
properly.
events-timeout:
title: Something is not right
message: >-
Dozzle UI timeed out while connecting to API. Please check network
connection and try again.
alert:
redirected:
title: Redirected to new container
message: Dozzle automatically redirected you to new container {containerId}.
similar-container-found:
title: Similar container found
message: >-
Dozzle found a similar container {containerId} that is running on the same
host. Do you want to switch to it?
title:
page-not-found: Page not found
login: Authentication Required
dashboard: 1 container | {count} containers
settings: Settings
button:
logout: Logout
login: Login
settings: Settings
placeholder:
search-containers: Search containers (⌘ + k, ⌃k)
settings:
@@ -53,6 +76,7 @@ settings:
search: Enable searching with Dozzle using
using-version: You are using Dozzle {version}.
update-available: >-
New version is available! Update to <a href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
New version is available! Update to <a href="{href}" target="_blank"
rel="noreferrer noopener">{nextVersion}</a>.
show-std: Show stdout and stderr labels
automatic-redirect: Automatically redirect to new containers with the same name
+22 -1
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: Uso total de la Memoria
dozzle-version: Versión de Dozzle
all: Todo
host: Host
password: Contraseña
username: Nombre de usuario
container-name: Nombre del contenedor
@@ -27,13 +28,32 @@ error:
invalid-auth: El nombre de usuario y la contraseña no son válidos.
logs-skipped: Omitidas {total} entrada/s
container-not-found: Contenedor no encontrado.
events-stream:
title: Error inesperado
message: >-
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red.
Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
events-timeout:
title: Algo no está bien
message: >-
Dozzle UI se agotó el tiempo de espera al conectarse a la API. Por favor, compruebe la conexión de red y vuelva a intentarlo.
alert:
redirected:
title: Redirigido a nuevo contenedor
message: Dozzle le redirigió automáticamente al nuevo contenedor {containerId}.
similar-container-found:
title: Contenedor similar encontrado
message: >-
Dozzle encontró un contenedor similar {containerId} que se está ejecutando en el mismo host. ¿Quieres cambiar a él?
title:
page-not-found: Página no encontrada
login: Autenticación requerida
dashboard: 1 contenedor | {count} contenedores
settings: Configuración
button:
logout: Cerrar la sesión
login: Iniciar sesión
settings: Configuración
placeholder:
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
settings:
@@ -53,5 +73,6 @@ settings:
using-version: Estás usando Dozzle {version}.
update-available: >-
¡La nueva versión está disponible! Actualizar a la
<a href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
+22 -1
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: Total de utilização da memória
dozzle-version: Versão Dozzle
all: Tudo
host: Anfitrião
password: Senha
username: Nome de usuário
container-name: Nome do contentor
@@ -27,13 +28,32 @@ error:
invalid-auth: O nome de usuário e a senha não são válidos.
logs-skipped: Saltado {total} entradas
container-not-found: Contentor não encontrado.
events-stream:
title: Erro inesperado
message: >-
Dozzle UI não conseguiu ligar à API. Por favor, verifique as suas definições de rede.
Se estiver a utilizar um proxy reverso, certifique-se de que está configurado correctamente.
events-timeout:
title: Algo não está bem
message: >-
Dozzle UI esgotou o tempo limite ao ligar à API. Por favor, verifique a ligação de rede e tente novamente.
alert:
redirected:
title: Redirecionado para novo contentor
message: Dozzle redirecionou-o automaticamente para o novo contentor {containerId}.
similar-container-found:
title: Contentor semelhante encontrado
message: >-
Dozzle encontrou um contentor semelhante {containerId} que está a ser executado no mesmo anfitrião. Quer mudar para ele?
title:
page-not-found: Página não encontrada
login: Autenticação Requerida
dashboard: 1 contentor | {count} contentores
settings: Configurações
button:
logout: Terminar sessão
login: Iniciar sessão
settings: Configurações
placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
settings:
@@ -53,5 +73,6 @@ settings:
using-version: Está a usar o Dozzle {version}.
update-available: >-
Está disponível uma nova versão! Actualização para
<a href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de saída padrão e saída de erro padrão
automatic-redirect: Redirecionar automaticamente para novos contentores com o mesmo nome
+22 -2
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: Использование памяти
dozzle-version: Версия Dozzle
all: Все
host: Хост
password: Пароль
username: Имя пользователя
container-name: Имя контейнера
@@ -27,13 +28,32 @@ error:
invalid-auth: Имя пользователя или пароль неверны.
logs-skipped: Пропущено {total} записей
container-not-found: Контейнер не найден.
events-stream:
title: Неожиданная ошибка
message: >-
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети.
Если вы используете обратный прокси, убедитесь, что он настроен правильно.
events-timeout:
title: Что-то не так
message: >-
Dozzle UI превысил время ожидания при подключении к API. Пожалуйста, проверьте сетевое подключение и повторите попытку.
alert:
redirected:
title: Перенаправлен на новый контейнер
message: Dozzle автоматически перенаправил вас на новый контейнер {containerId}.
similar-container-found:
title: Найден похожий контейнер
message: >-
Dozzle нашел похожий контейнер {containerId}, который работает на том же хосте. Хотите переключиться на него?
title:
page-not-found: Страница не найдена
login: Требуется авторизация
dashboard: 1 контейнер | {count} контейнеров
settings: Настройки
button:
logout: Выйти
login: Войти
settings: Настройки
placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
settings:
@@ -51,6 +71,6 @@ settings:
search: Включить поиск с помощью Dozzle, используя
using-version: Вы используете версию Dozzle {version}.
update-available: >-
Доступна новая версия! Обновить до <a href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
Доступна новая версия! Обновить до <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Показывать метки stdout и stderr
automatic-redirect: Автоматическое перенаправление на новые контейнеры с тем же именем.
+22 -2
View File
@@ -12,6 +12,7 @@ label:
total-mem-usage: 所有内存使用率
dozzle-version: Dozzle版本
all: 全部
host: 主机
password: 密码
username: 用户名
container-name: 容器名称
@@ -27,13 +28,32 @@ error:
invalid-auth: 用户名和密码无效。
logs-skipped: 跳过 {total} 条记录
container-not-found: 容器未找到。
events-stream:
title: 意外错误
message: >-
Dozzle UI无法连接到API。请检查您的网络设置。
如果您使用反向代理,请确保其正确配置。
events-timeout:
title: 出了点问题
message: >-
Dozzle UI在连接到API时超时。请检查网络连接并重试。
alert:
redirected:
title: 重定向到新容器
message: Dozzle自动将您重定向到新容器 {containerId}。
similar-container-found:
title: 找到相似的容器
message: >-
Dozzle发现了一个相似的容器 {containerId},它在同一主机上运行。您想切换到它吗?d
title:
page-not-found: 页面未找到
login: 需要身份验证
dashboard: 1 容器 | {count} 容器
settings: 设置
button:
logout: 退出
login: 登录
settings: 设置
placeholder:
search-containers: 搜索容器 (⌘ + k, ⌃k)
settings:
@@ -51,6 +71,6 @@ settings:
search: 使用Dozzle启用搜索
using-version: 您正在使用Dozzle {version}。
update-available: >-
新版本可用!更新到 <a href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>。
新版本可用!更新到 <a href="{href}" rel="noreferrer noopener">{nextVersion}</a>。
show-std: 显示stdout和stderr标签
automatic-redirect: 自动重定向到同名的新容器
+42 -34
View File
@@ -1,12 +1,12 @@
{
"name": "dozzle",
"version": "4.11.2",
"version": "5.0.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@8.6.12",
"packageManager": "pnpm@8.7.6",
"repository": {
"type": "git",
"url": "git+https://github.com/amir20/dozzle.git"
@@ -27,73 +27,81 @@
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.19",
"@iconify-json/carbon": "^1.1.21",
"@iconify-json/cil": "^1.1.5",
"@iconify-json/mdi": "^1.1.54",
"@iconify-json/mdi-light": "^1.1.7",
"@iconify-json/octicon": "^1.1.46",
"@intlify/unplugin-vue-i18n": "^0.12.3",
"@oruga-ui/oruga-next": "^0.6.0",
"@oruga-ui/theme-bulma": "^0.2.11",
"@vueuse/core": "^10.3.0",
"@vueuse/integrations": "^10.3.0",
"@vueuse/router": "^10.3.0",
"@iconify-json/octicon": "^1.1.49",
"@iconify-json/ph": "^1.1.6",
"@intlify/unplugin-vue-i18n": "^1.2.0",
"@vueuse/components": "^10.4.1",
"@vueuse/core": "^10.4.1",
"@vueuse/integrations": "^10.4.1",
"@vueuse/math": "^10.4.1",
"@vueuse/router": "^10.4.1",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"autoprefixer": "^10.4.16",
"d3-array": "^3.2.4",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "^3.8.1",
"date-fns": "^2.30.0",
"entities": "^4.5.0",
"fuse.js": "^6.6.2",
"lodash.debounce": "^4.0.8",
"pinia": "^2.1.6",
"postcss": "^8.4.30",
"postcss-mixins": "^9.0.4",
"splitpanes": "^3.1.5",
"tailwindcss": "^3.3.3",
"vite": "4.4.9",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4"
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@iconify-json/ph": "^1.1.6",
"@iconify-json/ic": "^1.1.14",
"@pinia/testing": "^0.1.3",
"@playwright/test": "^1.37.1",
"@types/d3-array": "^3.0.5",
"@playwright/test": "^1.38.1",
"@types/d3-array": "^3.0.8",
"@types/d3-ease": "^3.0.0",
"@types/d3-scale": "^4.0.3",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/d3-transition": "^3.0.3",
"@types/d3-scale": "^4.0.5",
"@types/d3-selection": "^3.0.7",
"@types/d3-shape": "^3.1.3",
"@types/d3-transition": "^3.0.5",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^20.5.1",
"@types/semver": "^7.5.0",
"@vitejs/plugin-vue": "4.3.1",
"@types/node": "^20.7.1",
"@types/semver": "^7.5.3",
"@vitejs/plugin-vue": "4.3.4",
"@vue/compiler-sfc": "^3.3.4",
"@vue/test-utils": "^2.4.1",
"bumpp": "^9.2.0",
"c8": "^8.0.1",
"concurrently": "^8.2.0",
"concurrently": "^8.2.1",
"eventsourcemock": "^2.0.0",
"jest-serializer-vue": "^3.1.0",
"jsdom": "^22.1.0",
"lint-staged": "^14.0.0",
"prettier": "^3.0.2",
"sass": "^1.66.1",
"lint-staged": "^14.0.1",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"simple-git-hooks": "^2.9.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"unplugin-vue-macros": "^2.4.8",
"unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.2",
"unplugin-vue-macros": "^2.6.0",
"vite": "4.4.9",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vitepress": "1.0.0-rc.4",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8"
"vitepress": "1.0.0-rc.20",
"vitest": "^0.34.5",
"vue-tsc": "^1.8.15"
},
"lint-staged": {
"*.{js,vue,css,ts,html,md}": [
+1258 -858
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
plugins: {
"postcss-mixins": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
};
+2 -2
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html>
<html class="bg-base text-base-content">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -26,7 +26,7 @@
<body>
<svg
aria-hidden="true"
class="is-hidden"
class="hidden"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
+35
View File
@@ -0,0 +1,35 @@
import type { Config } from "tailwindcss";
import DaisyUI from "daisyui";
export default {
content: ["./assets/**/*.{vue,js,ts}", "./public/index.html"],
theme: {
extend: {
animation: {
"bounce-fast": "bounce 0.5s 2 both",
},
colors: {
green: "hsl(177 100% 35%)",
red: "hsl(4 90% 58%)",
purple: "hsl(291 64% 42%)",
blue: "hsl(207 90% 54%)",
orange: "hsl(25 95% 53%)",
base: "hsl(var(--base-color) / <alpha-value>)",
"base-darker": "hsl(var(--base-darker-color) / <alpha-value>)",
"base-lighter": "hsl(var(--base-lighter-color) / <alpha-value>)",
"base-content": "hsl(var(--base-content-color) / <alpha-value>)",
primary: "hsl(var(--primary-color) / <alpha-value>)",
"primary-focus": "hsl(var(--primary-focus-color) / <alpha-value>)",
secondary: "hsl(var(--secondary-color) / <alpha-value>)",
"secondary-focus": "hsl(var(--secondary-focus-color) / <alpha-value>)",
},
},
},
plugins: [DaisyUI],
daisyui: {
themes: [],
base: false,
logs: false,
},
} satisfies Config;
+1 -7
View File
@@ -28,13 +28,7 @@ export default defineConfig(() => ({
plugins: [
VueMacros({
plugins: {
vue: Vue({
template: {
compilerOptions: {
whitespace: "preserve",
},
},
}),
vue: Vue(),
},
}),
Icons({