Compare commits
	
		
			52 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					492706367b | ||
| 
						 | 
					cbdbe65b7b | ||
| 
						 | 
					de84736ba4 | ||
| 
						 | 
					0043ed9291 | ||
| 
						 | 
					89f7d21739 | ||
| 
						 | 
					fb0b11e626 | ||
| 
						 | 
					70d72060d9 | ||
| 
						 | 
					cc8a7ee8e7 | ||
| 
						 | 
					58197f2b23 | ||
| 
						 | 
					d2473e0fcc | ||
| 
						 | 
					3a4de053b8 | ||
| 
						 | 
					b97dd31c9d | ||
| 
						 | 
					a632c744bc | ||
| 
						 | 
					7eeb7d8600 | ||
| 
						 | 
					8de492d16b | ||
| 
						 | 
					2b82a0816c | ||
| 
						 | 
					25daf6b502 | ||
| 
						 | 
					011cef8124 | ||
| 
						 | 
					d06eea2c26 | ||
| 
						 | 
					cdb6738941 | ||
| 
						 | 
					f39b6f50e3 | ||
| 
						 | 
					4d03a36940 | ||
| 
						 | 
					60fc2ab22a | ||
| 
						 | 
					0ad841a2d2 | ||
| 
						 | 
					58ce210924 | ||
| 
						 | 
					0214b212ea | ||
| 
						 | 
					ee37d7c30e | ||
| 
						 | 
					4395bc9dc5 | ||
| 
						 | 
					4ea945f0b4 | ||
| 
						 | 
					b221242db3 | ||
| 
						 | 
					ad6793f614 | ||
| 
						 | 
					72f080f795 | ||
| 
						 | 
					ee4210e1cc | ||
| 
						 | 
					fc68ba6391 | ||
| 
						 | 
					539829d00d | ||
| 
						 | 
					65c0d2a970 | ||
| 
						 | 
					696341b779 | ||
| 
						 | 
					658efd0538 | ||
| 
						 | 
					eba4cec7d6 | ||
| 
						 | 
					79f0e2127a | ||
| 
						 | 
					163b1c7e28 | ||
| 
						 | 
					db01579f04 | ||
| 
						 | 
					be7c154d6b | ||
| 
						 | 
					b1bc706de2 | ||
| 
						 | 
					40f5cb1301 | ||
| 
						 | 
					cedfbee983 | ||
| 
						 | 
					c835f51cc4 | ||
| 
						 | 
					5ab06d5906 | ||
| 
						 | 
					d44316fa9c | ||
| 
						 | 
					6ef3da9abd | ||
| 
						 | 
					752495ed6f | ||
| 
						 | 
					8f895e40bc | 
							
								
								
									
										2
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							@@ -52,8 +52,6 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: amir20/dozzle
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2.0.0
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2.0.0
 | 
			
		||||
      - name: Login to DockerHub
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,8 +17,6 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: amir20/dozzle
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2.0.0
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2.0.0
 | 
			
		||||
      - name: Login to DockerHub
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,12 +43,17 @@ jobs:
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2.0.0
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2.0.0
 | 
			
		||||
      - name: Login to DockerHub
 | 
			
		||||
        uses: docker/login-action@v2.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Build images
 | 
			
		||||
        run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
 | 
			
		||||
        run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
 | 
			
		||||
      - name: Push images
 | 
			
		||||
        run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
 | 
			
		||||
      - name: Set commit message for push
 | 
			
		||||
        if: github.event_name == 'push'
 | 
			
		||||
        run: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -6,3 +6,4 @@ static
 | 
			
		||||
dozzle
 | 
			
		||||
coverage
 | 
			
		||||
.pnpm-debug.log
 | 
			
		||||
.vscode
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,14 @@ RUN pnpm fetch --prod
 | 
			
		||||
# Copy files
 | 
			
		||||
COPY package.json .* vite.config.ts index.html ./
 | 
			
		||||
 | 
			
		||||
# Copy assets to build
 | 
			
		||||
# Copy assets and translations to build
 | 
			
		||||
COPY assets ./assets
 | 
			
		||||
COPY locales ./locales
 | 
			
		||||
 | 
			
		||||
# Install dependencies
 | 
			
		||||
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
 | 
			
		||||
 | 
			
		||||
FROM --platform=$BUILDPLATFORM golang:1.19.0-alpine AS builder
 | 
			
		||||
FROM --platform=$BUILDPLATFORM golang:1.19.1-alpine AS builder
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache ca-certificates && mkdir /dozzle
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										147
									
								
								assets/App.vue
									
									
									
									
									
								
							
							
						
						
									
										147
									
								
								assets/App.vue
									
									
									
									
									
								
							@@ -1,98 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <main>
 | 
			
		||||
    <mobile-menu v-if="isMobile && !authorizationNeeded"></mobile-menu>
 | 
			
		||||
 | 
			
		||||
    <splitpanes @resized="onResized($event)">
 | 
			
		||||
      <pane min-size="10" :size="menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
 | 
			
		||||
        <side-menu @search="showFuzzySearch"></side-menu>
 | 
			
		||||
      </pane>
 | 
			
		||||
      <pane min-size="10">
 | 
			
		||||
        <splitpanes>
 | 
			
		||||
          <pane class="has-min-height router-view">
 | 
			
		||||
            <router-view></router-view>
 | 
			
		||||
          </pane>
 | 
			
		||||
          <template v-if="!isMobile">
 | 
			
		||||
            <pane v-for="other in activeContainers" :key="other.id">
 | 
			
		||||
              <log-container
 | 
			
		||||
                :id="other.id"
 | 
			
		||||
                show-title
 | 
			
		||||
                scrollable
 | 
			
		||||
                closable
 | 
			
		||||
                @close="containerStore.removeActiveContainer(other)"
 | 
			
		||||
              ></log-container>
 | 
			
		||||
            </pane>
 | 
			
		||||
          </template>
 | 
			
		||||
        </splitpanes>
 | 
			
		||||
      </pane>
 | 
			
		||||
    </splitpanes>
 | 
			
		||||
    <button
 | 
			
		||||
      @click="collapseNav = !collapseNav"
 | 
			
		||||
      class="button is-rounded"
 | 
			
		||||
      :class="{ collapsed: collapseNav }"
 | 
			
		||||
      id="hide-nav"
 | 
			
		||||
      v-if="!isMobile && !authorizationNeeded"
 | 
			
		||||
    >
 | 
			
		||||
      <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>
 | 
			
		||||
  <router-view></router-view>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Splitpanes, Pane } from "splitpanes";
 | 
			
		||||
import { ref, onMounted, watchEffect } from "vue";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
 | 
			
		||||
import hotkeys from "hotkeys-js";
 | 
			
		||||
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
import { isMobile } from "@/composables/media";
 | 
			
		||||
import { smallerScrollbars, lightTheme, menuWidth } from "@/composables/settings";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
 | 
			
		||||
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
 | 
			
		||||
import LogContainer from "@/components/LogContainer.vue";
 | 
			
		||||
import SideMenu from "@/components/SideMenu.vue";
 | 
			
		||||
import MobileMenu from "@/components/MobileMenu.vue";
 | 
			
		||||
 | 
			
		||||
const collapseNav = ref(false);
 | 
			
		||||
const { oruga } = useProgrammatic();
 | 
			
		||||
const { authorizationNeeded } = config;
 | 
			
		||||
 | 
			
		||||
const containerStore = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (smallerScrollbars.value) {
 | 
			
		||||
    document.documentElement.classList.add("has-custom-scrollbars");
 | 
			
		||||
  }
 | 
			
		||||
  switch (lightTheme.value) {
 | 
			
		||||
    case "dark":
 | 
			
		||||
      document.documentElement.setAttribute("data-theme", "dark");
 | 
			
		||||
      break;
 | 
			
		||||
    case "light":
 | 
			
		||||
      document.documentElement.setAttribute("data-theme", "light");
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      document.documentElement.removeAttribute("data-theme");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hotkeys("command+k, ctrl+k", (event, handler) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    showFuzzySearch();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  setTitle(`${visibleContainers.value.length} containers`);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  if (smallerScrollbars.value) {
 | 
			
		||||
    document.documentElement.classList.add("has-custom-scrollbars");
 | 
			
		||||
@@ -111,59 +21,6 @@ watchEffect(() => {
 | 
			
		||||
      document.documentElement.removeAttribute("data-theme");
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function showFuzzySearch() {
 | 
			
		||||
  oruga.modal.open({
 | 
			
		||||
    // parent: this,
 | 
			
		||||
    component: FuzzySearchModal,
 | 
			
		||||
    animation: "false",
 | 
			
		||||
    width: 600,
 | 
			
		||||
    active: true,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
function onResized(e) {
 | 
			
		||||
  if (e.length == 2) {
 | 
			
		||||
    menuWidth.value = e[0].size;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
:deep(.splitpanes--vertical > .splitpanes__splitter) {
 | 
			
		||||
  min-width: 3px;
 | 
			
		||||
  background: var(--border-color);
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: var(--border-hover-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 768px) {
 | 
			
		||||
  .router-view {
 | 
			
		||||
    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;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.95);
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      left: -25px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										616
									
								
								assets/auto-imports.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										616
									
								
								assets/auto-imports.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,616 @@
 | 
			
		||||
// Generated by 'unplugin-auto-import'
 | 
			
		||||
export {}
 | 
			
		||||
declare global {
 | 
			
		||||
  const $$: typeof import('vue/macros')['$$']
 | 
			
		||||
  const $: typeof import('vue/macros')['$']
 | 
			
		||||
  const $computed: typeof import('vue/macros')['$computed']
 | 
			
		||||
  const $customRef: typeof import('vue/macros')['$customRef']
 | 
			
		||||
  const $ref: typeof import('vue/macros')['$ref']
 | 
			
		||||
  const $shallowRef: typeof import('vue/macros')['$shallowRef']
 | 
			
		||||
  const $toRef: typeof import('vue/macros')['$toRef']
 | 
			
		||||
  const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
 | 
			
		||||
  const EffectScope: typeof import('vue')['EffectScope']
 | 
			
		||||
  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
 | 
			
		||||
  const arrayEquals: typeof import('./utils/index')['arrayEquals']
 | 
			
		||||
  const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
 | 
			
		||||
  const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
 | 
			
		||||
  const computed: typeof import('vue')['computed']
 | 
			
		||||
  const computedAsync: typeof import('@vueuse/core')['computedAsync']
 | 
			
		||||
  const computedEager: typeof import('@vueuse/core')['computedEager']
 | 
			
		||||
  const computedInject: typeof import('@vueuse/core')['computedInject']
 | 
			
		||||
  const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
 | 
			
		||||
  const config: typeof import('./stores/config')['default']
 | 
			
		||||
  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 createGlobalState: typeof import('@vueuse/core')['createGlobalState']
 | 
			
		||||
  const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
 | 
			
		||||
  const createPinia: typeof import('pinia')['createPinia']
 | 
			
		||||
  const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
 | 
			
		||||
  const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
 | 
			
		||||
  const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
 | 
			
		||||
  const customRef: typeof import('vue')['customRef']
 | 
			
		||||
  const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
 | 
			
		||||
  const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
 | 
			
		||||
  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
 | 
			
		||||
  const defineComponent: typeof import('vue')['defineComponent']
 | 
			
		||||
  const defineStore: typeof import('pinia')['defineStore']
 | 
			
		||||
  const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
 | 
			
		||||
  const effectScope: typeof import('vue')['effectScope']
 | 
			
		||||
  const extendRef: typeof import('@vueuse/core')['extendRef']
 | 
			
		||||
  const flattenJSON: typeof import('./utils/index')['flattenJSON']
 | 
			
		||||
  const formatBytes: typeof import('./utils/index')['formatBytes']
 | 
			
		||||
  const getActivePinia: typeof import('pinia')['getActivePinia']
 | 
			
		||||
  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
 | 
			
		||||
  const getCurrentScope: typeof import('vue')['getCurrentScope']
 | 
			
		||||
  const getDeep: typeof import('./utils/index')['getDeep']
 | 
			
		||||
  const h: typeof import('vue')['h']
 | 
			
		||||
  const hourStyle: typeof import('./composables/settings')['hourStyle']
 | 
			
		||||
  const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
 | 
			
		||||
  const inject: typeof import('vue')['inject']
 | 
			
		||||
  const isDefined: typeof import('@vueuse/core')['isDefined']
 | 
			
		||||
  const isMobile: typeof import('./composables/media')['isMobile']
 | 
			
		||||
  const isObject: typeof import('./utils/index')['isObject']
 | 
			
		||||
  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 makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
 | 
			
		||||
  const mapActions: typeof import('pinia')['mapActions']
 | 
			
		||||
  const mapGetters: typeof import('pinia')['mapGetters']
 | 
			
		||||
  const mapState: typeof import('pinia')['mapState']
 | 
			
		||||
  const mapStores: typeof import('pinia')['mapStores']
 | 
			
		||||
  const mapWritableState: typeof import('pinia')['mapWritableState']
 | 
			
		||||
  const markRaw: typeof import('vue')['markRaw']
 | 
			
		||||
  const menuWidth: typeof import('./composables/settings')['menuWidth']
 | 
			
		||||
  const nextTick: typeof import('vue')['nextTick']
 | 
			
		||||
  const onActivated: typeof import('vue')['onActivated']
 | 
			
		||||
  const onBeforeMount: typeof import('vue')['onBeforeMount']
 | 
			
		||||
  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
 | 
			
		||||
  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
 | 
			
		||||
  const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
 | 
			
		||||
  const onDeactivated: typeof import('vue')['onDeactivated']
 | 
			
		||||
  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
 | 
			
		||||
  const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
 | 
			
		||||
  const onLongPress: typeof import('@vueuse/core')['onLongPress']
 | 
			
		||||
  const onMounted: typeof import('vue')['onMounted']
 | 
			
		||||
  const onRenderTracked: typeof import('vue')['onRenderTracked']
 | 
			
		||||
  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
 | 
			
		||||
  const onScopeDispose: typeof import('vue')['onScopeDispose']
 | 
			
		||||
  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
 | 
			
		||||
  const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
 | 
			
		||||
  const onUnmounted: typeof import('vue')['onUnmounted']
 | 
			
		||||
  const onUpdated: typeof import('vue')['onUpdated']
 | 
			
		||||
  const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
 | 
			
		||||
  const persistentVisibleKeys: typeof import('./utils/index')['persistentVisibleKeys']
 | 
			
		||||
  const provide: typeof import('vue')['provide']
 | 
			
		||||
  const reactify: typeof import('@vueuse/core')['reactify']
 | 
			
		||||
  const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
 | 
			
		||||
  const reactive: typeof import('vue')['reactive']
 | 
			
		||||
  const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
 | 
			
		||||
  const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
 | 
			
		||||
  const reactivePick: typeof import('@vueuse/core')['reactivePick']
 | 
			
		||||
  const readonly: typeof import('vue')['readonly']
 | 
			
		||||
  const ref: typeof import('vue')['ref']
 | 
			
		||||
  const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
 | 
			
		||||
  const refDebounced: typeof import('@vueuse/core')['refDebounced']
 | 
			
		||||
  const refDefault: typeof import('@vueuse/core')['refDefault']
 | 
			
		||||
  const refThrottled: typeof import('@vueuse/core')['refThrottled']
 | 
			
		||||
  const refWithControl: typeof import('@vueuse/core')['refWithControl']
 | 
			
		||||
  const resolveComponent: typeof import('vue')['resolveComponent']
 | 
			
		||||
  const resolveRef: typeof import('@vueuse/core')['resolveRef']
 | 
			
		||||
  const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
 | 
			
		||||
  const search: typeof import('./composables/settings')['search']
 | 
			
		||||
  const setActivePinia: typeof import('pinia')['setActivePinia']
 | 
			
		||||
  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
 | 
			
		||||
  const setTitle: typeof import('./composables/title')['setTitle']
 | 
			
		||||
  const settings: typeof import('./composables/settings')['settings']
 | 
			
		||||
  const shallowReactive: typeof import('vue')['shallowReactive']
 | 
			
		||||
  const shallowReadonly: typeof import('vue')['shallowReadonly']
 | 
			
		||||
  const shallowRef: typeof import('vue')['shallowRef']
 | 
			
		||||
  const showAllContainers: typeof import('./composables/settings')['showAllContainers']
 | 
			
		||||
  const showTimestamp: typeof import('./composables/settings')['showTimestamp']
 | 
			
		||||
  const size: typeof import('./composables/settings')['size']
 | 
			
		||||
  const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
 | 
			
		||||
  const softWrap: typeof import('./composables/settings')['softWrap']
 | 
			
		||||
  const storeToRefs: typeof import('pinia')['storeToRefs']
 | 
			
		||||
  const stripVersion: typeof import('./utils/index')['stripVersion']
 | 
			
		||||
  const syncRef: typeof import('@vueuse/core')['syncRef']
 | 
			
		||||
  const syncRefs: typeof import('@vueuse/core')['syncRefs']
 | 
			
		||||
  const templateRef: typeof import('@vueuse/core')['templateRef']
 | 
			
		||||
  const throttledRef: typeof import('@vueuse/core')['throttledRef']
 | 
			
		||||
  const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
 | 
			
		||||
  const toRaw: typeof import('vue')['toRaw']
 | 
			
		||||
  const toReactive: typeof import('@vueuse/core')['toReactive']
 | 
			
		||||
  const toRef: typeof import('vue')['toRef']
 | 
			
		||||
  const toRefs: typeof import('vue')['toRefs']
 | 
			
		||||
  const triggerRef: typeof import('vue')['triggerRef']
 | 
			
		||||
  const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
 | 
			
		||||
  const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
 | 
			
		||||
  const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
 | 
			
		||||
  const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
 | 
			
		||||
  const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
 | 
			
		||||
  const unref: typeof import('vue')['unref']
 | 
			
		||||
  const unrefElement: typeof import('@vueuse/core')['unrefElement']
 | 
			
		||||
  const until: typeof import('@vueuse/core')['until']
 | 
			
		||||
  const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
 | 
			
		||||
  const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
 | 
			
		||||
  const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
 | 
			
		||||
  const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
 | 
			
		||||
  const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
 | 
			
		||||
  const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
 | 
			
		||||
  const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
 | 
			
		||||
  const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
 | 
			
		||||
  const useArraySome: typeof import('@vueuse/core')['useArraySome']
 | 
			
		||||
  const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
 | 
			
		||||
  const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
 | 
			
		||||
  const useAttrs: typeof import('vue')['useAttrs']
 | 
			
		||||
  const useBase64: typeof import('@vueuse/core')['useBase64']
 | 
			
		||||
  const useBattery: typeof import('@vueuse/core')['useBattery']
 | 
			
		||||
  const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
 | 
			
		||||
  const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
 | 
			
		||||
  const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
 | 
			
		||||
  const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
 | 
			
		||||
  const useCached: typeof import('@vueuse/core')['useCached']
 | 
			
		||||
  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 useContainerStore: typeof import('./stores/container')['useContainerStore']
 | 
			
		||||
  const useCounter: typeof import('@vueuse/core')['useCounter']
 | 
			
		||||
  const useCssModule: typeof import('vue')['useCssModule']
 | 
			
		||||
  const useCssVar: typeof import('@vueuse/core')['useCssVar']
 | 
			
		||||
  const useCssVars: typeof import('vue')['useCssVars']
 | 
			
		||||
  const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
 | 
			
		||||
  const useCycleList: typeof import('@vueuse/core')['useCycleList']
 | 
			
		||||
  const useDark: typeof import('@vueuse/core')['useDark']
 | 
			
		||||
  const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
 | 
			
		||||
  const useDebounce: typeof import('@vueuse/core')['useDebounce']
 | 
			
		||||
  const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
 | 
			
		||||
  const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
 | 
			
		||||
  const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
 | 
			
		||||
  const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
 | 
			
		||||
  const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
 | 
			
		||||
  const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
 | 
			
		||||
  const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
 | 
			
		||||
  const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
 | 
			
		||||
  const useDraggable: typeof import('@vueuse/core')['useDraggable']
 | 
			
		||||
  const useDropZone: typeof import('@vueuse/core')['useDropZone']
 | 
			
		||||
  const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
 | 
			
		||||
  const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
 | 
			
		||||
  const useElementHover: typeof import('@vueuse/core')['useElementHover']
 | 
			
		||||
  const useElementSize: typeof import('@vueuse/core')['useElementSize']
 | 
			
		||||
  const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
 | 
			
		||||
  const useEventBus: typeof import('@vueuse/core')['useEventBus']
 | 
			
		||||
  const useEventListener: typeof import('@vueuse/core')['useEventListener']
 | 
			
		||||
  const useEventSource: typeof import('@vueuse/core')['useEventSource']
 | 
			
		||||
  const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
 | 
			
		||||
  const useFavicon: typeof import('@vueuse/core')['useFavicon']
 | 
			
		||||
  const useFetch: typeof import('@vueuse/core')['useFetch']
 | 
			
		||||
  const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
 | 
			
		||||
  const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
 | 
			
		||||
  const useFocus: typeof import('@vueuse/core')['useFocus']
 | 
			
		||||
  const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
 | 
			
		||||
  const useFps: typeof import('@vueuse/core')['useFps']
 | 
			
		||||
  const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
 | 
			
		||||
  const useGamepad: typeof import('@vueuse/core')['useGamepad']
 | 
			
		||||
  const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
 | 
			
		||||
  const useHead: typeof import('@vueuse/head')['useHead']
 | 
			
		||||
  const useI18n: typeof import('vue-i18n')['useI18n']
 | 
			
		||||
  const useIdle: typeof import('@vueuse/core')['useIdle']
 | 
			
		||||
  const useImage: typeof import('@vueuse/core')['useImage']
 | 
			
		||||
  const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
 | 
			
		||||
  const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
 | 
			
		||||
  const useInterval: typeof import('@vueuse/core')['useInterval']
 | 
			
		||||
  const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
 | 
			
		||||
  const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
 | 
			
		||||
  const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
 | 
			
		||||
  const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
 | 
			
		||||
  const useLogStream: typeof import('./composables/eventsource')['useLogStream']
 | 
			
		||||
  const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
 | 
			
		||||
  const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
 | 
			
		||||
  const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
 | 
			
		||||
  const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
 | 
			
		||||
  const useMemoize: typeof import('@vueuse/core')['useMemoize']
 | 
			
		||||
  const useMemory: typeof import('@vueuse/core')['useMemory']
 | 
			
		||||
  const useMounted: typeof import('@vueuse/core')['useMounted']
 | 
			
		||||
  const useMouse: typeof import('@vueuse/core')['useMouse']
 | 
			
		||||
  const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
 | 
			
		||||
  const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
 | 
			
		||||
  const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
 | 
			
		||||
  const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
 | 
			
		||||
  const useNetwork: typeof import('@vueuse/core')['useNetwork']
 | 
			
		||||
  const useNow: typeof import('@vueuse/core')['useNow']
 | 
			
		||||
  const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
 | 
			
		||||
  const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
 | 
			
		||||
  const useOnline: typeof import('@vueuse/core')['useOnline']
 | 
			
		||||
  const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
 | 
			
		||||
  const useParallax: typeof import('@vueuse/core')['useParallax']
 | 
			
		||||
  const usePermission: typeof import('@vueuse/core')['usePermission']
 | 
			
		||||
  const usePointer: typeof import('@vueuse/core')['usePointer']
 | 
			
		||||
  const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
 | 
			
		||||
  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 useRafFn: typeof import('@vueuse/core')['useRafFn']
 | 
			
		||||
  const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
 | 
			
		||||
  const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
 | 
			
		||||
  const useRoute: typeof import('vue-router')['useRoute']
 | 
			
		||||
  const useRouter: typeof import('vue-router')['useRouter']
 | 
			
		||||
  const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
 | 
			
		||||
  const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
 | 
			
		||||
  const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
 | 
			
		||||
  const useScroll: typeof import('@vueuse/core')['useScroll']
 | 
			
		||||
  const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
 | 
			
		||||
  const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
 | 
			
		||||
  const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
 | 
			
		||||
  const useShare: typeof import('@vueuse/core')['useShare']
 | 
			
		||||
  const useSlots: typeof import('vue')['useSlots']
 | 
			
		||||
  const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
 | 
			
		||||
  const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
 | 
			
		||||
  const useStepper: typeof import('@vueuse/core')['useStepper']
 | 
			
		||||
  const useStorage: typeof import('@vueuse/core')['useStorage']
 | 
			
		||||
  const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
 | 
			
		||||
  const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
 | 
			
		||||
  const useSupported: typeof import('@vueuse/core')['useSupported']
 | 
			
		||||
  const useSwipe: typeof import('@vueuse/core')['useSwipe']
 | 
			
		||||
  const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
 | 
			
		||||
  const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
 | 
			
		||||
  const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
 | 
			
		||||
  const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
 | 
			
		||||
  const useThrottle: typeof import('@vueuse/core')['useThrottle']
 | 
			
		||||
  const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
 | 
			
		||||
  const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
 | 
			
		||||
  const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
 | 
			
		||||
  const useTimeout: typeof import('@vueuse/core')['useTimeout']
 | 
			
		||||
  const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
 | 
			
		||||
  const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
 | 
			
		||||
  const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
 | 
			
		||||
  const useTitle: typeof import('@vueuse/core')['useTitle']
 | 
			
		||||
  const useToNumber: typeof import('@vueuse/core')['useToNumber']
 | 
			
		||||
  const useToString: typeof import('@vueuse/core')['useToString']
 | 
			
		||||
  const useToggle: typeof import('@vueuse/core')['useToggle']
 | 
			
		||||
  const useTransition: typeof import('@vueuse/core')['useTransition']
 | 
			
		||||
  const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
 | 
			
		||||
  const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
 | 
			
		||||
  const useVModel: typeof import('@vueuse/core')['useVModel']
 | 
			
		||||
  const useVModels: typeof import('@vueuse/core')['useVModels']
 | 
			
		||||
  const useVibrate: typeof import('@vueuse/core')['useVibrate']
 | 
			
		||||
  const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
 | 
			
		||||
  const useVisibleFilter: typeof import('./composables/visible')['useVisibleFilter']
 | 
			
		||||
  const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
 | 
			
		||||
  const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
 | 
			
		||||
  const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
 | 
			
		||||
  const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
 | 
			
		||||
  const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
 | 
			
		||||
  const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
 | 
			
		||||
  const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
 | 
			
		||||
  const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
 | 
			
		||||
  const watch: typeof import('vue')['watch']
 | 
			
		||||
  const watchArray: typeof import('@vueuse/core')['watchArray']
 | 
			
		||||
  const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
 | 
			
		||||
  const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
 | 
			
		||||
  const watchEffect: typeof import('vue')['watchEffect']
 | 
			
		||||
  const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
 | 
			
		||||
  const watchOnce: typeof import('@vueuse/core')['watchOnce']
 | 
			
		||||
  const watchPausable: typeof import('@vueuse/core')['watchPausable']
 | 
			
		||||
  const watchPostEffect: typeof import('vue')['watchPostEffect']
 | 
			
		||||
  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
 | 
			
		||||
  const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
 | 
			
		||||
  const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
 | 
			
		||||
  const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
 | 
			
		||||
  const whenever: typeof import('@vueuse/core')['whenever']
 | 
			
		||||
}
 | 
			
		||||
// for vue template auto import
 | 
			
		||||
import { UnwrapRef } from 'vue'
 | 
			
		||||
declare module '@vue/runtime-core' {
 | 
			
		||||
  interface ComponentCustomProperties {
 | 
			
		||||
    readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
 | 
			
		||||
    readonly $: UnwrapRef<typeof import('vue/macros')['$']>
 | 
			
		||||
    readonly $computed: UnwrapRef<typeof import('vue/macros')['$computed']>
 | 
			
		||||
    readonly $customRef: UnwrapRef<typeof import('vue/macros')['$customRef']>
 | 
			
		||||
    readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
 | 
			
		||||
    readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
 | 
			
		||||
    readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
 | 
			
		||||
    readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
 | 
			
		||||
    readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
 | 
			
		||||
    readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
 | 
			
		||||
    readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
 | 
			
		||||
    readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
 | 
			
		||||
    readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
 | 
			
		||||
    readonly computed: UnwrapRef<typeof import('vue')['computed']>
 | 
			
		||||
    readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
 | 
			
		||||
    readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
 | 
			
		||||
    readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
 | 
			
		||||
    readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
 | 
			
		||||
    readonly config: UnwrapRef<typeof import('./stores/config')['default']>
 | 
			
		||||
    readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
 | 
			
		||||
    readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
 | 
			
		||||
    readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
 | 
			
		||||
    readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
 | 
			
		||||
    readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
 | 
			
		||||
    readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
 | 
			
		||||
    readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
 | 
			
		||||
    readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
 | 
			
		||||
    readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
 | 
			
		||||
    readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
 | 
			
		||||
    readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
 | 
			
		||||
    readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
 | 
			
		||||
    readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
 | 
			
		||||
    readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
 | 
			
		||||
    readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
 | 
			
		||||
    readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
 | 
			
		||||
    readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
 | 
			
		||||
    readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
 | 
			
		||||
    readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
 | 
			
		||||
    readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
 | 
			
		||||
    readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
 | 
			
		||||
    readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
 | 
			
		||||
    readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
 | 
			
		||||
    readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
 | 
			
		||||
    readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
 | 
			
		||||
    readonly h: UnwrapRef<typeof import('vue')['h']>
 | 
			
		||||
    readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
 | 
			
		||||
    readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
 | 
			
		||||
    readonly inject: UnwrapRef<typeof import('vue')['inject']>
 | 
			
		||||
    readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
 | 
			
		||||
    readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
 | 
			
		||||
    readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
 | 
			
		||||
    readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
 | 
			
		||||
    readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
 | 
			
		||||
    readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
 | 
			
		||||
    readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
 | 
			
		||||
    readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
 | 
			
		||||
    readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
 | 
			
		||||
    readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
 | 
			
		||||
    readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
 | 
			
		||||
    readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
 | 
			
		||||
    readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
 | 
			
		||||
    readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
 | 
			
		||||
    readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
 | 
			
		||||
    readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
 | 
			
		||||
    readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
 | 
			
		||||
    readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
 | 
			
		||||
    readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
 | 
			
		||||
    readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
 | 
			
		||||
    readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
 | 
			
		||||
    readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
 | 
			
		||||
    readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
 | 
			
		||||
    readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
 | 
			
		||||
    readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
 | 
			
		||||
    readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
 | 
			
		||||
    readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
 | 
			
		||||
    readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
 | 
			
		||||
    readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
 | 
			
		||||
    readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
 | 
			
		||||
    readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
 | 
			
		||||
    readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
 | 
			
		||||
    readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
 | 
			
		||||
    readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
 | 
			
		||||
    readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
 | 
			
		||||
    readonly persistentVisibleKeys: UnwrapRef<typeof import('./utils/index')['persistentVisibleKeys']>
 | 
			
		||||
    readonly provide: UnwrapRef<typeof import('vue')['provide']>
 | 
			
		||||
    readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
 | 
			
		||||
    readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
 | 
			
		||||
    readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
 | 
			
		||||
    readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
 | 
			
		||||
    readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
 | 
			
		||||
    readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
 | 
			
		||||
    readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
 | 
			
		||||
    readonly ref: UnwrapRef<typeof import('vue')['ref']>
 | 
			
		||||
    readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
 | 
			
		||||
    readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
 | 
			
		||||
    readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
 | 
			
		||||
    readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
 | 
			
		||||
    readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
 | 
			
		||||
    readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
 | 
			
		||||
    readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
 | 
			
		||||
    readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
 | 
			
		||||
    readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
 | 
			
		||||
    readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
 | 
			
		||||
    readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
 | 
			
		||||
    readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
 | 
			
		||||
    readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
 | 
			
		||||
    readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
 | 
			
		||||
    readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
 | 
			
		||||
    readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
 | 
			
		||||
    readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
 | 
			
		||||
    readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
 | 
			
		||||
    readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
 | 
			
		||||
    readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
 | 
			
		||||
    readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
 | 
			
		||||
    readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
 | 
			
		||||
    readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
 | 
			
		||||
    readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
 | 
			
		||||
    readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
 | 
			
		||||
    readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
 | 
			
		||||
    readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
 | 
			
		||||
    readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
 | 
			
		||||
    readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
 | 
			
		||||
    readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
 | 
			
		||||
    readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
 | 
			
		||||
    readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
 | 
			
		||||
    readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
 | 
			
		||||
    readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
 | 
			
		||||
    readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
 | 
			
		||||
    readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
 | 
			
		||||
    readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
 | 
			
		||||
    readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
 | 
			
		||||
    readonly unref: UnwrapRef<typeof import('vue')['unref']>
 | 
			
		||||
    readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
 | 
			
		||||
    readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
 | 
			
		||||
    readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
 | 
			
		||||
    readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
 | 
			
		||||
    readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
 | 
			
		||||
    readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
 | 
			
		||||
    readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
 | 
			
		||||
    readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
 | 
			
		||||
    readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
 | 
			
		||||
    readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
 | 
			
		||||
    readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
 | 
			
		||||
    readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
 | 
			
		||||
    readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
 | 
			
		||||
    readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
 | 
			
		||||
    readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
 | 
			
		||||
    readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
 | 
			
		||||
    readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
 | 
			
		||||
    readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
 | 
			
		||||
    readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
 | 
			
		||||
    readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
 | 
			
		||||
    readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
 | 
			
		||||
    readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
 | 
			
		||||
    readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
 | 
			
		||||
    readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
 | 
			
		||||
    readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
 | 
			
		||||
    readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
 | 
			
		||||
    readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
 | 
			
		||||
    readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
 | 
			
		||||
    readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
 | 
			
		||||
    readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
 | 
			
		||||
    readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
 | 
			
		||||
    readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
 | 
			
		||||
    readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
 | 
			
		||||
    readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
 | 
			
		||||
    readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
 | 
			
		||||
    readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
 | 
			
		||||
    readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
 | 
			
		||||
    readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
 | 
			
		||||
    readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
 | 
			
		||||
    readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
 | 
			
		||||
    readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
 | 
			
		||||
    readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
 | 
			
		||||
    readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
 | 
			
		||||
    readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
 | 
			
		||||
    readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
 | 
			
		||||
    readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
 | 
			
		||||
    readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
 | 
			
		||||
    readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
 | 
			
		||||
    readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
 | 
			
		||||
    readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
 | 
			
		||||
    readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
 | 
			
		||||
    readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
 | 
			
		||||
    readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
 | 
			
		||||
    readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
 | 
			
		||||
    readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
 | 
			
		||||
    readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
 | 
			
		||||
    readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
 | 
			
		||||
    readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
 | 
			
		||||
    readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
 | 
			
		||||
    readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
 | 
			
		||||
    readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
 | 
			
		||||
    readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
 | 
			
		||||
    readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
 | 
			
		||||
    readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
 | 
			
		||||
    readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
 | 
			
		||||
    readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
 | 
			
		||||
    readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
 | 
			
		||||
    readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
 | 
			
		||||
    readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
 | 
			
		||||
    readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
 | 
			
		||||
    readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
 | 
			
		||||
    readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
 | 
			
		||||
    readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
 | 
			
		||||
    readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
 | 
			
		||||
    readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
 | 
			
		||||
    readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
 | 
			
		||||
    readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
 | 
			
		||||
    readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
 | 
			
		||||
    readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
 | 
			
		||||
    readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
 | 
			
		||||
    readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
 | 
			
		||||
    readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
 | 
			
		||||
    readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
 | 
			
		||||
    readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
 | 
			
		||||
    readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
 | 
			
		||||
    readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
 | 
			
		||||
    readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
 | 
			
		||||
    readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
 | 
			
		||||
    readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
 | 
			
		||||
    readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
 | 
			
		||||
    readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
 | 
			
		||||
    readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
 | 
			
		||||
    readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
 | 
			
		||||
    readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
 | 
			
		||||
    readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
 | 
			
		||||
    readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
 | 
			
		||||
    readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
 | 
			
		||||
    readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
 | 
			
		||||
    readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
 | 
			
		||||
    readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
 | 
			
		||||
    readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
 | 
			
		||||
    readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
 | 
			
		||||
    readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
 | 
			
		||||
    readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
 | 
			
		||||
    readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
 | 
			
		||||
    readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
 | 
			
		||||
    readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
 | 
			
		||||
    readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
 | 
			
		||||
    readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
 | 
			
		||||
    readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
 | 
			
		||||
    readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
 | 
			
		||||
    readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
 | 
			
		||||
    readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
 | 
			
		||||
    readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
 | 
			
		||||
    readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
 | 
			
		||||
    readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
 | 
			
		||||
    readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
 | 
			
		||||
    readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
 | 
			
		||||
    readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
 | 
			
		||||
    readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
 | 
			
		||||
    readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
 | 
			
		||||
    readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
 | 
			
		||||
    readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
 | 
			
		||||
    readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
 | 
			
		||||
    readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
 | 
			
		||||
    readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
 | 
			
		||||
    readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
 | 
			
		||||
    readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
 | 
			
		||||
    readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
 | 
			
		||||
    readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
 | 
			
		||||
    readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
 | 
			
		||||
    readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
 | 
			
		||||
    readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
 | 
			
		||||
    readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
 | 
			
		||||
    readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
 | 
			
		||||
    readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
 | 
			
		||||
    readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
 | 
			
		||||
    readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
 | 
			
		||||
    readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
 | 
			
		||||
    readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
 | 
			
		||||
    readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
 | 
			
		||||
    readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
 | 
			
		||||
    readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
 | 
			
		||||
    readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
 | 
			
		||||
    readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
 | 
			
		||||
    readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
 | 
			
		||||
    readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
 | 
			
		||||
    readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
 | 
			
		||||
    readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
 | 
			
		||||
    readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
 | 
			
		||||
    readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
 | 
			
		||||
    readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
 | 
			
		||||
    readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
 | 
			
		||||
    readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
 | 
			
		||||
    readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
 | 
			
		||||
    readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
 | 
			
		||||
    readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
 | 
			
		||||
    readonly watch: UnwrapRef<typeof import('vue')['watch']>
 | 
			
		||||
    readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
 | 
			
		||||
    readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
 | 
			
		||||
    readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
 | 
			
		||||
    readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
 | 
			
		||||
    readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
 | 
			
		||||
    readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
 | 
			
		||||
    readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
 | 
			
		||||
    readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
 | 
			
		||||
    readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
 | 
			
		||||
    readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
 | 
			
		||||
    readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
 | 
			
		||||
    readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
 | 
			
		||||
    readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								assets/components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								assets/components.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -10,16 +10,22 @@ declare module '@vue/runtime-core' {
 | 
			
		||||
    CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
 | 
			
		||||
    CilColumns: typeof import('~icons/cil/columns')['default']
 | 
			
		||||
    CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
 | 
			
		||||
    ContainerStat: typeof import('./components/ContainerStat.vue')['default']
 | 
			
		||||
    ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
 | 
			
		||||
    ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
 | 
			
		||||
    ComplexPayload: typeof import('./components/LogViewer/ComplexPayload.vue')['default']
 | 
			
		||||
    ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
 | 
			
		||||
    ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
 | 
			
		||||
    copy: typeof import('./components/LogViewer/DockerEventLogItem copy.vue')['default']
 | 
			
		||||
    DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
 | 
			
		||||
    DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
 | 
			
		||||
    FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
 | 
			
		||||
    FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
 | 
			
		||||
    InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
 | 
			
		||||
    JSONPayload: typeof import('./components/LogViewer/JSONPayload.vue')['default']
 | 
			
		||||
    LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
 | 
			
		||||
    LogContainer: typeof import('./components/LogContainer.vue')['default']
 | 
			
		||||
    LogEventSource: typeof import('./components/LogEventSource.vue')['default']
 | 
			
		||||
    LogViewer: typeof import('./components/LogViewer.vue')['default']
 | 
			
		||||
    LogViewerWithSource: typeof import('./components/LogViewerWithSource.vue')['default']
 | 
			
		||||
    LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
 | 
			
		||||
    LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
 | 
			
		||||
    LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
 | 
			
		||||
    LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
 | 
			
		||||
    MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
 | 
			
		||||
    MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
 | 
			
		||||
    MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
 | 
			
		||||
@@ -38,5 +44,8 @@ declare module '@vue/runtime-core' {
 | 
			
		||||
    ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
 | 
			
		||||
    Search: typeof import('./components/Search.vue')['default']
 | 
			
		||||
    SideMenu: typeof import('./components/SideMenu.vue')['default']
 | 
			
		||||
    SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
 | 
			
		||||
    SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
 | 
			
		||||
    StringPayload: typeof import('./components/LogViewer/StringPayload.vue')['default']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="is-size-7 is-uppercase columns is-marginless is-mobile">
 | 
			
		||||
    <div class="column is-narrow has-text-weight-bold">
 | 
			
		||||
      {{ state }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="column is-narrow" v-if="stat.memoryUsage !== null">
 | 
			
		||||
      <span class="has-text-weight-light has-spacer">mem</span>
 | 
			
		||||
      <span class="has-text-weight-bold">
 | 
			
		||||
        {{ formatBytes(stat.memoryUsage) }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="column is-narrow" v-if="stat.cpu !== null">
 | 
			
		||||
      <span class="has-text-weight-light has-spacer">load</span>
 | 
			
		||||
      <span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ContainerStat } from "@/types/Container";
 | 
			
		||||
import { PropType } from "vue";
 | 
			
		||||
import { formatBytes } from "@/utils";
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  stat: {
 | 
			
		||||
    type: Object as PropType<ContainerStat>,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  state: String,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.has-spacer {
 | 
			
		||||
  &::after {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -34,18 +34,11 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import fuzzysort from "fuzzysort";
 | 
			
		||||
import { computed, nextTick, onMounted, ref, reactive } from "vue";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { Container } from "@/types/Container";
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  maxResults: {
 | 
			
		||||
    default: 20,
 | 
			
		||||
    type: Number,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const { maxResults = 20 } = defineProps<{
 | 
			
		||||
  maxResults: number;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(["close"]);
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +61,7 @@ const preparedContainers = computed(() =>
 | 
			
		||||
 | 
			
		||||
const results = computed(() => {
 | 
			
		||||
  const options = {
 | 
			
		||||
    limit: props.maxResults,
 | 
			
		||||
    limit: maxResults,
 | 
			
		||||
    key: "preparedName",
 | 
			
		||||
  };
 | 
			
		||||
  if (query.value) {
 | 
			
		||||
@@ -88,7 +81,7 @@ const results = computed(() => {
 | 
			
		||||
onMounted(() => nextTick(() => autocomplete.value?.focus()));
 | 
			
		||||
 | 
			
		||||
function selected(item: { id: string; name: string }) {
 | 
			
		||||
  router.push({ name: "container", params: { id: item.id } });
 | 
			
		||||
  router.push({ name: "container-id", params: { id: item.id } });
 | 
			
		||||
  emit("close");
 | 
			
		||||
}
 | 
			
		||||
function addColumn(container: Container) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,30 +9,28 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  onLoadMore: Function,
 | 
			
		||||
  enabled: Boolean,
 | 
			
		||||
});
 | 
			
		||||
const { onLoadMore = () => {}, enabled } = defineProps<{
 | 
			
		||||
  onLoadMore: () => void;
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const isLoading = ref(false);
 | 
			
		||||
const root = ref<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
const observer = new IntersectionObserver(async (entries) => {
 | 
			
		||||
  if (entries[0].intersectionRatio <= 0) return;
 | 
			
		||||
  if (props.onLoadMore && props.enabled) {
 | 
			
		||||
    const scrollingParent = root.value.closest("[data-scrolling]") || document.documentElement;
 | 
			
		||||
  if (onLoadMore && enabled) {
 | 
			
		||||
    const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
 | 
			
		||||
    const previousHeight = scrollingParent.scrollHeight;
 | 
			
		||||
    isLoading.value = true;
 | 
			
		||||
    await props.onLoadMore();
 | 
			
		||||
    await onLoadMore();
 | 
			
		||||
    isLoading.value = false;
 | 
			
		||||
    await nextTick();
 | 
			
		||||
    scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => observer.observe(root.value));
 | 
			
		||||
onMounted(() => observer.observe(root.value!));
 | 
			
		||||
onUnmounted(() => observer.disconnect());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="level-right">
 | 
			
		||||
          <div class="level-item">Clear</div>
 | 
			
		||||
          <div class="level-item">{{ $t("toolbar.clear") }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </a>
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="level-right">
 | 
			
		||||
          <div class="level-item">Download</div>
 | 
			
		||||
          <div class="level-item">{{ $t("toolbar.download") }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </a>
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="level-right">
 | 
			
		||||
          <div class="level-item">Search</div>
 | 
			
		||||
          <div class="level-item">{{ $t("toolbar.search") }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </a>
 | 
			
		||||
@@ -41,32 +41,24 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, PropType } from "vue";
 | 
			
		||||
import { type ComputedRef } from "vue";
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
import hotkeys from "hotkeys-js";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { Container } from "@/types/Container";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
 | 
			
		||||
const { showSearch } = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
const { base } = config;
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  onClearClicked: {
 | 
			
		||||
    type: Function as PropType<(e: Event) => void>,
 | 
			
		||||
    default: (e: Event) => {},
 | 
			
		||||
  },
 | 
			
		||||
  container: {
 | 
			
		||||
    type: Object as () => Container,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const { onClearClicked = (e: Event) => {} } = defineProps<{
 | 
			
		||||
  onClearClicked: (e: Event) => void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const onHotkey = (event: Event) => {
 | 
			
		||||
  props.onClearClicked(event);
 | 
			
		||||
  onClearClicked(event);
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const container = inject("container") as ComputedRef<Container>;
 | 
			
		||||
 | 
			
		||||
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
 | 
			
		||||
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,133 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
 | 
			
		||||
  <slot :messages="messages"></slot>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, ref, watch, onUnmounted } from "vue";
 | 
			
		||||
import debounce from "lodash.debounce";
 | 
			
		||||
 | 
			
		||||
import { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
import InfiniteLoader from "./InfiniteLoader.vue";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { id } = toRefs(props);
 | 
			
		||||
const emit = defineEmits(["loading-more"]);
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const container = store.currentContainer(id);
 | 
			
		||||
 | 
			
		||||
const messages = ref<LogEntry[]>([]);
 | 
			
		||||
const buffer = ref<LogEntry[]>([]);
 | 
			
		||||
 | 
			
		||||
function flushNow() {
 | 
			
		||||
  messages.value.push(...buffer.value);
 | 
			
		||||
  buffer.value = [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
 | 
			
		||||
 | 
			
		||||
let es: EventSource | null = null;
 | 
			
		||||
let lastEventId = "";
 | 
			
		||||
 | 
			
		||||
function connect({ clear } = { clear: true }) {
 | 
			
		||||
  es?.close();
 | 
			
		||||
 | 
			
		||||
  if (clear) {
 | 
			
		||||
    flushBuffer.cancel();
 | 
			
		||||
    messages.value = [];
 | 
			
		||||
    buffer.value = [];
 | 
			
		||||
    lastEventId = "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  es = new EventSource(`${config.base}/api/logs/stream?id=${props.id}&lastEventId=${lastEventId}`);
 | 
			
		||||
  es.addEventListener("container-stopped", () => {
 | 
			
		||||
    es?.close();
 | 
			
		||||
    es = null;
 | 
			
		||||
    buffer.value.push({
 | 
			
		||||
      event: "container-stopped",
 | 
			
		||||
      message: "Container stopped",
 | 
			
		||||
      date: new Date(),
 | 
			
		||||
      key: new Date().toString(),
 | 
			
		||||
    });
 | 
			
		||||
    flushBuffer();
 | 
			
		||||
    flushBuffer.flush();
 | 
			
		||||
  });
 | 
			
		||||
  es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
 | 
			
		||||
  es.onmessage = (e) => {
 | 
			
		||||
    lastEventId = e.lastEventId;
 | 
			
		||||
    if (e.data) {
 | 
			
		||||
      buffer.value.push(parseMessage(e.data));
 | 
			
		||||
      flushBuffer();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadOlderLogs() {
 | 
			
		||||
  if (messages.value.length < 300) return;
 | 
			
		||||
 | 
			
		||||
  emit("loading-more", true);
 | 
			
		||||
  const to = messages.value[0].date;
 | 
			
		||||
  const last = messages.value[299].date;
 | 
			
		||||
  const delta = to.getTime() - last.getTime();
 | 
			
		||||
  const from = new Date(to.getTime() + delta);
 | 
			
		||||
  const logs = await (
 | 
			
		||||
    await fetch(`${config.base}/api/logs?id=${props.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
 | 
			
		||||
  ).text();
 | 
			
		||||
  if (logs) {
 | 
			
		||||
    const newMessages = logs
 | 
			
		||||
      .trim()
 | 
			
		||||
      .split("\n")
 | 
			
		||||
      .map((line) => parseMessage(line));
 | 
			
		||||
    messages.value.unshift(...newMessages);
 | 
			
		||||
  }
 | 
			
		||||
  emit("loading-more", false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseMessage(data: String): LogEntry {
 | 
			
		||||
  let i = data.indexOf(" ");
 | 
			
		||||
  if (i == -1) {
 | 
			
		||||
    i = data.length;
 | 
			
		||||
  }
 | 
			
		||||
  const key = data.substring(0, i);
 | 
			
		||||
  const date = new Date(key);
 | 
			
		||||
  const message = data.substring(i + 1);
 | 
			
		||||
  return { key, date, message };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => container.value.state,
 | 
			
		||||
  (newValue, oldValue) => {
 | 
			
		||||
    console.log("LogEventSource: container changed", newValue, oldValue);
 | 
			
		||||
    if (newValue == "running" && newValue != oldValue) {
 | 
			
		||||
      buffer.value.push({
 | 
			
		||||
        event: "container-started",
 | 
			
		||||
        message: "Container started",
 | 
			
		||||
        date: new Date(),
 | 
			
		||||
        key: new Date().toString(),
 | 
			
		||||
      });
 | 
			
		||||
      connect({ clear: false });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  if (es) {
 | 
			
		||||
    es.close();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
connect();
 | 
			
		||||
watch(id, () => connect());
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  clear: () => (messages.value = []),
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										48
									
								
								assets/components/LogViewer/ComplexLogItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								assets/components/LogViewer/ComplexLogItem.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <ul class="fields" @click="expanded = !expanded">
 | 
			
		||||
      <li v-for="(value, name) in logEntry.message">
 | 
			
		||||
        <template v-if="value">
 | 
			
		||||
          <span class="has-text-grey">{{ name }}=</span>
 | 
			
		||||
          <span class="has-text-weight-bold" v-html="markSearch(value)"></span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { type ComplexLogEntry } from "@/models/LogEntry";
 | 
			
		||||
 | 
			
		||||
const { markSearch } = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  logEntry: ComplexLogEntry;
 | 
			
		||||
  visibleKeys: string[][];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let expanded = $ref(false);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.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;
 | 
			
		||||
      font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  li {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin-left: 1em;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										33
									
								
								assets/components/LogViewer/ContainerStat.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								assets/components/LogViewer/ContainerStat.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
 | 
			
		||||
    <div class="column is-narrow has-text-weight-bold">
 | 
			
		||||
      {{ container.state }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
 | 
			
		||||
      <span class="has-text-weight-light has-spacer">mem</span>
 | 
			
		||||
      <span class="has-text-weight-bold">
 | 
			
		||||
        {{ formatBytes(container.stat.memoryUsage) }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="column is-narrow" v-if="container.stat.cpu !== null">
 | 
			
		||||
      <span class="has-text-weight-light has-spacer">load</span>
 | 
			
		||||
      <span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
import { type ComputedRef } from "vue";
 | 
			
		||||
 | 
			
		||||
const container = inject("container") as ComputedRef<Container>;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.has-spacer {
 | 
			
		||||
  &::after {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -8,14 +8,10 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Container } from "@/types/Container";
 | 
			
		||||
import { PropType } from "vue";
 | 
			
		||||
defineProps({
 | 
			
		||||
  container: {
 | 
			
		||||
    type: Object as PropType<Container>,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
import { type ComputedRef } from "vue";
 | 
			
		||||
 | 
			
		||||
const container = inject("container") as ComputedRef<Container>;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
							
								
								
									
										27
									
								
								assets/components/LogViewer/DockerEventLogItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								assets/components/LogViewer/DockerEventLogItem.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { DockerEventLogEntry } from "@/models/LogEntry";
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  logEntry: DockerEventLogEntry;
 | 
			
		||||
}>();
 | 
			
		||||
</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;
 | 
			
		||||
    &::before {
 | 
			
		||||
      content: " ";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										76
									
								
								assets/components/LogViewer/FieldList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								assets/components/LogViewer/FieldList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ul v-if="expanded" ref="root">
 | 
			
		||||
    <li v-for="(value, name) in fields">
 | 
			
		||||
      <template v-if="isObject(value)">
 | 
			
		||||
        <span class="has-text-grey">{{ name }}=</span>
 | 
			
		||||
        <field-list
 | 
			
		||||
          :fields="value"
 | 
			
		||||
          :parent-key="parentKey.concat(name)"
 | 
			
		||||
          :visible-keys="visibleKeys"
 | 
			
		||||
          expanded
 | 
			
		||||
        ></field-list>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-else-if="Array.isArray(value)">
 | 
			
		||||
        <a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
 | 
			
		||||
        <span class="has-text-grey">{{ name }}=</span>[
 | 
			
		||||
        <span class="has-text-weight-bold" v-for="(item, index) in value">
 | 
			
		||||
          {{ item }}
 | 
			
		||||
          <span v-if="index !== value.length - 1">,</span>
 | 
			
		||||
        </span>
 | 
			
		||||
        ]
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-else>
 | 
			
		||||
        <a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
 | 
			
		||||
        <span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
 | 
			
		||||
      </template>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { arrayEquals, isObject } from "@/utils";
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  fields,
 | 
			
		||||
  expanded = false,
 | 
			
		||||
  parentKey = [],
 | 
			
		||||
  visibleKeys = [],
 | 
			
		||||
} = defineProps<{
 | 
			
		||||
  fields: Record<string, any>;
 | 
			
		||||
  expanded?: boolean;
 | 
			
		||||
  parentKey?: string[];
 | 
			
		||||
  visibleKeys?: string[][];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const root = ref<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
async function toggleField(field: string) {
 | 
			
		||||
  const index = fieldIndex(field);
 | 
			
		||||
 | 
			
		||||
  if (index > -1) {
 | 
			
		||||
    visibleKeys.splice(index, 1);
 | 
			
		||||
  } else {
 | 
			
		||||
    visibleKeys.push(parentKey.concat(field));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await nextTick();
 | 
			
		||||
 | 
			
		||||
  root.value?.scrollIntoView({
 | 
			
		||||
    block: "center",
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hasField(field: string) {
 | 
			
		||||
  return fieldIndex(field) > -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fieldIndex(field: string) {
 | 
			
		||||
  const path = parentKey.concat(field);
 | 
			
		||||
  return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
ul {
 | 
			
		||||
  margin-left: 2em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -3,14 +3,14 @@
 | 
			
		||||
    <template #header v-if="showTitle">
 | 
			
		||||
      <div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
 | 
			
		||||
        <div class="column is-clipped is-paddingless">
 | 
			
		||||
          <container-title :container="container" @close="$emit('close')" />
 | 
			
		||||
          <container-title @close="$emit('close')" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-narrow is-paddingless">
 | 
			
		||||
          <container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
 | 
			
		||||
          <container-stat v-if="container.stat" />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="mr-2 column is-narrow is-paddingless">
 | 
			
		||||
          <log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
 | 
			
		||||
          <log-actions-toolbar :onClearClicked="onClearClicked" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="mr-2 column is-narrow is-paddingless" v-if="closable">
 | 
			
		||||
          <button class="delete is-medium" @click="emit('close')"></button>
 | 
			
		||||
@@ -18,41 +18,35 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #default="{ setLoading }">
 | 
			
		||||
      <log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
 | 
			
		||||
      <log-viewer-with-source ref="viewer" @loading-more="setLoading($event)" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </scrollable-view>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs } from "vue";
 | 
			
		||||
import LogViewerWithSource from "./LogViewerWithSource.vue";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  showTitle: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
  scrollable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
  closable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const {
 | 
			
		||||
  id,
 | 
			
		||||
  showTitle = false,
 | 
			
		||||
  scrollable = false,
 | 
			
		||||
  closable = false,
 | 
			
		||||
} = defineProps<{
 | 
			
		||||
  id: string;
 | 
			
		||||
  showTitle?: boolean;
 | 
			
		||||
  scrollable?: boolean;
 | 
			
		||||
  closable?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(["close"]);
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (event: "close"): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const { id } = toRefs(props);
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const container = store.currentContainer(id);
 | 
			
		||||
const container = store.currentContainer($$(id));
 | 
			
		||||
 | 
			
		||||
provide("container", container);
 | 
			
		||||
 | 
			
		||||
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
 | 
			
		||||
 | 
			
		||||
@@ -4,31 +4,12 @@ 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, beforeAll, afterAll } from "vitest";
 | 
			
		||||
import { computed, Ref } from "vue";
 | 
			
		||||
import { vi, describe, expect, beforeEach, test, beforeAll, afterAll, afterEach } from "vitest";
 | 
			
		||||
import { computed, nextTick } from "vue";
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router";
 | 
			
		||||
 | 
			
		||||
vi.mock("lodash.debounce", () => ({
 | 
			
		||||
  __esModule: true,
 | 
			
		||||
  default: vi.fn((fn) => {
 | 
			
		||||
    fn.cancel = () => {};
 | 
			
		||||
    return fn;
 | 
			
		||||
  }),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
vi.mock("@/stores/container", () => ({
 | 
			
		||||
  __esModule: true,
 | 
			
		||||
  useContainerStore() {
 | 
			
		||||
    return {
 | 
			
		||||
      currentContainer(id: Ref<string>) {
 | 
			
		||||
        return computed(() => ({ id: id.value }));
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
vi.mock("@/stores/config", () => ({
 | 
			
		||||
  __esModule: true,
 | 
			
		||||
  default: { base: "" },
 | 
			
		||||
@@ -47,6 +28,13 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      observe: vi.fn(),
 | 
			
		||||
      disconnect: vi.fn(),
 | 
			
		||||
    }));
 | 
			
		||||
    vi.useFakeTimers();
 | 
			
		||||
    vi.setSystemTime(1560336942459);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    vi.restoreAllMocks();
 | 
			
		||||
    vi.useRealTimers();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function createLogEventSource(
 | 
			
		||||
@@ -78,13 +66,17 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
        components: {
 | 
			
		||||
          LogViewer,
 | 
			
		||||
        },
 | 
			
		||||
        provide: {
 | 
			
		||||
          container: computed(() => ({ id: "abc", image: "test:v123" })),
 | 
			
		||||
          scrollingPaused: computed(() => false),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: `
 | 
			
		||||
        <template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
 | 
			
		||||
        `,
 | 
			
		||||
      },
 | 
			
		||||
      props: { id: "abc" },
 | 
			
		||||
      props: {},
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -111,68 +103,27 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
      data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
 | 
			
		||||
      data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const [message, _] = wrapper.vm.messages;
 | 
			
		||||
    const { key, ...messageWithoutKey } = message;
 | 
			
		||||
 | 
			
		||||
    expect(key).toBe("2019-06-12T10:55:42.459034602Z");
 | 
			
		||||
    expect(messageWithoutKey).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("should parse messages with loki's timestamp format", async () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
 | 
			
		||||
    vi.runAllTimers();
 | 
			
		||||
    await nextTick();
 | 
			
		||||
 | 
			
		||||
    const [message, _] = wrapper.vm.messages;
 | 
			
		||||
    const { key, ...messageWithoutKey } = message;
 | 
			
		||||
 | 
			
		||||
    expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
 | 
			
		||||
    expect(messageWithoutKey).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("should pass messages to slot", async () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
      data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
 | 
			
		||||
    });
 | 
			
		||||
    const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
 | 
			
		||||
 | 
			
		||||
    const { key, ...messageWithoutKey } = message;
 | 
			
		||||
 | 
			
		||||
    expect(key).toBe("2019-06-12T10:55:42.459034602Z");
 | 
			
		||||
 | 
			
		||||
    expect(messageWithoutKey).toMatchSnapshot();
 | 
			
		||||
    expect(message).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("render html correctly", () => {
 | 
			
		||||
    const RealDate = Date;
 | 
			
		||||
    beforeAll(() => {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      global.Date = class extends RealDate {
 | 
			
		||||
        constructor(arg: any | number) {
 | 
			
		||||
          super(arg);
 | 
			
		||||
          if (arg) {
 | 
			
		||||
            return new RealDate(arg);
 | 
			
		||||
          } else {
 | 
			
		||||
            return new RealDate(1560336936000);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    afterAll(() => (global.Date = RealDate));
 | 
			
		||||
 | 
			
		||||
    test("should render messages", async () => {
 | 
			
		||||
      const wrapper = createLogEventSource();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -180,10 +131,12 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      const wrapper = createLogEventSource();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
 | 
			
		||||
        data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -191,10 +144,12 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      const wrapper = createLogEventSource();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -202,10 +157,12 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      const wrapper = createLogEventSource({ hourStyle: "12" });
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -213,10 +170,12 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      const wrapper = createLogEventSource({ hourStyle: "24" });
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -224,13 +183,15 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      const wrapper = createLogEventSource({ searchFilter: "test" });
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-11T10:55:42.459034602Z Foo bar`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
 | 
			
		||||
      });
 | 
			
		||||
      sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
        data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
 | 
			
		||||
        data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      vi.runAllTimers();
 | 
			
		||||
      await nextTick();
 | 
			
		||||
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchSnapshot();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										27
									
								
								assets/components/LogViewer/LogEventSource.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								assets/components/LogViewer/LogEventSource.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
 | 
			
		||||
  <slot :messages="messages"></slot>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
import { type ComputedRef } from "vue";
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: "loading-more", value: boolean): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const container = inject("container") as ComputedRef<Container>;
 | 
			
		||||
const { connect, messages, loadOlderLogs } = useLogStream(container);
 | 
			
		||||
 | 
			
		||||
const beforeLoading = () => emit("loading-more", true);
 | 
			
		||||
const afterLoading = () => emit("loading-more", false);
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  clear: () => (messages.value = []),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
 | 
			
		||||
 | 
			
		||||
connect();
 | 
			
		||||
</script>
 | 
			
		||||
@@ -2,14 +2,13 @@
 | 
			
		||||
  <ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
 | 
			
		||||
    <li
 | 
			
		||||
      v-for="(item, index) in filtered"
 | 
			
		||||
      :key="item.key"
 | 
			
		||||
      :data-key="item.key"
 | 
			
		||||
      :data-event="item.event"
 | 
			
		||||
      :class="{ selected: item.selected }"
 | 
			
		||||
      :key="item.id"
 | 
			
		||||
      :data-key="item.id"
 | 
			
		||||
      :class="{ selected: 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.key}`">
 | 
			
		||||
          <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">
 | 
			
		||||
@@ -25,41 +24,36 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="line">
 | 
			
		||||
        <span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
 | 
			
		||||
        <span class="text" v-html="colorize(item.message)"></span>
 | 
			
		||||
        <component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
 | 
			
		||||
      </div>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { PropType, ref, toRefs, watch } from "vue";
 | 
			
		||||
import { type ComputedRef, toRaw } from "vue";
 | 
			
		||||
import { useRouteHash } from "@vueuse/router";
 | 
			
		||||
import { size, showTimestamp, softWrap } from "@/composables/settings";
 | 
			
		||||
import RelativeTime from "./RelativeTime.vue";
 | 
			
		||||
import AnsiConvertor from "ansi-to-html";
 | 
			
		||||
import { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
import { type JSONObject, type LogEntry } from "@/models/LogEntry";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  messages: {
 | 
			
		||||
    type: Array as PropType<LogEntry[]>,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  messages: LogEntry<string | JSONObject>[];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
 | 
			
		||||
 | 
			
		||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
 | 
			
		||||
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
 | 
			
		||||
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
 | 
			
		||||
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
 | 
			
		||||
const { messages } = toRefs(props);
 | 
			
		||||
const filtered = filteredMessages(messages);
 | 
			
		||||
const visible = filteredPayload(messages);
 | 
			
		||||
const filtered = filteredMessages(visible);
 | 
			
		||||
 | 
			
		||||
const events = ref<HTMLElement>();
 | 
			
		||||
let lastSelectedItem: LogEntry | undefined = undefined;
 | 
			
		||||
function handleJumpLineSelected(e: Event, item: LogEntry) {
 | 
			
		||||
  if (lastSelectedItem) {
 | 
			
		||||
    lastSelectedItem.selected = false;
 | 
			
		||||
  }
 | 
			
		||||
  lastSelectedItem = item;
 | 
			
		||||
  item.selected = true;
 | 
			
		||||
let lastSelectedItem = ref<LogEntry<string | JSONObject>>();
 | 
			
		||||
 | 
			
		||||
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
 | 
			
		||||
  lastSelectedItem.value = item;
 | 
			
		||||
  resetSearch();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -78,8 +72,7 @@ watch(
 | 
			
		||||
  font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
 | 
			
		||||
 | 
			
		||||
  &.disable-wrap {
 | 
			
		||||
    .line,
 | 
			
		||||
    .text {
 | 
			
		||||
    .line {
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -95,12 +88,7 @@ watch(
 | 
			
		||||
    &:nth-child(odd) {
 | 
			
		||||
      background-color: rgba(125, 125, 125, 0.08);
 | 
			
		||||
    }
 | 
			
		||||
    &[data-event="container-stopped"] {
 | 
			
		||||
      color: #f14668;
 | 
			
		||||
    }
 | 
			
		||||
    &[data-event="container-started"] {
 | 
			
		||||
      color: hsl(141, 53%, 53%);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.selected .date {
 | 
			
		||||
      background-color: var(--menu-item-active-background-color);
 | 
			
		||||
 | 
			
		||||
@@ -112,6 +100,7 @@ watch(
 | 
			
		||||
    & > .line {
 | 
			
		||||
      margin: auto 0;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
    & > .line-options {
 | 
			
		||||
      display: flex;
 | 
			
		||||
@@ -165,13 +154,7 @@ watch(
 | 
			
		||||
  padding-left: 5px;
 | 
			
		||||
  padding-right: 5px;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:deep(mark) {
 | 
			
		||||
							
								
								
									
										21
									
								
								assets/components/LogViewer/LogViewerWithSource.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								assets/components/LogViewer/LogViewerWithSource.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
 | 
			
		||||
    <log-viewer :messages="messages"></log-viewer>
 | 
			
		||||
  </log-event-source>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import LogEventSource from "./LogEventSource.vue";
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: "loading-more", value: boolean): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const source = $ref<InstanceType<typeof LogEventSource>>();
 | 
			
		||||
function clear() {
 | 
			
		||||
  source?.clear();
 | 
			
		||||
}
 | 
			
		||||
defineExpose({
 | 
			
		||||
  clear,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										30
									
								
								assets/components/LogViewer/SimpleLogItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								assets/components/LogViewer/SimpleLogItem.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="text" v-html="colorize(logEntry.message)"></span>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { SimpleLogEntry } from "@/models/LogEntry";
 | 
			
		||||
import AnsiConvertor from "ansi-to-html";
 | 
			
		||||
 | 
			
		||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
 | 
			
		||||
defineProps<{
 | 
			
		||||
  logEntry: SimpleLogEntry;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										24
									
								
								assets/components/LogViewer/SkippedEntriesLogItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								assets/components/LogViewer/SkippedEntriesLogItem.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="text">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { SkippedLogsEntry } from "@/models/LogEntry";
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  logEntry: SkippedLogsEntry;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
span {
 | 
			
		||||
  &.text {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    &::before {
 | 
			
		||||
      content: " ";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,169 +1,169 @@
 | 
			
		||||
// Vitest Snapshot v1
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 11:55:42 PM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42 AM</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 23:55:42</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">\\"This is a message.\\"</span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42 AM</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">This is a message.</span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42 AM</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a <mark>test</mark> <hi></hi></span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42 AM</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\"><mark>test</mark> bar</span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
  <li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
  <li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
    <div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
 | 
			
		||||
      <div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
        <div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><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 class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
          <div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
              <div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                <div class=\\"level-left\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                      <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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
 | 
			
		||||
                <div class=\\"level-right\\" data-v-2e92daca=\\"\\">
 | 
			
		||||
                  <div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
    <div class=\\"line\\" data-v-2e92daca=\\"\\"><span class=\\"date\\" data-v-2e92daca=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-2e92daca=\\"\\">06/12/2019 10:55:42 AM</time></span><span class=\\"text\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\"><test>foo bar</test></span></div>
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>"
 | 
			
		||||
`;
 | 
			
		||||
@@ -176,26 +176,13 @@ exports[`<LogEventSource /> > renders correctly 1`] = `
 | 
			
		||||
    <div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<ul class=\\"events medium\\" data-v-cce5b553=\\"\\"></ul>"
 | 
			
		||||
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > should parse messages 1`] = `
 | 
			
		||||
{
 | 
			
		||||
SimpleLogEntry {
 | 
			
		||||
  "_message": "This is a message.",
 | 
			
		||||
  "date": 2019-06-12T10:55:42.459Z,
 | 
			
		||||
  "message": "\\"This is a message.\\"",
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > should parse messages with loki's timestamp format 1`] = `
 | 
			
		||||
{
 | 
			
		||||
  "date": 2020-04-27T10:35:43.272Z,
 | 
			
		||||
  "message": "xxxxx",
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> > should pass messages to slot 1`] = `
 | 
			
		||||
{
 | 
			
		||||
  "date": 2019-06-12T10:55:42.459Z,
 | 
			
		||||
  "message": "\\"This is a message.\\"",
 | 
			
		||||
  "id": 1,
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
 | 
			
		||||
    <log-viewer :messages="messages"></log-viewer>
 | 
			
		||||
  </log-event-source>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import LogViewer from "./LogViewer.vue";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(["loading-more"]);
 | 
			
		||||
 | 
			
		||||
const source = ref<InstanceType<typeof LogViewer>>();
 | 
			
		||||
function clear() {
 | 
			
		||||
  source.value?.clear();
 | 
			
		||||
}
 | 
			
		||||
defineExpose({
 | 
			
		||||
  clear,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <aside>
 | 
			
		||||
    <div class="columns is-marginless is-gapless is-mobile is-vcentered">
 | 
			
		||||
      <div class="column is-narrow">
 | 
			
		||||
        <router-link :to="{ name: 'default' }">
 | 
			
		||||
        <router-link :to="{ name: 'index' }">
 | 
			
		||||
          <svg class="logo">
 | 
			
		||||
            <use href="#logo"></use>
 | 
			
		||||
          </svg>
 | 
			
		||||
@@ -24,10 +24,14 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
 | 
			
		||||
    <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 visibleContainers" :key="item.id">
 | 
			
		||||
        <router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
 | 
			
		||||
        <router-link
 | 
			
		||||
          :to="{ name: 'container-id', params: { id: item.id } }"
 | 
			
		||||
          active-class="is-active"
 | 
			
		||||
          :title="item.name"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="is-ellipsis">
 | 
			
		||||
            {{ item.name }}
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -38,11 +42,6 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const { visibleContainers, allContainersById } = storeToRefs(store);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,20 +3,15 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useIntervalFn } from "@vueuse/core";
 | 
			
		||||
import formatDistance from "date-fns/formatDistance";
 | 
			
		||||
import { PropType, ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  date: {
 | 
			
		||||
    required: true,
 | 
			
		||||
    type: Object as PropType<Date>,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const { date } = defineProps<{
 | 
			
		||||
  date: Date;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const text = ref<string>();
 | 
			
		||||
function updateFromNow() {
 | 
			
		||||
  text.value = formatDistance(props.date, new Date(), {
 | 
			
		||||
  text.value = formatDistance(date, new Date(), {
 | 
			
		||||
    addSuffix: true,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <time :datetime="date.toISOString()">{{ relativeTime(date, locale) }}</time>
 | 
			
		||||
  <time :datetime="date.toISOString()">{{ format(date) }}</time>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
const use24Hr =
 | 
			
		||||
  new Intl.DateTimeFormat(undefined, {
 | 
			
		||||
    hour: "numeric",
 | 
			
		||||
  })
 | 
			
		||||
    .formatToParts(new Date(2020, 0, 1, 13))
 | 
			
		||||
    .find((part) => part.type === "hour")?.value.length === 2;
 | 
			
		||||
 | 
			
		||||
const auto = use24Hr ? enGB : enUS;
 | 
			
		||||
const styles = { auto, 12: enUS, 24: enGB };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { formatRelative } from "date-fns";
 | 
			
		||||
import { hourStyle } from "@/composables/settings";
 | 
			
		||||
import enGB from "date-fns/locale/en-GB";
 | 
			
		||||
import enUS from "date-fns/locale/en-US";
 | 
			
		||||
import { computed, PropType } from "vue";
 | 
			
		||||
defineProps({
 | 
			
		||||
  date: {
 | 
			
		||||
    required: true,
 | 
			
		||||
    type: Object as PropType<Date>,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
defineProps<{
 | 
			
		||||
  date: Date;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const locale = computed(() => {
 | 
			
		||||
  const locale = styles[hourStyle.value];
 | 
			
		||||
  const oldFormatter = locale.formatRelative as (d: Date | number) => string;
 | 
			
		||||
  return {
 | 
			
		||||
    ...locale,
 | 
			
		||||
    formatRelative(date: Date | number) {
 | 
			
		||||
      return oldFormatter(date) + "p";
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
 | 
			
		||||
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false }[hourStyle.value]));
 | 
			
		||||
const timeFormatter = $computed(
 | 
			
		||||
  () => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function relativeTime(date: Date, locale: Locale) {
 | 
			
		||||
  return formatRelative(date, new Date(), { locale });
 | 
			
		||||
function format(date: Date) {
 | 
			
		||||
  const dateStr = dateFormatter.format(date);
 | 
			
		||||
  const timeStr = timeFormatter.format(date);
 | 
			
		||||
  return `${dateStr} ${timeStr}`;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,21 +18,10 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { useScroll } from "@vueuse/core";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { onMounted, ref, watch, watchPostEffect } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  indeterminate: {
 | 
			
		||||
    default: false,
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
  },
 | 
			
		||||
  autoHide: {
 | 
			
		||||
    default: true,
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const { indeterminate = false, autoHide = false } = defineProps<{
 | 
			
		||||
  indeterminate?: boolean;
 | 
			
		||||
  autoHide?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const scrollProgress = ref(0);
 | 
			
		||||
const animation = ref({ cancel: () => {} });
 | 
			
		||||
@@ -59,7 +48,7 @@ watchPostEffect(() => {
 | 
			
		||||
      : (scrollElement.value as HTMLElement);
 | 
			
		||||
  scrollProgress.value = scrollY.value / (parent.scrollHeight - parent.clientHeight);
 | 
			
		||||
  animation.value.cancel();
 | 
			
		||||
  if (props.autoHide && root.value) {
 | 
			
		||||
  if (autoHide && root.value) {
 | 
			
		||||
    animation.value = root.value.animate(
 | 
			
		||||
      { opacity: [1, 0] },
 | 
			
		||||
      {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
    <div class="is-scrollbar-notification">
 | 
			
		||||
      <transition name="fade">
 | 
			
		||||
        <button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
 | 
			
		||||
        <button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
 | 
			
		||||
          <mdi-light-chevron-double-down />
 | 
			
		||||
        </button>
 | 
			
		||||
      </transition>
 | 
			
		||||
@@ -24,61 +24,46 @@
 | 
			
		||||
  </section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    scrollable: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
 | 
			
		||||
 | 
			
		||||
  name: "ScrollableView",
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      paused: false,
 | 
			
		||||
      hasMore: false,
 | 
			
		||||
      loading: false,
 | 
			
		||||
      mutationObserver: null,
 | 
			
		||||
      intersectionObserver: null,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const { scrollableContent } = this.$refs;
 | 
			
		||||
    this.mutationObserver = new MutationObserver((e) => {
 | 
			
		||||
      if (!this.paused) {
 | 
			
		||||
        this.scrollToBottom("instant");
 | 
			
		||||
      } else {
 | 
			
		||||
        const record = e[e.length - 1];
 | 
			
		||||
        if (
 | 
			
		||||
          record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]
 | 
			
		||||
        ) {
 | 
			
		||||
          this.hasMore = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.mutationObserver.observe(scrollableContent, { childList: true, subtree: true });
 | 
			
		||||
let paused = $ref(false);
 | 
			
		||||
let hasMore = $ref(false);
 | 
			
		||||
let loading = $ref(false);
 | 
			
		||||
const scrollObserver = ref<HTMLElement>();
 | 
			
		||||
const scrollableContent = ref<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
    this.intersectionObserver = new IntersectionObserver(
 | 
			
		||||
      (entries) => (this.paused = entries[0].intersectionRatio == 0),
 | 
			
		||||
      { threshholds: [0, 1], rootMargin: "80px 0px" }
 | 
			
		||||
    );
 | 
			
		||||
    this.intersectionObserver.observe(this.$refs.scrollObserver);
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.mutationObserver.disconnect();
 | 
			
		||||
    this.intersectionObserver.disconnect();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    scrollToBottom(behavior = "instant") {
 | 
			
		||||
      this.$refs.scrollObserver.scrollIntoView({ behavior });
 | 
			
		||||
      this.hasMore = false;
 | 
			
		||||
    },
 | 
			
		||||
    setLoading(loading) {
 | 
			
		||||
      this.loading = loading;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
provide("scrollingPaused", $$(paused));
 | 
			
		||||
 | 
			
		||||
const mutationObserver = new MutationObserver((e) => {
 | 
			
		||||
  if (!paused) {
 | 
			
		||||
    scrollToBottom();
 | 
			
		||||
  } else {
 | 
			
		||||
    const record = e[e.length - 1];
 | 
			
		||||
    if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
 | 
			
		||||
      hasMore = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
 | 
			
		||||
  threshold: [0, 1],
 | 
			
		||||
  rootMargin: "80px 0px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
 | 
			
		||||
  intersectionObserver.observe(scrollObserver.value!);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
 | 
			
		||||
  scrollObserver.value?.scrollIntoView({ behavior });
 | 
			
		||||
  hasMore = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setLoading(value: boolean) {
 | 
			
		||||
  loading = value;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
section {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,6 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import hotkeys from "hotkeys-js";
 | 
			
		||||
 | 
			
		||||
import { search } from "@/composables/settings";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
import { ref, nextTick, onMounted, onUnmounted } from "vue";
 | 
			
		||||
 | 
			
		||||
const input = ref<HTMLInputElement>();
 | 
			
		||||
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,14 @@
 | 
			
		||||
  <aside>
 | 
			
		||||
    <div class="columns is-marginless">
 | 
			
		||||
      <div class="column is-paddingless">
 | 
			
		||||
        <router-link :to="{ name: 'default' }">
 | 
			
		||||
        <router-link :to="{ name: 'index' }">
 | 
			
		||||
          <svg class="logo">
 | 
			
		||||
            <use href="#logo"></use>
 | 
			
		||||
          </svg>
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column is-narrow has-text-right px-1">
 | 
			
		||||
        <button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
 | 
			
		||||
        <button class="button is-rounded" @click="$emit('search')" title="$t('tooltip.search')">
 | 
			
		||||
          <span class="icon">
 | 
			
		||||
            <mdi-light-magnify />
 | 
			
		||||
          </span>
 | 
			
		||||
@@ -23,10 +23,14 @@
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p class="menu-label is-hidden-mobile">Containers</p>
 | 
			
		||||
    <p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
 | 
			
		||||
    <ul class="menu-list is-hidden-mobile" v-if="ready">
 | 
			
		||||
      <li v-for="item in visibleContainers" :key="item.id" :class="item.state">
 | 
			
		||||
        <router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
 | 
			
		||||
        <router-link
 | 
			
		||||
          :to="{ name: 'container-id', params: { id: item.id } }"
 | 
			
		||||
          active-class="is-active"
 | 
			
		||||
          :title="item.name"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="container is-flex is-align-items-center">
 | 
			
		||||
            <div class="is-flex-grow-1 is-ellipsis">
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
@@ -36,7 +40,7 @@
 | 
			
		||||
                class="icon is-small"
 | 
			
		||||
                @click.stop.prevent="store.appendActiveContainer(item)"
 | 
			
		||||
                v-show="!activeContainersById[item.id]"
 | 
			
		||||
                title="Pin as column"
 | 
			
		||||
                title="$t('tooltip.pin-column')"
 | 
			
		||||
              >
 | 
			
		||||
                <cil-columns />
 | 
			
		||||
              </span>
 | 
			
		||||
@@ -46,15 +50,12 @@
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <ul class="menu-list is-hidden-mobile loading" v-else>
 | 
			
		||||
      <li v-for="index in 7" class="my-4"><o-skeleton animated size="large"></o-skeleton></li>
 | 
			
		||||
      <li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </aside>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import type { Container } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										124
									
								
								assets/composables/eventsource.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								assets/composables/eventsource.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
import { type ComputedRef, type Ref } from "vue";
 | 
			
		||||
import debounce from "lodash.debounce";
 | 
			
		||||
import {
 | 
			
		||||
  type LogEvent,
 | 
			
		||||
  type JSONObject,
 | 
			
		||||
  LogEntry,
 | 
			
		||||
  asLogEntry,
 | 
			
		||||
  DockerEventLogEntry,
 | 
			
		||||
  SkippedLogsEntry,
 | 
			
		||||
} from "@/models/LogEntry";
 | 
			
		||||
import { type Container } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
function parseMessage(data: string): LogEntry<string | JSONObject> {
 | 
			
		||||
  const e = JSON.parse(data) as LogEvent;
 | 
			
		||||
  return asLogEntry(e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useLogStream(container: ComputedRef<Container>) {
 | 
			
		||||
  let messages = $ref<LogEntry<string | JSONObject>[]>([]);
 | 
			
		||||
  let buffer = $ref<LogEntry<string | JSONObject>[]>([]);
 | 
			
		||||
  const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
 | 
			
		||||
 | 
			
		||||
  function flushNow() {
 | 
			
		||||
    if (messages.length > config.maxLogs) {
 | 
			
		||||
      if (scrollingPaused) {
 | 
			
		||||
        console.log("Skipping ", buffer.length, " log items");
 | 
			
		||||
        if (messages.at(-1) instanceof SkippedLogsEntry) {
 | 
			
		||||
          const lastEvent = messages.at(-1) as SkippedLogsEntry;
 | 
			
		||||
          const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
 | 
			
		||||
          lastEvent.addSkippedEntries(buffer.length, lastItem);
 | 
			
		||||
        } else {
 | 
			
		||||
          const firstItem = buffer.at(0) as LogEntry<string | JSONObject>;
 | 
			
		||||
          const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
 | 
			
		||||
          messages.push(new SkippedLogsEntry(new Date(), buffer.length, firstItem, lastItem));
 | 
			
		||||
        }
 | 
			
		||||
        buffer = [];
 | 
			
		||||
      } else {
 | 
			
		||||
        messages.push(...buffer);
 | 
			
		||||
        buffer = [];
 | 
			
		||||
        messages.splice(0, messages.length - config.maxLogs);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      messages.push(...buffer);
 | 
			
		||||
      buffer = [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
 | 
			
		||||
  let es: EventSource | null = null;
 | 
			
		||||
  let lastEventId = "";
 | 
			
		||||
 | 
			
		||||
  function connect({ clear } = { clear: true }) {
 | 
			
		||||
    es?.close();
 | 
			
		||||
 | 
			
		||||
    if (clear) {
 | 
			
		||||
      flushBuffer.cancel();
 | 
			
		||||
      messages = [];
 | 
			
		||||
      buffer = [];
 | 
			
		||||
      lastEventId = "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
 | 
			
		||||
    es.addEventListener("container-stopped", () => {
 | 
			
		||||
      es?.close();
 | 
			
		||||
      es = null;
 | 
			
		||||
      buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
 | 
			
		||||
 | 
			
		||||
      flushBuffer();
 | 
			
		||||
      flushBuffer.flush();
 | 
			
		||||
    });
 | 
			
		||||
    es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
 | 
			
		||||
    es.onmessage = (e) => {
 | 
			
		||||
      lastEventId = e.lastEventId;
 | 
			
		||||
      if (e.data) {
 | 
			
		||||
        buffer.push(parseMessage(e.data));
 | 
			
		||||
        flushBuffer();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
 | 
			
		||||
    if (messages.length < 300) return;
 | 
			
		||||
 | 
			
		||||
    beforeLoading();
 | 
			
		||||
    const to = messages[0].date;
 | 
			
		||||
    const last = messages[299].date;
 | 
			
		||||
    const delta = to.getTime() - last.getTime();
 | 
			
		||||
    const from = new Date(to.getTime() + delta);
 | 
			
		||||
    const logs = await (
 | 
			
		||||
      await fetch(`${config.base}/api/logs?id=${container.value.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
 | 
			
		||||
    ).text();
 | 
			
		||||
    if (logs) {
 | 
			
		||||
      const newMessages = logs
 | 
			
		||||
        .trim()
 | 
			
		||||
        .split("\n")
 | 
			
		||||
        .map((line) => parseMessage(line));
 | 
			
		||||
      messages.unshift(...newMessages);
 | 
			
		||||
    }
 | 
			
		||||
    afterLoading();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => container.value.state,
 | 
			
		||||
    (newValue, oldValue) => {
 | 
			
		||||
      console.log("LogEventSource: container changed", newValue, oldValue);
 | 
			
		||||
      if (newValue == "running" && newValue != oldValue) {
 | 
			
		||||
        buffer.push(new DockerEventLogEntry("Container started", new Date(), "container-started"));
 | 
			
		||||
        connect({ clear: false });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  onUnmounted(() => {
 | 
			
		||||
    if (es) {
 | 
			
		||||
      es.close();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => container.value.id,
 | 
			
		||||
    () => connect()
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return $$({ connect, messages, loadOlderLogs });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1 @@
 | 
			
		||||
import { useMediaQuery } from "@vueuse/core";
 | 
			
		||||
 | 
			
		||||
export const isMobile = useMediaQuery("(max-width: 770px)");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,41 @@
 | 
			
		||||
import { ref, computed, Ref } from "vue";
 | 
			
		||||
import { type Ref } from "vue";
 | 
			
		||||
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
 | 
			
		||||
 | 
			
		||||
const searchFilter = ref<string>("");
 | 
			
		||||
const debouncedSearchFilter = useDebounce(searchFilter);
 | 
			
		||||
const showSearch = ref(false);
 | 
			
		||||
 | 
			
		||||
import type { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
 | 
			
		||||
  for (const key in record) {
 | 
			
		||||
    const value = record[key];
 | 
			
		||||
    if (typeof value === "string" && regex.test(value)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    if (Array.isArray(value) && matchRecord(value, regex)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useSearchFilter() {
 | 
			
		||||
  const regex = computed(() => {
 | 
			
		||||
    const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
 | 
			
		||||
    return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
 | 
			
		||||
    const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
 | 
			
		||||
    return isSmartCase ? new RegExp(debouncedSearchFilter.value, "i") : new RegExp(debouncedSearchFilter.value);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function filteredMessages(messages: Ref<LogEntry[]>) {
 | 
			
		||||
  function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
 | 
			
		||||
    return computed(() => {
 | 
			
		||||
      if (searchFilter && searchFilter.value) {
 | 
			
		||||
      if (debouncedSearchFilter.value) {
 | 
			
		||||
        try {
 | 
			
		||||
          return messages.value.filter((d) => d.message.match(regex.value));
 | 
			
		||||
          return messages.value.filter((d) => {
 | 
			
		||||
            if (d instanceof SimpleLogEntry) {
 | 
			
		||||
              return regex.value.test(d.message);
 | 
			
		||||
            } else if (d instanceof ComplexLogEntry) {
 | 
			
		||||
              return matchRecord(d.message, regex.value);
 | 
			
		||||
            }
 | 
			
		||||
            throw new Error("Unknown message type");
 | 
			
		||||
          });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          if (e instanceof SyntaxError) {
 | 
			
		||||
            console.info(`Ignoring SyntaxError from search.`, e);
 | 
			
		||||
@@ -29,11 +49,17 @@ export function useSearchFilter() {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function markSearch(log: string) {
 | 
			
		||||
    if (searchFilter && searchFilter.value) {
 | 
			
		||||
      return log.replace(regex.value, `<mark>$&</mark>`);
 | 
			
		||||
  function markSearch(log: string): string;
 | 
			
		||||
  function markSearch(log: string[]): string[];
 | 
			
		||||
  function markSearch(log: string | string[]) {
 | 
			
		||||
    if (!debouncedSearchFilter.value) {
 | 
			
		||||
      return log;
 | 
			
		||||
    }
 | 
			
		||||
    return log;
 | 
			
		||||
    if (Array.isArray(log)) {
 | 
			
		||||
      return log.map((d) => markSearch(d));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return log.toString().replace(regex.value, (match) => `<mark>${match}</mark>`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function resetSearch() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,4 @@
 | 
			
		||||
import { useStorage } from "@vueuse/core";
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
 | 
			
		||||
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
 | 
			
		||||
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_SETTINGS: {
 | 
			
		||||
  search: boolean;
 | 
			
		||||
@@ -25,49 +22,62 @@ export const DEFAULT_SETTINGS: {
 | 
			
		||||
  softWrap: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
 | 
			
		||||
settings.value = {...DEFAULT_SETTINGS, ...settings.value};
 | 
			
		||||
const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
 | 
			
		||||
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
 | 
			
		||||
 | 
			
		||||
export const search = computed({
 | 
			
		||||
const search = computed({
 | 
			
		||||
  get: () => settings.value.search,
 | 
			
		||||
  set: (value) => (settings.value.search = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const size = computed({
 | 
			
		||||
const size = computed({
 | 
			
		||||
  get: () => settings.value.size,
 | 
			
		||||
  set: (value) => (settings.value.size = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menuWidth = computed({
 | 
			
		||||
const menuWidth = computed({
 | 
			
		||||
  get: () => settings.value.menuWidth,
 | 
			
		||||
  set: (value) => (settings.value.menuWidth = value),
 | 
			
		||||
});
 | 
			
		||||
export const smallerScrollbars = computed({
 | 
			
		||||
const smallerScrollbars = computed({
 | 
			
		||||
  get: () => settings.value.smallerScrollbars,
 | 
			
		||||
  set: (value) => (settings.value.smallerScrollbars = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const showTimestamp = computed({
 | 
			
		||||
const showTimestamp = computed({
 | 
			
		||||
  get: () => settings.value.showTimestamp,
 | 
			
		||||
  set: (value) => (settings.value.showTimestamp = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const showAllContainers = computed({
 | 
			
		||||
const showAllContainers = computed({
 | 
			
		||||
  get: () => settings.value.showAllContainers,
 | 
			
		||||
  set: (value) => (settings.value.showAllContainers = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const lightTheme = computed({
 | 
			
		||||
const lightTheme = computed({
 | 
			
		||||
  get: () => settings.value.lightTheme,
 | 
			
		||||
  set: (value) => (settings.value.lightTheme = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const hourStyle = computed({
 | 
			
		||||
const hourStyle = computed({
 | 
			
		||||
  get: () => settings.value.hourStyle,
 | 
			
		||||
  set: (value) => (settings.value.hourStyle = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const softWrap = computed({
 | 
			
		||||
const softWrap = computed({
 | 
			
		||||
  get: () => settings.value.softWrap,
 | 
			
		||||
  set: (value) => (settings.value.softWrap = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  softWrap,
 | 
			
		||||
  hourStyle,
 | 
			
		||||
  lightTheme,
 | 
			
		||||
  showAllContainers,
 | 
			
		||||
  showTimestamp,
 | 
			
		||||
  smallerScrollbars,
 | 
			
		||||
  menuWidth,
 | 
			
		||||
  size,
 | 
			
		||||
  search,
 | 
			
		||||
  settings
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,8 @@
 | 
			
		||||
import { useTitle } from "@vueuse/core";
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
let subtitle = $ref("");
 | 
			
		||||
const title = $computed(() => `${subtitle} - Dozzle`);
 | 
			
		||||
 | 
			
		||||
const subtitle = ref("");
 | 
			
		||||
 | 
			
		||||
const title = computed(() => `${subtitle.value} - Dozzle`);
 | 
			
		||||
 | 
			
		||||
useTitle(title);
 | 
			
		||||
useTitle($$(title));
 | 
			
		||||
 | 
			
		||||
export function setTitle(t: string) {
 | 
			
		||||
  subtitle.value = t;
 | 
			
		||||
  subtitle = t;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								assets/composables/visible.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								assets/composables/visible.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
 | 
			
		||||
import type { ComputedRef, Ref } from "vue";
 | 
			
		||||
 | 
			
		||||
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
 | 
			
		||||
  function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
 | 
			
		||||
    return computed(() => {
 | 
			
		||||
      return messages.value.map((d) => {
 | 
			
		||||
        if (d instanceof ComplexLogEntry) {
 | 
			
		||||
          return ComplexLogEntry.fromLogEvent(d, visibleKeys.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          return d;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { filteredPayload };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								assets/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								assets/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <main v-if="!authorizationNeeded">
 | 
			
		||||
    <mobile-menu v-if="isMobile"></mobile-menu>
 | 
			
		||||
    <splitpanes @resized="onResized($event)">
 | 
			
		||||
      <pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
 | 
			
		||||
        <side-menu @search="showFuzzySearch"></side-menu>
 | 
			
		||||
      </pane>
 | 
			
		||||
      <pane min-size="10">
 | 
			
		||||
        <splitpanes>
 | 
			
		||||
          <pane class="has-min-height router-view">
 | 
			
		||||
            <router-view></router-view>
 | 
			
		||||
          </pane>
 | 
			
		||||
          <template v-if="!isMobile">
 | 
			
		||||
            <pane v-for="other in activeContainers" :key="other.id">
 | 
			
		||||
              <log-container
 | 
			
		||||
                :id="other.id"
 | 
			
		||||
                show-title
 | 
			
		||||
                scrollable
 | 
			
		||||
                closable
 | 
			
		||||
                @close="containerStore.removeActiveContainer(other)"
 | 
			
		||||
              ></log-container>
 | 
			
		||||
            </pane>
 | 
			
		||||
          </template>
 | 
			
		||||
        </splitpanes>
 | 
			
		||||
      </pane>
 | 
			
		||||
    </splitpanes>
 | 
			
		||||
    <button
 | 
			
		||||
      @click="collapseNav = !collapseNav"
 | 
			
		||||
      class="button is-rounded"
 | 
			
		||||
      :class="{ collapsed: collapseNav }"
 | 
			
		||||
      id="hide-nav"
 | 
			
		||||
      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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Splitpanes, Pane } from "splitpanes";
 | 
			
		||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
 | 
			
		||||
import hotkeys from "hotkeys-js";
 | 
			
		||||
 | 
			
		||||
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
 | 
			
		||||
 | 
			
		||||
const collapseNav = ref(false);
 | 
			
		||||
const { oruga } = useProgrammatic();
 | 
			
		||||
const { authorizationNeeded } = config;
 | 
			
		||||
 | 
			
		||||
const containerStore = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  hotkeys("command+k, ctrl+k", (event, handler) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    showFuzzySearch();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  setTitle(`${visibleContainers.value.length} containers`);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function showFuzzySearch() {
 | 
			
		||||
  oruga.modal.open({
 | 
			
		||||
    // parent: this,
 | 
			
		||||
    component: FuzzySearchModal,
 | 
			
		||||
    animation: "false",
 | 
			
		||||
    width: 600,
 | 
			
		||||
    active: true,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
function onResized(e: any) {
 | 
			
		||||
  if (e.length == 2) {
 | 
			
		||||
    menuWidth.value = e[0].size;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
:deep(.splitpanes--vertical > .splitpanes__splitter) {
 | 
			
		||||
  min-width: 3px;
 | 
			
		||||
  background: var(--border-color);
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: var(--border-hover-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 768px) {
 | 
			
		||||
  .router-view {
 | 
			
		||||
    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;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.95);
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      left: -25px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										9
									
								
								assets/layouts/splash.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								assets/layouts/splash.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <main>
 | 
			
		||||
    <router-view></router-view>
 | 
			
		||||
  </main>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup></script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -1,68 +1,10 @@
 | 
			
		||||
import "./styles.scss";
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router";
 | 
			
		||||
import { Autocomplete, Button, Dropdown, Switch, Radio, Skeleton,  Field, Tooltip, Modal, Config } from "@oruga-ui/oruga-next";
 | 
			
		||||
import { bulmaConfig } from "@oruga-ui/theme-bulma";
 | 
			
		||||
import { createPinia } from "pinia";
 | 
			
		||||
import config from "./stores/config";
 | 
			
		||||
import { createApp, App as VueApp } from "vue";
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: "/",
 | 
			
		||||
    component: Index,
 | 
			
		||||
    name: "default",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/container/:id",
 | 
			
		||||
    component: Container,
 | 
			
		||||
    name: "container",
 | 
			
		||||
    props: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/container/:pathMatch(.*)",
 | 
			
		||||
    component: ContainerNotFound,
 | 
			
		||||
    name: "container-not-found",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/settings",
 | 
			
		||||
    component: Settings,
 | 
			
		||||
    name: "settings",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/show",
 | 
			
		||||
    component: Show,
 | 
			
		||||
    name: "show",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/login",
 | 
			
		||||
    component: Login,
 | 
			
		||||
    name: "login",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/:pathMatch(.*)*",
 | 
			
		||||
    component: PageNotFound,
 | 
			
		||||
    name: "page-not-found",
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
const app = createApp(App);
 | 
			
		||||
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach((i) =>
 | 
			
		||||
  i.install?.(app)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(`${config.base}/`),
 | 
			
		||||
  routes,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
createApp(App)
 | 
			
		||||
  .use(router)
 | 
			
		||||
  .use(createPinia())
 | 
			
		||||
  .use(Autocomplete)
 | 
			
		||||
  .use(Button)
 | 
			
		||||
  .use(Dropdown)
 | 
			
		||||
  .use(Switch)
 | 
			
		||||
  .use(Tooltip)
 | 
			
		||||
  .use(Modal)
 | 
			
		||||
  .use(Radio)
 | 
			
		||||
  .use(Field)
 | 
			
		||||
  .use(Skeleton)
 | 
			
		||||
  .use(Config, bulmaConfig)
 | 
			
		||||
  .mount("#app");
 | 
			
		||||
app.mount("#app");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								assets/models/LogEntry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								assets/models/LogEntry.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
import { Component, ComputedRef, Ref } from "vue";
 | 
			
		||||
import { flattenJSON, getDeep } from "@/utils";
 | 
			
		||||
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
 | 
			
		||||
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 interface LogEvent {
 | 
			
		||||
  readonly m: string | JSONObject;
 | 
			
		||||
  readonly ts: number;
 | 
			
		||||
  readonly id: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
 | 
			
		||||
  protected readonly _message: T;
 | 
			
		||||
  constructor(message: T, public readonly id: number, public readonly date: Date) {
 | 
			
		||||
    this._message = message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get message(): T {
 | 
			
		||||
    return this._message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abstract getComponent(): Component;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SimpleLogEntry extends LogEntry<string> {
 | 
			
		||||
  getComponent(): Component {
 | 
			
		||||
    return SimpleLogItem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ComplexLogEntry extends LogEntry<JSONObject> {
 | 
			
		||||
  private readonly filteredMessage: ComputedRef<JSONObject>;
 | 
			
		||||
 | 
			
		||||
  constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
 | 
			
		||||
    super(message, id, date);
 | 
			
		||||
    if (visibleKeys) {
 | 
			
		||||
      this.filteredMessage = computed(() => {
 | 
			
		||||
        if (!visibleKeys.value.length) {
 | 
			
		||||
          return flattenJSON(message);
 | 
			
		||||
        } else {
 | 
			
		||||
          return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      this.filteredMessage = computed(() => flattenJSON(message));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  getComponent(): Component {
 | 
			
		||||
    return ComplexLogItem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get message(): JSONObject {
 | 
			
		||||
    return this.filteredMessage.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get unfilteredMessage(): JSONObject {
 | 
			
		||||
    return this._message;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
 | 
			
		||||
    return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class DockerEventLogEntry extends LogEntry<string> {
 | 
			
		||||
  constructor(message: string, date: Date, public readonly event: string) {
 | 
			
		||||
    super(message, date.getTime(), date);
 | 
			
		||||
  }
 | 
			
		||||
  getComponent(): Component {
 | 
			
		||||
    return DockerEventLogItem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SkippedLogsEntry extends LogEntry<string> {
 | 
			
		||||
  private _totalSkipped = 0;
 | 
			
		||||
  private lastSkipped: LogEntry<string | JSONObject>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    date: Date,
 | 
			
		||||
    totalSkipped: number,
 | 
			
		||||
    public readonly firstSkipped: LogEntry<string | JSONObject>,
 | 
			
		||||
    lastSkipped: LogEntry<string | JSONObject>
 | 
			
		||||
  ) {
 | 
			
		||||
    super("", date.getTime(), date);
 | 
			
		||||
    this._totalSkipped = totalSkipped;
 | 
			
		||||
    this.lastSkipped = lastSkipped;
 | 
			
		||||
  }
 | 
			
		||||
  getComponent(): Component {
 | 
			
		||||
    return SkippedEntriesLogItem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get message(): string {
 | 
			
		||||
    return `Skipped ${this.totalSkipped} entries`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addSkippedEntries(totalSkipped: number, lastItem: LogEntry<string | JSONObject>) {
 | 
			
		||||
    this._totalSkipped += totalSkipped;
 | 
			
		||||
    this.lastSkipped = lastItem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get totalSkipped(): number {
 | 
			
		||||
    return this._totalSkipped;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
 | 
			
		||||
  if (typeof event.m === "string") {
 | 
			
		||||
    return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
 | 
			
		||||
  } else {
 | 
			
		||||
    return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								assets/modules/bulma.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								assets/modules/bulma.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import { type App } from "vue";
 | 
			
		||||
import {
 | 
			
		||||
  Autocomplete,
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Radio,
 | 
			
		||||
  Skeleton,
 | 
			
		||||
  Field,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  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(Tooltip)
 | 
			
		||||
    .use(Modal)
 | 
			
		||||
    .use(Radio)
 | 
			
		||||
    .use(Field)
 | 
			
		||||
    .use(Skeleton)
 | 
			
		||||
    .use(Config, bulmaConfig);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										20
									
								
								assets/modules/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								assets/modules/i18n.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import { type App } from "vue";
 | 
			
		||||
import { createI18n } from "vue-i18n";
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										8
									
								
								assets/modules/pinia.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								assets/modules/pinia.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { type App } from "vue";
 | 
			
		||||
import { createPinia } from "pinia";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const install = (app:App) => {
 | 
			
		||||
  const pinia = createPinia();
 | 
			
		||||
  app.use(pinia);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										16
									
								
								assets/modules/router.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								assets/modules/router.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { type App } from "vue";
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router";
 | 
			
		||||
import pages from "~pages";
 | 
			
		||||
import { setupLayouts } from "virtual:generated-layouts";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
 | 
			
		||||
export const install = (app: App) => {
 | 
			
		||||
  const routes = setupLayouts(pages);
 | 
			
		||||
 | 
			
		||||
  const router = createRouter({
 | 
			
		||||
    history: createWebHistory(`${config.base}/`),
 | 
			
		||||
    routes,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  app.use(router);
 | 
			
		||||
};
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <search></search>
 | 
			
		||||
  <log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, toRefs, watchEffect } from "vue";
 | 
			
		||||
import Search from "@/components/Search.vue";
 | 
			
		||||
import LogContainer from "@/components/LogContainer.vue";
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { id } = toRefs(props);
 | 
			
		||||
 | 
			
		||||
const currentContainer = store.currentContainer(id);
 | 
			
		||||
const { activeContainers } = storeToRefs(store);
 | 
			
		||||
 | 
			
		||||
setTitle("loading");
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  setTitle(currentContainer.value?.name);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchEffect(() => setTitle(currentContainer.value?.name));
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="hero is-halfheight">
 | 
			
		||||
    <div class="hero-body">
 | 
			
		||||
      <div class="container has-text-centered">
 | 
			
		||||
        <h1 class="title">
 | 
			
		||||
          Container not found.
 | 
			
		||||
          <small class="subtitle">It may have been removed.</small>
 | 
			
		||||
        </h1>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "ContainerNotFound",
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Container not found");
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -4,11 +4,8 @@
 | 
			
		||||
      <div class="hero-body">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
          <div class="columns">
 | 
			
		||||
            <div class="column">
 | 
			
		||||
              <h1 class="title">Hello, there!</h1>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="column is-narrow" v-if="secured">
 | 
			
		||||
              <a class="button is-primary is-small" :href="`${base}/logout`">Logout</a>
 | 
			
		||||
              <a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -18,31 +15,31 @@
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ containers.length }}</p>
 | 
			
		||||
          <p class="heading">Total Containers</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-containers") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ runningContainers.length }}</p>
 | 
			
		||||
          <p class="heading">Running</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.running") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title" data-ci-skip>{{ totalCpu }}%</p>
 | 
			
		||||
          <p class="heading">Total CPU Usage</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-cpu-usage") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
 | 
			
		||||
          <p class="heading">Total Mem Usage</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-mem-usage") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ version }}</p>
 | 
			
		||||
          <p class="heading">Dozzle Version</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.dozzle-version") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
@@ -50,13 +47,13 @@
 | 
			
		||||
    <section class="columns is-centered section is-marginless">
 | 
			
		||||
      <div class="column is-4">
 | 
			
		||||
        <div class="panel">
 | 
			
		||||
          <p class="panel-heading">Containers</p>
 | 
			
		||||
          <p class="panel-heading">{{ $t("label.containers") }}</p>
 | 
			
		||||
          <div class="panel-block">
 | 
			
		||||
            <p class="control has-icons-left">
 | 
			
		||||
              <input
 | 
			
		||||
                class="input"
 | 
			
		||||
                type="text"
 | 
			
		||||
                placeholder="Search Containers"
 | 
			
		||||
                :placeholder="$t('placeholder.search-containers')"
 | 
			
		||||
                v-model="search"
 | 
			
		||||
                @keyup.esc="search = null"
 | 
			
		||||
                @keyup.enter="onEnter()"
 | 
			
		||||
@@ -67,11 +64,11 @@
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <p class="panel-tabs" v-if="!search">
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
 | 
			
		||||
          </p>
 | 
			
		||||
          <router-link
 | 
			
		||||
            :to="{ name: 'container', params: { id: item.id, name: item.name } }"
 | 
			
		||||
            :to="{ name: 'container-id', params: { id: item.id } }"
 | 
			
		||||
            v-for="item in results.slice(0, 10)"
 | 
			
		||||
            :key="item.id"
 | 
			
		||||
            class="panel-block"
 | 
			
		||||
@@ -89,16 +86,8 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { formatBytes } from "@/utils";
 | 
			
		||||
import fuzzysort from "fuzzysort";
 | 
			
		||||
import SearchIcon from "~icons/mdi-light/magnify";
 | 
			
		||||
import PastTime from "../components/PastTime.vue";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { useIntervalFn } from "@vueuse/core";
 | 
			
		||||
 | 
			
		||||
const { base, version, secured } = config;
 | 
			
		||||
const containerStore = useContainerStore();
 | 
			
		||||
@@ -145,7 +134,7 @@ useIntervalFn(
 | 
			
		||||
function onEnter() {
 | 
			
		||||
  if (results.value.length == 1) {
 | 
			
		||||
    const [item] = results.value;
 | 
			
		||||
    router.push({ name: "container", params: { id: item.id, name: item.name } });
 | 
			
		||||
    router.push({ name: "container-id", params: { id: item.id } });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <form action="" method="post" @submit.prevent="onLogin" ref="form">
 | 
			
		||||
                  <div class="field">
 | 
			
		||||
                    <label class="label">Username</label>
 | 
			
		||||
                    <label class="label">{{ $t("label.username") }}</label>
 | 
			
		||||
                    <div class="control">
 | 
			
		||||
                      <input
 | 
			
		||||
                        class="input"
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
                  </div>
 | 
			
		||||
 | 
			
		||||
                  <div class="field">
 | 
			
		||||
                    <label class="label">Password</label>
 | 
			
		||||
                    <label class="label">{{ $t("label.password") }}</label>
 | 
			
		||||
                    <div class="control">
 | 
			
		||||
                      <input
 | 
			
		||||
                        class="input"
 | 
			
		||||
@@ -32,11 +32,11 @@
 | 
			
		||||
                        v-model="password"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <p class="help is-danger" v-if="error">Username and password are not valid.</p>
 | 
			
		||||
                    <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">Login</button>
 | 
			
		||||
                      <button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </form>
 | 
			
		||||
@@ -49,35 +49,31 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
export default {
 | 
			
		||||
  name: "Login",
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      username: null,
 | 
			
		||||
      password: null,
 | 
			
		||||
      error: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Authentication Required");
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async onLogin() {
 | 
			
		||||
      const response = await fetch(`${config.base}/api/validateCredentials`, {
 | 
			
		||||
        body: new FormData(this.$refs.form),
 | 
			
		||||
        method: "post",
 | 
			
		||||
      });
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
      if (response.status == 200) {
 | 
			
		||||
        this.error = false;
 | 
			
		||||
        window.location.href = `${config.base}/`;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.error = true;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
setTitle(t("title.login"));
 | 
			
		||||
 | 
			
		||||
let error = $ref(false);
 | 
			
		||||
let username = $ref("");
 | 
			
		||||
let password = $ref("");
 | 
			
		||||
let form = $ref();
 | 
			
		||||
 | 
			
		||||
async function onLogin() {
 | 
			
		||||
  const response = await fetch(`${config.base}/api/validateCredentials`, {
 | 
			
		||||
    body: new FormData(form),
 | 
			
		||||
    method: "post",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (response.status == 200) {
 | 
			
		||||
    error = false;
 | 
			
		||||
    window.location.href = `${config.base}/`;
 | 
			
		||||
  } else {
 | 
			
		||||
    error = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<route lang="yaml">
 | 
			
		||||
meta:
 | 
			
		||||
  layout: splash
 | 
			
		||||
</route>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,35 +2,32 @@
 | 
			
		||||
  <div>
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">About</h2>
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.about") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        You are using Dozzle <i>{{ currentVersion }}</i
 | 
			
		||||
        >.
 | 
			
		||||
        <span v-if="hasUpdate">
 | 
			
		||||
          New version is available! Update to
 | 
			
		||||
          <a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
 | 
			
		||||
            {{ nextRelease.name }}</a
 | 
			
		||||
          >.
 | 
			
		||||
        </span>
 | 
			
		||||
        <span v-html="$t('settings.using-version', { version: currentVersion })"></span>
 | 
			
		||||
        <div
 | 
			
		||||
          v-if="hasUpdate"
 | 
			
		||||
          v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">Display</h2>
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.display") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
 | 
			
		||||
        <o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="showTimestamp"> Show timestamps </o-switch>
 | 
			
		||||
        <o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="softWrap"> Soft wrap lines</o-switch>
 | 
			
		||||
        <o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
@@ -54,7 +51,7 @@
 | 
			
		||||
            </o-field>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="column">
 | 
			
		||||
            By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
 | 
			
		||||
            {{ $t("settings.12-24-format") }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -83,7 +80,7 @@
 | 
			
		||||
              </o-dropdown>
 | 
			
		||||
            </o-field>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="column">Font size to use for logs</div>
 | 
			
		||||
          <div class="column">{{ $t("settings.font-size") }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
@@ -111,33 +108,30 @@
 | 
			
		||||
              </o-dropdown>
 | 
			
		||||
            </o-field>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="column">Color scheme</div>
 | 
			
		||||
          <div class="column">{{ $t("settings.color-scheme") }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">Options</h2>
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.options") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="search">
 | 
			
		||||
          Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
 | 
			
		||||
          <span v-html="$t('settings.search')"></span>
 | 
			
		||||
        </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
 | 
			
		||||
        <o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import gt from "semver/functions/gt";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
import {
 | 
			
		||||
  search,
 | 
			
		||||
  lightTheme,
 | 
			
		||||
@@ -149,22 +143,28 @@ import {
 | 
			
		||||
  softWrap,
 | 
			
		||||
} from "@/composables/settings";
 | 
			
		||||
 | 
			
		||||
setTitle("Settings");
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
const currentVersion = config.version;
 | 
			
		||||
const nextRelease = ref({ html_url: "", name: "" });
 | 
			
		||||
const hasUpdate = ref(false);
 | 
			
		||||
setTitle(t("title.settings"));
 | 
			
		||||
 | 
			
		||||
const currentVersion = $ref(config.version);
 | 
			
		||||
let nextRelease = $ref({ html_url: "", name: "" });
 | 
			
		||||
let hasUpdate = $ref(false);
 | 
			
		||||
 | 
			
		||||
async function fetchNextRelease() {
 | 
			
		||||
  if (!["dev", "master"].includes(currentVersion)) {
 | 
			
		||||
    const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
      const release = await response.json();
 | 
			
		||||
      hasUpdate.value = gt(release.tag_name, currentVersion);
 | 
			
		||||
      nextRelease.value = release;
 | 
			
		||||
      hasUpdate = gt(release.tag_name, currentVersion);
 | 
			
		||||
      nextRelease = release;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    hasUpdate.value = true;
 | 
			
		||||
    hasUpdate = true;
 | 
			
		||||
    nextRelease = {
 | 
			
		||||
      html_url: "",
 | 
			
		||||
      name: "master",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,4 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { watch } from "vue";
 | 
			
		||||
import { useRoute, useRouter } from "vue-router";
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
 | 
			
		||||
@@ -15,15 +10,16 @@ watch(visibleContainers, (newValue) => {
 | 
			
		||||
    if (route.query.name) {
 | 
			
		||||
      const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
 | 
			
		||||
      if (container) {
 | 
			
		||||
        router.push({ name: "container", params: { id: container.id } });
 | 
			
		||||
        router.push({ name: "container-id", params: { id: container.id } });
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
 | 
			
		||||
        router.push({ name: "default" });
 | 
			
		||||
        router.push({ name: "index" });
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(`Expection query parameter name to be set. Redirecting to /`);
 | 
			
		||||
      router.push({ name: "default" });
 | 
			
		||||
      router.push({ name: "index" });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<template></template>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,14 @@
 | 
			
		||||
      <div class="container has-text-centered">
 | 
			
		||||
        <h1 class="title">
 | 
			
		||||
          404.
 | 
			
		||||
          <small class="subtitle">This page does not exist.</small>
 | 
			
		||||
          <small class="subtitle">{{ $t("error.page-not-found") }}</small>
 | 
			
		||||
        </h1>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
export default {
 | 
			
		||||
  name: "PageNotFound",
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Page not found");
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
setTitle(t("title.page-not-found"));
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										20
									
								
								assets/pages/container/[id].vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								assets/pages/container/[id].vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <search></search>
 | 
			
		||||
  <log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const { id } = defineProps<{ id: string }>();
 | 
			
		||||
 | 
			
		||||
const currentContainer = store.currentContainer($$(id));
 | 
			
		||||
const { activeContainers } = storeToRefs(store);
 | 
			
		||||
 | 
			
		||||
setTitle("loading");
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  setTitle(currentContainer.value?.name);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchEffect(() => setTitle(currentContainer.value?.name));
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
export { default as Index } from "./Index.vue";
 | 
			
		||||
export { default as ContainerNotFound } from "./ContainerNotFound.vue";
 | 
			
		||||
export { default as Show } from "./Show.vue";
 | 
			
		||||
export { default as Container } from "./Container.vue";
 | 
			
		||||
export { default as Settings } from "./Settings.vue";
 | 
			
		||||
export { default as PageNotFound } from "./PageNotFound.vue";
 | 
			
		||||
export { default as Login } from "./Login.vue";
 | 
			
		||||
							
								
								
									
										165
									
								
								assets/pages/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								assets/pages/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <section class="hero is-small mt-4">
 | 
			
		||||
      <div class="hero-body">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
          <div class="columns">
 | 
			
		||||
            <div class="column is-narrow" v-if="secured">
 | 
			
		||||
              <a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="level section">
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ containers.length }}</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-containers") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ runningContainers.length }}</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.running") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title" data-ci-skip>{{ totalCpu }}%</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-cpu-usage") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.total-mem-usage") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="level-item has-text-centered">
 | 
			
		||||
        <div>
 | 
			
		||||
          <p class="title">{{ version }}</p>
 | 
			
		||||
          <p class="heading">{{ $t("label.dozzle-version") }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    <section class="columns is-centered section is-marginless">
 | 
			
		||||
      <div class="column is-4">
 | 
			
		||||
        <div class="panel">
 | 
			
		||||
          <p class="panel-heading">{{ $t("label.containers") }}</p>
 | 
			
		||||
          <div class="panel-block">
 | 
			
		||||
            <p class="control has-icons-left">
 | 
			
		||||
              <input
 | 
			
		||||
                class="input"
 | 
			
		||||
                type="text"
 | 
			
		||||
                :placeholder="$t('placeholder.search-containers')"
 | 
			
		||||
                v-model="search"
 | 
			
		||||
                @keyup.esc="search = null"
 | 
			
		||||
                @keyup.enter="onEnter()"
 | 
			
		||||
              />
 | 
			
		||||
              <span class="icon is-left">
 | 
			
		||||
                <search-icon />
 | 
			
		||||
              </span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <p class="panel-tabs" v-if="!search">
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
 | 
			
		||||
            <a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
 | 
			
		||||
          </p>
 | 
			
		||||
          <router-link
 | 
			
		||||
            :to="{ name: 'container-id', params: { id: item.id } }"
 | 
			
		||||
            v-for="item in results.slice(0, 10)"
 | 
			
		||||
            :key="item.id"
 | 
			
		||||
            class="panel-block"
 | 
			
		||||
          >
 | 
			
		||||
            <span class="name">{{ item.name }}</span>
 | 
			
		||||
 | 
			
		||||
            <div class="subtitle is-7 status">
 | 
			
		||||
              <past-time :date="new Date(item.created * 1000)"></past-time>
 | 
			
		||||
            </div>
 | 
			
		||||
          </router-link>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import fuzzysort from "fuzzysort";
 | 
			
		||||
import SearchIcon from "~icons/mdi-light/magnify";
 | 
			
		||||
 | 
			
		||||
const { base, version, secured } = config;
 | 
			
		||||
const containerStore = useContainerStore();
 | 
			
		||||
const { containers } = storeToRefs(containerStore);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const sort = ref("running");
 | 
			
		||||
const search = ref();
 | 
			
		||||
 | 
			
		||||
const results = computed(() => {
 | 
			
		||||
  if (search.value) {
 | 
			
		||||
    return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
 | 
			
		||||
  }
 | 
			
		||||
  switch (sort.value) {
 | 
			
		||||
    case "all":
 | 
			
		||||
      return mostRecentContainers.value;
 | 
			
		||||
    case "running":
 | 
			
		||||
      return runningContainers.value;
 | 
			
		||||
    default:
 | 
			
		||||
      throw `Invalid sort order: ${sort.value}`;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
 | 
			
		||||
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
 | 
			
		||||
const totalCpu = ref(0);
 | 
			
		||||
useIntervalFn(
 | 
			
		||||
  () => {
 | 
			
		||||
    totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
 | 
			
		||||
  },
 | 
			
		||||
  1000,
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
const totalMem = ref(0);
 | 
			
		||||
 | 
			
		||||
useIntervalFn(
 | 
			
		||||
  () => {
 | 
			
		||||
    totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
 | 
			
		||||
  },
 | 
			
		||||
  1000,
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function onEnter() {
 | 
			
		||||
  if (results.value.length == 1) {
 | 
			
		||||
    const [item] = results.value;
 | 
			
		||||
    router.push({ name: "container-id", params: { id: item.id } });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
  padding: 10px 3px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										79
									
								
								assets/pages/login.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								assets/pages/login.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
<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>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
setTitle(t("title.login"));
 | 
			
		||||
 | 
			
		||||
let error = $ref(false);
 | 
			
		||||
let username = $ref("");
 | 
			
		||||
let password = $ref("");
 | 
			
		||||
let form = $ref();
 | 
			
		||||
 | 
			
		||||
async function onLogin() {
 | 
			
		||||
  const response = await fetch(`${config.base}/api/validateCredentials`, {
 | 
			
		||||
    body: new FormData(form),
 | 
			
		||||
    method: "post",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (response.status == 200) {
 | 
			
		||||
    error = false;
 | 
			
		||||
    window.location.href = `${config.base}/`;
 | 
			
		||||
  } else {
 | 
			
		||||
    error = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<route lang="yaml">
 | 
			
		||||
meta:
 | 
			
		||||
  layout: splash
 | 
			
		||||
</route>
 | 
			
		||||
							
								
								
									
										203
									
								
								assets/pages/settings.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								assets/pages/settings.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.about") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <span v-html="$t('settings.using-version', { version: currentVersion })"></span>
 | 
			
		||||
        <div
 | 
			
		||||
          v-if="hasUpdate"
 | 
			
		||||
          v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.display") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
 | 
			
		||||
      </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>
 | 
			
		||||
      <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>
 | 
			
		||||
      <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>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="section">
 | 
			
		||||
      <div class="has-underline">
 | 
			
		||||
        <h2 class="title is-4">{{ $t("settings.options") }}</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="search">
 | 
			
		||||
          <span v-html="$t('settings.search')"></span>
 | 
			
		||||
        </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import gt from "semver/functions/gt";
 | 
			
		||||
import {
 | 
			
		||||
  search,
 | 
			
		||||
  lightTheme,
 | 
			
		||||
  smallerScrollbars,
 | 
			
		||||
  showTimestamp,
 | 
			
		||||
  hourStyle,
 | 
			
		||||
  showAllContainers,
 | 
			
		||||
  size,
 | 
			
		||||
  softWrap,
 | 
			
		||||
} from "@/composables/settings";
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
setTitle(t("title.settings"));
 | 
			
		||||
 | 
			
		||||
const currentVersion = $ref(config.version);
 | 
			
		||||
let nextRelease = $ref({ html_url: "", name: "" });
 | 
			
		||||
let hasUpdate = $ref(false);
 | 
			
		||||
 | 
			
		||||
async function fetchNextRelease() {
 | 
			
		||||
  if (!["dev", "master"].includes(currentVersion)) {
 | 
			
		||||
    const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
      const release = await response.json();
 | 
			
		||||
      hasUpdate = gt(release.tag_name, currentVersion);
 | 
			
		||||
      nextRelease = release;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    hasUpdate = true;
 | 
			
		||||
    nextRelease = {
 | 
			
		||||
      html_url: "",
 | 
			
		||||
      name: "master",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.has-underline {
 | 
			
		||||
  border-bottom: 1px solid var(--border-color);
 | 
			
		||||
  padding: 1em 0px;
 | 
			
		||||
  margin-bottom: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item {
 | 
			
		||||
  padding: 1em 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  background-color: #444;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										25
									
								
								assets/pages/show.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								assets/pages/show.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const { visibleContainers } = storeToRefs(store);
 | 
			
		||||
 | 
			
		||||
watch(visibleContainers, (newValue) => {
 | 
			
		||||
  if (newValue) {
 | 
			
		||||
    if (route.query.name) {
 | 
			
		||||
      const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
 | 
			
		||||
      if (container) {
 | 
			
		||||
        router.push({ name: "container-id", params: { id: container.id } });
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
 | 
			
		||||
        router.push({ name: "index" });
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(`Expection query parameter name to be set. Redirecting to /`);
 | 
			
		||||
      router.push({ name: "index" });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<template></template>
 | 
			
		||||
@@ -1,6 +1,20 @@
 | 
			
		||||
const text = document.querySelector("script#config__json")?.textContent || "{}";
 | 
			
		||||
 | 
			
		||||
const config = JSON.parse(text);
 | 
			
		||||
interface Config {
 | 
			
		||||
  version: string;
 | 
			
		||||
  base: string;
 | 
			
		||||
  authorizationNeeded: boolean | "false" | "true";
 | 
			
		||||
  secured: boolean | "false" | "true";
 | 
			
		||||
  maxLogs: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const pageConfig = JSON.parse(text);
 | 
			
		||||
 | 
			
		||||
const config: Config = {
 | 
			
		||||
  maxLogs: 600,
 | 
			
		||||
  ...pageConfig,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (config.version == "{{ .Version }}") {
 | 
			
		||||
  config.version = "master";
 | 
			
		||||
  config.base = "";
 | 
			
		||||
@@ -11,4 +25,5 @@ if (config.version == "{{ .Version }}") {
 | 
			
		||||
  config.authorizationNeeded = config.authorizationNeeded === "true";
 | 
			
		||||
  config.secured = config.secured === "true";
 | 
			
		||||
}
 | 
			
		||||
export default config;
 | 
			
		||||
 | 
			
		||||
export default config as Config;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ $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);
 | 
			
		||||
@@ -38,8 +39,7 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
 | 
			
		||||
@import "splitpanes/dist/splitpanes.css";
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
[data-theme="dark"] {
 | 
			
		||||
@mixin dark {
 | 
			
		||||
  --scheme-main: #{$black};
 | 
			
		||||
  --scheme-main-bis: #{$black-bis};
 | 
			
		||||
  --scheme-main-ter: #{$black-ter};
 | 
			
		||||
@@ -53,6 +53,7 @@ html,
 | 
			
		||||
 | 
			
		||||
  --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%);
 | 
			
		||||
@@ -64,93 +65,58 @@ html,
 | 
			
		||||
 | 
			
		||||
  --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: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --primary-color: #{$turquoise};
 | 
			
		||||
  --secondary-color: #d8f0ca;
 | 
			
		||||
 | 
			
		||||
  --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 {
 | 
			
		||||
  --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};
 | 
			
		||||
 | 
			
		||||
  --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};
 | 
			
		||||
    @include dark;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: light) {
 | 
			
		||||
  html {
 | 
			
		||||
  --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: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --primary-color: #{$turquoise};
 | 
			
		||||
  --secondary-color: #d8f0ca;
 | 
			
		||||
 | 
			
		||||
  --body-background-color: #{$white-bis};
 | 
			
		||||
  --action-toolbar-background-color: #{$light-toolbar-color};
 | 
			
		||||
  --body-color: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --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};
 | 
			
		||||
    @include light;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[data-theme="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: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --primary-color: #{$turquoise};
 | 
			
		||||
  --secondary-color: #d8f0ca;
 | 
			
		||||
 | 
			
		||||
  --body-background-color: #{$white-bis};
 | 
			
		||||
  --action-toolbar-background-color: #{$light-toolbar-color};
 | 
			
		||||
  --body-color: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --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};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
  overflow-x: unset;
 | 
			
		||||
  overflow-y: unset;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								assets/types/Container.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								assets/types/Container.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -4,6 +4,7 @@ export interface Container {
 | 
			
		||||
  readonly image: string;
 | 
			
		||||
  readonly name: string;
 | 
			
		||||
  readonly status: string;
 | 
			
		||||
  readonly command: string;
 | 
			
		||||
  state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
 | 
			
		||||
  stat?: ContainerStat;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								assets/types/LogEntry.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								assets/types/LogEntry.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +0,0 @@
 | 
			
		||||
export interface LogEntry {
 | 
			
		||||
  date: Date;
 | 
			
		||||
  message: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  event?: string;
 | 
			
		||||
  selected?: boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,7 @@
 | 
			
		||||
import { Container } from "@/types/Container";
 | 
			
		||||
import { useStorage } from "@vueuse/core";
 | 
			
		||||
import { computed, ComputedRef } from "vue";
 | 
			
		||||
 | 
			
		||||
export function formatBytes(bytes: number, decimals = 2) {
 | 
			
		||||
  if (bytes === 0) return "0 Bytes";
 | 
			
		||||
  const k = 1024;
 | 
			
		||||
@@ -6,3 +10,38 @@ export function formatBytes(bytes: number, decimals = 2) {
 | 
			
		||||
  const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDeep(obj: Record<string, any>, path: string[]) {
 | 
			
		||||
  return path.reduce((acc, key) => acc?.[key], obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isObject(value: any): value is Record<string, any> {
 | 
			
		||||
  return typeof value === "object" && value !== null && !Array.isArray(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
 | 
			
		||||
  const result: Record<string, any> = {};
 | 
			
		||||
  Object.keys(obj).forEach((key) => {
 | 
			
		||||
    const value = obj[key];
 | 
			
		||||
    const newPath = path.concat(key);
 | 
			
		||||
    if (isObject(value)) {
 | 
			
		||||
      Object.assign(result, flattenJSON(value, newPath));
 | 
			
		||||
    } else {
 | 
			
		||||
      result[newPath.join(".")] = value;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function arrayEquals(a: string[], b: string[]): boolean {
 | 
			
		||||
  return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function persistentVisibleKeys(container: ComputedRef<Container>) {
 | 
			
		||||
  return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stripVersion(label: string) {
 | 
			
		||||
  const [name, _] = label.split(":");
 | 
			
		||||
  return name;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,6 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
 | 
			
		||||
				memPercent  = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			if cpuPercent > 0 || memUsage > 0 {
 | 
			
		||||
				select {
 | 
			
		||||
				case <-ctx.Done():
 | 
			
		||||
@@ -174,8 +173,10 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
 | 
			
		||||
	log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
 | 
			
		||||
 | 
			
		||||
	if since != "" {
 | 
			
		||||
		if sinceTime, err := time.Parse(time.RFC3339Nano, since); err == nil {
 | 
			
		||||
			since = sinceTime.Add(time.Microsecond).Format(time.RFC3339Nano)
 | 
			
		||||
		if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
 | 
			
		||||
			since = time.UnixMicro(millis).Add(time.Millisecond).Format(time.RFC3339Nano)
 | 
			
		||||
		} else {
 | 
			
		||||
			log.WithError(err).Debug("unable to parse since")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,3 +26,9 @@ type ContainerEvent struct {
 | 
			
		||||
	ActorID string `json:"actorId"`
 | 
			
		||||
	Name    string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LogEvent struct {
 | 
			
		||||
	Message   any    `json:"m,omitempty"`
 | 
			
		||||
	Timestamp int64  `json:"ts"`
 | 
			
		||||
	Id        uint32 `json:"id,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM cypress/included:10.4.0
 | 
			
		||||
FROM cypress/included:10.8.0
 | 
			
		||||
 | 
			
		||||
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { defineConfig } from "cypress";
 | 
			
		||||
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/dist/plugins';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  fixturesFolder: false,
 | 
			
		||||
@@ -6,7 +7,7 @@ export default defineConfig({
 | 
			
		||||
 | 
			
		||||
  e2e: {
 | 
			
		||||
    setupNodeEvents(on, config) {
 | 
			
		||||
      // implement node event listeners here
 | 
			
		||||
      initPlugin(on, config);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "DOZZLE_DEFAULT": "http://localhost:3000/"
 | 
			
		||||
  "DOZZLE_DEFAULT": "http://localhost:8080/",
 | 
			
		||||
  "DOZZLE_AUTH": "http://localhost:8080/"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 30 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										14
									
								
								e2e/cypress/e2e/dozze_auth.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								e2e/cypress/e2e/dozze_auth.cy.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_AUTH") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("login screen", () => {
 | 
			
		||||
    cy.get("input[name=username]").type("foo");
 | 
			
		||||
    cy.get("input[name=password]").type("bar");
 | 
			
		||||
    cy.get("button[type=submit]").click();
 | 
			
		||||
    cy.get("p.menu-label").should("contain", "Containers");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -5,8 +5,8 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it.skip("home screen", () => {
 | 
			
		||||
    cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImageSnapshot();
 | 
			
		||||
  it("home screen", () => {
 | 
			
		||||
    cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("correct title", () => {
 | 
			
		||||
							
								
								
									
										17
									
								
								e2e/cypress/e2e/dozze_i18n.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								e2e/cypress/e2e/dozze_i18n.cy.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle es lang", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/", {
 | 
			
		||||
      onBeforeLoad(win) {
 | 
			
		||||
        Object.defineProperty(win.navigator, "language", {
 | 
			
		||||
          value: "es_MX",
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should find contenedores", () => {
 | 
			
		||||
    cy.get("p.menu-label").should("contain", "Contenedores");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										15
									
								
								e2e/cypress/e2e/dozzle_custom_base.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								e2e/cypress/e2e/dozzle_custom_base.cy.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle custom base", { baseUrl: Cypress.env("DOZZLE_CUSTOM") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("custom base should work", () => {
 | 
			
		||||
    cy.get("p.menu-label").should("contain", "Containers");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("url should be custom", () => {
 | 
			
		||||
    cy.url().should("include", "foobarbase");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										12
									
								
								e2e/cypress/e2e/dozzle_dark.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								e2e/cypress/e2e/dozzle_dark.cy.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle dark mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
    cy.window().then((win) => win.document.documentElement.setAttribute("data-theme", "dark"));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("home screen", () => {
 | 
			
		||||
    cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context.skip("Dozzle light mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  before(() => {
 | 
			
		||||
    cy.visit("/settings");
 | 
			
		||||
    cy.contains("Use light theme").click();
 | 
			
		||||
  });
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("home screen", () => {
 | 
			
		||||
    cy.get("li.running", { timeout: 10000 }).removeDates().matchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
// ***********************************************************
 | 
			
		||||
// This example plugins/index.js can be used to load plugins
 | 
			
		||||
//
 | 
			
		||||
// You can change the location of this file or turn off loading
 | 
			
		||||
// the plugins file with the 'pluginsFile' configuration option.
 | 
			
		||||
//
 | 
			
		||||
// You can read more here:
 | 
			
		||||
// https://on.cypress.io/plugins-guide
 | 
			
		||||
// ***********************************************************
 | 
			
		||||
 | 
			
		||||
// This function is called when a project is opened or re-opened (e.g. due to
 | 
			
		||||
// the project's config changing)
 | 
			
		||||
 | 
			
		||||
const { addMatchImageSnapshotPlugin } = require("cypress-image-snapshot/plugin");
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type {Cypress.PluginConfig}
 | 
			
		||||
 */
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
module.exports = (on, config) => {
 | 
			
		||||
  // `on` is used to hook into various events Cypress emits
 | 
			
		||||
  // `config` is the resolved Cypress config
 | 
			
		||||
 | 
			
		||||
  addMatchImageSnapshotPlugin(on, config);
 | 
			
		||||
};
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
 | 
			
		||||
// Import commands.js using ES2015 syntax:
 | 
			
		||||
import './commands'
 | 
			
		||||
import '@frsource/cypress-plugin-visual-regression-diff/dist/support';
 | 
			
		||||
 | 
			
		||||
// Alternatively you can use CommonJS syntax:
 | 
			
		||||
// require('./commands')
 | 
			
		||||
// require('./commands')
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,25 @@ services:
 | 
			
		||||
      - DOZZLE_FILTER=name=custom_base
 | 
			
		||||
      - DOZZLE_BASE=/foobarbase
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8080:8080"
 | 
			
		||||
    image: amir20/dozzle_custom_cache
 | 
			
		||||
    build:
 | 
			
		||||
      context: ..
 | 
			
		||||
      cache_from:
 | 
			
		||||
        - amir20/dozzle_custom_cache:latest
 | 
			
		||||
  auth:
 | 
			
		||||
    container_name: auth
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock
 | 
			
		||||
    environment:
 | 
			
		||||
      - DOZZLE_FILTER=name=auth
 | 
			
		||||
      - DOZZLE_USERNAME=foo
 | 
			
		||||
      - DOZZLE_PASSWORD=bar
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
    image: amir20/dozzle_custom_cache
 | 
			
		||||
    build:
 | 
			
		||||
      context: ..
 | 
			
		||||
      cache_from:
 | 
			
		||||
        - amir20/dozzle_custom_cache:latest
 | 
			
		||||
  dozzle:
 | 
			
		||||
    container_name: dozzle
 | 
			
		||||
    volumes:
 | 
			
		||||
@@ -19,20 +34,25 @@ services:
 | 
			
		||||
    environment:
 | 
			
		||||
      - DOZZLE_FILTER=name=dozzle
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "9090:8080"
 | 
			
		||||
    image: amir20/dozzle_cache:latest
 | 
			
		||||
    build:
 | 
			
		||||
      context: ..
 | 
			
		||||
      cache_from:
 | 
			
		||||
        - amir20/dozzle_cache:latest
 | 
			
		||||
  cypress:
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      cache_from:
 | 
			
		||||
        - amir20/dozzle_cypress_cache:latest
 | 
			
		||||
    image: amir20/dozzle_cypress_cache:latest
 | 
			
		||||
    working_dir: /e2e
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./cypress:/e2e/cypress
 | 
			
		||||
      - ./cypress.config.ts:/e2e/cypress.config.ts
 | 
			
		||||
    environment:
 | 
			
		||||
      - CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
 | 
			
		||||
      - CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
 | 
			
		||||
      - CYPRESS_DOZZLE_AUTH=http://auth:8080/
 | 
			
		||||
      - CYPRESS_DOZZLE_CUSTOM=http://custom_base:8080/foobarbase
 | 
			
		||||
      - CYPRESS_RECORD_KEY=155c3cf8-b2dd-4f5e-9fb3-7635f5b79d4d
 | 
			
		||||
      - COMMIT_INFO_BRANCH=${GITHUB_REF_NAME}
 | 
			
		||||
      - COMMIT_INFO_AUTHOR=${GITHUB_ACTOR}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "e2e",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "scripts": {},
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "test": "cypress run"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "cypress": "^10.3.0",
 | 
			
		||||
    "cypress-image-snapshot": "^4.0.1",
 | 
			
		||||
    "typescript": "^4.7.4"
 | 
			
		||||
    "@frsource/cypress-plugin-visual-regression-diff": "^1.9.18",
 | 
			
		||||
    "cypress": "^10.7.0",
 | 
			
		||||
    "typescript": "^4.8.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										593
									
								
								e2e/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										593
									
								
								e2e/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -1,14 +1,14 @@
 | 
			
		||||
lockfileVersion: 5.4
 | 
			
		||||
 | 
			
		||||
specifiers:
 | 
			
		||||
  cypress: ^10.3.0
 | 
			
		||||
  cypress-image-snapshot: ^4.0.1
 | 
			
		||||
  typescript: ^4.7.4
 | 
			
		||||
  '@frsource/cypress-plugin-visual-regression-diff': ^1.9.18
 | 
			
		||||
  cypress: ^10.7.0
 | 
			
		||||
  typescript: ^4.8.3
 | 
			
		||||
 | 
			
		||||
dependencies:
 | 
			
		||||
  cypress: 10.3.0
 | 
			
		||||
  cypress-image-snapshot: 4.0.1_cypress@10.3.0
 | 
			
		||||
  typescript: 4.7.4
 | 
			
		||||
  '@frsource/cypress-plugin-visual-regression-diff': 1.9.18_cypress@10.7.0
 | 
			
		||||
  cypress: 10.7.0
 | 
			
		||||
  typescript: 4.8.3
 | 
			
		||||
 | 
			
		||||
packages:
 | 
			
		||||
 | 
			
		||||
@@ -52,8 +52,21 @@ packages:
 | 
			
		||||
      - supports-color
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@types/node/14.18.21:
 | 
			
		||||
    resolution: {integrity: sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==}
 | 
			
		||||
  /@frsource/cypress-plugin-visual-regression-diff/1.9.18_cypress@10.7.0:
 | 
			
		||||
    resolution: {integrity: sha512-QS52M0V4UFqQHW85cKRoTKVtzYWQ0BA88+rDco3BMkzSpOQokwgZ7iYlnKJbLuePNLakbaoHWNWSDjNrG9OP8w==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      cypress: '>=4.5.0'
 | 
			
		||||
    dependencies:
 | 
			
		||||
      cypress: 10.7.0
 | 
			
		||||
      move-file: 2.1.0
 | 
			
		||||
      pixelmatch: 5.3.0
 | 
			
		||||
      pngjs: 6.0.0
 | 
			
		||||
      sharp: 0.30.7
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@types/node/14.18.26:
 | 
			
		||||
    resolution: {integrity: sha512-0b+utRBSYj8L7XAp0d+DX7lI4cSmowNaaTkk6/1SKzbKkG+doLuPusB9EOvzLJ8ahJSk03bTLIL6cWaEd4dBKA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@types/sinonjs__fake-timers/8.1.1:
 | 
			
		||||
@@ -68,7 +81,7 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@types/node': 14.18.21
 | 
			
		||||
      '@types/node': 14.18.26
 | 
			
		||||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
@@ -92,28 +105,11 @@ packages:
 | 
			
		||||
      type-fest: 0.21.3
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ansi-regex/2.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ansi-regex/5.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ansi-styles/2.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ansi-styles/3.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      color-convert: 1.9.3
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ansi-styles/4.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -121,13 +117,6 @@ packages:
 | 
			
		||||
      color-convert: 2.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /app-path/3.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-EAgEXkdcxH1cgEePOSsmUtw9ItPl0KTxnh/pj9ZbhvbKbij9x0oX6PWpGnorDr0DS5AosLgoa5n3T/hZmKQpYA==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      execa: 1.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /arch/2.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -183,6 +172,14 @@ packages:
 | 
			
		||||
      tweetnacl: 0.14.5
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /bl/4.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      buffer: 5.7.1
 | 
			
		||||
      inherits: 2.0.4
 | 
			
		||||
      readable-stream: 3.6.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /blob-util/2.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -218,26 +215,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /chalk/1.1.3:
 | 
			
		||||
    resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      ansi-styles: 2.2.1
 | 
			
		||||
      escape-string-regexp: 1.0.5
 | 
			
		||||
      has-ansi: 2.0.0
 | 
			
		||||
      strip-ansi: 3.0.1
 | 
			
		||||
      supports-color: 2.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /chalk/2.4.2:
 | 
			
		||||
    resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      ansi-styles: 3.2.1
 | 
			
		||||
      escape-string-regexp: 1.0.5
 | 
			
		||||
      supports-color: 5.5.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /chalk/4.1.2:
 | 
			
		||||
    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
@@ -251,6 +228,10 @@ packages:
 | 
			
		||||
    engines: {node: '>= 0.8.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /chownr/1.1.4:
 | 
			
		||||
    resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ci-info/3.3.2:
 | 
			
		||||
    resolution: {integrity: sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -284,12 +265,6 @@ packages:
 | 
			
		||||
      string-width: 4.2.3
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color-convert/1.9.3:
 | 
			
		||||
    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      color-name: 1.1.3
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color-convert/2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
 | 
			
		||||
    engines: {node: '>=7.0.0'}
 | 
			
		||||
@@ -297,14 +272,25 @@ packages:
 | 
			
		||||
      color-name: 1.1.4
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color-name/1.1.3:
 | 
			
		||||
    resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color-name/1.1.4:
 | 
			
		||||
    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color-string/1.9.1:
 | 
			
		||||
    resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      color-name: 1.1.4
 | 
			
		||||
      simple-swizzle: 0.2.2
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /color/4.2.3:
 | 
			
		||||
    resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
 | 
			
		||||
    engines: {node: '>=12.5.0'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      color-convert: 2.0.1
 | 
			
		||||
      color-string: 1.9.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /colorette/2.0.19:
 | 
			
		||||
    resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -334,17 +320,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /cross-spawn/6.0.5:
 | 
			
		||||
    resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
 | 
			
		||||
    engines: {node: '>=4.8'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      nice-try: 1.0.5
 | 
			
		||||
      path-key: 2.0.1
 | 
			
		||||
      semver: 5.7.1
 | 
			
		||||
      shebang-command: 1.2.0
 | 
			
		||||
      which: 1.3.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /cross-spawn/7.0.3:
 | 
			
		||||
    resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
 | 
			
		||||
    engines: {node: '>= 8'}
 | 
			
		||||
@@ -354,32 +329,15 @@ packages:
 | 
			
		||||
      which: 2.0.2
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /cypress-image-snapshot/4.0.1_cypress@10.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-PBpnhX/XItlx3/DAk5ozsXQHUi72exybBNH5Mpqj1DVmjq+S5Jd9WE5CRa4q5q0zuMZb2V2VpXHth6MjFpgj9Q==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      cypress: ^4.5.0
 | 
			
		||||
    dependencies:
 | 
			
		||||
      chalk: 2.4.2
 | 
			
		||||
      cypress: 10.3.0
 | 
			
		||||
      fs-extra: 7.0.1
 | 
			
		||||
      glob: 7.2.0
 | 
			
		||||
      jest-image-snapshot: 4.2.0
 | 
			
		||||
      pkg-dir: 3.0.0
 | 
			
		||||
      term-img: 4.1.0
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - jest
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /cypress/10.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==}
 | 
			
		||||
  /cypress/10.7.0:
 | 
			
		||||
    resolution: {integrity: sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ==}
 | 
			
		||||
    engines: {node: '>=12.0.0'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@cypress/request': 2.88.10
 | 
			
		||||
      '@cypress/xvfb': 1.2.4_supports-color@8.1.1
 | 
			
		||||
      '@types/node': 14.18.21
 | 
			
		||||
      '@types/node': 14.18.26
 | 
			
		||||
      '@types/sinonjs__fake-timers': 8.1.1
 | 
			
		||||
      '@types/sizzle': 2.3.3
 | 
			
		||||
      arch: 2.2.0
 | 
			
		||||
@@ -393,10 +351,10 @@ packages:
 | 
			
		||||
      cli-table3: 0.6.2
 | 
			
		||||
      commander: 5.1.0
 | 
			
		||||
      common-tags: 1.8.2
 | 
			
		||||
      dayjs: 1.11.3
 | 
			
		||||
      dayjs: 1.11.5
 | 
			
		||||
      debug: 4.3.4_supports-color@8.1.1
 | 
			
		||||
      enquirer: 2.3.6
 | 
			
		||||
      eventemitter2: 6.4.5
 | 
			
		||||
      eventemitter2: 6.4.7
 | 
			
		||||
      execa: 4.1.0
 | 
			
		||||
      executable: 4.1.1
 | 
			
		||||
      extract-zip: 2.0.1_supports-color@8.1.1
 | 
			
		||||
@@ -428,8 +386,8 @@ packages:
 | 
			
		||||
      assert-plus: 1.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /dayjs/1.11.3:
 | 
			
		||||
    resolution: {integrity: sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==}
 | 
			
		||||
  /dayjs/1.11.5:
 | 
			
		||||
    resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /debug/3.2.7_supports-color@8.1.1:
 | 
			
		||||
@@ -457,11 +415,28 @@ packages:
 | 
			
		||||
      supports-color: 8.1.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /decompress-response/6.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      mimic-response: 3.1.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /deep-extend/0.6.0:
 | 
			
		||||
    resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
 | 
			
		||||
    engines: {node: '>=4.0.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /delayed-stream/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
 | 
			
		||||
    engines: {node: '>=0.4.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /detect-libc/2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ecc-jsbn/0.1.2:
 | 
			
		||||
    resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
@@ -487,25 +462,12 @@ packages:
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /escape-string-regexp/1.0.5:
 | 
			
		||||
    resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
 | 
			
		||||
    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
 | 
			
		||||
    engines: {node: '>=0.8.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /eventemitter2/6.4.5:
 | 
			
		||||
    resolution: {integrity: sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /execa/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      cross-spawn: 6.0.5
 | 
			
		||||
      get-stream: 4.1.0
 | 
			
		||||
      is-stream: 1.1.0
 | 
			
		||||
      npm-run-path: 2.0.2
 | 
			
		||||
      p-finally: 1.0.0
 | 
			
		||||
      signal-exit: 3.0.7
 | 
			
		||||
      strip-eof: 1.0.0
 | 
			
		||||
  /eventemitter2/6.4.7:
 | 
			
		||||
    resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /execa/4.1.0:
 | 
			
		||||
@@ -530,6 +492,11 @@ packages:
 | 
			
		||||
      pify: 2.3.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /expand-template/2.0.3:
 | 
			
		||||
    resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /extend/3.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -566,13 +533,6 @@ packages:
 | 
			
		||||
      escape-string-regexp: 1.0.5
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /find-up/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      locate-path: 3.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /forever-agent/0.6.1:
 | 
			
		||||
    resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -586,13 +546,8 @@ packages:
 | 
			
		||||
      mime-types: 2.1.35
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /fs-extra/7.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
 | 
			
		||||
    engines: {node: '>=6 <7 || >=8'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      graceful-fs: 4.2.8
 | 
			
		||||
      jsonfile: 4.0.0
 | 
			
		||||
      universalify: 0.1.2
 | 
			
		||||
  /fs-constants/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /fs-extra/9.1.0:
 | 
			
		||||
@@ -606,19 +561,7 @@ packages:
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /fs.realpath/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /get-stdin/5.0.1:
 | 
			
		||||
    resolution: {integrity: sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=}
 | 
			
		||||
    engines: {node: '>=0.12.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /get-stream/4.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      pump: 3.0.0
 | 
			
		||||
    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /get-stream/5.2.0:
 | 
			
		||||
@@ -640,15 +583,8 @@ packages:
 | 
			
		||||
      assert-plus: 1.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /glob/7.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      fs.realpath: 1.0.0
 | 
			
		||||
      inflight: 1.0.6
 | 
			
		||||
      inherits: 2.0.4
 | 
			
		||||
      minimatch: 3.0.4
 | 
			
		||||
      once: 1.4.0
 | 
			
		||||
      path-is-absolute: 1.0.1
 | 
			
		||||
  /github-from-package/0.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /glob/7.2.3:
 | 
			
		||||
@@ -669,30 +605,10 @@ packages:
 | 
			
		||||
      ini: 2.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /glur/1.1.2:
 | 
			
		||||
    resolution: {integrity: sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok=}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /graceful-fs/4.2.10:
 | 
			
		||||
    resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /graceful-fs/4.2.8:
 | 
			
		||||
    resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /has-ansi/2.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      ansi-regex: 2.1.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /has-flag/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /has-flag/4.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -722,7 +638,7 @@ packages:
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /inflight/1.0.6:
 | 
			
		||||
    resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
 | 
			
		||||
    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      once: 1.4.0
 | 
			
		||||
      wrappy: 1.0.2
 | 
			
		||||
@@ -732,11 +648,19 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ini/1.3.8:
 | 
			
		||||
    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ini/2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /is-arrayish/0.3.2:
 | 
			
		||||
    resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /is-ci/3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
@@ -762,11 +686,6 @@ packages:
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /is-stream/1.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /is-stream/2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -789,31 +708,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /iterm2-version/4.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-IoiNVk4SMPu6uTcK+1nA5QaHNok2BMDLjSl5UomrOixe5g4GkylhPwuiGdw00ysSCrXAKNMfFTu+u/Lk5f6OLQ==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      app-path: 3.3.0
 | 
			
		||||
      plist: 3.0.4
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /jest-image-snapshot/4.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-6aAqv2wtfOgxiJeBayBCqHo1zX+A12SUNNzo7rIxiXh6W6xYVu8QyHWkada8HeRi+QUTHddp0O0Xa6kmQr+xbQ==}
 | 
			
		||||
    engines: {node: '>= 10.14.2'}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      jest: '>=20 <=26'
 | 
			
		||||
    dependencies:
 | 
			
		||||
      chalk: 1.1.3
 | 
			
		||||
      get-stdin: 5.0.1
 | 
			
		||||
      glur: 1.1.2
 | 
			
		||||
      lodash: 4.17.21
 | 
			
		||||
      mkdirp: 0.5.5
 | 
			
		||||
      pixelmatch: 5.2.1
 | 
			
		||||
      pngjs: 3.4.0
 | 
			
		||||
      rimraf: 2.7.1
 | 
			
		||||
      ssim.js: 3.5.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /jsbn/0.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -826,12 +720,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /jsonfile/4.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
 | 
			
		||||
    optionalDependencies:
 | 
			
		||||
      graceful-fs: 4.2.8
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /jsonfile/6.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
@@ -870,19 +758,11 @@ packages:
 | 
			
		||||
      log-update: 4.0.0
 | 
			
		||||
      p-map: 4.0.0
 | 
			
		||||
      rfdc: 1.3.0
 | 
			
		||||
      rxjs: 7.5.5
 | 
			
		||||
      rxjs: 7.5.6
 | 
			
		||||
      through: 2.3.8
 | 
			
		||||
      wrap-ansi: 7.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /locate-path/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      p-locate: 3.0.0
 | 
			
		||||
      path-exists: 3.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /lodash.once/4.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -937,10 +817,9 @@ packages:
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /minimatch/3.0.4:
 | 
			
		||||
    resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      brace-expansion: 1.1.11
 | 
			
		||||
  /mimic-response/3.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /minimatch/3.1.2:
 | 
			
		||||
@@ -953,11 +832,15 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /mkdirp/0.5.5:
 | 
			
		||||
    resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
  /mkdirp-classic/0.5.3:
 | 
			
		||||
    resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /move-file/2.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
 | 
			
		||||
    engines: {node: '>=10.17'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      minimist: 1.2.6
 | 
			
		||||
      path-exists: 4.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ms/2.1.2:
 | 
			
		||||
@@ -968,15 +851,19 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /nice-try/1.0.5:
 | 
			
		||||
    resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
 | 
			
		||||
  /napi-build-utils/1.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /npm-run-path/2.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
  /node-abi/3.24.0:
 | 
			
		||||
    resolution: {integrity: sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      path-key: 2.0.1
 | 
			
		||||
      semver: 7.3.7
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /node-addon-api/5.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /npm-run-path/4.0.1:
 | 
			
		||||
@@ -987,7 +874,7 @@ packages:
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /once/1.4.0:
 | 
			
		||||
    resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
 | 
			
		||||
    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      wrappy: 1.0.2
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -1003,25 +890,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /p-finally/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /p-limit/2.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      p-try: 2.2.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /p-locate/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      p-limit: 2.3.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /p-map/4.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
@@ -1029,26 +897,16 @@ packages:
 | 
			
		||||
      aggregate-error: 3.1.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /p-try/2.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /path-exists/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
  /path-exists/4.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /path-is-absolute/1.0.1:
 | 
			
		||||
    resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
 | 
			
		||||
    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /path-key/2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /path-key/3.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -1067,36 +925,35 @@ packages:
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pixelmatch/5.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==}
 | 
			
		||||
  /pixelmatch/5.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      pngjs: 4.0.1
 | 
			
		||||
      pngjs: 6.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pkg-dir/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
  /pngjs/6.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
 | 
			
		||||
    engines: {node: '>=12.13.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /prebuild-install/7.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      find-up: 3.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /plist/3.0.4:
 | 
			
		||||
    resolution: {integrity: sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      base64-js: 1.5.1
 | 
			
		||||
      xmlbuilder: 9.0.7
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pngjs/3.4.0:
 | 
			
		||||
    resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
 | 
			
		||||
    engines: {node: '>=4.0.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pngjs/4.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==}
 | 
			
		||||
    engines: {node: '>=8.0.0'}
 | 
			
		||||
      detect-libc: 2.0.1
 | 
			
		||||
      expand-template: 2.0.3
 | 
			
		||||
      github-from-package: 0.0.0
 | 
			
		||||
      minimist: 1.2.6
 | 
			
		||||
      mkdirp-classic: 0.5.3
 | 
			
		||||
      napi-build-utils: 1.0.2
 | 
			
		||||
      node-abi: 3.24.0
 | 
			
		||||
      pump: 3.0.0
 | 
			
		||||
      rc: 1.2.8
 | 
			
		||||
      simple-get: 4.0.1
 | 
			
		||||
      tar-fs: 2.1.1
 | 
			
		||||
      tunnel-agent: 0.6.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pretty-bytes/5.6.0:
 | 
			
		||||
@@ -1108,8 +965,8 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /psl/1.8.0:
 | 
			
		||||
    resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==}
 | 
			
		||||
  /psl/1.9.0:
 | 
			
		||||
    resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /pump/3.0.0:
 | 
			
		||||
@@ -1129,6 +986,25 @@ packages:
 | 
			
		||||
    engines: {node: '>=0.6'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /rc/1.2.8:
 | 
			
		||||
    resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      deep-extend: 0.6.0
 | 
			
		||||
      ini: 1.3.8
 | 
			
		||||
      minimist: 1.2.6
 | 
			
		||||
      strip-json-comments: 2.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /readable-stream/3.6.0:
 | 
			
		||||
    resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
 | 
			
		||||
    engines: {node: '>= 6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      inherits: 2.0.4
 | 
			
		||||
      string_decoder: 1.3.0
 | 
			
		||||
      util-deprecate: 1.0.2
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /request-progress/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
@@ -1147,13 +1023,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /rimraf/2.7.1:
 | 
			
		||||
    resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      glob: 7.2.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /rimraf/3.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
@@ -1161,8 +1030,8 @@ packages:
 | 
			
		||||
      glob: 7.2.3
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /rxjs/7.5.5:
 | 
			
		||||
    resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
 | 
			
		||||
  /rxjs/7.5.6:
 | 
			
		||||
    resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      tslib: 2.4.0
 | 
			
		||||
    dev: false
 | 
			
		||||
@@ -1175,11 +1044,6 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /semver/5.7.1:
 | 
			
		||||
    resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /semver/7.3.7:
 | 
			
		||||
    resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
@@ -1188,11 +1052,19 @@ packages:
 | 
			
		||||
      lru-cache: 6.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /shebang-command/1.2.0:
 | 
			
		||||
    resolution: {integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
  /sharp/0.30.7:
 | 
			
		||||
    resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
 | 
			
		||||
    engines: {node: '>=12.13.0'}
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      shebang-regex: 1.0.0
 | 
			
		||||
      color: 4.2.3
 | 
			
		||||
      detect-libc: 2.0.1
 | 
			
		||||
      node-addon-api: 5.0.0
 | 
			
		||||
      prebuild-install: 7.1.1
 | 
			
		||||
      semver: 7.3.7
 | 
			
		||||
      simple-get: 4.0.1
 | 
			
		||||
      tar-fs: 2.1.1
 | 
			
		||||
      tunnel-agent: 0.6.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /shebang-command/2.0.0:
 | 
			
		||||
@@ -1202,11 +1074,6 @@ packages:
 | 
			
		||||
      shebang-regex: 3.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /shebang-regex/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /shebang-regex/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -1216,6 +1083,24 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /simple-concat/1.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /simple-get/4.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      decompress-response: 6.0.0
 | 
			
		||||
      once: 1.4.0
 | 
			
		||||
      simple-concat: 1.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /simple-swizzle/0.2.2:
 | 
			
		||||
    resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      is-arrayish: 0.3.2
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /slice-ansi/3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -1250,10 +1135,6 @@ packages:
 | 
			
		||||
      tweetnacl: 0.14.5
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ssim.js/3.5.0:
 | 
			
		||||
    resolution: {integrity: sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /string-width/4.2.3:
 | 
			
		||||
    resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
@@ -1263,11 +1144,10 @@ packages:
 | 
			
		||||
      strip-ansi: 6.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /strip-ansi/3.0.1:
 | 
			
		||||
    resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
  /string_decoder/1.3.0:
 | 
			
		||||
    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      ansi-regex: 2.1.1
 | 
			
		||||
      safe-buffer: 5.2.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /strip-ansi/6.0.1:
 | 
			
		||||
@@ -1277,26 +1157,14 @@ packages:
 | 
			
		||||
      ansi-regex: 5.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /strip-eof/1.0.0:
 | 
			
		||||
    resolution: {integrity: sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /strip-final-newline/2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /supports-color/2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
 | 
			
		||||
    engines: {node: '>=0.8.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /supports-color/5.5.0:
 | 
			
		||||
    resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      has-flag: 3.0.0
 | 
			
		||||
  /strip-json-comments/2.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
 | 
			
		||||
    engines: {node: '>=0.10.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /supports-color/7.2.0:
 | 
			
		||||
@@ -1313,12 +1181,24 @@ packages:
 | 
			
		||||
      has-flag: 4.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /term-img/4.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-DFpBhaF5j+2f7kheKFc1ajsAUUDGOaNPpKPtiIMxlbfud6mvfFZuWGnTRpaujUa5J7yl6cIw/h6nyr4mSsENPg==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
  /tar-fs/2.1.1:
 | 
			
		||||
    resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      ansi-escapes: 4.3.2
 | 
			
		||||
      iterm2-version: 4.2.0
 | 
			
		||||
      chownr: 1.1.4
 | 
			
		||||
      mkdirp-classic: 0.5.3
 | 
			
		||||
      pump: 3.0.0
 | 
			
		||||
      tar-stream: 2.2.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /tar-stream/2.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
 | 
			
		||||
    engines: {node: '>=6'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      bl: 4.1.0
 | 
			
		||||
      end-of-stream: 1.4.4
 | 
			
		||||
      fs-constants: 1.0.0
 | 
			
		||||
      inherits: 2.0.4
 | 
			
		||||
      readable-stream: 3.6.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /throttleit/1.0.0:
 | 
			
		||||
@@ -1340,7 +1220,7 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
 | 
			
		||||
    engines: {node: '>=0.8'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      psl: 1.8.0
 | 
			
		||||
      psl: 1.9.0
 | 
			
		||||
      punycode: 2.1.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
@@ -1363,17 +1243,12 @@ packages:
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /typescript/4.7.4:
 | 
			
		||||
    resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
 | 
			
		||||
  /typescript/4.8.3:
 | 
			
		||||
    resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
 | 
			
		||||
    engines: {node: '>=4.2.0'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /universalify/0.1.2:
 | 
			
		||||
    resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
 | 
			
		||||
    engines: {node: '>= 4.0.0'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /universalify/2.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
 | 
			
		||||
    engines: {node: '>= 10.0.0'}
 | 
			
		||||
@@ -1384,6 +1259,10 @@ packages:
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /util-deprecate/1.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /uuid/8.3.2:
 | 
			
		||||
    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
@@ -1398,13 +1277,6 @@ packages:
 | 
			
		||||
      extsprintf: 1.3.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /which/1.3.1:
 | 
			
		||||
    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      isexe: 2.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /which/2.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
 | 
			
		||||
    engines: {node: '>= 8'}
 | 
			
		||||
@@ -1432,12 +1304,7 @@ packages:
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /wrappy/1.0.2:
 | 
			
		||||
    resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /xmlbuilder/9.0.7:
 | 
			
		||||
    resolution: {integrity: sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=}
 | 
			
		||||
    engines: {node: '>=4.0'}
 | 
			
		||||
    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /yallist/4.0.0:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -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.7.1+incompatible // indirect
 | 
			
		||||
	github.com/docker/docker v20.10.17+incompatible
 | 
			
		||||
	github.com/docker/docker v20.10.18+incompatible
 | 
			
		||||
	github.com/docker/go-connections v0.4.0 // indirect
 | 
			
		||||
	github.com/docker/go-units v0.4.0 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -62,8 +62,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
 | 
			
		||||
github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc=
 | 
			
		||||
github.com/docker/docker v20.10.18+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.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import type { Config } from "@jest/types";
 | 
			
		||||
 | 
			
		||||
const config: Config.InitialOptions = {
 | 
			
		||||
  preset: "ts-jest",
 | 
			
		||||
  testEnvironment: "jsdom",
 | 
			
		||||
  testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/", "<rootDir>/e2e/"],
 | 
			
		||||
  transform: {
 | 
			
		||||
    "^.+\\.vue$": "@vue/vue3-jest",
 | 
			
		||||
  },
 | 
			
		||||
  moduleNameMapper: {
 | 
			
		||||
    "@/(.*)": ["<rootDir>/assets/$1"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										50
									
								
								locales/en.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								locales/en.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
toolbar:
 | 
			
		||||
  clear: Clear
 | 
			
		||||
  download: Download
 | 
			
		||||
  search: Search
 | 
			
		||||
label:
 | 
			
		||||
  containers: Containers
 | 
			
		||||
  total-containers: Total Containers
 | 
			
		||||
  running: Running
 | 
			
		||||
  total-cpu-usage: Total CPU Usage
 | 
			
		||||
  total-mem-usage: Total Mem Usage
 | 
			
		||||
  dozzle-version: Dozzle Version
 | 
			
		||||
  all: All
 | 
			
		||||
  password: Password
 | 
			
		||||
  username: Username
 | 
			
		||||
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.
 | 
			
		||||
  logs-skipped: Skipped {total} entries
 | 
			
		||||
title:
 | 
			
		||||
  page-not-found: Page not found
 | 
			
		||||
  login: Authentication Required
 | 
			
		||||
  settings: Settings
 | 
			
		||||
button:
 | 
			
		||||
  logout: Logout
 | 
			
		||||
  login: Login
 | 
			
		||||
placeholder:
 | 
			
		||||
  search-containers: Search Containers
 | 
			
		||||
settings:
 | 
			
		||||
  display: Display
 | 
			
		||||
  small-scrollbars: Use smaller scrollbars
 | 
			
		||||
  show-timesamps: Show timestamps
 | 
			
		||||
  soft-wrap: Soft wrap lines
 | 
			
		||||
  12-24-format: >-
 | 
			
		||||
    By default, Dozzle will use your browser's locale to format time. You can
 | 
			
		||||
    force to 12 or 24 hour style.
 | 
			
		||||
  font-size: Font size to use for logs
 | 
			
		||||
  color-scheme: Color scheme
 | 
			
		||||
  options: Options
 | 
			
		||||
  show-stopped-containers: Show stopped containers
 | 
			
		||||
  about: About
 | 
			
		||||
  search: >-
 | 
			
		||||
    Enable searching with Dozzle using <code>command+f</code> or
 | 
			
		||||
    <code>ctrl+f</code>
 | 
			
		||||
  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>.
 | 
			
		||||
							
								
								
									
										50
									
								
								locales/es.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								locales/es.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
toolbar:
 | 
			
		||||
  clear: Limpiar
 | 
			
		||||
  download: Descargar
 | 
			
		||||
  search: Buscar
 | 
			
		||||
label:
 | 
			
		||||
  containers: Contenedores
 | 
			
		||||
  total-containers: Contenedores Totales
 | 
			
		||||
  running: En ejecución
 | 
			
		||||
  total-cpu-usage: Uso total del Procesador
 | 
			
		||||
  total-mem-usage: Uso total de la Memoria
 | 
			
		||||
  dozzle-version: Versión de Dozzle
 | 
			
		||||
  all: Todo
 | 
			
		||||
  password: Contraseña
 | 
			
		||||
  username: Nombre de usuario
 | 
			
		||||
tooltip:
 | 
			
		||||
  search: Buscar contenedores (⌘ + K, CTRL + K)
 | 
			
		||||
  pin-column: Anclar como columna
 | 
			
		||||
error:
 | 
			
		||||
  page-not-found: Esta página no existe.
 | 
			
		||||
  invalid-auth: El nombre de usuario y la contraseña no son válidos.
 | 
			
		||||
  logs-skipped: Omitidas {total} entrada/s
 | 
			
		||||
title:
 | 
			
		||||
  page-not-found: Página no encontrada
 | 
			
		||||
  login: Autenticación requerida
 | 
			
		||||
  settings: Configuración
 | 
			
		||||
button:
 | 
			
		||||
  logout: Cerrar la sesión
 | 
			
		||||
  login: Iniciar sesión
 | 
			
		||||
placeholder:
 | 
			
		||||
  search-containers: Buscar Contenedores
 | 
			
		||||
settings:
 | 
			
		||||
  display: Vista
 | 
			
		||||
  small-scrollbars: Utilizar barras de desplazamiento más pequeñas
 | 
			
		||||
  show-timesamps: Mostrar marcas de tiempo
 | 
			
		||||
  soft-wrap: Líneas de texto con ajuste suave
 | 
			
		||||
  12-24-format: >-
 | 
			
		||||
    Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
 | 
			
		||||
    forzar el estilo de 12 o 24 horas.
 | 
			
		||||
  font-size: Tamaño de letra a utilizar para los registros
 | 
			
		||||
  color-scheme: Esquema de colores
 | 
			
		||||
  options: Opciones
 | 
			
		||||
  show-stopped-containers: Mostrar contenedores parados
 | 
			
		||||
  about: Acerca de
 | 
			
		||||
  search: >-
 | 
			
		||||
    Activar la búsqueda con Dozzle mediante <code>comando+f</code> o
 | 
			
		||||
    <code>ctrl+f</code>
 | 
			
		||||
  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>.
 | 
			
		||||
							
								
								
									
										50
									
								
								locales/pr.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								locales/pr.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
toolbar:
 | 
			
		||||
  clear: Limpar
 | 
			
		||||
  download: Descarregar
 | 
			
		||||
  search: Pesquisa
 | 
			
		||||
label:
 | 
			
		||||
  containers: Contentores
 | 
			
		||||
  total-containers: Contentores Totais
 | 
			
		||||
  running: Em execução
 | 
			
		||||
  total-cpu-usage: Utilização total da CPU
 | 
			
		||||
  total-mem-usage: Total de utilização da memória
 | 
			
		||||
  dozzle-version: Versão Dozzle
 | 
			
		||||
  all: Tudo
 | 
			
		||||
  password: Senha
 | 
			
		||||
  username: Nome de usuário
 | 
			
		||||
tooltip:
 | 
			
		||||
  search: Pesquisar contentores (⌘ + K, CTRL + K)
 | 
			
		||||
  pin-column: Alfinete como coluna
 | 
			
		||||
error:
 | 
			
		||||
  page-not-found: Esta página não existe.
 | 
			
		||||
  invalid-auth: O nome de usuário e a senha não são válidos.
 | 
			
		||||
  logs-skipped: Saltado {total} entradas
 | 
			
		||||
title:
 | 
			
		||||
  page-not-found: Página não encontrada
 | 
			
		||||
  login: Autenticação Requerida
 | 
			
		||||
  settings: Configurações
 | 
			
		||||
button:
 | 
			
		||||
  logout: Terminar sessão
 | 
			
		||||
  login: Iniciar sessão
 | 
			
		||||
placeholder:
 | 
			
		||||
  search-containers: Pesquisar Contentores
 | 
			
		||||
settings:
 | 
			
		||||
  display: Visão
 | 
			
		||||
  small-scrollbars: Usar barras de rolagem mais pequenas
 | 
			
		||||
  show-timesamps: Mostrar carimbos de tempo
 | 
			
		||||
  soft-wrap: Linhas de texto de embrulho suave
 | 
			
		||||
  12-24-format: >-
 | 
			
		||||
    Por defeito, Dozzle utilizará o locale do seu navegador para formatar a hora. Pode
 | 
			
		||||
    forçar ao estilo de 12 ou 24 horas.
 | 
			
		||||
  font-size: Tamanho de letra a utilizar para os registos
 | 
			
		||||
  color-scheme: Esquema de cores
 | 
			
		||||
  options: Opções
 | 
			
		||||
  show-stopped-containers: Mostrar contentores parados
 | 
			
		||||
  about: Acerca de
 | 
			
		||||
  search: >-
 | 
			
		||||
    Habilitar a pesquisa com Dozzle usando <code>comando+f</code> ou
 | 
			
		||||
    <code>ctrl+f</code>
 | 
			
		||||
  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>.
 | 
			
		||||
							
								
								
									
										1
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.go
									
									
									
									
									
								
							@@ -5,7 +5,6 @@ import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	_ "net/http/pprof"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"strings"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										56
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "dozzle",
 | 
			
		||||
  "version": "3.13.1",
 | 
			
		||||
  "version": "4.1.2",
 | 
			
		||||
  "description": "Realtime log viewer for docker containers. ",
 | 
			
		||||
  "homepage": "https://github.com/amir20/dozzle#readme",
 | 
			
		||||
  "bugs": {
 | 
			
		||||
@@ -24,39 +24,42 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@iconify-json/carbon": "^1.1.7",
 | 
			
		||||
    "@iconify-json/cil": "^1.1.2",
 | 
			
		||||
    "@iconify-json/mdi": "^1.1.29",
 | 
			
		||||
    "@iconify-json/mdi": "^1.1.32",
 | 
			
		||||
    "@iconify-json/mdi-light": "^1.1.2",
 | 
			
		||||
    "@iconify-json/octicon": "^1.1.15",
 | 
			
		||||
    "@oruga-ui/oruga-next": "^0.5.4",
 | 
			
		||||
    "@oruga-ui/theme-bulma": "^0.2.6",
 | 
			
		||||
    "@vitejs/plugin-vue": "3.0.1",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.2.37",
 | 
			
		||||
    "@vueuse/core": "^9.1.0",
 | 
			
		||||
    "@vueuse/router": "^9.1.0",
 | 
			
		||||
    "@iconify-json/octicon": "^1.1.17",
 | 
			
		||||
    "@intlify/vite-plugin-vue-i18n": "^6.0.1",
 | 
			
		||||
    "@oruga-ui/oruga-next": "^0.5.6",
 | 
			
		||||
    "@oruga-ui/theme-bulma": "^0.2.7",
 | 
			
		||||
    "@vitejs/plugin-vue": "3.1.0",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.2.39",
 | 
			
		||||
    "@vueuse/core": "^9.2.0",
 | 
			
		||||
    "@vueuse/router": "^9.2.0",
 | 
			
		||||
    "ansi-to-html": "^0.7.2",
 | 
			
		||||
    "bulma": "^0.9.4",
 | 
			
		||||
    "date-fns": "^2.29.1",
 | 
			
		||||
    "date-fns": "^2.29.3",
 | 
			
		||||
    "fuzzysort": "^2.0.1",
 | 
			
		||||
    "hotkeys-js": "^3.9.4",
 | 
			
		||||
    "hotkeys-js": "^3.10.0",
 | 
			
		||||
    "lodash.debounce": "^4.0.8",
 | 
			
		||||
    "pinia": "^2.0.17",
 | 
			
		||||
    "sass": "^1.54.2",
 | 
			
		||||
    "pinia": "^2.0.22",
 | 
			
		||||
    "sass": "^1.54.9",
 | 
			
		||||
    "semver": "^7.3.7",
 | 
			
		||||
    "splitpanes": "^3.1.1",
 | 
			
		||||
    "typescript": "^4.7.4",
 | 
			
		||||
    "unplugin-auto-import": "^0.10.3",
 | 
			
		||||
    "unplugin-icons": "^0.14.8",
 | 
			
		||||
    "unplugin-vue-components": "^0.22.0",
 | 
			
		||||
    "vite": "3.0.4",
 | 
			
		||||
    "vue": "^3.2.37",
 | 
			
		||||
    "vue-router": "^4.1.3"
 | 
			
		||||
    "typescript": "^4.8.3",
 | 
			
		||||
    "unplugin-auto-import": "^0.11.2",
 | 
			
		||||
    "unplugin-icons": "^0.14.9",
 | 
			
		||||
    "unplugin-vue-components": "^0.22.7",
 | 
			
		||||
    "vite": "3.1.1",
 | 
			
		||||
    "vite-plugin-pages": "^0.26.0",
 | 
			
		||||
    "vite-plugin-vue-layouts": "^0.7.0",
 | 
			
		||||
    "vue": "^3.2.39",
 | 
			
		||||
    "vue-i18n": "^9.2.2",
 | 
			
		||||
    "vue-router": "^4.1.5"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@pinia/testing": "^0.0.13",
 | 
			
		||||
    "@types/jest": "^28.1.6",
 | 
			
		||||
    "@pinia/testing": "^0.0.14",
 | 
			
		||||
    "@types/lodash.debounce": "^4.0.7",
 | 
			
		||||
    "@types/node": "^18.6.3",
 | 
			
		||||
    "@types/semver": "^7.3.10",
 | 
			
		||||
    "@types/node": "^18.7.18",
 | 
			
		||||
    "@types/semver": "^7.3.12",
 | 
			
		||||
    "@vue/test-utils": "^2.0.2",
 | 
			
		||||
    "c8": "^7.12.0",
 | 
			
		||||
    "eventsourcemock": "^2.0.0",
 | 
			
		||||
@@ -66,9 +69,10 @@
 | 
			
		||||
    "lint-staged": "^13.0.3",
 | 
			
		||||
    "npm-run-all": "^4.1.5",
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "release-it": "^15.2.0",
 | 
			
		||||
    "release-it": "^15.4.2",
 | 
			
		||||
    "ts-node": "^10.9.1",
 | 
			
		||||
    "vitest": "^0.20.3"
 | 
			
		||||
    "vitest": "^0.23.2",
 | 
			
		||||
    "vue-tsc": "^0.40.13"
 | 
			
		||||
  },
 | 
			
		||||
  "lint-staged": {
 | 
			
		||||
    "*.{js,vue,css}": [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1455
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1455
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -15,7 +15,9 @@
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["assets/*"]
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "types": ["vitest", "vite/client", "vue/ref-macros", "vite-plugin-pages/client", "vite-plugin-vue-layouts/client"]
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,11 @@ import { defineConfig } from "vite";
 | 
			
		||||
import vue from "@vitejs/plugin-vue";
 | 
			
		||||
import Icons from "unplugin-icons/vite";
 | 
			
		||||
import Components from "unplugin-vue-components/vite";
 | 
			
		||||
import AutoImport from "unplugin-auto-import/vite";
 | 
			
		||||
import IconsResolver from "unplugin-icons/resolver";
 | 
			
		||||
import Pages from "vite-plugin-pages";
 | 
			
		||||
import Layouts from "vite-plugin-vue-layouts";
 | 
			
		||||
import VueI18n from "@intlify/vite-plugin-vue-i18n";
 | 
			
		||||
 | 
			
		||||
export default defineConfig(({ mode }) => ({
 | 
			
		||||
  resolve: {
 | 
			
		||||
@@ -13,10 +17,19 @@ export default defineConfig(({ mode }) => ({
 | 
			
		||||
  },
 | 
			
		||||
  base: mode === "production" ? "/{{ .Base }}/" : "/",
 | 
			
		||||
  plugins: [
 | 
			
		||||
    vue(),
 | 
			
		||||
    vue({
 | 
			
		||||
      reactivityTransform: true,
 | 
			
		||||
    }),
 | 
			
		||||
    Icons({
 | 
			
		||||
      autoInstall: true,
 | 
			
		||||
    }),
 | 
			
		||||
    Pages({
 | 
			
		||||
      dirs: "assets/pages",
 | 
			
		||||
      importMode: "sync",
 | 
			
		||||
    }),
 | 
			
		||||
    Layouts({
 | 
			
		||||
      layoutsDirs: "assets/layouts",
 | 
			
		||||
    }),
 | 
			
		||||
    Components({
 | 
			
		||||
      dirs: ["assets/components"],
 | 
			
		||||
      resolvers: [
 | 
			
		||||
@@ -27,6 +40,17 @@ export default defineConfig(({ mode }) => ({
 | 
			
		||||
 | 
			
		||||
      dts: "assets/components.d.ts",
 | 
			
		||||
    }),
 | 
			
		||||
    AutoImport({
 | 
			
		||||
      imports: ["vue", "vue-router", "vue-i18n", "vue/macros", "pinia", "@vueuse/head", "@vueuse/core"],
 | 
			
		||||
      dts: "assets/auto-imports.d.ts",
 | 
			
		||||
      dirs: ["assets/composables", "assets/stores", "assets/utils"],
 | 
			
		||||
      vueTemplate: true,
 | 
			
		||||
    }),
 | 
			
		||||
    VueI18n({
 | 
			
		||||
      runtimeOnly: true,
 | 
			
		||||
      compositionOnly: true,
 | 
			
		||||
      include: [path.resolve(__dirname, "locales/**")],
 | 
			
		||||
    }),
 | 
			
		||||
    htmlPlugin(mode),
 | 
			
		||||
  ],
 | 
			
		||||
  server: {
 | 
			
		||||
@@ -41,7 +65,7 @@ export default defineConfig(({ mode }) => ({
 | 
			
		||||
const htmlPlugin = (mode) => {
 | 
			
		||||
  return {
 | 
			
		||||
    name: "html-transform",
 | 
			
		||||
    enforce: "post",
 | 
			
		||||
    enforce: "post" as const,
 | 
			
		||||
    transformIndexHtml(html) {
 | 
			
		||||
      return mode === "production" ? html.replaceAll("/{{ .Base }}/", "{{ .Base }}/") : html;
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -144,7 +144,7 @@ Connection: keep-alive
 | 
			
		||||
Content-Type: text/event-stream
 | 
			
		||||
X-Accel-Buffering: no
 | 
			
		||||
 | 
			
		||||
data: INFO Testing logs...
 | 
			
		||||
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898}
 | 
			
		||||
 | 
			
		||||
event: container-stopped
 | 
			
		||||
data: end of stream
 | 
			
		||||
@@ -170,8 +170,8 @@ Connection: keep-alive
 | 
			
		||||
Content-Type: text/event-stream
 | 
			
		||||
X-Accel-Buffering: no
 | 
			
		||||
 | 
			
		||||
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
 | 
			
		||||
id: 2020-05-13T18:55:37.772853839Z
 | 
			
		||||
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724}
 | 
			
		||||
id: 1589396137772
 | 
			
		||||
 | 
			
		||||
event: container-stopped
 | 
			
		||||
data: end of stream
 | 
			
		||||
							
								
								
									
										99
									
								
								web/logs.go
									
									
									
									
									
								
							
							
						
						
									
										99
									
								
								web/logs.go
									
									
									
									
									
								
							@@ -4,6 +4,8 @@ import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"hash/fnv"
 | 
			
		||||
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
@@ -13,29 +15,12 @@ import (
 | 
			
		||||
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/amir20/dozzle/docker"
 | 
			
		||||
	"github.com/dustin/go-humanize"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
 | 
			
		||||
 | 
			
		||||
	from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
 | 
			
		||||
	to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
 | 
			
		||||
	id := r.URL.Query().Get("id")
 | 
			
		||||
 | 
			
		||||
	reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	io.Copy(w, reader)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	id := r.URL.Query().Get("id")
 | 
			
		||||
	container, err := h.client.FindContainer(id)
 | 
			
		||||
@@ -63,6 +48,64 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	io.Copy(zw, reader)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
 | 
			
		||||
	return func() (docker.LogEvent, error) {
 | 
			
		||||
		message, readerError := reader.ReadString('\n')
 | 
			
		||||
 | 
			
		||||
		h := fnv.New32a()
 | 
			
		||||
		h.Write([]byte(message))
 | 
			
		||||
 | 
			
		||||
		logEvent := docker.LogEvent{Id: h.Sum32(), Message: message}
 | 
			
		||||
 | 
			
		||||
		if index := strings.IndexAny(message, " "); index != -1 {
 | 
			
		||||
			logId := message[:index]
 | 
			
		||||
			if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
 | 
			
		||||
				logEvent.Timestamp = timestamp.UnixMilli()
 | 
			
		||||
				message = strings.TrimSuffix(message[index+1:], "\n")
 | 
			
		||||
				logEvent.Message = message
 | 
			
		||||
				if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
 | 
			
		||||
					var data map[string]interface{}
 | 
			
		||||
					if err := json.Unmarshal([]byte(message), &data); err != nil {
 | 
			
		||||
						log.Errorf("json unmarshal error while streaming %v", err.Error())
 | 
			
		||||
					}
 | 
			
		||||
					logEvent.Message = data
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return logEvent, readerError
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
 | 
			
		||||
 | 
			
		||||
	from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
 | 
			
		||||
	to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
 | 
			
		||||
	id := r.URL.Query().Get("id")
 | 
			
		||||
 | 
			
		||||
	reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffered := bufio.NewReader(reader)
 | 
			
		||||
	eventIterator := logEventIterator(buffered)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		logEvent, readerError := eventIterator()
 | 
			
		||||
		if readerError != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if err := json.NewEncoder(w).Encode(logEvent); err != nil {
 | 
			
		||||
			log.Errorf("json encoding error while streaming %v", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	id := r.URL.Query().Get("id")
 | 
			
		||||
	if id == "" {
 | 
			
		||||
@@ -122,15 +165,19 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	buffered := bufio.NewReader(reader)
 | 
			
		||||
	var readerError error
 | 
			
		||||
	var message string
 | 
			
		||||
	eventIterator := logEventIterator(buffered)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		message, readerError = buffered.ReadString('\n')
 | 
			
		||||
		fmt.Fprintf(w, "data: %s\n", strings.TrimRight(message, "\n"))
 | 
			
		||||
		if index := strings.IndexAny(message, " "); index != -1 {
 | 
			
		||||
			id := message[:index]
 | 
			
		||||
			if _, err := time.Parse(time.RFC3339Nano, id); err == nil {
 | 
			
		||||
				fmt.Fprintf(w, "id: %s\n", id)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		var logEvent docker.LogEvent
 | 
			
		||||
		logEvent, readerError = eventIterator()
 | 
			
		||||
		if buf, err := json.Marshal(logEvent); err != nil {
 | 
			
		||||
			log.Errorf("json encoding error while streaming %v", err.Error())
 | 
			
		||||
		} else {
 | 
			
		||||
			fmt.Fprintf(w, "data: %s\n", buf)
 | 
			
		||||
		}
 | 
			
		||||
		if logEvent.Timestamp > 0 {
 | 
			
		||||
			fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Fprintf(w, "\n")
 | 
			
		||||
		f.Flush()
 | 
			
		||||
 
 | 
			
		||||
@@ -63,10 +63,6 @@ func createRouter(h *handler) *mux.Router {
 | 
			
		||||
	s.Handle("/version", authorizationRequired(h.version))
 | 
			
		||||
	s.HandleFunc("/healthcheck", h.healthcheck)
 | 
			
		||||
 | 
			
		||||
	if log.IsLevelEnabled(log.DebugLevel) {
 | 
			
		||||
		s.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if base != "/" {
 | 
			
		||||
		s.PathPrefix("/").Handler(http.StripPrefix(base+"/", http.HandlerFunc(h.index)))
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user