Compare commits
	
		
			189 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					5dc4d4c4d1 | ||
| 
						 | 
					87d80d8284 | ||
| 
						 | 
					e216f54d6e | ||
| 
						 | 
					cfa9c702d0 | ||
| 
						 | 
					15fa6ae8b0 | ||
| 
						 | 
					05ae16df8b | ||
| 
						 | 
					34232ef956 | ||
| 
						 | 
					da35a13d04 | ||
| 
						 | 
					cdca0efd05 | ||
| 
						 | 
					320bbfe8b2 | ||
| 
						 | 
					bf42fd4fea | ||
| 
						 | 
					958a1463e6 | ||
| 
						 | 
					4138630fc4 | ||
| 
						 | 
					91545f932c | ||
| 
						 | 
					36cc93dacc | ||
| 
						 | 
					43e777687d | ||
| 
						 | 
					037a76f5c7 | ||
| 
						 | 
					41c54a02eb | ||
| 
						 | 
					7901c21843 | ||
| 
						 | 
					257110bc64 | ||
| 
						 | 
					e2072d35c8 | ||
| 
						 | 
					4a303d3ffa | ||
| 
						 | 
					57d8a90000 | ||
| 
						 | 
					0d54a265d9 | ||
| 
						 | 
					412a10256d | ||
| 
						 | 
					215ea12e80 | ||
| 
						 | 
					b72e208f27 | ||
| 
						 | 
					0714809fd9 | ||
| 
						 | 
					17d43453cc | ||
| 
						 | 
					ce120ac194 | ||
| 
						 | 
					f19bbb8d38 | ||
| 
						 | 
					4f7cbb7cdf | ||
| 
						 | 
					3672a4729d | ||
| 
						 | 
					b0d1cd257c | ||
| 
						 | 
					be23ef93eb | ||
| 
						 | 
					07d3176178 | ||
| 
						 | 
					b01020dc0e | ||
| 
						 | 
					4e5fedb18f | ||
| 
						 | 
					dcd1fcfcde | ||
| 
						 | 
					fb777d4dbf | ||
| 
						 | 
					7b1f4f7f34 | ||
| 
						 | 
					d88eb339b4 | ||
| 
						 | 
					a84ef7be66 | ||
| 
						 | 
					fc798985fd | ||
| 
						 | 
					df176c39f5 | ||
| 
						 | 
					49b39fb3af | ||
| 
						 | 
					d9e8cca867 | ||
| 
						 | 
					bdead5c55d | ||
| 
						 | 
					05b0525a4b | ||
| 
						 | 
					fa502cdda3 | ||
| 
						 | 
					dee345b618 | ||
| 
						 | 
					d55f78829e | ||
| 
						 | 
					8f4264e26a | ||
| 
						 | 
					c79ce7237e | ||
| 
						 | 
					eeec34b018 | ||
| 
						 | 
					69acb24aee | ||
| 
						 | 
					61afc74215 | ||
| 
						 | 
					396f4be965 | ||
| 
						 | 
					0f7541dfab | ||
| 
						 | 
					4031fb4f0e | ||
| 
						 | 
					494982fb77 | ||
| 
						 | 
					aa1911cfcd | ||
| 
						 | 
					92cd724b31 | ||
| 
						 | 
					17a0e3a62d | ||
| 
						 | 
					6b3f5822a5 | ||
| 
						 | 
					23b722bac1 | ||
| 
						 | 
					b95494051d | ||
| 
						 | 
					2e9b673e27 | ||
| 
						 | 
					f6cdf9b691 | ||
| 
						 | 
					520a015c09 | ||
| 
						 | 
					06cc3693c6 | ||
| 
						 | 
					5dc4fb3b78 | ||
| 
						 | 
					310695a981 | ||
| 
						 | 
					dc9c10cc53 | ||
| 
						 | 
					bf0e0e2429 | ||
| 
						 | 
					86a6ff1f66 | ||
| 
						 | 
					6a756f62e4 | ||
| 
						 | 
					3f11a525b8 | ||
| 
						 | 
					cfe1f45cd2 | ||
| 
						 | 
					497da82485 | ||
| 
						 | 
					95bc2b475e | ||
| 
						 | 
					e603c709fc | ||
| 
						 | 
					f958af50fe | ||
| 
						 | 
					e8761fd2fd | ||
| 
						 | 
					9a9aa5864e | ||
| 
						 | 
					453b05f6c9 | ||
| 
						 | 
					b056fa8f88 | ||
| 
						 | 
					d3313ede2c | ||
| 
						 | 
					47a2c71f18 | ||
| 
						 | 
					105e6424f9 | ||
| 
						 | 
					0feab4cdf8 | ||
| 
						 | 
					4657817922 | ||
| 
						 | 
					279b5e21e0 | ||
| 
						 | 
					472344bcf4 | ||
| 
						 | 
					a60a5432e8 | ||
| 
						 | 
					60975b51c7 | ||
| 
						 | 
					240814c53a | ||
| 
						 | 
					97c7525d7e | ||
| 
						 | 
					d2a6631dec | ||
| 
						 | 
					69188fa8b6 | ||
| 
						 | 
					182e24fbc8 | ||
| 
						 | 
					f3ec0033aa | ||
| 
						 | 
					91a3653468 | ||
| 
						 | 
					5bbf0bccdb | ||
| 
						 | 
					f3c77a8081 | ||
| 
						 | 
					a37eefe0ad | ||
| 
						 | 
					2c0dd61b79 | ||
| 
						 | 
					cffb2f6100 | ||
| 
						 | 
					cf1d42ce89 | ||
| 
						 | 
					5d9c17a83b | ||
| 
						 | 
					7406db0999 | ||
| 
						 | 
					ef6bd38fe2 | ||
| 
						 | 
					689628d64f | ||
| 
						 | 
					08b991a7b1 | ||
| 
						 | 
					ef25d470f1 | ||
| 
						 | 
					41005f907e | ||
| 
						 | 
					7b6c77644c | ||
| 
						 | 
					cd18f527bf | ||
| 
						 | 
					13c8d9665a | ||
| 
						 | 
					14d4b03bdd | ||
| 
						 | 
					35cee8e686 | ||
| 
						 | 
					e55335440f | ||
| 
						 | 
					fefc0cca32 | ||
| 
						 | 
					60378e7482 | ||
| 
						 | 
					67e17e1953 | ||
| 
						 | 
					7c099b8ef3 | ||
| 
						 | 
					f7d5af24bc | ||
| 
						 | 
					0c63aae600 | ||
| 
						 | 
					e821251c4c | ||
| 
						 | 
					4e4f445000 | ||
| 
						 | 
					9c0d14e13a | ||
| 
						 | 
					d8e48c0784 | ||
| 
						 | 
					bef1fdce1b | ||
| 
						 | 
					d867b96e98 | ||
| 
						 | 
					d1c9a71d57 | ||
| 
						 | 
					424c052123 | ||
| 
						 | 
					a8b3ee33b2 | ||
| 
						 | 
					1ab4ced332 | ||
| 
						 | 
					a29cc15553 | ||
| 
						 | 
					edb2703c81 | ||
| 
						 | 
					c6005fa04c | ||
| 
						 | 
					dd7ea672d5 | ||
| 
						 | 
					033fb58adc | ||
| 
						 | 
					ebd18d9303 | ||
| 
						 | 
					0db49da124 | ||
| 
						 | 
					c8b228cd52 | ||
| 
						 | 
					aede88c5c1 | ||
| 
						 | 
					ecef6a2bab | ||
| 
						 | 
					225ad2a7be | ||
| 
						 | 
					9d5869a8f2 | ||
| 
						 | 
					aa2268c970 | ||
| 
						 | 
					f7f0aac880 | ||
| 
						 | 
					b6acd150b2 | ||
| 
						 | 
					af05eb6fe3 | ||
| 
						 | 
					f9f4ee601e | ||
| 
						 | 
					2d9ccab8f5 | ||
| 
						 | 
					a286b0f895 | ||
| 
						 | 
					e2a5812ffa | ||
| 
						 | 
					e8b33d351b | ||
| 
						 | 
					3dea6c0971 | ||
| 
						 | 
					015e43b700 | ||
| 
						 | 
					f5a595f899 | ||
| 
						 | 
					06a4244126 | ||
| 
						 | 
					8b190a3c02 | ||
| 
						 | 
					8c47496947 | ||
| 
						 | 
					675c70291c | ||
| 
						 | 
					c87feaa4cb | ||
| 
						 | 
					d8131d5e9e | ||
| 
						 | 
					e55a7e2768 | ||
| 
						 | 
					2c500775b8 | ||
| 
						 | 
					c87a157997 | ||
| 
						 | 
					51166b59ab | ||
| 
						 | 
					04c64ca20c | ||
| 
						 | 
					c8d435c61f | ||
| 
						 | 
					c71baddb73 | ||
| 
						 | 
					cc1654177a | ||
| 
						 | 
					a0e88f71fc | ||
| 
						 | 
					2a9dcc564b | ||
| 
						 | 
					9eb53b4934 | ||
| 
						 | 
					ceb86b3c79 | ||
| 
						 | 
					c0e635db94 | ||
| 
						 | 
					5af580004b | ||
| 
						 | 
					5db0c25382 | ||
| 
						 | 
					141eee9884 | ||
| 
						 | 
					4ae5ea978a | ||
| 
						 | 
					b2bf4a56c7 | ||
| 
						 | 
					8022f1a5db | ||
| 
						 | 
					d2b77b1c4d | ||
| 
						 | 
					8630f57832 | 
							
								
								
									
										8
									
								
								.babelrc
									
									
									
									
									
								
							
							
						
						@@ -1,8 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "presets": [["env", { "modules": false }]],
 | 
			
		||||
  "env": {
 | 
			
		||||
    "test": {
 | 
			
		||||
      "presets": [["env", { "targets": { "node": "current" } }]]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  "presets": ["@babel/preset-env"],
 | 
			
		||||
  "plugins": ["@babel/plugin-transform-runtime"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
node_modules
 | 
			
		||||
.cache
 | 
			
		||||
.idea
 | 
			
		||||
.github
 | 
			
		||||
dist
 | 
			
		||||
.git
 | 
			
		||||
static
 | 
			
		||||
integration
 | 
			
		||||
demo.gif
 | 
			
		||||
e2e
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -31,7 +31,7 @@ updates:
 | 
			
		||||
    schedule:
 | 
			
		||||
      interval: daily
 | 
			
		||||
  - package-ecosystem: npm
 | 
			
		||||
    directory: "/integration"
 | 
			
		||||
    directory: "/e2e"
 | 
			
		||||
    labels:
 | 
			
		||||
      - "npm"
 | 
			
		||||
      - "dependencies"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -9,13 +9,17 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Install Node
 | 
			
		||||
        uses: actions/setup-node@v2.4.1
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v2.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          version: 6.20.1
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: yarn
 | 
			
		||||
        run: pnpm install
 | 
			
		||||
      - name: Run Tests
 | 
			
		||||
        run: yarn test
 | 
			
		||||
        run: pnpm run test
 | 
			
		||||
  go-test:
 | 
			
		||||
    name: Go Tests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -25,7 +29,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: 1.17.x
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Run Go Tests with Coverage
 | 
			
		||||
        run: make test SKIP_ASSET=1
 | 
			
		||||
  int-test:
 | 
			
		||||
@@ -33,11 +37,11 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Build images
 | 
			
		||||
        run: docker-compose -f integration/docker-compose.test.yml build
 | 
			
		||||
        run: docker-compose -f e2e/docker-compose.yml build
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: docker-compose -f integration/docker-compose.test.yml run integration
 | 
			
		||||
        run: docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
 | 
			
		||||
  buildx:
 | 
			
		||||
    needs: [go-test, npm-test, int-test]
 | 
			
		||||
    name: Release
 | 
			
		||||
@@ -45,7 +49,7 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Docker meta
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: crazy-max/ghaction-docker-meta@v3.5.0
 | 
			
		||||
        uses: docker/metadata-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: amir20/dozzle
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
@@ -57,13 +61,6 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Cache Docker layers
 | 
			
		||||
        uses: actions/cache@v2.1.6
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/.buildx-cache
 | 
			
		||||
          key: ${{ runner.os }}-buildx-${{ github.sha }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-buildx-
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v2.7.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -72,29 +69,26 @@ jobs:
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          build-args: TAG=${{ steps.meta.outputs.version }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          cache-from: type=local,src=/tmp/.buildx-cache
 | 
			
		||||
          cache-to: type=local,dest=/tmp/.buildx-cache-new
 | 
			
		||||
      - # Temp fix
 | 
			
		||||
        # https://github.com/docker/build-push-action/issues/252
 | 
			
		||||
        # https://github.com/moby/buildkit/issues/1896
 | 
			
		||||
        name: Move cache
 | 
			
		||||
        run: |
 | 
			
		||||
          rm -rf /tmp/.buildx-cache
 | 
			
		||||
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 | 
			
		||||
          cache-from: type=registry,ref=amir20/dozzle:latest
 | 
			
		||||
          cache-to: type=inline
 | 
			
		||||
  git-release:
 | 
			
		||||
    needs: [buildx]
 | 
			
		||||
    name: Github Release
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
      - name: Install Node
 | 
			
		||||
        uses: actions/setup-node@v2.4.1
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v2.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          version: 6.20.1
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: yarn
 | 
			
		||||
        run: pnpm install
 | 
			
		||||
      - name: Release to Github
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        run: yarn release --github.release --no-increment --no-git --ci
 | 
			
		||||
        run: pnpm run release --github.release --no-increment --no-git --ci
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -2,15 +2,18 @@ on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
name: Push master container
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
name: Push container
 | 
			
		||||
jobs:
 | 
			
		||||
  buildx:
 | 
			
		||||
    name: Push master
 | 
			
		||||
    name: Push branches and PRs
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Docker meta
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: crazy-max/ghaction-docker-meta@v3.5.0
 | 
			
		||||
        uses: docker/metadata-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: amir20/dozzle
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
@@ -22,13 +25,6 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Cache Docker layers
 | 
			
		||||
        uses: actions/cache@v2.1.6
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/.buildx-cache
 | 
			
		||||
          key: ${{ runner.os }}-buildx-${{ github.sha }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-buildx-
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v2.7.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -37,12 +33,5 @@ jobs:
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          build-args: TAG=${{ steps.meta.outputs.version }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          cache-from: type=local,src=/tmp/.buildx-cache
 | 
			
		||||
          cache-to: type=local,dest=/tmp/.buildx-cache-new
 | 
			
		||||
      - # Temp fix
 | 
			
		||||
        # https://github.com/docker/build-push-action/issues/252
 | 
			
		||||
        # https://github.com/moby/buildkit/issues/1896
 | 
			
		||||
        name: Move cache
 | 
			
		||||
        run: |
 | 
			
		||||
          rm -rf /tmp/.buildx-cache
 | 
			
		||||
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 | 
			
		||||
          cache-from: type=registry,ref=amir20/dozzle:master
 | 
			
		||||
          cache-to: type=inline
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,10 @@
 | 
			
		||||
on: push
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - "**"
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
name: Test
 | 
			
		||||
jobs:
 | 
			
		||||
  npm-test:
 | 
			
		||||
@@ -6,13 +12,17 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Install Node
 | 
			
		||||
        uses: actions/setup-node@v2.4.1
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v2.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          version: 6.20.1
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: yarn
 | 
			
		||||
        run: pnpm install
 | 
			
		||||
      - name: Run Tests
 | 
			
		||||
        run: yarn test
 | 
			
		||||
        run: pnpm run test
 | 
			
		||||
  go-test:
 | 
			
		||||
    name: Go Tests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -22,7 +32,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: 1.17.x
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Run Go Tests with Coverage
 | 
			
		||||
        run: make test SKIP_ASSET=1
 | 
			
		||||
  int-test:
 | 
			
		||||
@@ -30,8 +40,8 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2.3.4
 | 
			
		||||
        uses: actions/checkout@v2.4.0
 | 
			
		||||
      - name: Build images
 | 
			
		||||
        run: docker-compose -f integration/docker-compose.test.yml build
 | 
			
		||||
        run: docker-compose -f e2e/docker-compose.yml build
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: docker-compose -f integration/docker-compose.test.yml run integration
 | 
			
		||||
        run: docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
. "$(dirname $0)/_/husky.sh"
 | 
			
		||||
 | 
			
		||||
yarn lint-staged
 | 
			
		||||
pnpm lint-staged
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -1,27 +1,29 @@
 | 
			
		||||
# Build assets
 | 
			
		||||
FROM node:16-alpine as node
 | 
			
		||||
FROM node:17-alpine as node
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache git openssh make g++ util-linux
 | 
			
		||||
RUN apk add --no-cache git openssh make g++ util-linux curl python3 && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
# Install dependencies
 | 
			
		||||
COPY package*.json yarn.lock ./
 | 
			
		||||
RUN yarn install --ignore-scripts --network-timeout 1000000
 | 
			
		||||
# Install dependencies from lock file
 | 
			
		||||
COPY pnpm-lock.yaml ./
 | 
			
		||||
RUN pnpm fetch --prod
 | 
			
		||||
 | 
			
		||||
# Copy config files
 | 
			
		||||
COPY .* webpack*.js ./
 | 
			
		||||
# Copy files
 | 
			
		||||
COPY package.json .* vite.config.ts index.html ./
 | 
			
		||||
 | 
			
		||||
# Copy assets to build
 | 
			
		||||
COPY assets ./assets
 | 
			
		||||
 | 
			
		||||
# Install dependencies
 | 
			
		||||
RUN pnpm install -r --offline --prod
 | 
			
		||||
 | 
			
		||||
# Do the build
 | 
			
		||||
RUN yarn build
 | 
			
		||||
RUN pnpm build
 | 
			
		||||
 | 
			
		||||
FROM golang:1.17.1-alpine AS builder
 | 
			
		||||
FROM golang:1.17.3-alpine AS builder
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache git ca-certificates
 | 
			
		||||
RUN mkdir /dozzle
 | 
			
		||||
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
 | 
			
		||||
 | 
			
		||||
WORKDIR /dozzle
 | 
			
		||||
 | 
			
		||||
@@ -30,10 +32,13 @@ COPY go.* ./
 | 
			
		||||
RUN go mod download
 | 
			
		||||
 | 
			
		||||
# Copy assets built with node
 | 
			
		||||
COPY --from=node /build/static ./static
 | 
			
		||||
COPY --from=node /build/dist ./dist
 | 
			
		||||
 | 
			
		||||
# Copy all other files
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY analytics ./analytics
 | 
			
		||||
COPY docker ./docker
 | 
			
		||||
COPY web ./web
 | 
			
		||||
COPY main.go ./
 | 
			
		||||
 | 
			
		||||
# Args
 | 
			
		||||
ARG TAG=dev
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						@@ -1,24 +1,24 @@
 | 
			
		||||
.PHONY: clean
 | 
			
		||||
clean:
 | 
			
		||||
	@rm -rf static
 | 
			
		||||
	@rm -rf dist
 | 
			
		||||
	@go clean -i
 | 
			
		||||
 | 
			
		||||
.PHONY: static
 | 
			
		||||
static:
 | 
			
		||||
	@yarn build
 | 
			
		||||
.PHONY: dist
 | 
			
		||||
dist:
 | 
			
		||||
	@pnpm build
 | 
			
		||||
 | 
			
		||||
.PHONY: fake_static
 | 
			
		||||
fake_static:
 | 
			
		||||
	@echo 'Skipping yarn build'
 | 
			
		||||
	@mkdir -p static
 | 
			
		||||
	@echo "yarn build was skipped" > static/index.html
 | 
			
		||||
.PHONY: fake_assets
 | 
			
		||||
fake_assets:
 | 
			
		||||
	@echo 'Skipping asset build'
 | 
			
		||||
	@mkdir -p dist
 | 
			
		||||
	@echo "assets build was skipped" > dist/index.html
 | 
			
		||||
 | 
			
		||||
.PHONY: test
 | 
			
		||||
test: fake_static
 | 
			
		||||
test: fake_assets
 | 
			
		||||
	go test -cover ./...
 | 
			
		||||
 | 
			
		||||
.PHONY: build
 | 
			
		||||
build: static
 | 
			
		||||
build: dist
 | 
			
		||||
	CGO_ENABLED=0 go build -ldflags "-s -w"
 | 
			
		||||
 | 
			
		||||
.PHONY: docker
 | 
			
		||||
@@ -27,8 +27,8 @@ docker:
 | 
			
		||||
 | 
			
		||||
.PHONY: dev
 | 
			
		||||
dev:
 | 
			
		||||
	yarn dev
 | 
			
		||||
	pnpm dev
 | 
			
		||||
 | 
			
		||||
.PHONY: int
 | 
			
		||||
int:
 | 
			
		||||
	docker-compose -f integration/docker-compose.test.yml up --build --force-recreate --exit-code-from integration
 | 
			
		||||
	docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,7 @@ Dozzle is a small lightweight application with a web based interface to monitor
 | 
			
		||||
 | 
			
		||||
[](https://goreportcard.com/report/github.com/amir20/dozzle)
 | 
			
		||||
[](https://hub.docker.com/r/amir20/dozzle/)
 | 
			
		||||
[](https://hub.docker.com/r/amir20/dozzle/)
 | 
			
		||||
[](https://hub.docker.com/r/amir20/dozzle/)
 | 
			
		||||
[](https://hub.docker.com/r/amir20/dozzle/)
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
@@ -172,5 +171,5 @@ To Build and test locally:
 | 
			
		||||
1. Install NodeJs.
 | 
			
		||||
2. Install Go.
 | 
			
		||||
3. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle.
 | 
			
		||||
4. Install node modules with `yarn`.
 | 
			
		||||
5. Do `yarn dev`
 | 
			
		||||
4. Install node modules with `pnpm`.
 | 
			
		||||
5. Do `pnpm dev`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
import EventSource from "eventsourcemock";
 | 
			
		||||
import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
 | 
			
		||||
import Vuex from "vuex";
 | 
			
		||||
import App from "./App";
 | 
			
		||||
 | 
			
		||||
jest.mock("./store/config.js", () => ({ base: "" }));
 | 
			
		||||
 | 
			
		||||
const localVue = createLocalVue();
 | 
			
		||||
 | 
			
		||||
localVue.use(Vuex);
 | 
			
		||||
 | 
			
		||||
describe("<App />", () => {
 | 
			
		||||
  const stubs = { RouterLink: RouterLinkStub, "router-view": true, icon: true };
 | 
			
		||||
  let store;
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    global.EventSource = EventSource;
 | 
			
		||||
    const state = {
 | 
			
		||||
      settings: { menuWidth: 15 },
 | 
			
		||||
      containers: [{ id: "abc", name: "Test 1" }],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getters = {
 | 
			
		||||
      visibleContainers(store) {
 | 
			
		||||
        return store.containers;
 | 
			
		||||
      },
 | 
			
		||||
      activeContainers() {
 | 
			
		||||
        return [];
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    store = new Vuex.Store({
 | 
			
		||||
      state,
 | 
			
		||||
      getters,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("has right title", async () => {
 | 
			
		||||
    const wrapper = shallowMount(App, { stubs, store, localVue });
 | 
			
		||||
    wrapper.vm.$store.state.containers = [
 | 
			
		||||
      { id: "abc", name: "Test 1" },
 | 
			
		||||
      { id: "xyz", name: "Test 2" },
 | 
			
		||||
    ];
 | 
			
		||||
    await wrapper.vm.$nextTick();
 | 
			
		||||
 | 
			
		||||
    expect(wrapper.vm.title).toContain("2 containers");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("renders correctly", async () => {
 | 
			
		||||
    const wrapper = shallowMount(App, { stubs, store, localVue });
 | 
			
		||||
    await wrapper.vm.$nextTick();
 | 
			
		||||
    expect(wrapper.element).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										177
									
								
								assets/App.vue
									
									
									
									
									
								
							
							
						
						@@ -3,7 +3,7 @@
 | 
			
		||||
    <mobile-menu v-if="isMobile && !authorizationNeeded"></mobile-menu>
 | 
			
		||||
 | 
			
		||||
    <splitpanes @resized="onResized($event)">
 | 
			
		||||
      <pane min-size="10" :size="settings.menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
 | 
			
		||||
      <pane min-size="10" :size="menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
 | 
			
		||||
        <side-menu @search="showFuzzySearch"></side-menu>
 | 
			
		||||
      </pane>
 | 
			
		||||
      <pane min-size="10">
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
                show-title
 | 
			
		||||
                scrollable
 | 
			
		||||
                closable
 | 
			
		||||
                @close="removeActiveContainer(other)"
 | 
			
		||||
                @close="containerStore.removeActiveContainer(other)"
 | 
			
		||||
              ></log-container>
 | 
			
		||||
            </pane>
 | 
			
		||||
          </template>
 | 
			
		||||
@@ -27,122 +27,97 @@
 | 
			
		||||
    </splitpanes>
 | 
			
		||||
    <button
 | 
			
		||||
      @click="collapseNav = !collapseNav"
 | 
			
		||||
      class="button is-small is-rounded is-settings-control"
 | 
			
		||||
      class="button is-rounded"
 | 
			
		||||
      :class="{ collapsed: collapseNav }"
 | 
			
		||||
      id="hide-nav"
 | 
			
		||||
      v-if="!isMobile && !authorizationNeeded"
 | 
			
		||||
    >
 | 
			
		||||
      <span class="icon">
 | 
			
		||||
        <icon :name="collapseNav ? 'chevron-right' : 'chevron-left'"></icon>
 | 
			
		||||
      <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>
 | 
			
		||||
import { mapActions, mapGetters, mapState } from "vuex";
 | 
			
		||||
<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 LogContainer from "./components/LogContainer";
 | 
			
		||||
import SideMenu from "./components/SideMenu";
 | 
			
		||||
import MobileMenu from "./components/MobileMenu";
 | 
			
		||||
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 PastTime from "./components/PastTime";
 | 
			
		||||
import Icon from "./components/Icon";
 | 
			
		||||
import FuzzySearchModal from "./components/FuzzySearchModal";
 | 
			
		||||
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
 | 
			
		||||
import LogContainer from "@/components/LogContainer.vue";
 | 
			
		||||
import SideMenu from "@/components/SideMenu.vue";
 | 
			
		||||
import MobileMenu from "@/components/MobileMenu.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "App",
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
    SideMenu,
 | 
			
		||||
    LogContainer,
 | 
			
		||||
    MobileMenu,
 | 
			
		||||
    Splitpanes,
 | 
			
		||||
    PastTime,
 | 
			
		||||
    Pane,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "",
 | 
			
		||||
      collapseNav: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: this.title,
 | 
			
		||||
      titleTemplate: "%s - Dozzle",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    if (this.hasSmallerScrollbars) {
 | 
			
		||||
      document.documentElement.classList.add("has-custom-scrollbars");
 | 
			
		||||
    }
 | 
			
		||||
    if (this.hasLightTheme) {
 | 
			
		||||
      document.documentElement.setAttribute("data-theme", "light");
 | 
			
		||||
    }
 | 
			
		||||
    this.menuWidth = this.settings.menuWidth;
 | 
			
		||||
    hotkeys("command+k, ctrl+k", (event, handler) => {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      this.showFuzzySearch();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    hasSmallerScrollbars(newValue, oldValue) {
 | 
			
		||||
      if (newValue) {
 | 
			
		||||
        document.documentElement.classList.add("has-custom-scrollbars");
 | 
			
		||||
      } else {
 | 
			
		||||
        document.documentElement.classList.remove("has-custom-scrollbars");
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    hasLightTheme(newValue, oldValue) {
 | 
			
		||||
      if (newValue) {
 | 
			
		||||
        document.documentElement.setAttribute("data-theme", "light");
 | 
			
		||||
      } else {
 | 
			
		||||
        document.documentElement.removeAttribute("data-theme");
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    visibleContainers() {
 | 
			
		||||
      this.title = `${this.visibleContainers.length} containers`;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["isMobile", "settings", "containers", "authorizationNeeded"]),
 | 
			
		||||
    ...mapGetters(["visibleContainers", "activeContainers"]),
 | 
			
		||||
    hasSmallerScrollbars() {
 | 
			
		||||
      return this.settings.smallerScrollbars;
 | 
			
		||||
    },
 | 
			
		||||
    hasLightTheme() {
 | 
			
		||||
      return this.settings.lightTheme;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      removeActiveContainer: "REMOVE_ACTIVE_CONTAINER",
 | 
			
		||||
      updateSetting: "UPDATE_SETTING",
 | 
			
		||||
    }),
 | 
			
		||||
    onResized(e) {
 | 
			
		||||
      if (e.length == 2) {
 | 
			
		||||
        const menuWidth = e[0].size;
 | 
			
		||||
        this.updateSetting({ menuWidth });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    showFuzzySearch() {
 | 
			
		||||
      this.$buefy.modal.open({
 | 
			
		||||
        parent: this,
 | 
			
		||||
        component: FuzzySearchModal,
 | 
			
		||||
        animation: "false",
 | 
			
		||||
        width: 600,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
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");
 | 
			
		||||
  }
 | 
			
		||||
  if (lightTheme.value) {
 | 
			
		||||
    document.documentElement.setAttribute("data-theme", "light");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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");
 | 
			
		||||
  } else {
 | 
			
		||||
    document.documentElement.classList.remove("has-custom-scrollbars");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (lightTheme.value) {
 | 
			
		||||
    document.documentElement.setAttribute("data-theme", "light");
 | 
			
		||||
  } else {
 | 
			
		||||
    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">
 | 
			
		||||
::v-deep .splitpanes--vertical > .splitpanes__splitter {
 | 
			
		||||
:deep(.splitpanes--vertical > .splitpanes__splitter) {
 | 
			
		||||
  min-width: 3px;
 | 
			
		||||
  background: var(--border-color);
 | 
			
		||||
  &:hover {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
			
		||||
 | 
			
		||||
exports[`<App /> renders correctly 1`] = `
 | 
			
		||||
<main>
 | 
			
		||||
  <!---->
 | 
			
		||||
   
 | 
			
		||||
  <splitpanes-stub
 | 
			
		||||
    dblclicksplitter="true"
 | 
			
		||||
    pushotherpanes="true"
 | 
			
		||||
  >
 | 
			
		||||
    <pane-stub
 | 
			
		||||
      maxsize="100"
 | 
			
		||||
      minsize="10"
 | 
			
		||||
      size="15"
 | 
			
		||||
    >
 | 
			
		||||
      <side-menu-stub />
 | 
			
		||||
    </pane-stub>
 | 
			
		||||
     
 | 
			
		||||
    <pane-stub
 | 
			
		||||
      maxsize="100"
 | 
			
		||||
      minsize="10"
 | 
			
		||||
    >
 | 
			
		||||
      <splitpanes-stub
 | 
			
		||||
        dblclicksplitter="true"
 | 
			
		||||
        pushotherpanes="true"
 | 
			
		||||
      >
 | 
			
		||||
        <pane-stub
 | 
			
		||||
          class="has-min-height router-view"
 | 
			
		||||
          maxsize="100"
 | 
			
		||||
          minsize="0"
 | 
			
		||||
        >
 | 
			
		||||
          <router-view-stub />
 | 
			
		||||
        </pane-stub>
 | 
			
		||||
         
 | 
			
		||||
      </splitpanes-stub>
 | 
			
		||||
    </pane-stub>
 | 
			
		||||
  </splitpanes-stub>
 | 
			
		||||
   
 | 
			
		||||
  <button
 | 
			
		||||
    class="button is-small is-rounded is-settings-control"
 | 
			
		||||
    id="hide-nav"
 | 
			
		||||
  >
 | 
			
		||||
    <span
 | 
			
		||||
      class="icon"
 | 
			
		||||
    >
 | 
			
		||||
      <icon-stub
 | 
			
		||||
        name="chevron-left"
 | 
			
		||||
      />
 | 
			
		||||
    </span>
 | 
			
		||||
  </button>
 | 
			
		||||
</main>
 | 
			
		||||
`;
 | 
			
		||||
							
								
								
									
										37
									
								
								assets/components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
// generated by unplugin-vue-components
 | 
			
		||||
// We suggest you to commit this file into source control
 | 
			
		||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
 | 
			
		||||
 | 
			
		||||
declare module 'vue' {
 | 
			
		||||
  export interface GlobalComponents {
 | 
			
		||||
    CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
 | 
			
		||||
    CilColumns: typeof import('~icons/cil/columns')['default']
 | 
			
		||||
    ContainerStat: typeof import('./components/ContainerStat.vue')['default']
 | 
			
		||||
    ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
 | 
			
		||||
    FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
 | 
			
		||||
    InfiniteLoader: typeof import('./components/InfiniteLoader.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']
 | 
			
		||||
    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']
 | 
			
		||||
    MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
 | 
			
		||||
    MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
 | 
			
		||||
    MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
 | 
			
		||||
    MobileMenu: typeof import('./components/MobileMenu.vue')['default']
 | 
			
		||||
    OcticonContainer24: typeof import('~icons/octicon/container24')['default']
 | 
			
		||||
    OcticonDownload24: typeof import('~icons/octicon/download24')['default']
 | 
			
		||||
    OcticonTrash24: typeof import('~icons/octicon/trash24')['default']
 | 
			
		||||
    PastTime: typeof import('./components/PastTime.vue')['default']
 | 
			
		||||
    RelativeTime: typeof import('./components/RelativeTime.vue')['default']
 | 
			
		||||
    ScrollableView: typeof import('./components/ScrollableView.vue')['default']
 | 
			
		||||
    ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
 | 
			
		||||
    Search: typeof import('./components/Search.vue')['default']
 | 
			
		||||
    SideMenu: typeof import('./components/SideMenu.vue')['default']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { }
 | 
			
		||||
@@ -4,37 +4,44 @@
 | 
			
		||||
      {{ state }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="column is-narrow" v-if="stat.memoryUsage !== null">
 | 
			
		||||
      <span class="has-text-weight-light">mem</span>
 | 
			
		||||
      <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">load</span>
 | 
			
		||||
      <span class="has-text-weight-light has-spacer">load</span>
 | 
			
		||||
      <span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    stat: Object,
 | 
			
		||||
    state: String,
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ContainerStat } from "@/types/Container";
 | 
			
		||||
import { PropType } from "vue";
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  stat: {
 | 
			
		||||
    type: Object as PropType<ContainerStat>,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  name: "ContainerStat",
 | 
			
		||||
  methods: {
 | 
			
		||||
    formatBytes(bytes, decimals = 2) {
 | 
			
		||||
      if (bytes === 0) return "0 Bytes";
 | 
			
		||||
      const k = 1024;
 | 
			
		||||
      const dm = decimals < 0 ? 0 : decimals;
 | 
			
		||||
      const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 | 
			
		||||
      const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
      return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  state: String,
 | 
			
		||||
});
 | 
			
		||||
function formatBytes(bytes: number, decimals = 2) {
 | 
			
		||||
  if (bytes === 0) return "0 Bytes";
 | 
			
		||||
  const k = 1024;
 | 
			
		||||
  const dm = decimals < 0 ? 0 : decimals;
 | 
			
		||||
  const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
 | 
			
		||||
  const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.has-spacer {
 | 
			
		||||
  &::after {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="columns is-marginless has-text-weight-bold is-family-monospace">
 | 
			
		||||
    <span class="column is-ellipsis">{{ value }}</span>
 | 
			
		||||
    <span class="column is-ellipsis">
 | 
			
		||||
      {{ container.name }}
 | 
			
		||||
      <span class="tag is-dark">{{ container.image.replace(/@sha.*/, "") }}</span>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    value: String,
 | 
			
		||||
  },
 | 
			
		||||
  name: "ContainerTitle",
 | 
			
		||||
};
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
defineProps({
 | 
			
		||||
  container: Object,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="panel">
 | 
			
		||||
    <b-autocomplete
 | 
			
		||||
    <o-autocomplete
 | 
			
		||||
      ref="autocomplete"
 | 
			
		||||
      v-model="query"
 | 
			
		||||
      placeholder="Search containers using ⌘ + k, ⌃k"
 | 
			
		||||
      placeholder="Search containers using ⌘ + k or ctrl + k"
 | 
			
		||||
      field="name"
 | 
			
		||||
      open-on-focus
 | 
			
		||||
      keep-first
 | 
			
		||||
@@ -11,101 +11,94 @@
 | 
			
		||||
      :data="results"
 | 
			
		||||
      @select="selected"
 | 
			
		||||
    >
 | 
			
		||||
      <template slot-scope="props">
 | 
			
		||||
      <template #default="props">
 | 
			
		||||
        <div class="media">
 | 
			
		||||
          <div class="media-left">
 | 
			
		||||
            <span class="icon is-small" :class="props.option.state"><icon name="crate"></icon></span>
 | 
			
		||||
            <span class="icon is-small" :class="props.option.state">
 | 
			
		||||
              <octicon-container-24 />
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="media-content">
 | 
			
		||||
            {{ props.option.name }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="media-right">
 | 
			
		||||
            <span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
 | 
			
		||||
              <icon name="column"></icon>
 | 
			
		||||
              <cil-columns />
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
    </b-autocomplete>
 | 
			
		||||
    </o-autocomplete>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapState, mapActions } from "vuex";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import fuzzysort from "fuzzysort";
 | 
			
		||||
import { computed, nextTick, onMounted, ref } from "vue";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { Container } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
import PastTime from "./PastTime";
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  maxResults: {
 | 
			
		||||
    default: 20,
 | 
			
		||||
    type: Number,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    maxResults: {
 | 
			
		||||
      default: 20,
 | 
			
		||||
      type: Number,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      query: "",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  name: "FuzzySearchModal",
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
    PastTime,
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$nextTick(() => this.$refs.autocomplete.focus());
 | 
			
		||||
  },
 | 
			
		||||
  watch: {},
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      appendActiveContainer: "APPEND_ACTIVE_CONTAINER",
 | 
			
		||||
    }),
 | 
			
		||||
    selected(item) {
 | 
			
		||||
      this.$router.push({ name: "container", params: { id: item.id, name: item.name } });
 | 
			
		||||
      this.$emit("close");
 | 
			
		||||
    },
 | 
			
		||||
    addColumn(container) {
 | 
			
		||||
      this.appendActiveContainer(container);
 | 
			
		||||
      this.$emit("close");
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["containers"]),
 | 
			
		||||
    preparedContainers() {
 | 
			
		||||
      return this.containers.map((c) => ({
 | 
			
		||||
        name: c.name,
 | 
			
		||||
        id: c.id,
 | 
			
		||||
        created: c.created,
 | 
			
		||||
        state: c.state,
 | 
			
		||||
        preparedName: fuzzysort.prepare(c.name),
 | 
			
		||||
      }));
 | 
			
		||||
    },
 | 
			
		||||
    results() {
 | 
			
		||||
      const options = {
 | 
			
		||||
        limit: this.maxResults,
 | 
			
		||||
        key: "preparedName",
 | 
			
		||||
      };
 | 
			
		||||
      if (this.query) {
 | 
			
		||||
        const results = fuzzysort.go(this.query, this.preparedContainers, options);
 | 
			
		||||
        results.forEach((result) => {
 | 
			
		||||
          if (result.obj.state === "running") {
 | 
			
		||||
            result.score += 1;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        return results.sort((a, b) => b.score - a.score).map((i) => i.obj);
 | 
			
		||||
      } else {
 | 
			
		||||
        return [...this.containers].sort((a, b) => b.created - a.created);
 | 
			
		||||
const emit = defineEmits(["close"]);
 | 
			
		||||
 | 
			
		||||
const query = ref("");
 | 
			
		||||
const autocomplete = ref<HTMLElement>();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const { containers } = storeToRefs(store);
 | 
			
		||||
const preparedContainers = computed(() =>
 | 
			
		||||
  containers.value.map(({ name, id, created, state }) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    id,
 | 
			
		||||
    created,
 | 
			
		||||
    state,
 | 
			
		||||
    preparedName: fuzzysort.prepare(name),
 | 
			
		||||
  }))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const results = computed(() => {
 | 
			
		||||
  const options = {
 | 
			
		||||
    limit: props.maxResults,
 | 
			
		||||
    key: "preparedName",
 | 
			
		||||
  };
 | 
			
		||||
  if (query.value) {
 | 
			
		||||
    const results = fuzzysort.go(query.value, preparedContainers.value, options);
 | 
			
		||||
    results.forEach((result) => {
 | 
			
		||||
      if (result.obj.state === "running") {
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        result.score += 1;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
    });
 | 
			
		||||
    return [...results].sort((a, b) => b.score - a.score).map((i) => i.obj);
 | 
			
		||||
  } else {
 | 
			
		||||
    return [...containers.value].sort((a, b) => b.created - a.created);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => nextTick(() => autocomplete.value?.focus()));
 | 
			
		||||
 | 
			
		||||
function selected(item: { id: string; name: string }) {
 | 
			
		||||
  router.push({ name: "container", params: { id: item.id, name: item.name } });
 | 
			
		||||
  emit("close");
 | 
			
		||||
}
 | 
			
		||||
function addColumn(container: Container) {
 | 
			
		||||
  store.appendActiveContainer(container);
 | 
			
		||||
  emit("close");
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.panel {
 | 
			
		||||
  min-height: 400px;
 | 
			
		||||
  width: 580px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.running {
 | 
			
		||||
@@ -122,7 +115,7 @@ export default {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::v-deep a.dropdown-item {
 | 
			
		||||
:deep(a.dropdown-item) {
 | 
			
		||||
  padding-right: 1em;
 | 
			
		||||
  .media-right {
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
@@ -131,4 +124,8 @@ export default {
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
<template functional>
 | 
			
		||||
  <svg class="icomoon" :class="['icon-' + props.name]">
 | 
			
		||||
    <use :href="'#icon-' + props.name"></use>
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    name: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  name: "Icon",
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.icomoon {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: 1em;
 | 
			
		||||
  height: 1em;
 | 
			
		||||
  stroke-width: 0;
 | 
			
		||||
  stroke: currentColor;
 | 
			
		||||
  fill: currentColor;
 | 
			
		||||
 | 
			
		||||
  .icon:not(.keep-size) & {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div ref="observer" class="infinte-loader">
 | 
			
		||||
  <div ref="root" class="infinte-loader">
 | 
			
		||||
    <div class="spinner" v-show="isLoading">
 | 
			
		||||
      <div class="bounce1"></div>
 | 
			
		||||
      <div class="bounce2"></div>
 | 
			
		||||
@@ -8,40 +8,34 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: "InfiniteLoader",
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isLoading: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    onLoadMore: Function,
 | 
			
		||||
    enabled: Boolean,
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const intersectionObserver = new IntersectionObserver(
 | 
			
		||||
      async (entries) => {
 | 
			
		||||
        if (entries[0].intersectionRatio <= 0) return;
 | 
			
		||||
        if (this.onLoadMore && this.enabled) {
 | 
			
		||||
          const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
 | 
			
		||||
          const previousHeight = scrollingParent.scrollHeight;
 | 
			
		||||
          this.isLoading = true;
 | 
			
		||||
          await this.onLoadMore();
 | 
			
		||||
          this.isLoading = false;
 | 
			
		||||
          this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      { threshholds: 1 }
 | 
			
		||||
    );
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
 | 
			
		||||
 | 
			
		||||
    intersectionObserver.observe(this.$refs.observer);
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  onLoadMore: Function,
 | 
			
		||||
  enabled: Boolean,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
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;
 | 
			
		||||
    const previousHeight = scrollingParent.scrollHeight;
 | 
			
		||||
    isLoading.value = true;
 | 
			
		||||
    await props.onLoadMore();
 | 
			
		||||
    isLoading.value = false;
 | 
			
		||||
    await nextTick();
 | 
			
		||||
    scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => observer.observe(root.value));
 | 
			
		||||
onUnmounted(() => observer.disconnect());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.infinte-loader {
 | 
			
		||||
  min-height: 1px;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										120
									
								
								assets/components/LogActionsToolbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,120 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="dropdown is-right is-hoverable">
 | 
			
		||||
    <div class="dropdown-trigger">
 | 
			
		||||
      <button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
 | 
			
		||||
        <span class="icon">
 | 
			
		||||
          <mdi-dots-vertical />
 | 
			
		||||
        </span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="dropdown-menu" id="dropdown-menu" role="menu">
 | 
			
		||||
      <div class="dropdown-content">
 | 
			
		||||
        <a class="dropdown-item" @click="onClearClicked">
 | 
			
		||||
          <div class="level is-justify-content-start">
 | 
			
		||||
            <div class="level-left">
 | 
			
		||||
              <div class="level-item">
 | 
			
		||||
                <octicon-trash-24 class="mr-4" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="level-right">
 | 
			
		||||
              <div class="level-item">Clear</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
        <a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}`">
 | 
			
		||||
          <div class="level is-justify-content-start">
 | 
			
		||||
            <div class="level-left">
 | 
			
		||||
              <div class="level-item">
 | 
			
		||||
                <octicon-download-24 class="mr-4" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="level-right">
 | 
			
		||||
              <div class="level-item">Download</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
        <hr class="dropdown-divider" />
 | 
			
		||||
        <a class="dropdown-item" @click="showSearch = true">
 | 
			
		||||
          <div class="level is-justify-content-start">
 | 
			
		||||
            <div class="level-left">
 | 
			
		||||
              <div class="level-item">
 | 
			
		||||
                <mdi-light-magnify class="mr-4" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="level-right">
 | 
			
		||||
              <div class="level-item">Search</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, PropType } from "vue";
 | 
			
		||||
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 onHotkey = (event: Event) => {
 | 
			
		||||
  props.onClearClicked(event);
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
 | 
			
		||||
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
#download.button,
 | 
			
		||||
#clear.button {
 | 
			
		||||
  .icon {
 | 
			
		||||
    height: 80%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: var(--primary-color);
 | 
			
		||||
    border-color: var(--primary-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toolbar {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  transform: translateX(-50%);
 | 
			
		||||
  width: 200px;
 | 
			
		||||
  background-color: var(--action-toolbar-background-color);
 | 
			
		||||
  border-radius: 8em;
 | 
			
		||||
  margin-top: 0.5em;
 | 
			
		||||
 | 
			
		||||
  & > div {
 | 
			
		||||
    margin: 0 2em;
 | 
			
		||||
    padding: 0.5em 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .button {
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0) !important;
 | 
			
		||||
 | 
			
		||||
    &.is-small {
 | 
			
		||||
      font-size: 0.65rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,79 +1,64 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <scrollable-view :scrollable="scrollable" v-if="container">
 | 
			
		||||
    <template v-slot:header v-if="showTitle">
 | 
			
		||||
      <div class="mr-0 columns is-vcentered is-hidden-mobile">
 | 
			
		||||
        <div class="column is-clipped">
 | 
			
		||||
          <container-title :value="container.name" @close="$emit('close')"></container-title>
 | 
			
		||||
    <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')" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-clipped">
 | 
			
		||||
          <container-stat :stat="container.stat" :state="container.state"></container-stat>
 | 
			
		||||
        <div class="column is-narrow is-paddingless">
 | 
			
		||||
          <container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-narrow">
 | 
			
		||||
          <a
 | 
			
		||||
            class="button is-small is-outlined"
 | 
			
		||||
            id="download"
 | 
			
		||||
            :href="`${base}/api/logs/download?id=${container.id}`"
 | 
			
		||||
            download
 | 
			
		||||
          >
 | 
			
		||||
            <span class="icon">
 | 
			
		||||
              <icon name="save"></icon>
 | 
			
		||||
            </span>
 | 
			
		||||
            Download
 | 
			
		||||
          </a>
 | 
			
		||||
 | 
			
		||||
        <div class="mr-2 column is-narrow is-paddingless">
 | 
			
		||||
          <log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-narrow" v-if="closable">
 | 
			
		||||
          <button class="delete is-medium" @click="$emit('close')"></button>
 | 
			
		||||
        <div class="mr-2 column is-narrow is-paddingless" v-if="closable">
 | 
			
		||||
          <button class="delete is-medium" @click="emit('close')"></button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot="{ setLoading }">
 | 
			
		||||
      <log-viewer-with-source :id="id" @loading-more="setLoading($event)"></log-viewer-with-source>
 | 
			
		||||
    <template #default="{ setLoading }">
 | 
			
		||||
      <log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </scrollable-view>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import LogViewerWithSource from "./LogViewerWithSource";
 | 
			
		||||
import ScrollableView from "./ScrollableView";
 | 
			
		||||
import ContainerTitle from "./ContainerTitle";
 | 
			
		||||
import ContainerStat from "./ContainerStat";
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
import config from "../store/config";
 | 
			
		||||
import containerMixin from "./mixins/container";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, toRefs } from "vue";
 | 
			
		||||
import LogViewerWithSource from "./LogViewerWithSource.vue";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  mixins: [containerMixin],
 | 
			
		||||
  props: {
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    showTitle: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
    scrollable: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
    closable: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  name: "LogContainer",
 | 
			
		||||
  components: {
 | 
			
		||||
    LogViewerWithSource,
 | 
			
		||||
    ScrollableView,
 | 
			
		||||
    ContainerTitle,
 | 
			
		||||
    ContainerStat,
 | 
			
		||||
    Icon,
 | 
			
		||||
  showTitle: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    base() {
 | 
			
		||||
      return config.base;
 | 
			
		||||
    },
 | 
			
		||||
  scrollable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  closable: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(["close"]);
 | 
			
		||||
 | 
			
		||||
const { id } = toRefs(props);
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const container = store.currentContainer(id);
 | 
			
		||||
 | 
			
		||||
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
 | 
			
		||||
 | 
			
		||||
function onClearClicked() {
 | 
			
		||||
  viewer.value?.clear();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
button.delete {
 | 
			
		||||
@@ -88,16 +73,4 @@ button.delete {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#download.button {
 | 
			
		||||
  .icon {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
    height: 80%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: var(--primary-color);
 | 
			
		||||
    border-color: var(--primary-color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,14 @@
 | 
			
		||||
import { mount } from "@vue/test-utils";
 | 
			
		||||
import { createTestingPinia } from "@pinia/testing";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import EventSource, { sources } from "eventsourcemock";
 | 
			
		||||
import debounce from "lodash.debounce";
 | 
			
		||||
import EventSource from "eventsourcemock";
 | 
			
		||||
import { sources } from "eventsourcemock";
 | 
			
		||||
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
 | 
			
		||||
import Vuex from "vuex";
 | 
			
		||||
import LogEventSource from "./LogEventSource.vue";
 | 
			
		||||
import LogViewer from "./LogViewer.vue";
 | 
			
		||||
import { settings } from "../composables/settings";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
import { mocked } from "ts-jest/utils";
 | 
			
		||||
import { computed, Ref } from "vue";
 | 
			
		||||
 | 
			
		||||
jest.mock("lodash.debounce", () =>
 | 
			
		||||
  jest.fn((fn) => {
 | 
			
		||||
@@ -13,69 +17,77 @@ jest.mock("lodash.debounce", () =>
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
jest.mock("../store/config.js", () => ({ base: "" }));
 | 
			
		||||
 | 
			
		||||
describe("<LogEventSource />", () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    global.EventSource = EventSource;
 | 
			
		||||
    window.scrollTo = jest.fn();
 | 
			
		||||
    const observe = jest.fn();
 | 
			
		||||
    const disconnect = jest.fn();
 | 
			
		||||
    global.IntersectionObserver = jest.fn(() => ({
 | 
			
		||||
      observe,
 | 
			
		||||
      disconnect,
 | 
			
		||||
    }));
 | 
			
		||||
    debounce.mockClear();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function createLogEventSource({ hourStyle = "auto", searchFilter = null } = {}) {
 | 
			
		||||
    const localVue = createLocalVue();
 | 
			
		||||
    localVue.use(Vuex);
 | 
			
		||||
 | 
			
		||||
    localVue.component("log-viewer", LogViewer);
 | 
			
		||||
 | 
			
		||||
    const state = { searchFilter, settings: { size: "medium", showTimestamp: true, hourStyle } };
 | 
			
		||||
    const getters = {
 | 
			
		||||
      allContainersById() {
 | 
			
		||||
        return {
 | 
			
		||||
          abc: { state: "running" },
 | 
			
		||||
        };
 | 
			
		||||
jest.mock("@/stores/container", () => ({
 | 
			
		||||
  __esModule: true,
 | 
			
		||||
  useContainerStore() {
 | 
			
		||||
    return {
 | 
			
		||||
      currentContainer(id: Ref<string>) {
 | 
			
		||||
        return computed(() => ({ id: id.value }));
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
    const store = new Vuex.Store({
 | 
			
		||||
      state,
 | 
			
		||||
      getters,
 | 
			
		||||
    });
 | 
			
		||||
jest.mock("@/stores/config", () => ({ base: "" }));
 | 
			
		||||
 | 
			
		||||
describe("<LogEventSource />", () => {
 | 
			
		||||
  const search = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    global.EventSource = EventSource;
 | 
			
		||||
    window.scrollTo = jest.fn();
 | 
			
		||||
    global.IntersectionObserver = jest.fn().mockImplementation(() => ({
 | 
			
		||||
      observe: jest.fn(),
 | 
			
		||||
      disconnect: jest.fn(),
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    mocked(debounce).mockClear();
 | 
			
		||||
    jest.resetModules();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function createLogEventSource(
 | 
			
		||||
    {
 | 
			
		||||
      searchFilter = undefined,
 | 
			
		||||
      hourStyle = "auto",
 | 
			
		||||
    }: { searchFilter?: string | undefined; hourStyle?: "auto" | "24" | "12" } = {
 | 
			
		||||
      hourStyle: "auto",
 | 
			
		||||
    }
 | 
			
		||||
  ) {
 | 
			
		||||
    settings.value.hourStyle = hourStyle;
 | 
			
		||||
    search.searchFilter.value = searchFilter;
 | 
			
		||||
    return mount(LogEventSource, {
 | 
			
		||||
      localVue,
 | 
			
		||||
      store,
 | 
			
		||||
      scopedSlots: {
 | 
			
		||||
      global: {
 | 
			
		||||
        plugins: [createTestingPinia()],
 | 
			
		||||
        components: {
 | 
			
		||||
          LogViewer,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: `
 | 
			
		||||
        <log-viewer :messages="props.messages"></log-viewer>
 | 
			
		||||
        <template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
 | 
			
		||||
        `,
 | 
			
		||||
      },
 | 
			
		||||
      propsData: { id: "abc" },
 | 
			
		||||
      props: { id: "abc" },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  test("renders correctly", async () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    expect(wrapper.element).toMatchSnapshot();
 | 
			
		||||
    expect(wrapper.html()).toMatchSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("should connect to EventSource", async () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
    expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
 | 
			
		||||
    wrapper.destroy();
 | 
			
		||||
    wrapper.unmount();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("should close EventSource", async () => {
 | 
			
		||||
    const wrapper = createLogEventSource();
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
 | 
			
		||||
    wrapper.destroy();
 | 
			
		||||
    wrapper.unmount();
 | 
			
		||||
    expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -121,7 +133,7 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
    sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
 | 
			
		||||
      data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
 | 
			
		||||
    });
 | 
			
		||||
    const [message, _] = wrapper.findComponent(LogViewer).vm.messages;
 | 
			
		||||
    const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
 | 
			
		||||
 | 
			
		||||
    const { key, ...messageWithoutKey } = message;
 | 
			
		||||
 | 
			
		||||
@@ -138,8 +150,10 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
  describe("render html correctly", () => {
 | 
			
		||||
    const RealDate = Date;
 | 
			
		||||
    beforeAll(() => {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      global.Date = class extends RealDate {
 | 
			
		||||
        constructor(arg) {
 | 
			
		||||
        constructor(arg: any | number) {
 | 
			
		||||
          super(arg);
 | 
			
		||||
          if (arg) {
 | 
			
		||||
            return new RealDate(arg);
 | 
			
		||||
          } else {
 | 
			
		||||
@@ -158,11 +172,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">"This is a message."</span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\">today at 10:55:42 AM</time></span><span class=\\"text\\">\\"This is a message.\\"</span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("should render messages with color", async () => {
 | 
			
		||||
@@ -173,11 +185,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\">today at 10:55:42 AM</time></span><span class=\\"text\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("should render messages with html entities", async () => {
 | 
			
		||||
@@ -188,11 +198,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"><test>foo bar</test></span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\">today at 10:55:42 AM</time></span><span class=\\"text\\"><test>foo bar</test></span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("should render dates with 12 hour style", async () => {
 | 
			
		||||
@@ -203,11 +211,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 11:55:42 PM</time></span> <span class="text"><test>foo bar</test></span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\">today at 11:55:42 PM</time></span><span class=\\"text\\"><test>foo bar</test></span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("should render dates with 24 hour style", async () => {
 | 
			
		||||
@@ -218,11 +224,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 23:55:42</time></span> <span class="text"><test>foo bar</test></span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\">today at 23:55:42</time></span><span class=\\"text\\"><test>foo bar</test></span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("should render messages with filter", async () => {
 | 
			
		||||
@@ -236,11 +240,9 @@ describe("<LogEventSource />", () => {
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await wrapper.vm.$nextTick();
 | 
			
		||||
      expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
 | 
			
		||||
        <ul class="events medium">
 | 
			
		||||
          <li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">This is a <mark>test</mark> <hi></hi></span></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      `);
 | 
			
		||||
      expect(wrapper.find("ul.events").html()).toMatchInlineSnapshot(
 | 
			
		||||
        `"<ul class=\\"events medium\\"><li><span class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\">today at 10:55:42 AM</time></span><span class=\\"text\\">This is a <mark>test</mark> <hi></hi></span></li></ul>"`
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,122 +1,133 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
 | 
			
		||||
    <slot :messages="messages"></slot>
 | 
			
		||||
  </div>
 | 
			
		||||
  <infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
 | 
			
		||||
  <slot :messages="messages"></slot>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, ref, watch, onUnmounted } from "vue";
 | 
			
		||||
import debounce from "lodash.debounce";
 | 
			
		||||
import InfiniteLoader from "./InfiniteLoader";
 | 
			
		||||
import config from "../store/config";
 | 
			
		||||
import containerMixin from "./mixins/container";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ["id"],
 | 
			
		||||
  mixins: [containerMixin],
 | 
			
		||||
  name: "LogEventSource",
 | 
			
		||||
  components: {
 | 
			
		||||
    InfiniteLoader,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      messages: [],
 | 
			
		||||
      buffer: [],
 | 
			
		||||
      es: null,
 | 
			
		||||
      lastEventId: null,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 });
 | 
			
		||||
    this.loadLogs();
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.es.close();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    loadLogs() {
 | 
			
		||||
      this.reset();
 | 
			
		||||
      this.connect();
 | 
			
		||||
    },
 | 
			
		||||
    onContainerStopped() {
 | 
			
		||||
      this.es.close();
 | 
			
		||||
      this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date(), key: new Date() });
 | 
			
		||||
      this.flushBuffer();
 | 
			
		||||
      this.flushBuffer.flush();
 | 
			
		||||
    },
 | 
			
		||||
    onMessage(e) {
 | 
			
		||||
      this.lastEventId = e.lastEventId;
 | 
			
		||||
      this.buffer.push(this.parseMessage(e.data));
 | 
			
		||||
      this.flushBuffer();
 | 
			
		||||
    },
 | 
			
		||||
    onContainerStateChange(newValue, oldValue) {
 | 
			
		||||
      if (newValue == "running" && newValue != oldValue) {
 | 
			
		||||
        this.buffer.push({
 | 
			
		||||
          event: "container-started",
 | 
			
		||||
          message: "Container started",
 | 
			
		||||
          date: new Date(),
 | 
			
		||||
          key: new Date(),
 | 
			
		||||
        });
 | 
			
		||||
        this.connect();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    connect() {
 | 
			
		||||
      this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}&lastEventId=${this.lastEventId ?? ""}`);
 | 
			
		||||
      this.es.addEventListener("container-stopped", (e) => this.onContainerStopped());
 | 
			
		||||
      this.es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
 | 
			
		||||
      this.es.onmessage = (e) => this.onMessage(e);
 | 
			
		||||
    },
 | 
			
		||||
    flushNow() {
 | 
			
		||||
      this.messages.push(...this.buffer);
 | 
			
		||||
      this.buffer = [];
 | 
			
		||||
    },
 | 
			
		||||
    reset() {
 | 
			
		||||
      if (this.es) {
 | 
			
		||||
        this.es.close();
 | 
			
		||||
      }
 | 
			
		||||
      this.flushBuffer.cancel();
 | 
			
		||||
      this.es = null;
 | 
			
		||||
      this.messages = [];
 | 
			
		||||
      this.buffer = [];
 | 
			
		||||
      this.lastEventId = null;
 | 
			
		||||
    },
 | 
			
		||||
    async loadOlderLogs() {
 | 
			
		||||
      if (this.messages.length < 300) return;
 | 
			
		||||
import { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
import InfiniteLoader from "./InfiniteLoader.vue";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
 | 
			
		||||
      this.$emit("loading-more", true);
 | 
			
		||||
      const to = this.messages[0].date;
 | 
			
		||||
      const last = this.messages[299].date;
 | 
			
		||||
      const delta = to - last;
 | 
			
		||||
      const from = new Date(to.getTime() + delta);
 | 
			
		||||
      const logs = await (
 | 
			
		||||
        await fetch(`${config.base}/api/logs?id=${this.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
 | 
			
		||||
      ).text();
 | 
			
		||||
      if (logs) {
 | 
			
		||||
        const newMessages = logs
 | 
			
		||||
          .trim()
 | 
			
		||||
          .split("\n")
 | 
			
		||||
          .map((line) => this.parseMessage(line));
 | 
			
		||||
        this.messages.unshift(...newMessages);
 | 
			
		||||
      }
 | 
			
		||||
      this.$emit("loading-more", false);
 | 
			
		||||
    },
 | 
			
		||||
    parseMessage(data) {
 | 
			
		||||
      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 };
 | 
			
		||||
    },
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    id(newValue, oldValue) {
 | 
			
		||||
      if (oldValue !== newValue) {
 | 
			
		||||
        this.loadLogs();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ul class="events" :class="settings.size">
 | 
			
		||||
  <ul class="events" :class="size">
 | 
			
		||||
    <li v-for="item in filtered" :key="item.key" :data-event="item.event">
 | 
			
		||||
      <span class="date" v-if="settings.showTimestamp"><relative-time :date="item.date"></relative-time></span>
 | 
			
		||||
      <span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
 | 
			
		||||
      <span class="text" v-html="colorize(item.message)"></span>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import { mapState } from "vuex";
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { PropType, toRefs } from "vue";
 | 
			
		||||
 | 
			
		||||
import { size, showTimestamp } from "@/composables/settings";
 | 
			
		||||
import RelativeTime from "./RelativeTime.vue";
 | 
			
		||||
import AnsiConvertor from "ansi-to-html";
 | 
			
		||||
import DOMPurify from "dompurify";
 | 
			
		||||
import RelativeTime from "./RelativeTime";
 | 
			
		||||
import { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  messages: {
 | 
			
		||||
    type: Array as PropType<LogEntry[]>,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
 | 
			
		||||
 | 
			
		||||
if (window.trustedTypes && trustedTypes.createPolicy) {
 | 
			
		||||
  trustedTypes.createPolicy("default", {
 | 
			
		||||
    createHTML: (string, sink) => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ["messages"],
 | 
			
		||||
  name: "LogViewer",
 | 
			
		||||
  components: { RelativeTime },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showSearch: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    colorize: function (value) {
 | 
			
		||||
      return ansiConvertor.toHtml(value).replace("<mark>", "<mark>").replace("</mark>", "</mark>");
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["searchFilter", "settings"]),
 | 
			
		||||
    filtered() {
 | 
			
		||||
      const { searchFilter, messages } = this;
 | 
			
		||||
      if (searchFilter) {
 | 
			
		||||
        const isSmartCase = searchFilter === searchFilter.toLowerCase();
 | 
			
		||||
        try {
 | 
			
		||||
          const regex = isSmartCase ? new RegExp(searchFilter, "i") : new RegExp(searchFilter);
 | 
			
		||||
          return messages
 | 
			
		||||
            .filter((d) => d.message.match(regex))
 | 
			
		||||
            .map((d) => ({
 | 
			
		||||
              ...d,
 | 
			
		||||
              message: d.message.replace(regex, "<mark>$&</mark>"),
 | 
			
		||||
            }));
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          if (e instanceof SyntaxError) {
 | 
			
		||||
            console.info(`Ignoring SytaxError from search.`, e);
 | 
			
		||||
            return messages;
 | 
			
		||||
          }
 | 
			
		||||
          throw e;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return messages;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const colorize = (value: string) =>
 | 
			
		||||
  ansiConvertor.toHtml(value).replace("<mark>", "<mark>").replace("</mark>", "</mark>");
 | 
			
		||||
const { messages } = toRefs(props);
 | 
			
		||||
const filtered = useSearchFilter().filteredMessages(messages);
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.events {
 | 
			
		||||
@@ -108,9 +76,12 @@ export default {
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: " ";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::v-deep mark {
 | 
			
		||||
:deep(mark) {
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  background-color: var(--secondary-color);
 | 
			
		||||
  animation: pops 200ms ease-out;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <log-event-source :id="id" v-slot="eventSource" @loading-more="$emit('loading-more', $event)">
 | 
			
		||||
    <log-viewer :messages="eventSource.messages"></log-viewer>
 | 
			
		||||
  <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>
 | 
			
		||||
import LogEventSource from "./LogEventSource";
 | 
			
		||||
import LogViewer from "./LogViewer";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ["id"],
 | 
			
		||||
  name: "LogViewerWithSource",
 | 
			
		||||
  components: {
 | 
			
		||||
    LogEventSource,
 | 
			
		||||
    LogViewer,
 | 
			
		||||
<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>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column ml-4 is-family-monospace is-ellipsis" v-if="$route.name == 'container'">
 | 
			
		||||
        {{ allContainersById[$route.params.id].name }}
 | 
			
		||||
        {{ allContainersById[route.params.id].name }}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="column is-narrow push-right">
 | 
			
		||||
@@ -41,32 +41,26 @@
 | 
			
		||||
  </aside>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from "vuex";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [],
 | 
			
		||||
  name: "MobileMenu",
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showNav: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["visibleContainers", "allContainersById"]),
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    $route(to, from) {
 | 
			
		||||
      this.showNav = false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const { visibleContainers, allContainersById } = storeToRefs(store);
 | 
			
		||||
 | 
			
		||||
const showNav = ref(false);
 | 
			
		||||
 | 
			
		||||
watch(route, () => {
 | 
			
		||||
  showNav.value = false;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
aside {
 | 
			
		||||
  padding: 1em;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  background: var(--scheme-main-ter);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <time :datetime="date.toISOString()">{{ text }}</time>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import formatDistance from "date-fns/formatDistance";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@@ -14,7 +14,7 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      text: "",
 | 
			
		||||
      text: "" as string,
 | 
			
		||||
      interval: null,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <time :datetime="date.toISOString()">{{ date | relativeTime(locale) }}</time>
 | 
			
		||||
  <time :datetime="date.toISOString()">{{ relativeTime(date, locale) }}</time>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapState } from "vuex";
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@@ -27,11 +27,9 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  name: "RelativeTime",
 | 
			
		||||
  components: {},
 | 
			
		||||
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["settings"]),
 | 
			
		||||
    locale() {
 | 
			
		||||
      const locale = styles[this.settings.hourStyle];
 | 
			
		||||
      const locale = styles[hourStyle.value];
 | 
			
		||||
      const oldFormatter = locale.formatRelative;
 | 
			
		||||
      return {
 | 
			
		||||
        ...locale,
 | 
			
		||||
@@ -41,7 +39,7 @@ export default {
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  filters: {
 | 
			
		||||
  methods: {
 | 
			
		||||
    relativeTime(date, locale) {
 | 
			
		||||
      return formatRelative(date, new Date(), { locale });
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="scroll-progress">
 | 
			
		||||
  <div class="scroll-progress" ref="root">
 | 
			
		||||
    <svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
 | 
			
		||||
      <circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
 | 
			
		||||
    </svg>
 | 
			
		||||
@@ -17,79 +17,76 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from "vuex";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import throttle from "lodash.throttle";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { onMounted, onUnmounted, ref, watchPostEffect } from "vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "ScrollProgress",
 | 
			
		||||
  props: {
 | 
			
		||||
    indeterminate: {
 | 
			
		||||
      default: false,
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
    autoHide: {
 | 
			
		||||
      default: true,
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  indeterminate: {
 | 
			
		||||
    default: false,
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      scrollProgress: 0,
 | 
			
		||||
      animation: { cancel: () => {} },
 | 
			
		||||
      parentElement: document,
 | 
			
		||||
    };
 | 
			
		||||
  autoHide: {
 | 
			
		||||
    default: true,
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.onScrollThrottled = throttle(this.onScroll, 150);
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.attachEvents();
 | 
			
		||||
    this.$once("hook:beforeDestroy", this.detachEvents);
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    activeContainers() {
 | 
			
		||||
      this.detachEvents();
 | 
			
		||||
      this.attachEvents();
 | 
			
		||||
    },
 | 
			
		||||
    indeterminate() {
 | 
			
		||||
      this.$nextTick(() => this.onScroll());
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["activeContainers"]),
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    attachEvents() {
 | 
			
		||||
      this.parentElement = this.$el.closest("[data-scrolling]") || document;
 | 
			
		||||
      this.parentElement.addEventListener("scroll", this.onScrollThrottled);
 | 
			
		||||
    },
 | 
			
		||||
    detachEvents() {
 | 
			
		||||
      this.parentElement.removeEventListener("scroll", this.onScrollThrottled);
 | 
			
		||||
    },
 | 
			
		||||
    onScroll() {
 | 
			
		||||
      const p = this.parentElement == document ? document.documentElement : this.parentElement;
 | 
			
		||||
      this.scrollProgress = p.scrollTop / (p.scrollHeight - p.clientHeight);
 | 
			
		||||
      this.animation.cancel();
 | 
			
		||||
      if (this.autoHide) {
 | 
			
		||||
        this.animation = this.$el.animate(
 | 
			
		||||
          { opacity: [1, 0] },
 | 
			
		||||
          {
 | 
			
		||||
            duration: 500,
 | 
			
		||||
            delay: 2000,
 | 
			
		||||
            fill: "both",
 | 
			
		||||
            easing: "ease-out",
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const scrollProgress = ref(0);
 | 
			
		||||
const animation = ref({ cancel: () => {} });
 | 
			
		||||
const parentElement = ref<Node>(document);
 | 
			
		||||
const root = ref<HTMLElement>();
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
const { activeContainers } = storeToRefs(store);
 | 
			
		||||
const onScrollThrottled = throttle(onScroll, 150);
 | 
			
		||||
 | 
			
		||||
function onScroll() {
 | 
			
		||||
  const parent = parentElement.value == document ? document.documentElement : (parentElement.value as HTMLElement);
 | 
			
		||||
  scrollProgress.value = parent.scrollTop / (parent.scrollHeight - parent.clientHeight);
 | 
			
		||||
  animation.value.cancel();
 | 
			
		||||
  if (props.autoHide && root.value) {
 | 
			
		||||
    animation.value = root.value.animate(
 | 
			
		||||
      { opacity: [1, 0] },
 | 
			
		||||
      {
 | 
			
		||||
        duration: 500,
 | 
			
		||||
        delay: 2000,
 | 
			
		||||
        fill: "both",
 | 
			
		||||
        easing: "ease-out",
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function attachEvents() {
 | 
			
		||||
  parentElement.value = root.value?.closest("[data-scrolling]") || document;
 | 
			
		||||
  parentElement.value.addEventListener("scroll", onScrollThrottled);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function detachEvents() {
 | 
			
		||||
  parentElement.value.removeEventListener("scroll", onScrollThrottled);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  attachEvents();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  detachEvents();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watchPostEffect(() => {
 | 
			
		||||
  activeContainers.value.length;
 | 
			
		||||
  detachEvents();
 | 
			
		||||
  attachEvents();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.scroll-progress {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
 | 
			
		||||
  svg {
 | 
			
		||||
    filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    <header v-if="$slots.header">
 | 
			
		||||
      <slot name="header"></slot>
 | 
			
		||||
    </header>
 | 
			
		||||
    <main ref="content" :data-scrolling="scrollable">
 | 
			
		||||
    <main ref="content" :data-scrolling="scrollable ? true : undefined">
 | 
			
		||||
      <div class="is-scrollbar-progress is-hidden-mobile">
 | 
			
		||||
        <scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -14,17 +14,14 @@
 | 
			
		||||
    <div class="is-scrollbar-notification">
 | 
			
		||||
      <transition name="fade">
 | 
			
		||||
        <button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
 | 
			
		||||
          <icon name="download"></icon>
 | 
			
		||||
          <mdi-light-chevron-double-down />
 | 
			
		||||
        </button>
 | 
			
		||||
      </transition>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
import ScrollProgress from "./ScrollProgress";
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    scrollable: {
 | 
			
		||||
@@ -32,21 +29,20 @@ export default {
 | 
			
		||||
      default: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
    ScrollProgress,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  name: "ScrollableView",
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      paused: false,
 | 
			
		||||
      hasMore: false,
 | 
			
		||||
      loading: false,
 | 
			
		||||
      mutationObserver: null,
 | 
			
		||||
      intersectionObserver: null,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const { content } = this.$refs;
 | 
			
		||||
    const mutationObserver = new MutationObserver((e) => {
 | 
			
		||||
    this.mutationObserver = new MutationObserver((e) => {
 | 
			
		||||
      if (!this.paused) {
 | 
			
		||||
        this.scrollToBottom("instant");
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -58,17 +54,18 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    mutationObserver.observe(content, { childList: true, subtree: true });
 | 
			
		||||
    this.$once("hook:beforeDestroy", () => mutationObserver.disconnect());
 | 
			
		||||
    this.mutationObserver.observe(content, { childList: true, subtree: true });
 | 
			
		||||
 | 
			
		||||
    const intersectionObserver = new IntersectionObserver(
 | 
			
		||||
    this.intersectionObserver = new IntersectionObserver(
 | 
			
		||||
      (entries) => (this.paused = entries[0].intersectionRatio == 0),
 | 
			
		||||
      { threshholds: [0, 1], rootMargin: "80px 0px" }
 | 
			
		||||
    );
 | 
			
		||||
    intersectionObserver.observe(this.$refs.scrollObserver);
 | 
			
		||||
    this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
 | 
			
		||||
    this.intersectionObserver.observe(this.$refs.scrollObserver);
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.mutationObserver.disconnect();
 | 
			
		||||
    this.intersectionObserver.disconnect();
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    scrollToBottom(behavior = "instant") {
 | 
			
		||||
      this.$refs.scrollObserver.scrollIntoView({ behavior });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="settings.search">
 | 
			
		||||
  <div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="search">
 | 
			
		||||
    <div class="column">
 | 
			
		||||
      <p class="control has-icons-left">
 | 
			
		||||
        <input
 | 
			
		||||
          class="input"
 | 
			
		||||
          type="text"
 | 
			
		||||
          placeholder="Find / RegEx"
 | 
			
		||||
          ref="filter"
 | 
			
		||||
          v-model="filter"
 | 
			
		||||
          ref="input"
 | 
			
		||||
          v-model="searchFilter"
 | 
			
		||||
          @keyup.esc="resetSearch()"
 | 
			
		||||
        />
 | 
			
		||||
        <span class="icon is-left">
 | 
			
		||||
          <icon name="search"></icon>
 | 
			
		||||
          <mdi-light-magnify />
 | 
			
		||||
        </span>
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -21,57 +21,36 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapActions, mapState } from "vuex";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import hotkeys from "hotkeys-js";
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [],
 | 
			
		||||
  name: "Search",
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showSearch: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    hotkeys("command+f, ctrl+f", (event, handler) => {
 | 
			
		||||
      this.showSearch = true;
 | 
			
		||||
      this.$nextTick(() => this.$refs.filter.focus() || this.$refs.filter.select());
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
    hotkeys("esc", (event, handler) => {
 | 
			
		||||
      this.resetSearch();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.updateSearchFilter("");
 | 
			
		||||
    hotkeys.unbind("command+f, ctrl+f, esc");
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      updateSearchFilter: "SET_SEARCH",
 | 
			
		||||
    }),
 | 
			
		||||
    resetSearch() {
 | 
			
		||||
      this.showSearch = false;
 | 
			
		||||
      this.filter = "";
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["searchFilter", "settings"]),
 | 
			
		||||
    filter: {
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.searchFilter;
 | 
			
		||||
      },
 | 
			
		||||
      set(value) {
 | 
			
		||||
        this.updateSearchFilter(value);
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
import { search } from "@/composables/settings";
 | 
			
		||||
import { useSearchFilter } from "@/composables/search";
 | 
			
		||||
import { ref, nextTick, onMounted, onUnmounted } from "vue";
 | 
			
		||||
 | 
			
		||||
const input = ref<HTMLInputElement>();
 | 
			
		||||
const { searchFilter, showSearch } = useSearchFilter();
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  hotkeys("command+f, ctrl+f", (event, handler) => {
 | 
			
		||||
    showSearch.value = true;
 | 
			
		||||
    nextTick(() => input.value?.focus() || input.value?.select());
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
  });
 | 
			
		||||
  hotkeys("esc", () => resetSearch());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  searchFilter.value = "";
 | 
			
		||||
  showSearch.value = false;
 | 
			
		||||
  hotkeys.unbind("command+f, ctrl+f");
 | 
			
		||||
  hotkeys.unbind("esc");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function resetSearch() {
 | 
			
		||||
  searchFilter.value = "";
 | 
			
		||||
  showSearch.value = false;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,24 +9,16 @@
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column is-narrow has-text-right px-1">
 | 
			
		||||
        <button
 | 
			
		||||
          class="button is-small is-rounded is-settings-control"
 | 
			
		||||
          @click="$emit('search')"
 | 
			
		||||
          title="Search containers (⌘ + k, ⌃k)"
 | 
			
		||||
        >
 | 
			
		||||
        <button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
 | 
			
		||||
          <span class="icon">
 | 
			
		||||
            <icon name="search"></icon>
 | 
			
		||||
            <mdi-light-magnify />
 | 
			
		||||
          </span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="column is-narrow has-text-right px-0">
 | 
			
		||||
        <router-link
 | 
			
		||||
          :to="{ name: 'settings' }"
 | 
			
		||||
          active-class="is-active"
 | 
			
		||||
          class="button is-small is-rounded is-settings-control"
 | 
			
		||||
        >
 | 
			
		||||
        <router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
 | 
			
		||||
          <span class="icon">
 | 
			
		||||
            <icon name="cog"></icon>
 | 
			
		||||
            <mdi-light-cog />
 | 
			
		||||
          </span>
 | 
			
		||||
        </router-link>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -46,11 +38,11 @@
 | 
			
		||||
            <div class="is-flex-shrink-1 column-icon">
 | 
			
		||||
              <span
 | 
			
		||||
                class="icon is-small"
 | 
			
		||||
                @click.stop.prevent="appendActiveContainer(item)"
 | 
			
		||||
                @click.stop.prevent="store.appendActiveContainer(item)"
 | 
			
		||||
                v-show="!activeContainersById[item.id]"
 | 
			
		||||
                title="Pin as column"
 | 
			
		||||
              >
 | 
			
		||||
                <icon name="column"></icon>
 | 
			
		||||
                <cil-columns />
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -60,35 +52,22 @@
 | 
			
		||||
  </aside>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapActions, mapGetters, mapState } from "vuex";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import type { Container } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [],
 | 
			
		||||
  name: "SideMenu",
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["visibleContainers", "activeContainers"]),
 | 
			
		||||
    activeContainersById() {
 | 
			
		||||
      return this.activeContainers.reduce((map, obj) => {
 | 
			
		||||
        map[obj.id] = obj;
 | 
			
		||||
        return map;
 | 
			
		||||
      }, {});
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      appendActiveContainer: "APPEND_ACTIVE_CONTAINER",
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const { activeContainers, visibleContainers } = storeToRefs(store);
 | 
			
		||||
 | 
			
		||||
const activeContainersById = computed(() =>
 | 
			
		||||
  activeContainers.value.reduce((acc, item) => {
 | 
			
		||||
    acc[item.id] = item;
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, {} as Record<string, Container>)
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
aside {
 | 
			
		||||
@@ -116,6 +95,10 @@ li.exited a {
 | 
			
		||||
.menu-list li {
 | 
			
		||||
  .column-icon {
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
 | 
			
		||||
    & > span {
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover .column-icon {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> renders correctly 1`] = `
 | 
			
		||||
<div>
 | 
			
		||||
  <div
 | 
			
		||||
    class="infinte-loader"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="spinner"
 | 
			
		||||
      style="display: none;"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        class="bounce1"
 | 
			
		||||
      />
 | 
			
		||||
       
 | 
			
		||||
      <div
 | 
			
		||||
        class="bounce2"
 | 
			
		||||
      />
 | 
			
		||||
       
 | 
			
		||||
      <div
 | 
			
		||||
        class="bounce3"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
   
 | 
			
		||||
  <ul
 | 
			
		||||
    class="events medium"
 | 
			
		||||
  />
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
							
								
								
									
										12
									
								
								assets/components/__snapshots__/LogEventSource.spec.ts.snap
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
			
		||||
 | 
			
		||||
exports[`<LogEventSource /> renders correctly 1`] = `
 | 
			
		||||
"<div class=\\"infinte-loader\\">
 | 
			
		||||
  <div class=\\"spinner\\" style=\\"display: none;\\">
 | 
			
		||||
    <div class=\\"bounce1\\"></div>
 | 
			
		||||
    <div class=\\"bounce2\\"></div>
 | 
			
		||||
    <div class=\\"bounce3\\"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<ul class=\\"events medium\\"></ul>"
 | 
			
		||||
`;
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import { mapGetters } from "vuex";
 | 
			
		||||
export default {
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["allContainersById"]),
 | 
			
		||||
    container() {
 | 
			
		||||
      return this.allContainersById[this.id];
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    ["container.state"](newValue, oldValue) {
 | 
			
		||||
      if (newValue == "running" && newValue != oldValue) {
 | 
			
		||||
        this.onContainerStateChange(newValue, oldValue);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    onContainerStateChange(newValue, oldValue) {},
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										3
									
								
								assets/composables/media.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
import { useMediaQuery } from "@vueuse/core";
 | 
			
		||||
 | 
			
		||||
export const isMobile = useMediaQuery("(max-width: 770px)");
 | 
			
		||||
							
								
								
									
										39
									
								
								assets/composables/search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
import { ref, computed, Ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const searchFilter = ref<string>();
 | 
			
		||||
const showSearch = ref(false);
 | 
			
		||||
 | 
			
		||||
import type { LogEntry } from "@/types/LogEntry";
 | 
			
		||||
 | 
			
		||||
export function useSearchFilter() {
 | 
			
		||||
  function filteredMessages(messages: Ref<LogEntry[]>) {
 | 
			
		||||
    return computed(() => {
 | 
			
		||||
      if (searchFilter && searchFilter.value) {
 | 
			
		||||
        const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
 | 
			
		||||
        try {
 | 
			
		||||
          const regex = isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
 | 
			
		||||
          return messages.value
 | 
			
		||||
            .filter((d) => d.message.match(regex))
 | 
			
		||||
            .map((d) => ({
 | 
			
		||||
              ...d,
 | 
			
		||||
              message: d.message.replace(regex, "<mark>$&</mark>"),
 | 
			
		||||
            }));
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          if (e instanceof SyntaxError) {
 | 
			
		||||
            console.info(`Ignoring SytaxError from search.`, e);
 | 
			
		||||
            return messages.value;
 | 
			
		||||
          }
 | 
			
		||||
          throw e;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return messages.value;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    filteredMessages,
 | 
			
		||||
    searchFilter,
 | 
			
		||||
    showSearch
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								assets/composables/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,65 @@
 | 
			
		||||
import { useStorage } from "@vueuse/core";
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
 | 
			
		||||
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_SETTINGS: {
 | 
			
		||||
  search: boolean;
 | 
			
		||||
  size: "small" | "medium" | "large";
 | 
			
		||||
  menuWidth: number;
 | 
			
		||||
  smallerScrollbars: boolean;
 | 
			
		||||
  showTimestamp: boolean;
 | 
			
		||||
  showAllContainers: boolean;
 | 
			
		||||
  lightTheme: boolean;
 | 
			
		||||
  hourStyle: "auto" | "24" | "12";
 | 
			
		||||
} = {
 | 
			
		||||
  search: true,
 | 
			
		||||
  size: "medium",
 | 
			
		||||
  menuWidth: 15,
 | 
			
		||||
  smallerScrollbars: false,
 | 
			
		||||
  showTimestamp: true,
 | 
			
		||||
  showAllContainers: false,
 | 
			
		||||
  lightTheme: false,
 | 
			
		||||
  hourStyle: "auto",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
 | 
			
		||||
 | 
			
		||||
export const search = computed({
 | 
			
		||||
  get: () => settings.value.search,
 | 
			
		||||
  set: (value) => (settings.value.search = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const size = computed({
 | 
			
		||||
  get: () => settings.value.size,
 | 
			
		||||
  set: (value) => (settings.value.size = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menuWidth = computed({
 | 
			
		||||
  get: () => settings.value.menuWidth,
 | 
			
		||||
  set: (value) => (settings.value.menuWidth = value),
 | 
			
		||||
});
 | 
			
		||||
export const smallerScrollbars = computed({
 | 
			
		||||
  get: () => settings.value.smallerScrollbars,
 | 
			
		||||
  set: (value) => (settings.value.smallerScrollbars = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const showTimestamp = computed({
 | 
			
		||||
  get: () => settings.value.showTimestamp,
 | 
			
		||||
  set: (value) => (settings.value.showTimestamp = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const showAllContainers = computed({
 | 
			
		||||
  get: () => settings.value.showAllContainers,
 | 
			
		||||
  set: (value) => (settings.value.showAllContainers = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const lightTheme = computed({
 | 
			
		||||
  get: () => settings.value.lightTheme,
 | 
			
		||||
  set: (value) => (settings.value.lightTheme = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const hourStyle = computed({
 | 
			
		||||
  get: () => settings.value.hourStyle,
 | 
			
		||||
  set: (value) => (settings.value.hourStyle = value),
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										12
									
								
								assets/composables/title.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
import { useTitle } from "@vueuse/core";
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
 | 
			
		||||
const subtitle = ref("");
 | 
			
		||||
 | 
			
		||||
const title = computed(() => `${subtitle.value} - Dozzle`);
 | 
			
		||||
 | 
			
		||||
useTitle(title);
 | 
			
		||||
 | 
			
		||||
export function setTitle(t: string) {
 | 
			
		||||
  subtitle.value = t;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import VueRouter from "vue-router";
 | 
			
		||||
import Meta from "vue-meta";
 | 
			
		||||
import Switch from "buefy/dist/esm/switch";
 | 
			
		||||
import Radio from "buefy/dist/esm/radio";
 | 
			
		||||
import Field from "buefy/dist/esm/field";
 | 
			
		||||
import Modal from "buefy/dist/esm/modal";
 | 
			
		||||
import Autocomplete from "buefy/dist/esm/autocomplete";
 | 
			
		||||
 | 
			
		||||
import store from "./store";
 | 
			
		||||
import config from "./store/config";
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
 | 
			
		||||
 | 
			
		||||
Vue.use(VueRouter);
 | 
			
		||||
Vue.use(Meta);
 | 
			
		||||
Vue.use(Switch);
 | 
			
		||||
Vue.use(Radio);
 | 
			
		||||
Vue.use(Field);
 | 
			
		||||
Vue.use(Modal);
 | 
			
		||||
Vue.use(Autocomplete);
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: "/",
 | 
			
		||||
    component: Index,
 | 
			
		||||
    name: "default",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/container/:id",
 | 
			
		||||
    component: Container,
 | 
			
		||||
    name: "container",
 | 
			
		||||
    props: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/container/*",
 | 
			
		||||
    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: "/*",
 | 
			
		||||
    component: PageNotFound,
 | 
			
		||||
    name: "page-not-found",
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = new VueRouter({
 | 
			
		||||
  mode: "history",
 | 
			
		||||
  base: config.base + "/",
 | 
			
		||||
  routes,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
new Vue({
 | 
			
		||||
  router,
 | 
			
		||||
  store,
 | 
			
		||||
  render: (h) => h(App),
 | 
			
		||||
}).$mount("#app");
 | 
			
		||||
							
								
								
									
										67
									
								
								assets/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,67 @@
 | 
			
		||||
import "./styles.scss";
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router";
 | 
			
		||||
import { Autocomplete, Button, Dropdown, Switch, Radio, 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 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 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(Config, bulmaConfig)
 | 
			
		||||
  .mount("#app");
 | 
			
		||||
@@ -5,43 +5,33 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from "vuex";
 | 
			
		||||
import Search from "../components/Search";
 | 
			
		||||
import LogContainer from "../components/LogContainer";
 | 
			
		||||
<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";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: ["id"],
 | 
			
		||||
  name: "Container",
 | 
			
		||||
  components: {
 | 
			
		||||
    LogContainer,
 | 
			
		||||
    Search,
 | 
			
		||||
const store = useContainerStore();
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "loading",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: this.title,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    if (this.allContainersById[this.id]) {
 | 
			
		||||
      this.title = this.allContainersById[this.id].name;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(["allContainersById", "activeContainers"]),
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    id() {
 | 
			
		||||
      this.title = this.allContainersById[this.id].name;
 | 
			
		||||
    },
 | 
			
		||||
    allContainersById() {
 | 
			
		||||
      this.title = this.allContainersById[this.id].name;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,13 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "ContainerNotFound",
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "Not Found",
 | 
			
		||||
    };
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Container not found");
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@
 | 
			
		||||
                @keyup.enter="onEnter()"
 | 
			
		||||
              />
 | 
			
		||||
              <span class="icon is-left">
 | 
			
		||||
                <icon name="search"></icon>
 | 
			
		||||
                <search-icon />
 | 
			
		||||
              </span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -76,60 +76,47 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapState } from "vuex";
 | 
			
		||||
import Icon from "../components/Icon";
 | 
			
		||||
import PastTime from "../components/PastTime";
 | 
			
		||||
import config from "../store/config";
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import fuzzysort from "fuzzysort";
 | 
			
		||||
import SearchIcon from "~icons/mdi-light/magnify";
 | 
			
		||||
import PastTime from "../components/PastTime.vue";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: "Index",
 | 
			
		||||
  components: { Icon, PastTime },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      version: config.version,
 | 
			
		||||
      search: null,
 | 
			
		||||
      sort: "running",
 | 
			
		||||
      secured: config.secured,
 | 
			
		||||
      base: config.base,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    onEnter() {
 | 
			
		||||
      if (this.results.length == 1) {
 | 
			
		||||
        const [item] = this.results;
 | 
			
		||||
        this.$router.push({ name: "container", params: { id: item.id, name: item.name } });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["containers"]),
 | 
			
		||||
    mostRecentContainers() {
 | 
			
		||||
      return [...this.containers].sort((a, b) => b.created - a.created);
 | 
			
		||||
    },
 | 
			
		||||
    runningContainers() {
 | 
			
		||||
      return this.mostRecentContainers.filter((c) => c.state === "running");
 | 
			
		||||
    },
 | 
			
		||||
    allContainers() {
 | 
			
		||||
      return this.containers;
 | 
			
		||||
    },
 | 
			
		||||
    results() {
 | 
			
		||||
      if (this.search) {
 | 
			
		||||
        return fuzzysort.go(this.search, this.allContainers, { key: "name" }).map((i) => i.obj);
 | 
			
		||||
      }
 | 
			
		||||
      switch (this.sort) {
 | 
			
		||||
        case "all":
 | 
			
		||||
          return this.mostRecentContainers;
 | 
			
		||||
        case "running":
 | 
			
		||||
          return this.runningContainers;
 | 
			
		||||
const { base, version, secured } = config;
 | 
			
		||||
const containerStore = useContainerStore();
 | 
			
		||||
const { containers } = storeToRefs(containerStore);
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
          throw `Invalid sort order: ${this.sort}`;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
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"));
 | 
			
		||||
 | 
			
		||||
function onEnter() {
 | 
			
		||||
  if (results.value.length == 1) {
 | 
			
		||||
    const [item] = results.value;
 | 
			
		||||
    router.push({ name: "container", params: { id: item.id, name: item.name } });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.panel {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,8 +49,9 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import config from "../store/config";
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
export default {
 | 
			
		||||
  name: "Login",
 | 
			
		||||
  data() {
 | 
			
		||||
@@ -60,10 +61,8 @@ export default {
 | 
			
		||||
      error: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "Authentication Required",
 | 
			
		||||
    };
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Authentication Required");
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async onLogin() {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,21 +3,20 @@
 | 
			
		||||
    <div class="hero-body">
 | 
			
		||||
      <div class="container has-text-centered">
 | 
			
		||||
        <h1 class="title">
 | 
			
		||||
          Oops,
 | 
			
		||||
          <small class="subtitle">this page doesn't exist</small>
 | 
			
		||||
          404.
 | 
			
		||||
          <small class="subtitle">This page does not exist.</small>
 | 
			
		||||
        </h1>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
export default {
 | 
			
		||||
  name: "PageNotFound",
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "404 Error",
 | 
			
		||||
    };
 | 
			
		||||
  setup() {
 | 
			
		||||
    setTitle("Page not found");
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,8 @@
 | 
			
		||||
        >.
 | 
			
		||||
        <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
 | 
			
		||||
          <a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
 | 
			
		||||
            {{ nextRelease.name }}</a
 | 
			
		||||
          >.
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -25,16 +24,22 @@
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <div class="columns is-vcentered">
 | 
			
		||||
          <div class="column is-narrow">
 | 
			
		||||
            <b-field>
 | 
			
		||||
              <b-radio-button
 | 
			
		||||
                v-model="hourStyle"
 | 
			
		||||
                :native-value="value"
 | 
			
		||||
                v-for="value in ['auto', '12', '24']"
 | 
			
		||||
                :key="value"
 | 
			
		||||
              >
 | 
			
		||||
                <span class="is-capitalized">{{ value }}</span>
 | 
			
		||||
              </b-radio-button>
 | 
			
		||||
            </b-field>
 | 
			
		||||
            <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">
 | 
			
		||||
            By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
 | 
			
		||||
@@ -42,26 +47,37 @@
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="item">
 | 
			
		||||
          <b-switch v-model="smallerScrollbars"> Use smaller scrollbars </b-switch>
 | 
			
		||||
          <o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="item">
 | 
			
		||||
          <b-switch v-model="showTimestamp"> Show timestamps </b-switch>
 | 
			
		||||
          <o-switch v-model="showTimestamp"> Show timestamps </o-switch>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <div class="columns is-vcentered">
 | 
			
		||||
          <div class="column is-narrow">
 | 
			
		||||
            <b-field>
 | 
			
		||||
              <b-radio-button
 | 
			
		||||
                v-model="size"
 | 
			
		||||
                :native-value="value"
 | 
			
		||||
                v-for="value in ['small', 'medium', 'large']"
 | 
			
		||||
                :key="value"
 | 
			
		||||
              >
 | 
			
		||||
                <span class="is-capitalized">{{ value }}</span>
 | 
			
		||||
              </b-radio-button>
 | 
			
		||||
            </b-field>
 | 
			
		||||
            <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">Font size to use for logs</div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -73,78 +89,57 @@
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <b-switch v-model="search">
 | 
			
		||||
        <o-switch v-model="search">
 | 
			
		||||
          Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
 | 
			
		||||
        </b-switch>
 | 
			
		||||
        </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <b-switch v-model="showAllContainers"> Show stopped containers </b-switch>
 | 
			
		||||
        <o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="item">
 | 
			
		||||
        <b-switch v-model="lightTheme"> Use light theme </b-switch>
 | 
			
		||||
        <o-switch v-model="lightTheme"> Use light theme </o-switch>
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import gt from "semver/functions/gt";
 | 
			
		||||
import { mapActions, mapState } from "vuex";
 | 
			
		||||
import Icon from "../components/Icon";
 | 
			
		||||
import config from "../store/config";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import { setTitle } from "@/composables/title";
 | 
			
		||||
import {
 | 
			
		||||
  search,
 | 
			
		||||
  lightTheme,
 | 
			
		||||
  smallerScrollbars,
 | 
			
		||||
  showTimestamp,
 | 
			
		||||
  hourStyle,
 | 
			
		||||
  showAllContainers,
 | 
			
		||||
  size,
 | 
			
		||||
} from "@/composables/settings";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: [],
 | 
			
		||||
  name: "Settings",
 | 
			
		||||
  components: {
 | 
			
		||||
    Icon,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      currentVersion: config.version,
 | 
			
		||||
      nextRelease: null,
 | 
			
		||||
      hasUpdate: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  async created() {
 | 
			
		||||
    const releases = await (await fetch("https://api.github.com/repos/amir20/dozzle/releases")).json();
 | 
			
		||||
    if (this.currentVersion !== "master") {
 | 
			
		||||
      this.hasUpdate = gt(releases[0].tag_name, this.currentVersion);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.hasUpdate = true;
 | 
			
		||||
setTitle("Settings");
 | 
			
		||||
 | 
			
		||||
const currentVersion = config.version;
 | 
			
		||||
const nextRelease = ref({ html_url: "", name: "" });
 | 
			
		||||
const hasUpdate = ref(false);
 | 
			
		||||
 | 
			
		||||
async function fetchNextRelease() {
 | 
			
		||||
  if (!["dev", "master"].includes(currentVersion)) {
 | 
			
		||||
    const response = await fetch("https://api.github.com/repos/dozzle/dozzle/releases/latest");
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
      const releases = await response.json();
 | 
			
		||||
      hasUpdate.value = gt(releases[0].tag_name, currentVersion);
 | 
			
		||||
      nextRelease.value = releases[0];
 | 
			
		||||
    }
 | 
			
		||||
    this.nextRelease = releases[0];
 | 
			
		||||
  },
 | 
			
		||||
  metaInfo() {
 | 
			
		||||
    return {
 | 
			
		||||
      title: "Settings",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      updateSetting: "UPDATE_SETTING",
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState(["settings"]),
 | 
			
		||||
    ...["search", "size", "smallerScrollbars", "showTimestamp", "showAllContainers", "lightTheme", "hourStyle"].reduce(
 | 
			
		||||
      (map, name) => {
 | 
			
		||||
        map[name] = {
 | 
			
		||||
          get() {
 | 
			
		||||
            return this.settings[name];
 | 
			
		||||
          },
 | 
			
		||||
          set(value) {
 | 
			
		||||
            this.updateSetting({ [name]: value });
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
        return map;
 | 
			
		||||
      },
 | 
			
		||||
      {}
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  } else {
 | 
			
		||||
    hasUpdate.value = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fetchNextRelease();
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.title {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,29 @@
 | 
			
		||||
<template></template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { useContainerStore } from "@/stores/container";
 | 
			
		||||
import { storeToRefs } from "pinia";
 | 
			
		||||
import { watch } from "vue";
 | 
			
		||||
import { useRoute, useRouter } from "vue-router";
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapActions, mapGetters, mapState } from "vuex";
 | 
			
		||||
export default {
 | 
			
		||||
  props: [],
 | 
			
		||||
  name: "Show",
 | 
			
		||||
  computed: mapGetters(["visibleContainers"]),
 | 
			
		||||
  watch: {
 | 
			
		||||
    visibleContainers(newValue) {
 | 
			
		||||
      if (newValue) {
 | 
			
		||||
        if (this.$route.query.name) {
 | 
			
		||||
          const [container, _] = this.visibleContainers.filter((c) => c.name == this.$route.query.name);
 | 
			
		||||
          if (container) {
 | 
			
		||||
            this.$router.push({ name: "container", params: { id: container.id } });
 | 
			
		||||
          } else {
 | 
			
		||||
            console.error(`No containers found matching name=${this.$route.query.name}. Redirecting to /`);
 | 
			
		||||
            this.$router.push({ name: "default" });
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(`Expection query parameter name to be set. Redirecting to /`);
 | 
			
		||||
          this.$router.push({ name: "default" });
 | 
			
		||||
        }
 | 
			
		||||
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", params: { id: container.id } });
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
 | 
			
		||||
        router.push({ name: "default" });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(`Expection query parameter name to be set. Redirecting to /`);
 | 
			
		||||
      router.push({ name: "default" });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								assets/shims-vue.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
declare module "*.vue" {
 | 
			
		||||
  import type { DefineComponent } from "vue";
 | 
			
		||||
  const component: DefineComponent<{}, {}, any>;
 | 
			
		||||
  export default component;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,120 +0,0 @@
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import Vuex from "vuex";
 | 
			
		||||
import storage from "store/dist/store.modern";
 | 
			
		||||
import { DEFAULT_SETTINGS, DOZZLE_SETTINGS_KEY } from "./settings";
 | 
			
		||||
import config from "./config";
 | 
			
		||||
 | 
			
		||||
Vue.use(Vuex);
 | 
			
		||||
 | 
			
		||||
const mql = window.matchMedia("(max-width: 770px)");
 | 
			
		||||
 | 
			
		||||
storage.set(DOZZLE_SETTINGS_KEY, { ...DEFAULT_SETTINGS, ...storage.get(DOZZLE_SETTINGS_KEY) });
 | 
			
		||||
 | 
			
		||||
const state = {
 | 
			
		||||
  containers: [],
 | 
			
		||||
  activeContainerIds: [],
 | 
			
		||||
  searchFilter: null,
 | 
			
		||||
  isMobile: mql.matches,
 | 
			
		||||
  settings: storage.get(DOZZLE_SETTINGS_KEY),
 | 
			
		||||
  authorizationNeeded: config.authorizationNeeded,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mutations = {
 | 
			
		||||
  SET_CONTAINERS(state, containers) {
 | 
			
		||||
    const containersById = getters.allContainersById({ containers });
 | 
			
		||||
 | 
			
		||||
    containers.forEach((container) => {
 | 
			
		||||
      container.stat =
 | 
			
		||||
        containersById[container.id] && containersById[container.id].stat
 | 
			
		||||
          ? containersById[container.id].stat
 | 
			
		||||
          : { memoryUsage: 0, cpu: 0 };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    state.containers = containers;
 | 
			
		||||
  },
 | 
			
		||||
  ADD_ACTIVE_CONTAINERS(state, { id }) {
 | 
			
		||||
    state.activeContainerIds.push(id);
 | 
			
		||||
  },
 | 
			
		||||
  REMOVE_ACTIVE_CONTAINER(state, { id }) {
 | 
			
		||||
    state.activeContainerIds.splice(state.activeContainerIds.indexOf(id), 1);
 | 
			
		||||
  },
 | 
			
		||||
  SET_SEARCH(state, filter) {
 | 
			
		||||
    state.searchFilter = filter;
 | 
			
		||||
  },
 | 
			
		||||
  SET_MOBILE_WIDTH(state, value) {
 | 
			
		||||
    state.isMobile = value;
 | 
			
		||||
  },
 | 
			
		||||
  UPDATE_SETTINGS(state, newValues) {
 | 
			
		||||
    state.settings = { ...state.settings, ...newValues };
 | 
			
		||||
    storage.set(DOZZLE_SETTINGS_KEY, state.settings);
 | 
			
		||||
  },
 | 
			
		||||
  UPDATE_CONTAINER(_, { container, data }) {
 | 
			
		||||
    for (const [key, value] of Object.entries(data)) {
 | 
			
		||||
      Vue.set(container, key, value);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const actions = {
 | 
			
		||||
  APPEND_ACTIVE_CONTAINER({ commit }, container) {
 | 
			
		||||
    commit("ADD_ACTIVE_CONTAINERS", container);
 | 
			
		||||
  },
 | 
			
		||||
  REMOVE_ACTIVE_CONTAINER({ commit }, container) {
 | 
			
		||||
    commit("REMOVE_ACTIVE_CONTAINER", container);
 | 
			
		||||
  },
 | 
			
		||||
  SET_SEARCH({ commit }, filter) {
 | 
			
		||||
    commit("SET_SEARCH", filter);
 | 
			
		||||
  },
 | 
			
		||||
  UPDATE_SETTING({ commit }, setting) {
 | 
			
		||||
    commit("UPDATE_SETTINGS", setting);
 | 
			
		||||
  },
 | 
			
		||||
  UPDATE_STATS({ commit, getters: { allContainersById } }, stat) {
 | 
			
		||||
    const container = allContainersById[stat.id];
 | 
			
		||||
    if (container) {
 | 
			
		||||
      commit("UPDATE_CONTAINER", { container, data: { stat } });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  UPDATE_CONTAINER({ commit, getters: { allContainersById } }, event) {
 | 
			
		||||
    switch (event.name) {
 | 
			
		||||
      case "die":
 | 
			
		||||
        const container = allContainersById[event.actorId];
 | 
			
		||||
        commit("UPDATE_CONTAINER", { container, data: { state: "exited" } });
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getters = {
 | 
			
		||||
  allContainersById({ containers }) {
 | 
			
		||||
    return containers.reduce((map, obj) => {
 | 
			
		||||
      map[obj.id] = obj;
 | 
			
		||||
      return map;
 | 
			
		||||
    }, {});
 | 
			
		||||
  },
 | 
			
		||||
  visibleContainers({ containers, settings: { showAllContainers } }) {
 | 
			
		||||
    const filter = showAllContainers ? () => true : (c) => c.state === "running";
 | 
			
		||||
    return containers.filter(filter);
 | 
			
		||||
  },
 | 
			
		||||
  activeContainers({ activeContainerIds }, { allContainersById }) {
 | 
			
		||||
    return activeContainerIds.map((id) => allContainersById[id]);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (!config.authorizationNeeded) {
 | 
			
		||||
  const es = new EventSource(`${config.base}/api/events/stream`);
 | 
			
		||||
  es.addEventListener("containers-changed", (e) => store.commit("SET_CONTAINERS", JSON.parse(e.data)), false);
 | 
			
		||||
  es.addEventListener("container-stat", (e) => store.dispatch("UPDATE_STATS", JSON.parse(e.data)), false);
 | 
			
		||||
  es.addEventListener("container-die", (e) => store.dispatch("UPDATE_CONTAINER", JSON.parse(e.data)), false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mql.addEventListener("change", (e) => store.commit("SET_MOBILE_WIDTH", e.matches));
 | 
			
		||||
 | 
			
		||||
const store = new Vuex.Store({
 | 
			
		||||
  state,
 | 
			
		||||
  getters,
 | 
			
		||||
  actions,
 | 
			
		||||
  mutations,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default store;
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
 | 
			
		||||
export const DEFAULT_SETTINGS = {
 | 
			
		||||
  search: true,
 | 
			
		||||
  size: "medium",
 | 
			
		||||
  menuWidth: 15,
 | 
			
		||||
  smallerScrollbars: false,
 | 
			
		||||
  showTimestamp: true,
 | 
			
		||||
  showAllContainers: false,
 | 
			
		||||
  lightTheme: false,
 | 
			
		||||
  hourStyle: "auto",
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
const config = JSON.parse(document.querySelector("script#config__json").textContent);
 | 
			
		||||
const text = document.querySelector("script#config__json")?.textContent || "{}";
 | 
			
		||||
 | 
			
		||||
const config = JSON.parse(text);
 | 
			
		||||
if (config.version == "{{ .Version }}") {
 | 
			
		||||
  config.version = "master";
 | 
			
		||||
  config.base = "";
 | 
			
		||||
@@ -9,5 +11,4 @@ if (config.version == "{{ .Version }}") {
 | 
			
		||||
  config.authorizationNeeded = config.authorizationNeeded === "true";
 | 
			
		||||
  config.secured = config.secured === "true";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										66
									
								
								assets/stores/container.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,66 @@
 | 
			
		||||
import { acceptHMRUpdate, defineStore } from "pinia";
 | 
			
		||||
import { ref, Ref, computed } from "vue";
 | 
			
		||||
 | 
			
		||||
import { showAllContainers } from "@/composables/settings";
 | 
			
		||||
import config from "@/stores/config";
 | 
			
		||||
import type { Container, ContainerStat } from "@/types/Container";
 | 
			
		||||
 | 
			
		||||
export const useContainerStore = defineStore("container", () => {
 | 
			
		||||
  const containers = ref<Container[]>([]);
 | 
			
		||||
  const activeContainerIds = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
  const allContainersById = computed(() =>
 | 
			
		||||
    containers.value.reduce((acc, container) => {
 | 
			
		||||
      acc[container.id] = container;
 | 
			
		||||
      return acc;
 | 
			
		||||
    }, {} as Record<string, Container>)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const visibleContainers = computed(() => {
 | 
			
		||||
    const filter = showAllContainers.value ? () => true : (c: Container) => c.state === "running";
 | 
			
		||||
    return containers.value.filter(filter);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
 | 
			
		||||
 | 
			
		||||
  const es = new EventSource(`${config.base}/api/events/stream`);
 | 
			
		||||
  es.addEventListener(
 | 
			
		||||
    "containers-changed",
 | 
			
		||||
    (e: Event) => (containers.value = JSON.parse((e as MessageEvent).data)),
 | 
			
		||||
    false
 | 
			
		||||
  );
 | 
			
		||||
  es.addEventListener(
 | 
			
		||||
    "container-stat",
 | 
			
		||||
    (e) => {
 | 
			
		||||
      const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
 | 
			
		||||
      const container = allContainersById.value[stat.id];
 | 
			
		||||
      if (container) {
 | 
			
		||||
        container.stat = stat;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    false
 | 
			
		||||
  );
 | 
			
		||||
  // es.addEventListener("container-die", (e) => store.dispatch("UPDATE_CONTAINER", JSON.parse(e.data)), false);
 | 
			
		||||
 | 
			
		||||
  const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
 | 
			
		||||
  const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
 | 
			
		||||
  const removeActiveContainer = ({ id }: Container) =>
 | 
			
		||||
    activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
 | 
			
		||||
    
 | 
			
		||||
  return {
 | 
			
		||||
    containers,
 | 
			
		||||
    activeContainerIds,
 | 
			
		||||
    allContainersById,
 | 
			
		||||
    visibleContainers,
 | 
			
		||||
    activeContainers,
 | 
			
		||||
    currentContainer,
 | 
			
		||||
    appendActiveContainer,
 | 
			
		||||
    removeActiveContainer,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
if (import.meta.hot) {
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
@charset "utf-8";
 | 
			
		||||
@import "~bulma/sass/utilities/initial-variables.sass";
 | 
			
		||||
 | 
			
		||||
@import "bulma/sass/utilities/initial-variables.sass";
 | 
			
		||||
 | 
			
		||||
$body-background-color: var(--body-background-color);
 | 
			
		||||
 | 
			
		||||
@@ -25,13 +24,18 @@ $panel-heading-color: var(--panel-heading-color);
 | 
			
		||||
$link: $turquoise;
 | 
			
		||||
$link-active: $grey-dark;
 | 
			
		||||
 | 
			
		||||
@import "~bulma";
 | 
			
		||||
@import "../node_modules/splitpanes/dist/splitpanes.css";
 | 
			
		||||
@import "~buefy/src/scss/utils/_all";
 | 
			
		||||
@import "~buefy/src/scss/components/_switch";
 | 
			
		||||
@import "~buefy/src/scss/components/_radio";
 | 
			
		||||
@import "~buefy/src/scss/components/_modal";
 | 
			
		||||
@import "~buefy/src/scss/components/_autocomplete";
 | 
			
		||||
$dark-toolbar-color: rgba($black-bis, 0.7);
 | 
			
		||||
$light-toolbar-color: rgba($grey-darker, 0.7);
 | 
			
		||||
 | 
			
		||||
@import "bulma/bulma.sass";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/utils/all.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/autocomplete.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/tooltip.scss";
 | 
			
		||||
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
 | 
			
		||||
@import "splitpanes/dist/splitpanes.css";
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
  --scheme-main: #{$black};
 | 
			
		||||
@@ -46,6 +50,7 @@ html {
 | 
			
		||||
  --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%);
 | 
			
		||||
@@ -72,6 +77,7 @@ html {
 | 
			
		||||
  --secondary-color: #d8f0ca;
 | 
			
		||||
 | 
			
		||||
  --body-background-color: #{$white-bis};
 | 
			
		||||
  --action-toolbar-background-color: #{$light-toolbar-color};
 | 
			
		||||
  --body-color: #{$grey-darker};
 | 
			
		||||
 | 
			
		||||
  --menu-item-color: #{$grey-dark};
 | 
			
		||||
@@ -121,22 +127,6 @@ html.has-custom-scrollbars {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-settings-control {
 | 
			
		||||
  background: rgba(0, 0, 0, 0.4);
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  border-color: transparent;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    border-color: var(--border-hover-color) !important;
 | 
			
		||||
    background: rgba(0, 0, 0, 0.8) !important;
 | 
			
		||||
    color: #fff !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:focus {
 | 
			
		||||
    box-shadow: none !important;
 | 
			
		||||
    color: unset;
 | 
			
		||||
    border-color: transparent;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media screen and (min-width: 770px) {
 | 
			
		||||
  .splitpanes__pane {
 | 
			
		||||
    overflow: unset;
 | 
			
		||||
@@ -156,3 +146,7 @@ html.has-custom-scrollbars {
 | 
			
		||||
.modal {
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button .button-wrapper > span {
 | 
			
		||||
  display: contents;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								assets/types/Container.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
export interface Container {
 | 
			
		||||
  readonly id: string;
 | 
			
		||||
  readonly created: number;
 | 
			
		||||
  readonly image: string;
 | 
			
		||||
  readonly name: string;
 | 
			
		||||
  readonly state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
 | 
			
		||||
  readonly status: string;
 | 
			
		||||
  stat?: ContainerStat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ContainerStat {
 | 
			
		||||
  readonly id: string;
 | 
			
		||||
  readonly cpu: number;
 | 
			
		||||
  readonly memory: number;
 | 
			
		||||
  readonly memoryUsage: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								assets/types/LogEntry.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
export interface LogEntry {
 | 
			
		||||
  date: Date;
 | 
			
		||||
  message: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  event?: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								e2e/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
videos
 | 
			
		||||
screenshots
 | 
			
		||||
__diff_output__
 | 
			
		||||
							
								
								
									
										12
									
								
								e2e/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
FROM cypress/included:9.0.0
 | 
			
		||||
 | 
			
		||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
 | 
			
		||||
 | 
			
		||||
WORKDIR /e2e
 | 
			
		||||
 | 
			
		||||
COPY pnpm-lock.yaml ./
 | 
			
		||||
RUN pnpm fetch
 | 
			
		||||
 | 
			
		||||
COPY package.json ./
 | 
			
		||||
RUN pnpm install -r --offline
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								e2e/cypress.env.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "DOZZLE_DEFAULT": "http://localhost:3000/"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								e2e/cypress.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "fixturesFolder": false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								e2e/cypress/integration/dozze_dark.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("home screen", () => {
 | 
			
		||||
    cy.get("li.running", { timeout: 10000 }).removeDates().matchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("correct title", () => {
 | 
			
		||||
    cy.title().should("eq", "1 containers - Dozzle");
 | 
			
		||||
 | 
			
		||||
    cy.get("li.running:first a").click();
 | 
			
		||||
 | 
			
		||||
    cy.title().should("include", "- Dozzle");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("settings page", () => {
 | 
			
		||||
    cy.get("a[href='/settings']").click();
 | 
			
		||||
 | 
			
		||||
    cy.contains("About");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										20
									
								
								e2e/cypress/integration/dozze_settings.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle settings mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.visit("/version").clearLocalStorage().visit("/settings");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("scrollbars", () => {
 | 
			
		||||
    cy.contains("Use smaller scrollbars").click();
 | 
			
		||||
    cy.get("html").should("have.class", "has-custom-scrollbars");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("stopped containers", () => {
 | 
			
		||||
    cy.contains("Show stopped containers")
 | 
			
		||||
      .click()
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        expect(JSON.parse(localStorage.getItem("DOZZLE_SETTINGS")).showAllContainers).to.be.true;
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										15
									
								
								e2e/cypress/integration/dozzle_light.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								e2e/cypress/integration/dozzle_routes.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
context("Dozzle routes", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
 | 
			
		||||
  it("show", () => {
 | 
			
		||||
    cy.visit("/show?name=dozzle").url().should("include", "/container/");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										26
									
								
								e2e/cypress/plugins/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
/// <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);
 | 
			
		||||
};
 | 
			
		||||
| 
		 After Width: | Height: | Size: 30 KiB  | 
| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										33
									
								
								e2e/cypress/support/commands.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
			
		||||
// ***********************************************
 | 
			
		||||
// This example commands.js shows you how to
 | 
			
		||||
// create various custom commands and overwrite
 | 
			
		||||
// existing commands.
 | 
			
		||||
//
 | 
			
		||||
// For more comprehensive examples of custom
 | 
			
		||||
// commands please read more here:
 | 
			
		||||
// https://on.cypress.io/custom-commands
 | 
			
		||||
// ***********************************************
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// -- This is a parent command --
 | 
			
		||||
// Cypress.Commands.add('login', (email, password) => { ... })
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// -- This is a child command --
 | 
			
		||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// -- This is a dual command --
 | 
			
		||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// -- This will overwrite an existing command --
 | 
			
		||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
 | 
			
		||||
 | 
			
		||||
import { addMatchImageSnapshotCommand } from "cypress-image-snapshot/command";
 | 
			
		||||
 | 
			
		||||
addMatchImageSnapshotCommand();
 | 
			
		||||
 | 
			
		||||
Cypress.Commands.add("removeDates", () => {
 | 
			
		||||
  cy.window().then((win) => win.document.querySelectorAll("time").forEach((el) => el.remove()));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										20
									
								
								e2e/cypress/support/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
// ***********************************************************
 | 
			
		||||
// This example support/index.js is processed and
 | 
			
		||||
// loaded automatically before your test files.
 | 
			
		||||
//
 | 
			
		||||
// This is a great place to put global configuration and
 | 
			
		||||
// behavior that modifies Cypress.
 | 
			
		||||
//
 | 
			
		||||
// You can change the location of this file or turn off
 | 
			
		||||
// automatically serving support files with the
 | 
			
		||||
// 'supportFile' configuration option.
 | 
			
		||||
//
 | 
			
		||||
// You can read more here:
 | 
			
		||||
// https://on.cypress.io/configuration
 | 
			
		||||
// ***********************************************************
 | 
			
		||||
 | 
			
		||||
// Import commands.js using ES2015 syntax:
 | 
			
		||||
import "./commands";
 | 
			
		||||
 | 
			
		||||
// Alternatively you can use CommonJS syntax:
 | 
			
		||||
// require('./commands')
 | 
			
		||||
@@ -8,6 +8,8 @@ services:
 | 
			
		||||
      - DOZZLE_FILTER=name=custom_base
 | 
			
		||||
      - DOZZLE_BASE=/foobarbase
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8080:8080"
 | 
			
		||||
    build:
 | 
			
		||||
      context: ..
 | 
			
		||||
  dozzle:
 | 
			
		||||
@@ -17,18 +19,20 @@ services:
 | 
			
		||||
    environment:
 | 
			
		||||
      - DOZZLE_FILTER=name=dozzle
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "9090:8080"
 | 
			
		||||
    build:
 | 
			
		||||
      context: ..
 | 
			
		||||
  integration:
 | 
			
		||||
  cypress:
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
    command: yarn test
 | 
			
		||||
    working_dir: /e2e
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./__tests__:/app/__tests__
 | 
			
		||||
      - ./cypress:/e2e/cypress
 | 
			
		||||
      - ./cypress.json:/e2e/cypress.json
 | 
			
		||||
    environment:
 | 
			
		||||
      - DEFAULT_URL=http://dozzle:8080/
 | 
			
		||||
      - CUSTOM_URL=http://custom_base:8080/foobarbase
 | 
			
		||||
      - DOZZLE_NO_ANALYTICS=1
 | 
			
		||||
      - CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
 | 
			
		||||
      - CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - dozzle
 | 
			
		||||
      - custom_base
 | 
			
		||||
							
								
								
									
										10
									
								
								e2e/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "e2e",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "scripts": {},
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "cypress": "^9.0.0",
 | 
			
		||||
    "cypress-image-snapshot": "^4.0.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1447
									
								
								e2e/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						@@ -1,12 +1,12 @@
 | 
			
		||||
module github.com/amir20/dozzle
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/Microsoft/go-winio v0.5.0 // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.5.1 // indirect
 | 
			
		||||
	github.com/alexflint/go-arg v1.4.2
 | 
			
		||||
	github.com/beme/abide v0.0.0-20190723115211-635a09831760
 | 
			
		||||
	github.com/containerd/containerd v1.5.5 // indirect
 | 
			
		||||
	github.com/containerd/containerd v1.5.7 // indirect
 | 
			
		||||
	github.com/docker/distribution v2.7.1+incompatible // indirect
 | 
			
		||||
	github.com/docker/docker v20.10.8+incompatible
 | 
			
		||||
	github.com/docker/docker v20.10.11+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
 | 
			
		||||
@@ -23,10 +23,10 @@ require (
 | 
			
		||||
	github.com/spf13/afero v1.6.0
 | 
			
		||||
	github.com/stretchr/objx v0.3.0 // indirect
 | 
			
		||||
	github.com/stretchr/testify v1.7.0
 | 
			
		||||
	golang.org/x/net v0.0.0-20210420072503-d25e30425868 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
 | 
			
		||||
	google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.40.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
 | 
			
		||||
	google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.42.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						@@ -45,8 +45,8 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
 | 
			
		||||
@@ -54,7 +54,7 @@ github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg3
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.18/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
 | 
			
		||||
github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
 | 
			
		||||
github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
 | 
			
		||||
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
 | 
			
		||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 | 
			
		||||
@@ -109,7 +109,11 @@ github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
 | 
			
		||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
			
		||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 | 
			
		||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 | 
			
		||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 | 
			
		||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
			
		||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
			
		||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
			
		||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 | 
			
		||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 | 
			
		||||
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
 | 
			
		||||
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
 | 
			
		||||
@@ -143,8 +147,8 @@ github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo
 | 
			
		||||
github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
 | 
			
		||||
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
 | 
			
		||||
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
 | 
			
		||||
github.com/containerd/containerd v1.5.5 h1:q1gxsZsGZ8ddVe98yO6pR21b5xQSMiR61lD0W96pgQo=
 | 
			
		||||
github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo=
 | 
			
		||||
github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM=
 | 
			
		||||
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 | 
			
		||||
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 | 
			
		||||
@@ -230,8 +234,8 @@ github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TT
 | 
			
		||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
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.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM=
 | 
			
		||||
github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 | 
			
		||||
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
 | 
			
		||||
github.com/docker/docker v20.10.11+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-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
 | 
			
		||||
@@ -254,6 +258,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 | 
			
		||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 | 
			
		||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
			
		||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 | 
			
		||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 | 
			
		||||
@@ -385,6 +390,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
 | 
			
		||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 | 
			
		||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 | 
			
		||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
			
		||||
@@ -482,7 +488,7 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P
 | 
			
		||||
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 | 
			
		||||
github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 | 
			
		||||
github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
 | 
			
		||||
github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
 | 
			
		||||
github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
 | 
			
		||||
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 | 
			
		||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 | 
			
		||||
github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 | 
			
		||||
@@ -613,7 +619,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
 | 
			
		||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 | 
			
		||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 | 
			
		||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 | 
			
		||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
 | 
			
		||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
 | 
			
		||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
 | 
			
		||||
@@ -667,7 +672,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
 | 
			
		||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
 | 
			
		||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 | 
			
		||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 | 
			
		||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 | 
			
		||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 | 
			
		||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 | 
			
		||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 | 
			
		||||
@@ -676,7 +680,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 | 
			
		||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 | 
			
		||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
@@ -713,8 +716,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
 | 
			
		||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
			
		||||
golang.org/x/net v0.0.0-20210420072503-d25e30425868 h1:mHVdVrNGft0Bv5N0WIf3/ujpDOQOe6KxvwlIikPbMr0=
 | 
			
		||||
golang.org/x/net v0.0.0-20210420072503-d25e30425868/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 | 
			
		||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963 h1:8gJUadZl+kWvZBqG/LautX0X6qe5qTC2VI/3V3NBRAY=
 | 
			
		||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
@@ -729,7 +732,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@@ -791,9 +793,11 @@ golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
 | 
			
		||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
@@ -847,7 +851,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
 | 
			
		||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 | 
			
		||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
@@ -893,8 +896,8 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 h1:3V2dxSZpz4zozWWUq36vUxXEKnSYitEH2LdsAx+RUmg=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247 h1:ZONpjmFT5e+I/0/xE3XXbG5OIvX2hRYzol04MhKBl2E=
 | 
			
		||||
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 | 
			
		||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 | 
			
		||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
			
		||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 | 
			
		||||
@@ -911,8 +914,9 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
 | 
			
		||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 | 
			
		||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 | 
			
		||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 | 
			
		||||
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
 | 
			
		||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 | 
			
		||||
google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
 | 
			
		||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
			
		||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
node_modules
 | 
			
		||||
							
								
								
									
										1
									
								
								integration/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1 +0,0 @@
 | 
			
		||||
__diff_output__
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
FROM amir20/docker-alpine-puppeteer:v1
 | 
			
		||||
 | 
			
		||||
COPY package*.json yarn.lock /app/
 | 
			
		||||
RUN yarn
 | 
			
		||||
 | 
			
		||||
COPY . /app/
 | 
			
		||||
 | 
			
		||||
CMD ["yarn", "test"]
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 38 KiB  | 
| 
		 Before Width: | Height: | Size: 68 KiB  | 
| 
		 Before Width: | Height: | Size: 79 KiB  | 
| 
		 Before Width: | Height: | Size: 39 KiB  | 
| 
		 Before Width: | Height: | Size: 70 KiB  | 
| 
		 Before Width: | Height: | Size: 81 KiB  | 
@@ -1,22 +0,0 @@
 | 
			
		||||
const { removeTimes } = require("../utils");
 | 
			
		||||
const { CUSTOM_URL: URL } = process.env;
 | 
			
		||||
 | 
			
		||||
describe("Dozzle with custom base", () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await page.goto(URL, { waitUntil: "networkidle2" });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders full page on desktop", async () => {
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot({ fullPage: true });
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("and shows one container with correct title", async () => {
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
 | 
			
		||||
 | 
			
		||||
    expect(menuTitle).toEqual("custom_base");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,76 +0,0 @@
 | 
			
		||||
const puppeteer = require("puppeteer");
 | 
			
		||||
const { removeTimes } = require("../utils");
 | 
			
		||||
const iPhoneX = puppeteer.devices["iPhone X"];
 | 
			
		||||
const iPadLandscape = puppeteer.devices["iPad landscape"];
 | 
			
		||||
 | 
			
		||||
const { DEFAULT_URL: URL } = process.env;
 | 
			
		||||
 | 
			
		||||
describe("home page", () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await page.goto(URL, { waitUntil: "networkidle2" });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders full page on desktop", async () => {
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot({ fullPage: true });
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders ipad viewport", async () => {
 | 
			
		||||
    await page.emulate(iPadLandscape);
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot();
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders iphone viewport", async () => {
 | 
			
		||||
    await page.emulate(iPhoneX);
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot();
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("displays iphone menu", async () => {
 | 
			
		||||
    await page.emulate(iPhoneX);
 | 
			
		||||
    await page.click("a.navbar-burger");
 | 
			
		||||
 | 
			
		||||
    const menuText = await page.$eval("aside ul.menu-list.is-hidden-mobile li a", (e) => e.textContent);
 | 
			
		||||
    expect(menuText.trim()).toEqual("dozzle");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("has menu visible", () => {
 | 
			
		||||
    beforeAll(async () => {
 | 
			
		||||
      await jestPuppeteer.resetBrowser();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      await page.goto(URL, { waitUntil: "networkidle2" });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("and shows one container with correct title", async () => {
 | 
			
		||||
      const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
 | 
			
		||||
 | 
			
		||||
      expect(menuTitle).toEqual("dozzle");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("and menu is clickable", async () => {
 | 
			
		||||
      await page.click("aside ul.menu-list li a");
 | 
			
		||||
 | 
			
		||||
      const className = await page.$eval("aside ul.menu-list li a", (e) => e.className);
 | 
			
		||||
 | 
			
		||||
      expect(className).toContain("router-link-exact-active");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("and when clicked shows logs", async () => {
 | 
			
		||||
      await page.click("aside ul.menu-list li a");
 | 
			
		||||
 | 
			
		||||
      await page.waitForSelector("ul.events li span.text");
 | 
			
		||||
      const text = await page.$eval("ul.events li:nth-child(1) span.text", (e) => e.textContent);
 | 
			
		||||
 | 
			
		||||
      expect(text).toContain("Dozzle version dev");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
const puppeteer = require("puppeteer");
 | 
			
		||||
const { removeTimes } = require("../utils");
 | 
			
		||||
const iPhoneX = puppeteer.devices["iPhone X"];
 | 
			
		||||
const iPadLandscape = puppeteer.devices["iPad landscape"];
 | 
			
		||||
 | 
			
		||||
const { DEFAULT_URL: URL } = process.env;
 | 
			
		||||
 | 
			
		||||
describe("Dozzle with light mode", () => {
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    await page.goto(URL + "/settings", { waitUntil: "networkidle2" });
 | 
			
		||||
    await page.$$eval("label.switch", (elements) => {
 | 
			
		||||
      elements.filter((e) => e.textContent.trim() === "Use light theme")[0].click();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await page.goto(URL, { waitUntil: "networkidle2" });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders full page on desktop", async () => {
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot({ fullPage: true });
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders ipad viewport", async () => {
 | 
			
		||||
    await page.emulate(iPadLandscape);
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot();
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("renders iphone viewport", async () => {
 | 
			
		||||
    await page.emulate(iPhoneX);
 | 
			
		||||
    await removeTimes(page);
 | 
			
		||||
    const image = await page.screenshot();
 | 
			
		||||
 | 
			
		||||
    expect(image).toMatchImageSnapshot();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  launch: {
 | 
			
		||||
    headless: process.env.HEADLESS !== "false",
 | 
			
		||||
    defaultViewport: { width: 1920, height: 1200 },
 | 
			
		||||
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
 | 
			
		||||
    executablePath: process.env.CHROME_EXE_PATH || "",
 | 
			
		||||
  },
 | 
			
		||||
  browserContext: "incognito",
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
const { toMatchImageSnapshot } = require("jest-image-snapshot");
 | 
			
		||||
 | 
			
		||||
expect.extend({ toMatchImageSnapshot });
 | 
			
		||||
 | 
			
		||||
jest.setTimeout(5000);
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
 "name": "test",
 | 
			
		||||
 "version": "1.0.0",
 | 
			
		||||
 "description": "",
 | 
			
		||||
 "scripts": {
 | 
			
		||||
  "test": "jest"
 | 
			
		||||
 },
 | 
			
		||||
 "author": "",
 | 
			
		||||
 "license": "ISC",
 | 
			
		||||
 "dependencies": {
 | 
			
		||||
  "jest": "^27.0.6",
 | 
			
		||||
  "jest-image-snapshot": "^4.0.0",
 | 
			
		||||
  "puppeteer": "^10.4.0"
 | 
			
		||||
 },
 | 
			
		||||
 "jest": {
 | 
			
		||||
  "preset": "jest-puppeteer",
 | 
			
		||||
  "setupFilesAfterEnv": [
 | 
			
		||||
   "<rootDir>/jest-setup.js"
 | 
			
		||||
  ]
 | 
			
		||||
 },
 | 
			
		||||
 "devDependencies": {
 | 
			
		||||
  "jest-puppeteer": "^6.0.0"
 | 
			
		||||
 }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
async function removeTimes(page) {
 | 
			
		||||
  await page.waitForSelector("time");
 | 
			
		||||
  await page.evaluate(() => {
 | 
			
		||||
    (document.querySelectorAll("time") || []).forEach((el) => el.remove());
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = { removeTimes };
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  clearMocks: true,
 | 
			
		||||
  testEnvironment: "jsdom",
 | 
			
		||||
  moduleFileExtensions: ["js", "json", "vue"],
 | 
			
		||||
  coveragePathIgnorePatterns: ["node_modules"],
 | 
			
		||||
  testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/"],
 | 
			
		||||
  transformIgnorePatterns: ["node_modules"],
 | 
			
		||||
  watchPathIgnorePatterns: ["<rootDir>/node_modules/"],
 | 
			
		||||
  snapshotSerializers: ["jest-serializer-vue"],
 | 
			
		||||
  transform: {
 | 
			
		||||
    ".*\\.vue$": "vue-jest",
 | 
			
		||||
    "^.+\\.js$": "babel-jest",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										15
									
								
								jest.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
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;
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "include": ["./assets/**/*"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								main.go
									
									
									
									
									
								
							
							
						
						@@ -41,7 +41,7 @@ func (args) Version() string {
 | 
			
		||||
	return version
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//go:embed static
 | 
			
		||||
//go:embed dist
 | 
			
		||||
var content embed.FS
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
@@ -95,17 +95,17 @@ func main() {
 | 
			
		||||
		Password: args.Password,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static, err := fs.Sub(content, "static")
 | 
			
		||||
	assets, err := fs.Sub(content, "dist")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Could not open embedded static folder: %v", err)
 | 
			
		||||
		log.Fatalf("Could not open embedded dist folder: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, ok := os.LookupEnv("LIVE_FS"); ok {
 | 
			
		||||
		log.Info("Using live filesystem at ./static")
 | 
			
		||||
		static = os.DirFS("./static")
 | 
			
		||||
		log.Info("Using live filesystem at ./dist")
 | 
			
		||||
		assets = os.DirFS("./dist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	srv := web.CreateServer(dockerClient, static, config)
 | 
			
		||||
	srv := web.CreateServer(dockerClient, assets, config)
 | 
			
		||||
	go doStartEvent(args)
 | 
			
		||||
	go func() {
 | 
			
		||||
		log.Infof("Accepting connections on %s", srv.Addr)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,96 +1,89 @@
 | 
			
		||||
{
 | 
			
		||||
 "name": "dozzle",
 | 
			
		||||
 "version": "3.8.1",
 | 
			
		||||
 "description": "Realtime log viewer for docker containers. ",
 | 
			
		||||
 "scripts": {
 | 
			
		||||
  "watch": "npm-run-all -p watch:*",
 | 
			
		||||
  "watch:assets": "webpack --mode=development --watch",
 | 
			
		||||
  "watch:server": "LIVE_FS=true reflex -c .reflex",
 | 
			
		||||
  "predev": "make fake_static",
 | 
			
		||||
  "dev": "npm-run-all -p dev-server watch:server",
 | 
			
		||||
  "dev-server": "webpack serve --mode=development",
 | 
			
		||||
  "prebuild": "yarn clean",
 | 
			
		||||
  "build": "yarn webpack --mode=production",
 | 
			
		||||
  "clean": "rm -rf static",
 | 
			
		||||
  "release": "release-it",
 | 
			
		||||
  "test": "TZ=UTC jest",
 | 
			
		||||
  "postinstall": "husky install"
 | 
			
		||||
 },
 | 
			
		||||
 "repository": {
 | 
			
		||||
  "type": "git",
 | 
			
		||||
  "url": "git+https://github.com/amir20/dozzle.git"
 | 
			
		||||
 },
 | 
			
		||||
 "author": "",
 | 
			
		||||
 "license": "ISC",
 | 
			
		||||
 "bugs": {
 | 
			
		||||
  "url": "https://github.com/amir20/dozzle/issues"
 | 
			
		||||
 },
 | 
			
		||||
 "homepage": "https://github.com/amir20/dozzle#readme",
 | 
			
		||||
 "dependencies": {
 | 
			
		||||
  "ansi-to-html": "^0.7.1",
 | 
			
		||||
  "buefy": "^0.9.10",
 | 
			
		||||
  "bulma": "^0.9.3",
 | 
			
		||||
  "date-fns": "^2.24.0",
 | 
			
		||||
  "dompurify": "^2.3.3",
 | 
			
		||||
  "fuzzysort": "^1.1.4",
 | 
			
		||||
  "hotkeys-js": "^3.8.7",
 | 
			
		||||
  "lodash.debounce": "^4.0.8",
 | 
			
		||||
  "lodash.throttle": "^4.1.1",
 | 
			
		||||
  "semver": "^7.3.5",
 | 
			
		||||
  "splitpanes": "^2.3.8",
 | 
			
		||||
  "store": "^2.0.12",
 | 
			
		||||
  "vue": "^2.6.14",
 | 
			
		||||
  "vue-meta": "^2.4.0",
 | 
			
		||||
  "vue-router": "^3.5.2",
 | 
			
		||||
  "vuex": "^3.6.2"
 | 
			
		||||
 },
 | 
			
		||||
 "devDependencies": {
 | 
			
		||||
  "@babel/core": "^7.15.5",
 | 
			
		||||
  "@babel/plugin-transform-runtime": "^7.15.0",
 | 
			
		||||
  "@vue/component-compiler-utils": "^3.2.2",
 | 
			
		||||
  "@vue/test-utils": "^1.2.2",
 | 
			
		||||
  "autoprefixer": "^10.3.6",
 | 
			
		||||
  "babel-core": "^7.0.0-bridge.0",
 | 
			
		||||
  "babel-jest": "^27.2.4",
 | 
			
		||||
  "babel-preset-env": "^1.7.0",
 | 
			
		||||
  "caniuse-lite": "^1.0.30001264",
 | 
			
		||||
  "css-loader": "^6.3.0",
 | 
			
		||||
  "eventsourcemock": "^2.0.0",
 | 
			
		||||
  "html-webpack-plugin": "^5.3.2",
 | 
			
		||||
  "husky": "^7.0.2",
 | 
			
		||||
  "jest": "^27.2.4",
 | 
			
		||||
  "jest-serializer-vue": "^2.0.2",
 | 
			
		||||
  "lint-staged": "^11.1.2",
 | 
			
		||||
  "mini-css-extract-plugin": "^2.3.0",
 | 
			
		||||
  "npm-run-all": "^4.1.5",
 | 
			
		||||
  "postcss": "^8.3.8",
 | 
			
		||||
  "postcss-loader": "^6.1.1",
 | 
			
		||||
  "prettier": "^2.4.1",
 | 
			
		||||
  "release-it": "^14.11.6",
 | 
			
		||||
  "sass": "^1.42.1",
 | 
			
		||||
  "sass-loader": "^12.1.0",
 | 
			
		||||
  "vue-hot-reload-api": "^2.3.4",
 | 
			
		||||
  "vue-jest": "^3.0.7",
 | 
			
		||||
  "vue-loader": "^15.9.8",
 | 
			
		||||
  "vue-style-loader": "^4.1.3",
 | 
			
		||||
  "vue-template-compiler": "^2.6.14",
 | 
			
		||||
  "webpack": "^5.56.1",
 | 
			
		||||
  "webpack-cli": "^4.8.0",
 | 
			
		||||
  "webpack-dev-server": "^4.3.0",
 | 
			
		||||
  "webpack-pwa-manifest": "^4.3.0"
 | 
			
		||||
 },
 | 
			
		||||
 "lint-staged": {
 | 
			
		||||
  "*.{js,vue,css}": [
 | 
			
		||||
   "prettier --write"
 | 
			
		||||
  ]
 | 
			
		||||
 },
 | 
			
		||||
 "release-it": {
 | 
			
		||||
  "github": {
 | 
			
		||||
   "release": false,
 | 
			
		||||
   "releaseNotes": "git log --pretty=format:\"* %s (%h)\" $(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))...HEAD~1"
 | 
			
		||||
  "name": "dozzle",
 | 
			
		||||
  "version": "3.10.0",
 | 
			
		||||
  "description": "Realtime log viewer for docker containers. ",
 | 
			
		||||
  "homepage": "https://github.com/amir20/dozzle#readme",
 | 
			
		||||
  "bugs": {
 | 
			
		||||
    "url": "https://github.com/amir20/dozzle/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "npm": {
 | 
			
		||||
   "publish": false
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "git+https://github.com/amir20/dozzle.git"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "author": "Amir Raminfar <findamir@gmail.com>",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "watch:assets": "vite --open",
 | 
			
		||||
    "watch:server": "LIVE_FS=true DOZZLE_ADDR=:3100 reflex -c .reflex",
 | 
			
		||||
    "dev": "make fake_assets && npm-run-all -p watch:assets watch:server",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "release": "release-it",
 | 
			
		||||
    "test": "TZ=UTC jest",
 | 
			
		||||
    "postinstall": "husky install"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@iconify-json/carbon": "^1.0.11",
 | 
			
		||||
    "@iconify-json/cil": "^1.0.1",
 | 
			
		||||
    "@iconify-json/mdi": "^1.0.11",
 | 
			
		||||
    "@iconify-json/mdi-light": "^1.0.1",
 | 
			
		||||
    "@iconify-json/octicon": "^1.0.5",
 | 
			
		||||
    "@oruga-ui/oruga-next": "^0.4.8",
 | 
			
		||||
    "@oruga-ui/theme-bulma": "^0.1.5",
 | 
			
		||||
    "@vitejs/plugin-vue": "^1.10.1",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.2.23",
 | 
			
		||||
    "@vueuse/core": "^7.1.2",
 | 
			
		||||
    "ansi-to-html": "^0.7.2",
 | 
			
		||||
    "bulma": "^0.9.3",
 | 
			
		||||
    "date-fns": "^2.26.0",
 | 
			
		||||
    "fuzzysort": "^1.1.4",
 | 
			
		||||
    "hotkeys-js": "^3.8.7",
 | 
			
		||||
    "lodash.debounce": "^4.0.8",
 | 
			
		||||
    "lodash.throttle": "^4.1.1",
 | 
			
		||||
    "pinia": "^2.0.4",
 | 
			
		||||
    "sass": "^1.43.5",
 | 
			
		||||
    "semver": "^7.3.5",
 | 
			
		||||
    "splitpanes": "^3.0.6",
 | 
			
		||||
    "typescript": "^4.5.2",
 | 
			
		||||
    "unplugin-auto-import": "^0.5.1",
 | 
			
		||||
    "unplugin-icons": "^0.12.20",
 | 
			
		||||
    "unplugin-vue-components": "^0.17.2",
 | 
			
		||||
    "vite": "^2.6.14",
 | 
			
		||||
    "vue": "^3.2.22",
 | 
			
		||||
    "vue-router": "^4.0.12"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/plugin-transform-runtime": "^7.16.4",
 | 
			
		||||
    "@babel/preset-env": "^7.16.4",
 | 
			
		||||
    "@pinia/testing": "^0.0.6",
 | 
			
		||||
    "@types/jest": "^27.0.3",
 | 
			
		||||
    "@types/lodash.debounce": "^4.0.6",
 | 
			
		||||
    "@types/lodash.throttle": "^4.1.6",
 | 
			
		||||
    "@types/semver": "^7.3.9",
 | 
			
		||||
    "@vue/test-utils": "^2.0.0-rc.16",
 | 
			
		||||
    "@vue/vue3-jest": "^27.0.0-alpha.4",
 | 
			
		||||
    "eventsourcemock": "^2.0.0",
 | 
			
		||||
    "husky": "^7.0.4",
 | 
			
		||||
    "jest": "^27.4.0",
 | 
			
		||||
    "jest-serializer-vue": "^2.0.2",
 | 
			
		||||
    "lint-staged": "^12.1.2",
 | 
			
		||||
    "npm-run-all": "^4.1.5",
 | 
			
		||||
    "prettier": "^2.5.0",
 | 
			
		||||
    "release-it": "^14.11.8",
 | 
			
		||||
    "ts-jest": "^27.0.7",
 | 
			
		||||
    "ts-node": "^10.4.0"
 | 
			
		||||
  },
 | 
			
		||||
  "lint-staged": {
 | 
			
		||||
    "*.{js,vue,css}": [
 | 
			
		||||
      "prettier --write"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "release-it": {
 | 
			
		||||
    "github": {
 | 
			
		||||
      "release": false,
 | 
			
		||||
      "releaseNotes": "git log --pretty=format:\"* %s (%h)\" $(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))...HEAD~1"
 | 
			
		||||
    },
 | 
			
		||||
    "npm": {
 | 
			
		||||
      "publish": false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||