Compare commits

...

89 Commits

Author SHA1 Message Date
kodiakhq[bot]
20425bf6b1 Merge pull request #1913 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.2.0
Bump docker/setup-buildx-action from 2.1.0 to 2.2.0
2022-10-18 09:17:58 +00:00
dependabot[bot]
828c288570 Bump docker/setup-buildx-action from 2.1.0 to 2.2.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-18 09:15:02 +00:00
Amir Raminfar
cde2589755 Fixes iOS font size changing 2022-10-17 10:48:11 -07:00
kodiakhq[bot]
5fb2f452e2 Merge pull request #1911 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.4
Bump pnpm/action-setup from 2.2.3 to 2.2.4
2022-10-17 09:16:40 +00:00
dependabot[bot]
6aea252d3e Bump pnpm/action-setup from 2.2.3 to 2.2.4
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 09:13:39 +00:00
Amir Raminfar
7337dcb5d4 Updates modules 2022-10-14 14:57:46 -07:00
Amir Raminfar
deeb5fc100 Release 4.2.0 2022-10-14 13:23:59 -07:00
Amir Raminfar
5b15a0b29d stat cleanup (#1910)
* Updates modules

* Cleans up stats
2022-10-14 13:00:57 -07:00
kodiakhq[bot]
e2ad2e0193 Merge pull request #1909 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.19incompatible
Bump github.com/docker/docker from 20.10.18+incompatible to 20.10.19+incompatible
2022-10-14 09:11:40 +00:00
dependabot[bot]
5b48426fc1 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.18+incompatible to 20.10.19+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.18...v20.10.19)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-14 09:08:05 +00:00
kodiakhq[bot]
a24947ab3b Merge pull request #1908 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.1.0
Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
2022-10-13 09:19:44 +00:00
dependabot[bot]
8926b451d0 Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-13 09:16:51 +00:00
Amir Raminfar
afd37d3455 Add trending CPU and Memory usage (#1896)
* Refactors to stat history

* Uses markRaw

* Cleans up proxies

* Removes id from snapshots

* Adds d3

* Adds more d3 modules

* Fixes package

* Cleans up packages

* Updates modules

* Adds initital d3 chart

* Cleans up svg

* Fixes @types/d3-array

* Adds memory

* Moves charts around
2022-10-12 14:21:41 -07:00
kodiakhq[bot]
4f84beb835 Merge pull request #1906 from amir20/dependabot/docker/e2e/cypress/included-10.10.0
Bump cypress/included from 10.9.0 to 10.10.0 in /e2e
2022-10-12 10:43:39 +00:00
dependabot[bot]
d54d894a66 Bump cypress/included from 10.9.0 to 10.10.0 in /e2e
Bumps cypress/included from 10.9.0 to 10.10.0.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 10:39:36 +00:00
kodiakhq[bot]
7c854d31a7 Merge pull request #1905 from amir20/dependabot/github_actions/docker/login-action-2.1.0
Bump docker/login-action from 2.0.0 to 2.1.0
2022-10-12 09:33:27 +00:00
kodiakhq[bot]
4c2caad4a0 Merge pull request #1904 from amir20/dependabot/github_actions/docker/build-push-action-3.2.0
Bump docker/build-push-action from 3.1.1 to 3.2.0
2022-10-12 09:33:14 +00:00
dependabot[bot]
9684fd978b Bump docker/login-action from 2.0.0 to 2.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 09:29:20 +00:00
dependabot[bot]
a79a3f680f Bump docker/build-push-action from 3.1.1 to 3.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 09:29:17 +00:00
kodiakhq[bot]
1e18423b04 Merge pull request #1903 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.3
Bump pnpm/action-setup from 2.2.2 to 2.2.3
2022-10-11 09:14:13 +00:00
dependabot[bot]
82b208c0bf Bump pnpm/action-setup from 2.2.2 to 2.2.3
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 09:08:59 +00:00
Amir Raminfar
8a3e9504a4 Updates modules 2022-10-10 15:38:05 -07:00
Amir Raminfar
d8748c6e27 Updats modules 2022-10-08 13:26:47 -07:00
kodiakhq[bot]
af8192fbc0 Merge pull request #1897 from amir20/dependabot/docker/golang-1.19.2-alpine
Bump golang from 1.19.1-alpine to 1.19.2-alpine
2022-10-05 09:20:23 +00:00
dependabot[bot]
3492587d63 Bump golang from 1.19.1-alpine to 1.19.2-alpine
Bumps golang from 1.19.1-alpine to 1.19.2-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-05 09:16:19 +00:00
Amir Raminfar
b8c522021d Updates modules 2022-10-04 13:26:11 -07:00
James Cote
5850c319f2 Fix json log line still says "expand json" when expanded (#1894) 2022-10-01 15:06:59 -07:00
Amir Raminfar
afbed43185 Release 4.1.9 2022-09-28 12:29:26 -07:00
Altynbek Kaliakbarov
d083430c73 Change calculation of memory usage. (#1892) 2022-09-28 12:23:24 -07:00
kodiakhq[bot]
79f553ff0c Merge pull request #1890 from amir20/dependabot/docker/e2e/cypress/included-10.9.0
Bump cypress/included from 10.8.0 to 10.9.0 in /e2e
2022-09-28 09:38:56 +00:00
dependabot[bot]
a02551f5ec Bump cypress/included from 10.8.0 to 10.9.0 in /e2e
Bumps cypress/included from 10.8.0 to 10.9.0.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-28 09:33:10 +00:00
Amir Raminfar
7c486d57fc Release 4.1.8 2022-09-27 10:27:57 -07:00
Amir Raminfar
41acf28be9 Updates modules 2022-09-26 09:54:25 -07:00
Amir Raminfar
21e5f4fc56 Removes fuzzysearch and uses fuse.js (#1888)
* Removes fuzzysearch and uses fuse.js

* Fix search bug

* Adds locale

* Fixes more locale

* Fixes search to not update with stats

* Fixes sort order
2022-09-23 09:54:55 -07:00
kodiakhq[bot]
2d54cbba9c Merge pull request #1887 from amir20/keyboard
Improves keyboard short cuts and adds a test
2022-09-22 18:19:41 +00:00
Amir Raminfar
88844c895c Fixes comment 2022-09-22 11:15:43 -07:00
Amir Raminfar
7aa7f42c52 Improves keyboard short cuts and adds a test 2022-09-22 11:13:28 -07:00
Amir Raminfar
59f4b0da4f Release 4.1.7 2022-09-20 14:31:07 -07:00
Amir Raminfar
e99e6ebd49 Fixes bubble for search and #1886 2022-09-20 14:30:54 -07:00
Amir Raminfar
3268c32627 Release 4.1.6 2022-09-20 14:03:27 -07:00
Amir Raminfar
5cb5cae113 Fixes spacing bug 2022-09-20 14:03:20 -07:00
Amir Raminfar
dbd1050948 Release 4.1.5 2022-09-20 13:15:04 -07:00
Amir Raminfar
69c647336e Cleans up log viewer and adds zigzag line for skipped logs (#1885)
* Cleans up log viewer and adds zigzag line for skipped logs

* Updates components

* Cleans up css

* Cleans up more css

* Fixes tests

* Fixes typing

* Fixes typescript errors

* Fixes selected color
2022-09-20 13:14:31 -07:00
Amir Raminfar
1a1dd74142 Updates snapshots 2022-09-19 10:32:35 -07:00
Amir Raminfar
307dcd1929 Updates modules 2022-09-19 09:57:16 -07:00
Amir Raminfar
06bde85e03 Removes hotkeyjs in favor of vueuse (#1883) 2022-09-16 09:09:21 -07:00
Amir Raminfar
9cbb55d780 Updates tests 2022-09-16 08:37:03 -07:00
Amir Raminfar
744bc11a2e Release 4.1.4 2022-09-15 15:29:32 -07:00
Amir Raminfar
4fe8964d66 Fixes search for test 2022-09-15 15:29:26 -07:00
Amir Raminfar
3f13ef28a9 Release 4.1.3 2022-09-15 15:21:32 -07:00
Amir Raminfar
28c569ce2a Fixes find in context bug 2022-09-15 15:21:15 -07:00
Amir Raminfar
c1dd3c1131 Search is broken with unknow types. See #1881 2022-09-15 11:16:21 -07:00
Amir Raminfar
492706367b Release 4.1.2 2022-09-15 09:26:34 -07:00
Amir Raminfar
cbdbe65b7b Fixes scrollback bug where messages weren't being dropped 2022-09-15 09:26:21 -07:00
Amir Raminfar
de84736ba4 Updates modules 2022-09-15 09:13:26 -07:00
Amir Raminfar
0043ed9291 Release 4.1.1 2022-09-14 13:54:53 -07:00
Amir Raminfar
89f7d21739 Adds nowrap for date 2022-09-14 13:15:10 -07:00
Amir Raminfar
fb0b11e626 Release 4.1.0 2022-09-14 12:23:25 -07:00
Amir Raminfar
70d72060d9 Cleans up define props with typescript and cleans up css (#1879)
* Tries to clean up defineprops

* Cleans up define props for all components
2022-09-14 12:00:36 -07:00
Ángel Fernández Sánchez
cc8a7ee8e7 Update es.yml (#1877)
The variable 'logs-skipped' is added to the translation.
2022-09-14 11:56:09 -07:00
Ángel Fernández Sánchez
58197f2b23 Update pr.yml (#1878)
The variable 'logs-skipped' is added to the portuguese translation.
2022-09-14 11:55:46 -07:00
kodiakhq[bot]
d2473e0fcc Merge pull request #1876 from amir20/dependabot/docker/e2e/cypress/included-10.8.0
Bump cypress/included from 10.7.0 to 10.8.0 in /e2e
2022-09-14 09:44:08 +00:00
dependabot[bot]
3a4de053b8 Bump cypress/included from 10.7.0 to 10.8.0 in /e2e
Bumps cypress/included from 10.7.0 to 10.8.0.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-14 09:39:42 +00:00
Amir Raminfar
b97dd31c9d Does localization 2022-09-13 14:39:39 -07:00
Amir Raminfar
a632c744bc Fixes bug with search 2022-09-13 14:32:20 -07:00
Amir Raminfar
7eeb7d8600 Updates moodules 2022-09-13 14:30:22 -07:00
Amir Raminfar
8de492d16b Trims top events depending on scroll status (#1860)
* Trims top events depending on scroll status

* Cleans up models and adds better OOP around logging events

* Adds log entries being skipped component

* Fixes type errors

* Fixes tets

* Styles skipping logs
2022-09-13 14:28:53 -07:00
Amir Raminfar
2b82a0816c Removes debug from golang (#1875) 2022-09-12 09:40:55 -07:00
Amir Raminfar
25daf6b502 Uses locale date and time depending on locale (#1874)
* Uses locale date and time depending on locale

* Fixes tests
2022-09-12 09:34:24 -07:00
Ángel Fernández Sánchez
011cef8124 Spanish and Portuguese languages updates (#1872)
* Create pr.yml

* Update pr.yml

* Update es.yml

File updated with spanish translation

* Update es.yml

* Update pr.yml

Portuguese translation updated
2022-09-12 08:17:42 -07:00
kodiakhq[bot]
d06eea2c26 Merge pull request #1873 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.18incompatible
Bump github.com/docker/docker from 20.10.17+incompatible to 20.10.18+incompatible
2022-09-12 09:26:56 +00:00
dependabot[bot]
cdb6738941 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.17+incompatible to 20.10.18+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.17...v20.10.18)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-12 09:22:27 +00:00
Amir Raminfar
f39b6f50e3 Fixes pnpm 2022-09-11 18:48:32 -07:00
Amir Raminfar
4d03a36940 Adds layouts (#1871) 2022-09-09 14:17:58 -07:00
Amir Raminfar
60fc2ab22a Updates other modules 2022-09-09 12:40:18 -07:00
Amir Raminfar
0ad841a2d2 Updates modules for cypress 2022-09-09 12:40:01 -07:00
Amir Raminfar
58ce210924 Adds more tests 2022-09-09 12:39:41 -07:00
Amir Raminfar
0214b212ea Clean up 2022-09-08 16:16:28 -07:00
Amir Raminfar
ee37d7c30e Adds i18n support with vue-i18n (#1870)
* Adds asset changes for i18n

* Adds asset changes for i18n

* Adds vite configs

* Adds auto import and cleans up props

* Initital localzation

* Fixes dockerfile

* Fixes tests

* Updates fixutres

* Updates default lang
2022-09-08 15:40:26 -07:00
Amir Raminfar
4395bc9dc5 Cleans up main ts to modules (#1869)
* Cleans up main ts to modules

* Uses vite pages

* Updates vite-plugin-pages to prod

* Fixes title

* Fixes show

* Fixes file case

* Uses sync mode
2022-09-07 19:48:48 -07:00
kodiakhq[bot]
4ea945f0b4 Merge pull request #1868 from amir20/dependabot/docker/golang-1.19.1-alpine
Bump golang from 1.19.0-alpine to 1.19.1-alpine
2022-09-08 02:33:56 +00:00
dependabot[bot]
b221242db3 Bump golang from 1.19.0-alpine to 1.19.1-alpine
Bumps golang from 1.19.0-alpine to 1.19.1-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 09:12:20 +00:00
Amir Raminfar
ad6793f614 Updates modules 2022-09-06 10:57:29 -07:00
Amir Raminfar
72f080f795 Caches builds for e2e (#1863)
* Caches builds for e2e

* Adds docker login

* Removes typo
2022-09-02 14:57:57 -07:00
Amir Raminfar
ee4210e1cc Updates modules 2022-09-02 14:45:16 -07:00
Amir Raminfar
fc68ba6391 Updates cypress 2022-08-31 10:27:58 -07:00
kodiakhq[bot]
539829d00d Merge pull request #1862 from amir20/dependabot/docker/e2e/cypress/included-10.7.0
Bump cypress/included from 10.6.0 to 10.7.0 in /e2e
2022-08-31 09:46:18 +00:00
dependabot[bot]
65c0d2a970 Bump cypress/included from 10.6.0 to 10.7.0 in /e2e
Bumps cypress/included from 10.6.0 to 10.7.0.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 09:41:35 +00:00
Amir Raminfar
696341b779 Fixes typo 2022-08-27 11:50:43 -07:00
99 changed files with 4334 additions and 2305 deletions

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies
@@ -52,17 +52,15 @@ jobs:
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.0
- name: Login to DockerHub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -83,7 +81,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies

View File

@@ -17,17 +17,15 @@ jobs:
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.0
- name: Login to DockerHub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8

View File

@@ -16,7 +16,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies
@@ -43,12 +43,17 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.0
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
- name: Set commit message for push
if: github.event_name == 'push'
run: |

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

View File

@@ -8,18 +8,21 @@ WORKDIR /build
# Install dependencies from lock file
COPY pnpm-lock.yaml ./
RUN pnpm fetch --prod
RUN pnpm fetch
# Copy files
COPY package.json .* vite.config.ts index.html ./
# Copy package.json and install dependencies
COPY package.json ./
RUN pnpm install -r --offline --ignore-scripts
# Copy assets to build
# Copy assets and translations to build
COPY .* vite.config.ts index.html ./
COPY assets ./assets
COPY locales ./locales
# Install dependencies
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.19.0-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.19.2-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle

View File

@@ -1,84 +1,8 @@
<template>
<main>
<mobile-menu v-if="isMobile && !authorizationNeeded"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
<pane v-for="other in activeContainers" :key="other.id">
<log-container
:id="other.id"
show-title
scrollable
closable
@close="containerStore.removeActiveContainer(other)"
></log-container>
</pane>
</template>
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapseNav = !collapseNav"
class="button is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
v-if="!isMobile && !authorizationNeeded"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
</span>
</button>
</main>
<router-view></router-view>
</template>
<script lang="ts" setup>
import { Splitpanes, Pane } from "splitpanes";
import { ref, onMounted, watchEffect } from "vue";
import { storeToRefs } from "pinia";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import hotkeys from "hotkeys-js";
import { setTitle } from "@/composables/title";
import { isMobile } from "@/composables/media";
import { smallerScrollbars, lightTheme, menuWidth } from "@/composables/settings";
import { useContainerStore } from "@/stores/container";
import config from "@/stores/config";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
import LogContainer from "@/components/LogContainer.vue";
import SideMenu from "@/components/SideMenu.vue";
import MobileMenu from "@/components/MobileMenu.vue";
const collapseNav = ref(false);
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
onMounted(() => {
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");
@@ -97,59 +21,6 @@ watchEffect(() => {
document.documentElement.removeAttribute("data-theme");
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}
}
</script>
<style scoped lang="scss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
}
@media screen and (max-width: 768px) {
.router-view {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
background: rgba(0, 0, 0, 0.95);
&:hover {
left: -25px;
}
}
}
</style>
<style scoped lang="scss"></style>

616
assets/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,616 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
const $: typeof import('vue/macros')['$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const config: typeof import('./stores/config')['default']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON']
const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composables/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./composables/settings')['lightTheme']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./composables/settings')['menuWidth']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./utils/index')['persistentVisibleKeys']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle']
const settings: typeof import('./composables/settings')['settings']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
const softWrap: typeof import('./composables/settings')['softWrap']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stripVersion: typeof import('./utils/index')['stripVersion']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useHead: typeof import('@vueuse/head')['useHead']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVisibleFilter: typeof import('./composables/visible')['useVisibleFilter']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
readonly $computed: UnwrapRef<typeof import('vue/macros')['$computed']>
readonly $customRef: UnwrapRef<typeof import('vue/macros')['$customRef']>
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./utils/index')['persistentVisibleKeys']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

View File

@@ -10,18 +10,21 @@ declare module '@vue/runtime-core' {
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
CilColumns: typeof import('~icons/cil/columns')['default']
CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
CpuSparkline: typeof import('./components/StatSparkline.vue')['default']
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
FieldList: typeof import('./components/FieldList.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
JSONPayload: typeof import('./components/JSONPayload.vue')['default']
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogContainer.vue')['default']
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
LogViewer: typeof import('./components/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewerWithSource.vue')['default']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
@@ -40,5 +43,9 @@ declare module '@vue/runtime-core' {
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}

View File

@@ -1,34 +0,0 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
<div class="column is-narrow has-text-weight-bold">
{{ container.state }}
</div>
<div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
<span class="has-text-weight-light has-spacer">mem</span>
<span class="has-text-weight-bold">
{{ formatBytes(container.stat.memoryUsage) }}
</span>
</div>
<div class="column is-narrow" v-if="container.stat.cpu !== null">
<span class="has-text-weight-light has-spacer">load</span>
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
</div>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/types/Container";
import { ComputedRef, inject } from "vue";
import { formatBytes } from "@/utils";
const container = inject("container") as ComputedRef<Container>;
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
</style>

View File

@@ -3,26 +3,29 @@
<o-autocomplete
ref="autocomplete"
v-model="query"
placeholder="Search containers using ⌘ + k or ctrl + k"
field="name"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="results"
:data="data"
@select="selected"
>
<template #default="props">
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="props.option.state">
<span class="icon is-small" :class="item.state">
<octicon-container-24 />
</span>
</div>
<div class="media-content">
{{ props.option.name }}
{{ item.name }}
</div>
<div class="media-right">
<span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
</div>
@@ -33,62 +36,61 @@
</template>
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import { computed, nextTick, onMounted, ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { Container } from "@/types/Container";
import { Container } from "@/models/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
const props = defineProps({
maxResults: {
default: 20,
type: Number,
},
});
const { maxResults: resultLimit = 20 } = defineProps<{
maxResults?: number;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits<{
(e: "close"): void;
}>();
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 }) =>
reactive({
name,
const list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return {
id,
created,
name,
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 [...preparedContainers.value].sort((a, b) => b.created - a.created);
}
};
});
});
onMounted(() => nextTick(() => autocomplete.value?.focus()));
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
resultLimit,
matchAllWhenSearchEmpty: true,
});
function selected(item: { id: string; name: string }) {
router.push({ name: "container", params: { id: item.id } });
const data = computed(() => {
return results.value
.sort((a, b) => {
if (a.score === b.score) {
if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else if (a.score && b.score) {
return a.score - b.score;
} else {
return 0;
}
})
.map(({ item }) => item);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
emit("close");
}
function addColumn(container: Container) {

View File

@@ -9,23 +9,21 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
const props = defineProps({
onLoadMore: Function,
enabled: Boolean,
});
const { onLoadMore = () => {}, enabled } = defineProps<{
onLoadMore: () => void;
enabled: boolean;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (props.onLoadMore && props.enabled) {
if (onLoadMore && enabled) {
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await props.onLoadMore();
await onLoadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;

View File

@@ -1,55 +0,0 @@
<template>
<ul class="fields" @click="expanded = !expanded">
<li v-for="(value, name) in logEntry.message">
<template v-if="value">
<span class="has-text-grey">{{ name }}=</span>
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
</template>
</li>
</ul>
<field-list :fields="logEntry.unfilteredPayload" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</template>
<script lang="ts" setup>
import { useSearchFilter } from "@/composables/search";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { PropType, ref } from "vue";
const { markSearch } = useSearchFilter();
defineProps({
logEntry: {
type: Object as PropType<VisibleLogEntry>,
required: true,
},
visibleKeys: {
type: Array as PropType<string[][]>,
default: [],
},
});
const expanded = ref(false);
</script>
<style lang="scss" scoped>
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
li {
display: inline-block;
margin-left: 1em;
}
}
</style>

View File

@@ -1,203 +0,0 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
:key="item.id"
:data-key="item.id"
:data-event="item.event"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<div class="line">
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
<JSONPayload :log-entry="item" :visible-keys="visibleKeys.value" v-if="item.isComplex()"></JSONPayload>
<span class="text" v-html="colorize(item.message)" v-if="item.isSimple()"></span>
</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import { ComputedRef, inject, PropType, ref, toRefs, watch, toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { size, showTimestamp, softWrap } from "@/composables/settings";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { LogEntry } from "@/types/LogEntry";
import { useSearchFilter } from "@/composables/search";
import { useVisibleFilter } from "@/composables/visible";
import { Container } from "@/types/Container";
import { persistentVisibleKeys } from "@/utils";
import RelativeTime from "./RelativeTime.vue";
import AnsiConvertor from "ansi-to-html";
import JSONPayload from "./JSONPayload.vue";
const props = defineProps({
messages: {
type: Array as PropType<LogEntry[]>,
required: true,
},
});
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const { messages } = toRefs(props);
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem = ref<VisibleLogEntry>();
function handleJumpLineSelected(e: Event, item: VisibleLogEntry) {
lastSelectedItem.value = item;
resetSearch();
}
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
},
{ immediate: true, flush: "post" }
);
</script>
<style scoped lang="scss">
.events {
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
&.disable-wrap {
.line,
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
&::before {
content: " ";
}
}
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.selected .date {
background-color: var(--menu-item-active-background-color);
color: var(--text-color);
}
&.selected > .date {
background-color: white;
}
& > .line {
margin: auto 0;
width: 100%;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
}
}
&.small {
font-size: 60%;
}
&.medium {
font-size: 80%;
}
&.large {
font-size: 120%;
}
}
@media (prefers-color-scheme: dark) {
.date {
background-color: #262626;
color: #258ccd;
}
}
[data-theme="dark"] {
.date {
background-color: #262626;
color: #258ccd;
}
}
@media (prefers-color-scheme: light) {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
[data-theme="light"] {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
}
:deep(mark) {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="columns is-1 is-variable">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
<li v-for="(value, name) in validValues(logEntry.message)">
<span class="has-text-grey">{{ name }}=</span>
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
</div>
</template>
<script lang="ts" setup>
import { type ComplexLogEntry } from "@/models/LogEntry";
const { markSearch } = useSearchFilter();
const { logEntry } = defineProps<{
logEntry: ComplexLogEntry;
visibleKeys: string[][];
}>();
let expanded = $ref(false);
function validValues(obj: Record<string, any>) {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
}
</script>
<style lang="scss" scoped>
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
&.expanded:hover {
&::after {
content: "collapse json";
}
}
li {
display: inline-block;
& + li {
margin-left: 1em;
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<div class="column is-narrow has-text-weight-bold">
{{ container.state }}
</div>
<div class="column is-narrow has-text-centered is-relative">
<div class="has-border">
<stat-sparkline :data="memoryData"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">mem</span>
<span class="has-text-weight-bold">
{{ formatBytes(container.stat.memoryUsage) }}
</span>
</div>
</div>
<div class="column is-narrow has-text-centered is-relative">
<div class="has-border">
<stat-sparkline :data="cpuData"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">load</span>
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const cpuData = computedWithControl(
() => container.value.getLastStat(),
() => {
const history = container.value.getStatHistory();
return history.map((stat, i) => ({ x: history.length - i, y: stat.snapshot.cpu }));
}
);
const memoryData = computedWithControl(
() => container.value.getLastStat(),
() => {
const history = container.value.getStatHistory();
return history.map((stat, i) => ({ x: history.length - i, y: stat.snapshot.memory }));
}
);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>

View File

@@ -8,8 +8,9 @@
</template>
<script lang="ts" setup>
import { Container } from "@/types/Container";
import { inject, ComputedRef } from "vue";
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
</script>

View File

@@ -0,0 +1,24 @@
<template>
<span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
defineProps<{
logEntry: DockerEventLogEntry;
}>();
</script>
<style lang="scss" scoped>
span {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.text {
white-space: pre-wrap;
}
}
</style>

View File

@@ -28,26 +28,18 @@
</template>
<script lang="ts" setup>
import { arrayEquals, isObject } from "@/utils";
import { nextTick, PropType, ref, toRaw } from "vue";
const props = defineProps({
fields: {
type: Object as PropType<Record<string, any>>,
required: true,
},
expanded: {
type: Boolean,
default: false,
},
parentKey: {
type: Array as PropType<string[]>,
default: [],
},
visibleKeys: {
type: Array as PropType<string[][]>,
default: [],
},
});
const {
fields,
expanded = false,
parentKey = [],
visibleKeys = [],
} = defineProps<{
fields: Record<string, any>;
expanded?: boolean;
parentKey?: string[];
visibleKeys?: string[][];
}>();
const root = ref<HTMLElement>();
@@ -55,9 +47,9 @@ async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
props.visibleKeys.splice(index, 1);
visibleKeys.splice(index, 1);
} else {
props.visibleKeys.push(props.parentKey.concat(field));
visibleKeys.push(parentKey.concat(field));
}
await nextTick();
@@ -72,8 +64,8 @@ function hasField(field: string) {
}
function fieldIndex(field: string) {
const path = props.parentKey.concat(field);
return props.visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
const path = parentKey.concat(field);
return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>

View File

@@ -8,7 +8,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Clear</div>
<div class="level-item">{{ $t("toolbar.clear") }}</div>
</div>
</div>
</a>
@@ -20,7 +20,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Download</div>
<div class="level-item">{{ $t("toolbar.download") }}</div>
</div>
</div>
</a>
@@ -33,7 +33,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Search</div>
<div class="level-item">{{ $t("toolbar.search") }}</div>
</div>
</div>
</a>
@@ -41,32 +41,17 @@
</template>
<script lang="ts" setup>
import { inject, onMounted, onUnmounted, PropType, ComputedRef } from "vue";
import hotkeys from "hotkeys-js";
import config from "@/stores/config";
import { useSearchFilter } from "@/composables/search";
import { Container } from "@/types/Container";
import { type ComputedRef } from "vue";
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const props = defineProps({
onClearClicked: {
type: Function as PropType<(e: Event) => void>,
default: (e: Event) => {},
},
});
const onHotkey = (event: Event) => {
props.onClearClicked(event);
event.preventDefault();
};
const { onClearClicked = (e: Event) => {} } = defineProps<{
onClearClicked: (e: Event) => void;
}>();
const container = inject("container") as ComputedRef<Container>;
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
</script>
<style lang="scss" scoped>

View File

@@ -6,7 +6,7 @@
<container-title @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat v-if="container.stat" />
<container-stat />
</div>
<div class="mr-2 column is-narrow is-paddingless">
@@ -24,35 +24,27 @@
</template>
<script lang="ts" setup>
import { provide, ref, toRefs } from "vue";
import LogViewerWithSource from "./LogViewerWithSource.vue";
import { useContainerStore } from "@/stores/container";
const props = defineProps({
id: {
type: String,
required: true,
},
showTitle: {
type: Boolean,
default: false,
},
scrollable: {
type: Boolean,
default: false,
},
closable: {
type: Boolean,
default: false,
},
});
const {
id,
showTitle = false,
scrollable = false,
closable = false,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { id } = toRefs(props);
const store = useContainerStore();
const container = store.currentContainer(id);
const container = store.currentContainer($$(id));
provide("container", container);

View File

@@ -0,0 +1,45 @@
<template>
<relative-time :date="date" class="date"></relative-time>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
<style lang="scss" scoped>
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
white-space: nowrap;
}
@media (prefers-color-scheme: dark) {
.date {
background-color: #262626;
color: #258ccd;
}
}
[data-theme="dark"] {
.date {
background-color: #262626;
color: #258ccd;
}
}
@media (prefers-color-scheme: light) {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
[data-theme="light"] {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
</style>

View File

@@ -4,9 +4,9 @@ import { createTestingPinia } from "@pinia/testing";
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../composables/settings";
import { settings } from "../../composables/settings";
import { useSearchFilter } from "@/composables/search";
import { vi, describe, expect, beforeEach, test, beforeAll, afterAll, afterEach } from "vitest";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { computed, nextTick } from "vue";
import { createRouter, createWebHistory } from "vue-router";
@@ -23,6 +23,7 @@ describe("<LogEventSource />", () => {
beforeEach(() => {
global.EventSource = EventSource;
// @ts-ignore
window.scrollTo = vi.fn();
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
@@ -47,6 +48,9 @@ describe("<LogEventSource />", () => {
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
if(searchFilter){
search.showSearch.value = true;
}
const router = createRouter({
history: createWebHistory("/"),
@@ -68,6 +72,7 @@ describe("<LogEventSource />", () => {
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123" })),
scrollingPaused: computed(() => false),
},
},
slots: {
@@ -108,6 +113,7 @@ describe("<LogEventSource />", () => {
vi.runAllTimers();
await nextTick();
// @ts-ignore
const [message, _] = wrapper.vm.messages;
expect(message).toMatchSnapshot();
});

View File

@@ -4,13 +4,15 @@
</template>
<script lang="ts" setup>
import { useEventSource } from "@/composables/eventsource";
import { Container } from "@/types/Container";
import { inject, ComputedRef } from "vue";
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const emit = defineEmits(["loading-more"]);
const container = inject("container") as ComputedRef<Container>;
const { connect, messages, loadOlderLogs } = useEventSource(container);
const { messages, loadOlderLogs } = useLogStream(container);
const beforeLoading = () => emit("loading-more", true);
const afterLoading = () => emit("loading-more", false);
@@ -20,6 +22,4 @@ defineExpose({
});
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
connect();
</script>

View File

@@ -0,0 +1,106 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
:key="item.id"
:data-key="item.id"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
</li>
</ul>
</template>
<script lang="ts" setup>
import { type ComputedRef, toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { Container } from "@/models/Container";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
}>();
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry<string | JSONObject> | undefined = $ref(undefined);
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
lastSelectedItem = item;
resetSearch();
}
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
},
{ immediate: true, flush: "post" }
);
</script>
<style scoped lang="scss">
.events {
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&.selected {
border: 1px var(--secondary-color) solid;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
}
}
&.small {
font-size: 60%;
}
&.medium {
font-size: 80%;
}
&.large {
font-size: 120%;
}
}
</style>

View File

@@ -5,14 +5,15 @@
</template>
<script lang="ts" setup>
import LogViewer from "./LogViewer.vue";
import { ref } from "vue";
import LogEventSource from "./LogEventSource.vue";
const emit = defineEmits(["loading-more"]);
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const source = ref<InstanceType<typeof LogViewer>>();
const source = $ref<InstanceType<typeof LogEventSource>>();
function clear() {
source.value?.clear();
source?.clear();
}
defineExpose({
clear,

View File

@@ -0,0 +1,32 @@
<template>
<div class="columns is-1 is-variable">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
</div>
</template>
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
defineProps<{
logEntry: SimpleLogEntry;
}>();
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
</script>
<style lang="scss" scoped>
.disable-wrap {
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="is-flex-grow-1 has-text-centered my-4">
<div class="is-relative">
<zig-zag class="is-overlay mt-2"></zig-zag>
<span class="text is-relative py-2 px-4">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { SkippedLogsEntry } from "@/models/LogEntry";
defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
<style lang="scss" scoped>
.text {
font-weight: bold;
white-space: pre-wrap;
background-color: var(--body-background-color);
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<svg :width="width" :height="height">
<path :d="path" class="area" />
</svg>
</template>
<script lang="ts" setup>
import { extent } from "d3-array";
import { scaleLinear } from "d3-scale";
import { area, curveStep } from "d3-shape";
const d3 = { extent, scaleLinear, area, curveStep };
const { data, width = 150, height = 30 } = defineProps<{ data: Point[]; width?: number; height?: number }>();
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const shape = d3
.area<Point>()
.curve(d3.curveStep)
.x((d) => x(d.x))
.y0(height)
.y1((d) => y(d.y));
const path = computed(() => {
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
y.domain(d3.extent(data, (d) => d.y) as [number, number]);
return shape(data) ?? "";
});
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<svg width="100%" height="8" class="zigzag">
<defs>
<pattern id="zigzag" x="0" y="0" width="30" height="8" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="15" y2="8" class="line" />
<line x1="15" y1="8" x2="30" y2="0" class="line" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.line {
stroke: var(--primary-color);
stroke-width: 1;
stroke-linecap: round;
}
</style>

View File

@@ -0,0 +1,206 @@
// Vitest Snapshot v1
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
SimpleLogEntry {
"_message": "This is a message.",
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
}
`;

View File

@@ -2,7 +2,7 @@
<aside>
<div class="columns is-marginless is-gapless is-mobile is-vcentered">
<div class="column is-narrow">
<router-link :to="{ name: 'default' }">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
@@ -24,10 +24,14 @@
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in visibleContainers" :key="item.id">
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="is-ellipsis">
{{ item.name }}
</div>
@@ -38,19 +42,14 @@
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
const store = useContainerStore();
const route = useRoute();
const { visibleContainers, allContainersById } = storeToRefs(store);
const showNav = ref(false);
let showNav = $ref(false);
watch(route, () => {
showNav.value = false;
showNav = false;
});
</script>
<style scoped lang="scss">

View File

@@ -3,20 +3,15 @@
</template>
<script lang="ts" setup>
import { useIntervalFn } from "@vueuse/core";
import formatDistance from "date-fns/formatDistance";
import { PropType, ref } from "vue";
const props = defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
});
const { date } = defineProps<{
date: Date;
}>();
const text = ref<string>();
function updateFromNow() {
text.value = formatDistance(props.date, new Date(), {
text.value = formatDistance(date, new Date(), {
addSuffix: true,
});
}

View File

@@ -1,44 +1,21 @@
<template>
<time :datetime="date.toISOString()">{{ relativeTime(date, locale) }}</time>
<time :datetime="date.toISOString()">{{ format(date) }}</time>
</template>
<script lang="ts">
const use24Hr =
new Intl.DateTimeFormat(undefined, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")?.value.length === 2;
const auto = use24Hr ? enGB : enUS;
const styles = { auto, 12: enUS, 24: enGB };
</script>
<script lang="ts" setup>
import { formatRelative } from "date-fns";
import { hourStyle } from "@/composables/settings";
import enGB from "date-fns/locale/en-GB";
import enUS from "date-fns/locale/en-US";
import { computed, PropType } from "vue";
defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
});
defineProps<{
date: Date;
}>();
const locale = computed(() => {
const locale = styles[hourStyle.value];
const oldFormatter = locale.formatRelative as (d: Date | number) => string;
return {
...locale,
formatRelative(date: Date | number) {
return oldFormatter(date) + "p";
},
};
});
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false }[hourStyle.value]));
const timeFormatter = $computed(
() => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
);
function relativeTime(date: Date, locale: Locale) {
return formatRelative(date, new Date(), { locale });
function format(date: Date) {
const dateStr = dateFormatter.format(date);
const timeStr = timeFormatter.format(date);
return `${dateStr} ${timeStr}`;
}
</script>

View File

@@ -18,21 +18,10 @@
</template>
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { useScroll } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch, watchPostEffect } from "vue";
const props = defineProps({
indeterminate: {
default: false,
type: Boolean,
},
autoHide: {
default: true,
type: Boolean,
},
});
const { indeterminate = false, autoHide = false } = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
}>();
const scrollProgress = ref(0);
const animation = ref({ cancel: () => {} });
@@ -59,7 +48,7 @@ watchPostEffect(() => {
: (scrollElement.value as HTMLElement);
scrollProgress.value = scrollY.value / (parent.scrollHeight - parent.clientHeight);
animation.value.cancel();
if (props.autoHide && root.value) {
if (autoHide && root.value) {
animation.value = root.value.animate(
{ opacity: [1, 0] },
{

View File

@@ -25,34 +25,29 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
defineProps({
scrollable: {
type: Boolean,
default: true,
},
});
const paused = ref(false);
const hasMore = ref(false);
const loading = ref(false);
let paused = $ref(false);
let hasMore = $ref(false);
let loading = $ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
provide("scrollingPaused", $$(paused));
const mutationObserver = new MutationObserver((e) => {
if (!paused.value) {
if (!paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
hasMore = true;
}
}
});
const intersectionObserver = new IntersectionObserver((entries) => (paused.value = entries[0].intersectionRatio == 0), {
threshholds: [0, 1],
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "80px 0px",
});
@@ -63,11 +58,11 @@ onMounted(() => {
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
hasMore.value = false;
hasMore = false;
}
function setLoading(value: boolean) {
loading.value = value;
loading = value;
}
</script>
<style scoped lang="scss">

View File

@@ -22,30 +22,18 @@
</template>
<script lang="ts" setup>
import hotkeys from "hotkeys-js";
import { search } from "@/composables/settings";
import { useSearchFilter } from "@/composables/search";
import { ref, nextTick, onMounted, onUnmounted } from "vue";
const input = ref<HTMLInputElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
onMounted(() => {
hotkeys("command+f, ctrl+f", (event, handler) => {
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
showSearch.value = true;
nextTick(() => input.value?.focus() || input.value?.select());
event.preventDefault();
});
hotkeys("esc", () => resetSearch());
e.preventDefault();
}
});
onUnmounted(() => {
searchFilter.value = "";
showSearch.value = false;
hotkeys.unbind("command+f, ctrl+f");
hotkeys.unbind("esc");
});
onUnmounted(() => resetSearch());
</script>
<style lang="scss" scoped>

View File

@@ -2,14 +2,14 @@
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<router-link :to="{ name: 'default' }">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
</div>
<div class="column is-narrow has-text-right px-1">
<button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
<button class="button is-rounded" @click="$emit('search')" title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
@@ -23,10 +23,14 @@
</router-link>
</div>
</div>
<p class="menu-label is-hidden-mobile">Containers</p>
<p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" v-if="ready">
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
{{ item.name }}
@@ -36,7 +40,7 @@
class="icon is-small"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
title="Pin as column"
title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
@@ -46,15 +50,12 @@
</li>
</ul>
<ul class="menu-list is-hidden-mobile loading" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large"></o-skeleton></li>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
</aside>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { useContainerStore } from "@/stores/container";
import type { Container } from "@/types/Container";
const store = useContainerStore();

View File

@@ -1,200 +0,0 @@
// Vitest Snapshot v1
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a message.</span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><mark>test</mark> bar</span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-cce5b553=\\"\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
{
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
"message": "This is a message.",
}
`;

View File

@@ -1,26 +1,48 @@
import { ref, watch, onUnmounted, ComputedRef } from "vue";
import { type ComputedRef, type Ref } from "vue";
import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
LogEntry,
asLogEntry,
DockerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Container } from "@/models/Container";
import type { LogEntry, LogEvent } from "@/types/LogEntry";
import config from "@/stores/config";
import { Container } from "@/types/Container";
function parseMessage(data: string): LogEntry {
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data) as LogEvent;
const id = e.id;
const date = new Date(e.ts);
return { id, date, message: e.m };
return asLogEntry(e);
}
export function useEventSource(container: ComputedRef<Container>) {
const messages = ref<LogEntry[]>([]);
const buffer = ref<LogEntry[]>([]);
export function useLogStream(container: ComputedRef<Container>) {
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
function flushNow() {
messages.value.push(...buffer.value);
buffer.value = [];
if (messages.length > config.maxLogs) {
if (scrollingPaused) {
console.log("Skipping ", buffer.length, " log items");
if (messages.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.at(-1) as SkippedLogsEntry;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.length, lastItem);
} else {
const firstItem = buffer.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
messages.push(new SkippedLogsEntry(new Date(), buffer.length, firstItem, lastItem));
}
buffer = [];
} else {
messages.push(...buffer);
buffer = [];
messages.splice(0, messages.length - config.maxLogs);
}
} else {
messages.push(...buffer);
buffer = [];
}
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
@@ -31,8 +53,8 @@ export function useEventSource(container: ComputedRef<Container>) {
if (clear) {
flushBuffer.cancel();
messages.value = [];
buffer.value = [];
messages = [];
buffer = [];
lastEventId = "";
}
@@ -40,12 +62,8 @@ export function useEventSource(container: ComputedRef<Container>) {
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
buffer.value.push({
event: "container-stopped",
message: "Container stopped",
date: new Date(),
id: new Date().getTime(),
});
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
flushBuffer();
flushBuffer.flush();
});
@@ -53,18 +71,18 @@ export function useEventSource(container: ComputedRef<Container>) {
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.value.push(parseMessage(e.data));
buffer.push(parseMessage(e.data));
flushBuffer();
}
};
}
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
if (messages.value.length < 300) return;
if (messages.length < 300) return;
beforeLoading();
const to = messages.value[0].date;
const last = messages.value[299].date;
const to = messages[0].date;
const last = messages[299].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const logs = await (
@@ -75,7 +93,7 @@ export function useEventSource(container: ComputedRef<Container>) {
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value.unshift(...newMessages);
messages.unshift(...newMessages);
}
afterLoading();
}
@@ -85,12 +103,7 @@ export function useEventSource(container: ComputedRef<Container>) {
(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(),
id: new Date().getTime(),
});
buffer.push(new DockerEventLogEntry("Container started", new Date(), "container-started"));
connect({ clear: false });
}
}
@@ -104,8 +117,9 @@ export function useEventSource(container: ComputedRef<Container>) {
watch(
() => container.value.id,
() => connect()
() => connect(),
{ immediate: true }
);
return { connect, messages, loadOlderLogs };
return { ...$$({ messages }), loadOlderLogs };
}

View File

@@ -1,3 +1 @@
import { useMediaQuery } from "@vueuse/core";
export const isMobile = useMediaQuery("(max-width: 770px)");

View File

@@ -1,6 +1,5 @@
import { ref, computed, Ref } from "vue";
import { useDebounce } from "@vueuse/core";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { type Ref } from "vue";
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
const searchFilter = ref<string>("");
const debouncedSearchFilter = useDebounce(searchFilter);
@@ -25,17 +24,17 @@ export function useSearchFilter() {
return isSmartCase ? new RegExp(debouncedSearchFilter.value, "i") : new RegExp(debouncedSearchFilter.value);
});
function filteredMessages(messages: Ref<VisibleLogEntry[]>) {
function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
if (debouncedSearchFilter.value) {
if (debouncedSearchFilter.value && showSearch.value) {
try {
return messages.value.filter((d) => {
if (d.isSimple()) {
if (d instanceof SimpleLogEntry) {
return regex.value.test(d.message);
} else if (d.isComplex()) {
} else if (d instanceof ComplexLogEntry) {
return matchRecord(d.message, regex.value);
}
throw new Error("Unknown message type");
return false;
});
} catch (e) {
if (e instanceof SyntaxError) {
@@ -50,9 +49,9 @@ export function useSearchFilter() {
});
}
function markSearch(log: string): string;
function markSearch(log: { toString(): string }): string;
function markSearch(log: string[]): string[];
function markSearch(log: string | string[]) {
function markSearch(log: { toString(): string } | string[]) {
if (!debouncedSearchFilter.value) {
return log;
}

View File

@@ -1,7 +1,4 @@
import { useStorage } from "@vueuse/core";
import { computed } from "vue";
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
search: boolean;
@@ -25,49 +22,62 @@ export const DEFAULT_SETTINGS: {
softWrap: true,
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = {...DEFAULT_SETTINGS, ...settings.value};
const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
export const search = computed({
const search = computed({
get: () => settings.value.search,
set: (value) => (settings.value.search = value),
});
export const size = computed({
const size = computed({
get: () => settings.value.size,
set: (value) => (settings.value.size = value),
});
export const menuWidth = computed({
const menuWidth = computed({
get: () => settings.value.menuWidth,
set: (value) => (settings.value.menuWidth = value),
});
export const smallerScrollbars = computed({
const smallerScrollbars = computed({
get: () => settings.value.smallerScrollbars,
set: (value) => (settings.value.smallerScrollbars = value),
});
export const showTimestamp = computed({
const showTimestamp = computed({
get: () => settings.value.showTimestamp,
set: (value) => (settings.value.showTimestamp = value),
});
export const showAllContainers = computed({
const showAllContainers = computed({
get: () => settings.value.showAllContainers,
set: (value) => (settings.value.showAllContainers = value),
});
export const lightTheme = computed({
const lightTheme = computed({
get: () => settings.value.lightTheme,
set: (value) => (settings.value.lightTheme = value),
});
export const hourStyle = computed({
const hourStyle = computed({
get: () => settings.value.hourStyle,
set: (value) => (settings.value.hourStyle = value),
});
export const softWrap = computed({
const softWrap = computed({
get: () => settings.value.softWrap,
set: (value) => (settings.value.softWrap = value),
});
export {
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
smallerScrollbars,
menuWidth,
size,
search,
settings
};

View File

@@ -1,12 +1,8 @@
import { useTitle } from "@vueuse/core";
import { ref, computed } from "vue";
let subtitle = $ref("");
const title = $computed(() => `${subtitle} - Dozzle`);
const subtitle = ref("");
const title = computed(() => `${subtitle.value} - Dozzle`);
useTitle(title);
useTitle($$(title));
export function setTitle(t: string) {
subtitle.value = t;
subtitle = t;
}

View File

@@ -1,11 +1,16 @@
import { LogEntry } from "@/types/LogEntry";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { computed, ComputedRef, Ref } from "vue";
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { ComputedRef, Ref } from "vue";
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
function filteredPayload(messages: Ref<LogEntry[]>) {
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
return messages.value.map((d) => new VisibleLogEntry(d, visibleKeys.value));
return messages.value.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys.value);
} else {
return d;
}
});
});
}

122
assets/layouts/default.vue Normal file
View File

@@ -0,0 +1,122 @@
<template>
<main v-if="!authorizationNeeded">
<mobile-menu v-if="isMobile"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
<pane v-for="other in activeContainers" :key="other.id">
<log-container
:id="other.id"
show-title
scrollable
closable
@close="containerStore.removeActiveContainer(other)"
></log-container>
</pane>
</template>
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapseNav = !collapseNav"
class="button is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
</span>
</button>
</main>
</template>
<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
const collapseNav = ref(false);
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
});
onKeyStroke("k", (e) => {
if (e.ctrlKey || e.metaKey) {
showFuzzySearch();
e.preventDefault();
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}
}
</script>
<style scoped lang="scss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
}
@media screen and (max-width: 768px) {
.router-view {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
background: rgba(0, 0, 0, 0.95);
&:hover {
left: -25px;
}
}
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<main>
<router-view></router-view>
</main>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

View File

@@ -1,68 +1,10 @@
import "./styles.scss";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { Autocomplete, Button, Dropdown, Switch, Radio, Skeleton, Field, Tooltip, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
import { createPinia } from "pinia";
import config from "./stores/config";
import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
const routes = [
{
path: "/",
component: Index,
name: "default",
},
{
path: "/container/:id",
component: Container,
name: "container",
props: true,
},
{
path: "/container/:pathMatch(.*)",
component: ContainerNotFound,
name: "container-not-found",
},
{
path: "/settings",
component: Settings,
name: "settings",
},
{
path: "/show",
component: Show,
name: "show",
},
{
path: "/login",
component: Login,
name: "login",
},
{
path: "/:pathMatch(.*)*",
component: PageNotFound,
name: "page-not-found",
},
];
const app = createApp(App);
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach((i) =>
i.install?.(app)
);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
createApp(App)
.use(router)
.use(createPinia())
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Skeleton)
.use(Config, bulmaConfig)
.mount("#app");
app.mount("#app");

View File

@@ -0,0 +1,31 @@
import type { ContainerStat, ContainerState } from "@/types/Container";
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
export class Container {
public stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
constructor(
public readonly id: string,
public readonly created: number,
public readonly image: string,
public readonly name: string,
public readonly command: string,
public status: string,
public state: ContainerState
) {
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
}
public getStatHistory() {
return unref(this.throttledStatHistory.history);
}
public getLastStat() {
return unref(this.throttledStatHistory.last);
}
}

121
assets/models/LogEntry.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Component, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export interface HasComponent {
getComponent(): Component;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
protected readonly _message: T;
constructor(message: T, public readonly id: number, public readonly date: Date) {
this._message = message;
}
public get message(): T {
return this._message;
}
abstract getComponent(): Component;
}
export class SimpleLogEntry extends LogEntry<string> {
getComponent(): Component {
return SimpleLogItem;
}
}
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
super(message, id, date);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
} else {
this.filteredMessage = computed(() => flattenJSON(message));
}
}
getComponent(): Component {
return ComplexLogItem;
}
public get message(): JSONObject {
return this.filteredMessage.value;
}
public get unfilteredMessage(): JSONObject {
return this._message;
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
}
}
export class DockerEventLogEntry extends LogEntry<string> {
constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date);
}
getComponent(): Component {
return DockerEventLogItem;
}
}
export class SkippedLogsEntry extends LogEntry<string> {
private _totalSkipped = 0;
private lastSkipped: LogEntry<string | JSONObject>;
constructor(
date: Date,
totalSkipped: number,
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>
) {
super("", date.getTime(), date);
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
getComponent(): Component {
return SkippedEntriesLogItem;
}
public get message(): string {
return `Skipped ${this.totalSkipped} entries`;
}
public addSkippedEntries(totalSkipped: number, lastItem: LogEntry<string | JSONObject>) {
this._totalSkipped += totalSkipped;
this.lastSkipped = lastItem;
}
public get totalSkipped(): number {
return this._totalSkipped;
}
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
} else {
return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
}
}

28
assets/modules/bulma.ts Normal file
View File

@@ -0,0 +1,28 @@
import { type App } from "vue";
import {
Autocomplete,
Button,
Dropdown,
Switch,
Radio,
Skeleton,
Field,
Tooltip,
Modal,
Config,
} from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
app
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Skeleton)
.use(Config, bulmaConfig);
};

20
assets/modules/i18n.ts Normal file
View File

@@ -0,0 +1,20 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
}
)
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};

8
assets/modules/pinia.ts Normal file
View File

@@ -0,0 +1,8 @@
import { type App } from "vue";
import { createPinia } from "pinia";
export const install = (app:App) => {
const pinia = createPinia();
app.use(pinia);
};

16
assets/modules/router.ts Normal file
View File

@@ -0,0 +1,16 @@
import { type App } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import pages from "~pages";
import { setupLayouts } from "virtual:generated-layouts";
import config from "@/stores/config";
export const install = (app: App) => {
const routes = setupLayouts(pages);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
app.use(router);
};

View File

@@ -1,35 +0,0 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</template>
<script lang="ts" setup>
import { onMounted, toRefs, watchEffect } from "vue";
import Search from "@/components/Search.vue";
import LogContainer from "@/components/LogContainer.vue";
import { setTitle } from "@/composables/title";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
const store = useContainerStore();
const props = defineProps({
id: {
type: String,
required: true,
},
});
const { id } = toRefs(props);
const currentContainer = store.currentContainer(id);
const { activeContainers } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>

View File

@@ -1,23 +0,0 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
Container not found.
<small class="subtitle">It may have been removed.</small>
</h1>
</div>
</div>
</div>
</template>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "ContainerNotFound",
setup() {
setTitle("Container not found");
},
};
</script>

View File

@@ -4,11 +4,8 @@
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<h1 class="title">Hello, there!</h1>
</div>
<div class="column is-narrow" v-if="secured">
<a class="button is-primary is-small" :href="`${base}/logout`">Logout</a>
<a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
</div>
</div>
</div>
@@ -18,31 +15,31 @@
<div class="level-item has-text-centered">
<div>
<p class="title">{{ containers.length }}</p>
<p class="heading">Total Containers</p>
<p class="heading">{{ $t("label.total-containers") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">Running</p>
<p class="heading">{{ $t("label.running") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
<p class="heading">Total CPU Usage</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
<p class="heading">Total Mem Usage</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">Dozzle Version</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</section>
@@ -50,15 +47,15 @@
<section class="columns is-centered section is-marginless">
<div class="column is-4">
<div class="panel">
<p class="panel-heading">Containers</p>
<p class="panel-heading">{{ $t("label.containers") }}</p>
<div class="panel-block">
<p class="control has-icons-left">
<input
class="input"
type="text"
placeholder="Search Containers"
v-model="search"
@keyup.esc="search = null"
:placeholder="$t('placeholder.search-containers')"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
@@ -66,13 +63,13 @@
</span>
</p>
</div>
<p class="panel-tabs" v-if="!search">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
v-for="item in results.slice(0, 10)"
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
@@ -89,63 +86,60 @@
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { formatBytes } from "@/utils";
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime.vue";
import config from "@/stores/config";
import { useIntervalFn } from "@vueuse/core";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = ref("running");
const search = ref();
const sort = $ref("running");
const query = ref("");
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort.value) {
switch (sort) {
case "all":
return mostRecentContainers.value;
return mostRecentContainers;
case "running":
return runningContainers.value;
return runningContainers;
default:
throw `Invalid sort order: ${sort.value}`;
throw `Invalid sort order: ${sort}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
const totalCpu = ref(0);
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
const totalMem = ref(0);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
router.push({ name: "container", params: { id: item.id, name: item.name } });
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}
</script>

View File

@@ -8,7 +8,7 @@
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">Username</label>
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
@@ -22,7 +22,7 @@
</div>
<div class="field">
<label class="label">Password</label>
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
@@ -32,11 +32,11 @@
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">Username and password are not valid.</p>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">Login</button>
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
@@ -49,35 +49,31 @@
</div>
</template>
<script lang="ts">
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
export default {
name: "Login",
data() {
return {
username: null,
password: null,
error: false,
};
},
setup() {
setTitle("Authentication Required");
},
methods: {
async onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
body: new FormData(this.$refs.form),
method: "post",
});
<script lang="ts" setup>
const { t } = useI18n();
if (response.status == 200) {
this.error = false;
window.location.href = `${config.base}/`;
} else {
this.error = true;
}
},
},
};
setTitle(t("title.login"));
let error = $ref(false);
let username = $ref("");
let password = $ref("");
let form: HTMLFormElement = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
body: new FormData(form),
method: "post",
});
if (response.status == 200) {
error = false;
window.location.href = `${config.base}/`;
} else {
error = true;
}
}
</script>
<route lang="yaml">
meta:
layout: splash
</route>

View File

@@ -2,35 +2,32 @@
<div>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">About</h2>
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
</div>
<div>
You are using Dozzle <i>{{ currentVersion }}</i
>.
<span v-if="hasUpdate">
New version is available! Update to
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
{{ nextRelease.name }}</a
>.
</span>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
></div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Display</h2>
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> Show timestamps </o-switch>
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> Soft wrap lines</o-switch>
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
</div>
<div class="item">
@@ -54,7 +51,7 @@
</o-field>
</div>
<div class="column">
By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
{{ $t("settings.12-24-format") }}
</div>
</div>
</div>
@@ -83,7 +80,7 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Font size to use for logs</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
</div>
<div class="item">
@@ -111,33 +108,30 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Color scheme</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Options</h2>
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
</div>
<div class="item">
<o-switch v-model="search">
Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
<span v-html="$t('settings.search')"></span>
</o-switch>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import gt from "semver/functions/gt";
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
import {
search,
lightTheme,
@@ -149,22 +143,28 @@ import {
softWrap,
} from "@/composables/settings";
setTitle("Settings");
const { t } = useI18n();
const currentVersion = config.version;
const nextRelease = ref({ html_url: "", name: "" });
const hasUpdate = ref(false);
setTitle(t("title.settings"));
const currentVersion = $ref(config.version);
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate.value = gt(release.tag_name, currentVersion);
nextRelease.value = release;
hasUpdate = gt(release.tag_name, currentVersion);
nextRelease = release;
}
} else {
hasUpdate.value = true;
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}

View File

@@ -1,9 +1,4 @@
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
@@ -15,15 +10,16 @@ watch(visibleContainers, (newValue) => {
if (route.query.name) {
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
if (container) {
router.push({ name: "container", params: { id: container.id } });
router.push({ name: "container-id", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
}
});
</script>
<template></template>

View File

@@ -4,19 +4,14 @@
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">This page does not exist.</small>
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
</div>
</div>
</div>
</template>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "PageNotFound",
setup() {
setTitle("Page not found");
},
};
<script lang="ts" setup>
const { t } = useI18n();
setTitle(t("title.page-not-found"));
</script>

View File

@@ -0,0 +1,20 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</template>
<script lang="ts" setup>
const store = useContainerStore();
const { id } = defineProps<{ id: string }>();
const currentContainer = store.currentContainer($$(id));
const { activeContainers } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>

View File

@@ -1,7 +0,0 @@
export { default as Index } from "./Index.vue";
export { default as ContainerNotFound } from "./ContainerNotFound.vue";
export { default as Show } from "./Show.vue";
export { default as Container } from "./Container.vue";
export { default as Settings } from "./Settings.vue";
export { default as PageNotFound } from "./PageNotFound.vue";
export { default as Login } from "./Login.vue";

170
assets/pages/index.vue Normal file
View File

@@ -0,0 +1,170 @@
<template>
<div>
<section class="hero is-small mt-4">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column is-narrow" v-if="secured">
<a class="button is-primary is-small" :href="`${base}/logout`">{{ $t("button.logout") }}</a>
</div>
</div>
</div>
</div>
</section>
<section class="level section">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ containers.length }}</p>
<p class="heading">{{ $t("label.total-containers") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">{{ $t("label.running") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</section>
<section class="columns is-centered section is-marginless">
<div class="column is-4">
<div class="panel">
<p class="panel-heading">{{ $t("label.containers") }}</p>
<div class="panel-block">
<p class="control has-icons-left">
<input
class="input"
type="text"
:placeholder="$t('placeholder.search-containers')"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
<search-icon />
</span>
</p>
</div>
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
<span class="name">{{ item.name }}</span>
<div class="subtitle is-7 status">
<past-time :date="new Date(item.created * 1000)"></past-time>
</div>
</router-link>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import SearchIcon from "~icons/mdi-light/magnify";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = $ref("running");
const query = ref("");
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort) {
case "all":
return mostRecentContainers;
case "running":
return runningContainers;
default:
throw `Invalid sort order: ${sort}`;
}
});
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}
</script>
<style lang="scss" scoped>
.panel {
border: 1px solid var(--border-color);
.panel-block,
.panel-tabs {
border-color: var(--border-color);
.is-active {
border-color: var(--border-hover-color);
}
.name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.status {
margin-left: auto;
white-space: nowrap;
}
}
}
.icon {
padding: 10px 3px;
}
</style>

79
assets/pages/login.vue Normal file
View File

@@ -0,0 +1,79 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container">
<section class="columns is-centered section">
<div class="column is-4">
<div class="card">
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const { t } = useI18n();
setTitle(t("title.login"));
let error = $ref(false);
let username = $ref("");
let password = $ref("");
let form: HTMLFormElement = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
body: new FormData(form),
method: "post",
});
if (response.status == 200) {
error = false;
window.location.href = `${config.base}/`;
} else {
error = true;
}
}
</script>
<route lang="yaml">
meta:
layout: splash
</route>

203
assets/pages/settings.vue Normal file
View File

@@ -0,0 +1,203 @@
<template>
<div>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
</div>
<div>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
></div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="hourStyle" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">
{{ $t("settings.12-24-format") }}
</div>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="lightTheme" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['auto', 'dark', 'light']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
</div>
<div class="item">
<o-switch v-model="search">
<span v-html="$t('settings.search')"></span>
</o-switch>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import gt from "semver/functions/gt";
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
hourStyle,
showAllContainers,
size,
softWrap,
} from "@/composables/settings";
const { t } = useI18n();
setTitle(t("title.settings"));
const currentVersion = $ref(config.version);
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate = gt(release.tag_name, currentVersion);
nextRelease = release;
}
} else {
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}
fetchNextRelease();
</script>
<style lang="scss" scoped>
.title {
color: var(--title-color);
}
a.next-release {
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
.section {
padding: 1rem 1.5rem;
}
.has-underline {
border-bottom: 1px solid var(--border-color);
padding: 1em 0px;
margin-bottom: 1em;
}
.item {
padding: 1em 0;
}
code {
border-radius: 4px;
background-color: #444;
}
</style>

25
assets/pages/show.vue Normal file
View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
const router = useRouter();
const route = useRoute();
const store = useContainerStore();
const { visibleContainers } = storeToRefs(store);
watch(visibleContainers, (newValue) => {
if (newValue) {
if (route.query.name) {
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
if (container) {
router.push({ name: "container-id", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
router.push({ name: "index" });
}
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
router.push({ name: "index" });
}
}
});
</script>
<template></template>

View File

@@ -1,6 +1,20 @@
const text = document.querySelector("script#config__json")?.textContent || "{}";
const config = JSON.parse(text);
interface Config {
version: string;
base: string;
authorizationNeeded: boolean | "false" | "true";
secured: boolean | "false" | "true";
maxLogs: number;
}
const pageConfig = JSON.parse(text);
const config: Config = {
maxLogs: 600,
...pageConfig,
};
if (config.version == "{{ .Version }}") {
config.version = "master";
config.base = "";
@@ -11,4 +25,5 @@ if (config.version == "{{ .Version }}") {
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
}
export default config;
export default config as Config;

View File

@@ -1,14 +1,11 @@
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";
import { watchOnce } from "@vueuse/core";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
export const useContainerStore = defineStore("container", () => {
const containers = ref<Container[]>([]);
const activeContainerIds = ref<string[]>([]);
const containers: Ref<Container[]> = ref([]);
const activeContainerIds: Ref<string[]> = ref([]);
const allContainersById = computed(() =>
containers.value.reduce((acc, container) => {
@@ -25,33 +22,36 @@ export const useContainerStore = defineStore("container", () => {
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("containers-changed", (e: Event) =>
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
);
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;
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
}
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
const setContainers = (newContainers: ContainerJson[]) => {
containers.value = newContainers.map((c) => {
const existing = allContainersById.value[c.id];
if (existing) {
existing.status = c.status;
existing.state = c.state;
return existing;
}
},
false
);
es.addEventListener(
"container-die",
(e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
},
false
);
return new Container(c.id, c.created, c.image, c.name, c.command, c.status, c.state);
});
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);

View File

@@ -159,6 +159,12 @@ html.has-custom-scrollbars {
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
.splitpanes__splitter {
z-index: 99;
}
@@ -176,3 +182,19 @@ html.has-custom-scrollbars {
.button .button-wrapper > span {
display: contents;
}
mark {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}

View File

@@ -1,17 +1,18 @@
export interface Container {
readonly id: string;
readonly created: number;
readonly image: string;
readonly name: string;
readonly status: string;
readonly command: string;
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
stat?: ContainerStat;
}
export interface ContainerStat {
readonly id: string;
readonly cpu: number;
readonly memory: number;
readonly memoryUsage: number;
}
export type ContainerJson = {
readonly id: string;
readonly created: number;
readonly image: string;
readonly name: string;
readonly command: string;
readonly status: string;
readonly state: ContainerState;
};
export type ContainerState = "created" | "running" | "exited" | "dead" | "paused" | "restarting";

View File

@@ -1,16 +0,0 @@
export interface LogEntry {
readonly date: Date;
readonly message: string | JSONObject;
readonly id: number;
event?: string;
selected?: boolean;
}
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };

1
assets/types/Point.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
type Point = { x: number; y: number };

View File

@@ -1,54 +0,0 @@
import { computed, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import type { JSONObject, LogEntry } from "./LogEntry";
export class VisibleLogEntry implements LogEntry {
private readonly entry: LogEntry;
filteredMessage: undefined | ComputedRef<Record<string, any>>;
constructor(entry: LogEntry, visibleKeys: Ref<string[][]>) {
this.entry = entry;
this.filteredMessage = undefined;
if (this.isComplex()) {
const message = this.message;
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
}
}
public isComplex(): this is { message: JSONObject } {
return typeof this.entry.message === "object";
}
public isSimple(): this is { message: string } {
return !this.isComplex();
}
public get unfilteredPayload(): JSONObject {
if (typeof this.entry.message === "string") {
throw new Error("Cannot get unfiltered payload of a simple message");
}
return this.entry.message;
}
public get date(): Date {
return this.entry.date;
}
public get message(): string | JSONObject {
return this.filteredMessage?.value ?? this.entry.message;
}
public get id(): number {
return this.entry.id;
}
public get event(): string | undefined {
return this.entry.event;
}
}

View File

@@ -1,4 +1,4 @@
import { Container } from "@/types/Container";
import { Container } from "@/models/Container";
import { useStorage } from "@vueuse/core";
import { computed, ComputedRef } from "vue";

17
docker/calculation.go Normal file
View File

@@ -0,0 +1,17 @@
package docker
import "github.com/docker/docker/api/types"
func calculateMemUsageUnixNoCache(mem types.MemoryStats) float64 {
// re implementation of the docker calculation
// https://github.com/docker/cli/blob/53f8ed4bec07084db4208f55987a2ea94b7f01d6/cli/command/container/stats_helpers.go#L227-L249
// cgroup v1
if v, isCGroup := mem.Stats["total_inactive_file"]; isCGroup && v < mem.Usage {
return float64(mem.Usage - v)
}
// cgroup v2
if v := mem.Stats["inactive_file"]; v < mem.Usage {
return float64(mem.Usage - v)
}
return float64(mem.Usage)
}

View File

@@ -0,0 +1,57 @@
package docker
import (
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"testing"
)
func Test_calculateMemUsageUnixNoCache(t *testing.T) {
type args struct {
mem types.MemoryStats
}
tests := []struct {
name string
args args
want float64
}{
{
name: "with cgroup v1",
args: args{
mem: types.MemoryStats{
Usage: 100,
Stats: map[string]uint64{
"total_inactive_file": 1,
},
},
},
want: 99,
},
{
name: "with cgroup v2",
args: args{
mem: types.MemoryStats{
Usage: 100,
Stats: map[string]uint64{
"inactive_file": 2,
},
},
},
want: 98,
},
{
name: "without cgroup",
args: args{
mem: types.MemoryStats{
Usage: 100,
},
},
want: 100,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, calculateMemUsageUnixNoCache(tt.args.mem), "calculateMemUsageUnixNoCache(%v)", tt.args.mem)
})
}
}

View File

@@ -147,7 +147,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(v.PreCPUStats.CPUUsage.TotalUsage)
systemDelta = float64(v.CPUStats.SystemUsage) - float64(v.PreCPUStats.SystemUsage)
cpuPercent = int64((cpuDelta / systemDelta) * float64(ncpus) * 100)
memUsage = int64(v.MemoryStats.Usage - v.MemoryStats.Stats["cache"])
memUsage = int64(calculateMemUsageUnixNoCache(v.MemoryStats))
memPercent = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
)

View File

@@ -1,4 +1,4 @@
FROM cypress/included:10.6.0
FROM cypress/included:10.10.0
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm

View File

@@ -1,3 +1,4 @@
{
"DOZZLE_DEFAULT": "http://localhost:8080/"
"DOZZLE_DEFAULT": "http://localhost:8080/",
"DOZZLE_AUTH": "http://localhost:8080/"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,14 @@
/// <reference types="cypress" />
context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_AUTH") }, () => {
beforeEach(() => {
cy.visit("/");
});
it("login screen", () => {
cy.get("input[name=username]").type("foo");
cy.get("input[name=password]").type("bar");
cy.get("button[type=submit]").click();
cy.get("p.menu-label").should("contain", "Containers");
});
});

View File

@@ -9,7 +9,7 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
});
it("correct title", () => {
it("correct title is shown", () => {
cy.title().should("eq", "1 containers - Dozzle");
cy.get("li.running:first a").click();
@@ -17,9 +17,15 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
cy.title().should("include", "- Dozzle");
});
it("settings page", () => {
it("navigating to setting page works ", () => {
cy.get("a[href='/settings']").click();
cy.contains("About");
});
it("shortcut for fuzzy search works", () => {
cy.get("body").type("{ctrl}k");
cy.get("input[placeholder='Search containers (⌘ + k, ⌃k)']").should("be.visible");
});
});

View File

@@ -0,0 +1,17 @@
/// <reference types="cypress" />
context("Dozzle es lang", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
beforeEach(() => {
cy.visit("/", {
onBeforeLoad(win) {
Object.defineProperty(win.navigator, "language", {
value: "es_MX",
});
},
});
});
it("should find contenedores", () => {
cy.get("p.menu-label").should("contain", "Contenedores");
});
});

View File

@@ -0,0 +1,15 @@
/// <reference types="cypress" />
context("Dozzle custom base", { baseUrl: Cypress.env("DOZZLE_CUSTOM") }, () => {
beforeEach(() => {
cy.visit("/");
});
it("custom base should work", () => {
cy.get("p.menu-label").should("contain", "Containers");
});
it("url should be custom", () => {
cy.url().should("include", "foobarbase");
});
});

View File

@@ -8,10 +8,25 @@ services:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
ports:
- "8080:8080"
image: amir20/dozzle_custom_cache
build:
context: ..
cache_from:
- amir20/dozzle_custom_cache:latest
auth:
container_name: auth
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=auth
- DOZZLE_USERNAME=foo
- DOZZLE_PASSWORD=bar
- DOZZLE_NO_ANALYTICS=1
image: amir20/dozzle_custom_cache
build:
context: ..
cache_from:
- amir20/dozzle_custom_cache:latest
dozzle:
container_name: dozzle
volumes:
@@ -19,20 +34,25 @@ services:
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- "9090:8080"
image: amir20/dozzle_cache:latest
build:
context: ..
cache_from:
- amir20/dozzle_cache:latest
cypress:
build:
context: .
cache_from:
- amir20/dozzle_cypress_cache:latest
image: amir20/dozzle_cypress_cache:latest
working_dir: /e2e
volumes:
- ./cypress:/e2e/cypress
- ./cypress.config.ts:/e2e/cypress.config.ts
environment:
- CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
- CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
- CYPRESS_DOZZLE_AUTH=http://auth:8080/
- CYPRESS_DOZZLE_CUSTOM=http://custom_base:8080/foobarbase
- CYPRESS_RECORD_KEY=155c3cf8-b2dd-4f5e-9fb3-7635f5b79d4d
- COMMIT_INFO_BRANCH=${GITHUB_REF_NAME}
- COMMIT_INFO_AUTHOR=${GITHUB_ACTOR}

View File

@@ -1,11 +1,13 @@
{
"name": "e2e",
"version": "1.0.0",
"scripts": {},
"scripts": {
"test": "cypress run"
},
"license": "ISC",
"dependencies": {
"@frsource/cypress-plugin-visual-regression-diff": "^1.9.2",
"cypress": "^10.6.0",
"typescript": "^4.7.4"
"@frsource/cypress-plugin-visual-regression-diff": "^2.0.0",
"cypress": "^10.8.0",
"typescript": "^4.8.3"
}
}

50
e2e/pnpm-lock.yaml generated
View File

@@ -1,14 +1,14 @@
lockfileVersion: 5.4
specifiers:
'@frsource/cypress-plugin-visual-regression-diff': ^1.9.2
cypress: ^10.6.0
typescript: ^4.7.4
'@frsource/cypress-plugin-visual-regression-diff': ^2.0.0
cypress: ^10.8.0
typescript: ^4.8.3
dependencies:
'@frsource/cypress-plugin-visual-regression-diff': 1.9.2_cypress@10.6.0
cypress: 10.6.0
typescript: 4.7.4
'@frsource/cypress-plugin-visual-regression-diff': 2.0.0_cypress@10.8.0
cypress: 10.8.0
typescript: 4.8.3
packages:
@@ -52,21 +52,21 @@ packages:
- supports-color
dev: false
/@frsource/cypress-plugin-visual-regression-diff/1.9.2_cypress@10.6.0:
resolution: {integrity: sha512-jtriX93BwHC1AIetlOU3xKKTmm/q1a+zysBOZqmAQ3e9uv/vD34F09uF/7JXKLoeza3OZf7eWQIERjA3yPa+bA==}
/@frsource/cypress-plugin-visual-regression-diff/2.0.0_cypress@10.8.0:
resolution: {integrity: sha512-hwMEZzD0tFbNNNbEjdF8q2ELpxhby25VQ665QUJy1soGPiGwY+rBDdktFg3EXVGs/Euq0Nj8hq/K18BWYoX10g==}
engines: {node: '>=10'}
peerDependencies:
cypress: '>=4.5.0'
dependencies:
cypress: 10.6.0
cypress: 10.8.0
move-file: 2.1.0
pixelmatch: 5.3.0
pngjs: 6.0.0
sharp: 0.30.7
dev: false
/@types/node/14.18.24:
resolution: {integrity: sha512-aJdn8XErcSrfr7k8ZDDfU6/2OgjZcB2Fu9d+ESK8D7Oa5mtsv8Fa8GpcwTA0v60kuZBaalKPzuzun4Ov1YWO/w==}
/@types/node/14.18.29:
resolution: {integrity: sha512-LhF+9fbIX4iPzhsRLpK5H7iPdvW8L4IwGciXQIOEcuF62+9nw/VQVsOViAOOGxY3OlOKGLFv0sWwJXdwQeTn6A==}
dev: false
/@types/sinonjs__fake-timers/8.1.1:
@@ -81,7 +81,7 @@ packages:
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
requiresBuild: true
dependencies:
'@types/node': 14.18.24
'@types/node': 14.18.29
dev: false
optional: true
@@ -232,8 +232,8 @@ packages:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/ci-info/3.3.2:
resolution: {integrity: sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==}
/ci-info/3.4.0:
resolution: {integrity: sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==}
dev: false
/clean-stack/2.2.0:
@@ -248,8 +248,8 @@ packages:
restore-cursor: 3.1.0
dev: false
/cli-table3/0.6.2:
resolution: {integrity: sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==}
/cli-table3/0.6.3:
resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==}
engines: {node: 10.* || >= 12.*}
dependencies:
string-width: 4.2.3
@@ -313,7 +313,7 @@ packages:
dev: false
/concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: false
/core-util-is/1.0.2:
@@ -329,15 +329,15 @@ packages:
which: 2.0.2
dev: false
/cypress/10.6.0:
resolution: {integrity: sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==}
/cypress/10.8.0:
resolution: {integrity: sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==}
engines: {node: '>=12.0.0'}
hasBin: true
requiresBuild: true
dependencies:
'@cypress/request': 2.88.10
'@cypress/xvfb': 1.2.4_supports-color@8.1.1
'@types/node': 14.18.24
'@types/node': 14.18.29
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.3
arch: 2.2.0
@@ -348,7 +348,7 @@ packages:
chalk: 4.1.2
check-more-types: 2.24.0
cli-cursor: 3.1.0
cli-table3: 0.6.2
cli-table3: 0.6.3
commander: 5.1.0
common-tags: 1.8.2
dayjs: 1.11.5
@@ -665,7 +665,7 @@ packages:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
hasBin: true
dependencies:
ci-info: 3.3.2
ci-info: 3.4.0
dev: false
/is-fullwidth-code-point/3.0.0:
@@ -1243,8 +1243,8 @@ packages:
engines: {node: '>=10'}
dev: false
/typescript/4.7.4:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
/typescript/4.8.3:
resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: false
@@ -1269,7 +1269,7 @@ packages:
dev: false
/verror/1.10.0:
resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=}
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
dependencies:
assert-plus: 1.0.0

2
go.mod
View File

@@ -5,7 +5,7 @@ require (
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.17+incompatible
github.com/docker/docker v20.10.19+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0

4
go.sum
View File

@@ -62,8 +62,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.19+incompatible h1:lzEmjivyNHFHMNAFLXORMBXyGIhw/UP4DvJwvyKYq64=
github.com/docker/docker v20.10.19+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=

50
locales/en.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Clear
download: Download
search: Search
label:
containers: Containers
total-containers: Total Containers
running: Running
total-cpu-usage: Total CPU Usage
total-mem-usage: Total Mem Usage
dozzle-version: Dozzle Version
all: All
password: Password
username: Username
tooltip:
search: Search containers (⌘ + k, ⌃k)
pin-column: Pin as column
error:
page-not-found: This page does not exist.
invalid-auth: Username and password are not valid.
logs-skipped: Skipped {total} entries
title:
page-not-found: Page not found
login: Authentication Required
settings: Settings
button:
logout: Logout
login: Login
placeholder:
search-containers: Search containers (⌘ + k, ⌃k)
settings:
display: Display
small-scrollbars: Use smaller scrollbars
show-timesamps: Show timestamps
soft-wrap: Soft wrap lines
12-24-format: >-
By default, Dozzle will use your browser's locale to format time. You can
force to 12 or 24 hour style.
font-size: Font size to use for logs
color-scheme: Color scheme
options: Options
show-stopped-containers: Show stopped containers
about: About
search: >-
Enable searching with Dozzle using <code>command+f</code> or
<code>ctrl+f</code>
using-version: You are using Dozzle {version}.
update-available: >-
New version is available! Update to <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

50
locales/es.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Limpiar
download: Descargar
search: Buscar
label:
containers: Contenedores
total-containers: Contenedores Totales
running: En ejecución
total-cpu-usage: Uso total del Procesador
total-mem-usage: Uso total de la Memoria
dozzle-version: Versión de Dozzle
all: Todo
password: Contraseña
username: Nombre de usuario
tooltip:
search: Buscar contenedores (⌘ + K, CTRL + K)
pin-column: Anclar como columna
error:
page-not-found: Esta página no existe.
invalid-auth: El nombre de usuario y la contraseña no son válidos.
logs-skipped: Omitidas {total} entrada/s
title:
page-not-found: Página no encontrada
login: Autenticación requerida
settings: Configuración
button:
logout: Cerrar la sesión
login: Iniciar sesión
placeholder:
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
settings:
display: Vista
small-scrollbars: Utilizar barras de desplazamiento más pequeñas
show-timesamps: Mostrar marcas de tiempo
soft-wrap: Líneas de texto con ajuste suave
12-24-format: >-
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
forzar el estilo de 12 o 24 horas.
font-size: Tamaño de letra a utilizar para los registros
color-scheme: Esquema de colores
options: Opciones
show-stopped-containers: Mostrar contenedores parados
about: Acerca de
search: >-
Activar la búsqueda con Dozzle mediante <code>comando+f</code> o
<code>ctrl+f</code>
using-version: Estás usando Dozzle {version}.
update-available: >-
¡La nueva versión está disponible! Actualizar a la
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

50
locales/pr.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Limpar
download: Descarregar
search: Pesquisa
label:
containers: Contentores
total-containers: Contentores Totais
running: Em execução
total-cpu-usage: Utilização total da CPU
total-mem-usage: Total de utilização da memória
dozzle-version: Versão Dozzle
all: Tudo
password: Senha
username: Nome de usuário
tooltip:
search: Pesquisar contentores (⌘ + K, CTRL + K)
pin-column: Alfinete como coluna
error:
page-not-found: Esta página não existe.
invalid-auth: O nome de usuário e a senha não são válidos.
logs-skipped: Saltado {total} entradas
title:
page-not-found: Página não encontrada
login: Autenticação Requerida
settings: Configurações
button:
logout: Terminar sessão
login: Iniciar sessão
placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
settings:
display: Visão
small-scrollbars: Usar barras de rolagem mais pequenas
show-timesamps: Mostrar carimbos de tempo
soft-wrap: Linhas de texto de embrulho suave
12-24-format: >-
Por defeito, Dozzle utilizará o locale do seu navegador para formatar a hora. Pode
forçar ao estilo de 12 ou 24 horas.
font-size: Tamanho de letra a utilizar para os registos
color-scheme: Esquema de cores
options: Opções
show-stopped-containers: Mostrar contentores parados
about: Acerca de
search: >-
Habilitar a pesquisa com Dozzle usando <code>comando+f</code> ou
<code>ctrl+f</code>
using-version: Está a usar o Dozzle {version}.
update-available: >-
Está disponível uma nova versão! Actualização para
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

View File

@@ -5,7 +5,6 @@ import (
"embed"
"io/fs"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"strings"

View File

@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "4.0.2",
"version": "4.2.0",
"description": "Realtime log viewer for docker containers. ",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
@@ -22,54 +22,69 @@
"postinstall": "husky install"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.7",
"@iconify-json/carbon": "^1.1.8",
"@iconify-json/cil": "^1.1.2",
"@iconify-json/mdi": "^1.1.30",
"@iconify-json/mdi": "^1.1.34",
"@iconify-json/mdi-light": "^1.1.2",
"@iconify-json/octicon": "^1.1.16",
"@oruga-ui/oruga-next": "^0.5.4",
"@oruga-ui/theme-bulma": "^0.2.6",
"@vitejs/plugin-vue": "3.0.3",
"@vue/compiler-sfc": "^3.2.37",
"@vueuse/core": "^9.1.0",
"@vueuse/router": "^9.1.0",
"@iconify-json/octicon": "^1.1.20",
"@oruga-ui/oruga-next": "^0.5.6",
"@oruga-ui/theme-bulma": "^0.2.7",
"@vueuse/core": "^9.3.0",
"@vueuse/integrations": "^9.3.0",
"@vueuse/router": "^9.3.0",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"date-fns": "^2.29.2",
"fuzzysort": "^2.0.1",
"hotkeys-js": "^3.9.4",
"d3-array": "^3.2.0",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.1.0",
"d3-transition": "^3.0.1",
"date-fns": "^2.29.3",
"fuse.js": "^6.6.2",
"lodash.debounce": "^4.0.8",
"pinia": "^2.0.20",
"sass": "^1.54.4",
"semver": "^7.3.7",
"pinia": "^2.0.23",
"semver": "^7.3.8",
"splitpanes": "^3.1.1",
"typescript": "^4.7.4",
"unplugin-auto-import": "^0.11.2",
"unplugin-icons": "^0.14.8",
"unplugin-vue-components": "^0.22.4",
"vite": "3.0.9",
"vue": "^3.2.37",
"vue-router": "^4.1.3"
"vue": "^3.2.41",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@pinia/testing": "^0.0.14",
"@types/jest": "^28.1.7",
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-scale": "^4.0.2",
"@types/d3-selection": "^3.0.3",
"@types/d3-shape": "^3.1.0",
"@types/d3-transition": "^3.0.2",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^18.7.7",
"@types/node": "^18.11.0",
"@types/semver": "^7.3.12",
"@vue/test-utils": "^2.0.2",
"@vitejs/plugin-vue": "3.1.2",
"@vue/compiler-sfc": "^3.2.41",
"@vue/test-utils": "^2.1.0",
"c8": "^7.12.0",
"eventsourcemock": "^2.0.0",
"husky": "^8.0.1",
"jest-serializer-vue": "^2.0.2",
"jsdom": "^20.0.0",
"jsdom": "^20.0.1",
"lint-staged": "^13.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"release-it": "^15.3.0",
"release-it": "^15.5.0",
"sass": "^1.55.0",
"ts-node": "^10.9.1",
"vitest": "^0.22.1",
"vue-tsc": "^0.40.1"
"typescript": "^4.8.4",
"unplugin-auto-import": "^0.11.2",
"unplugin-icons": "^0.14.11",
"unplugin-vue-components": "^0.22.8",
"vite": "3.1.8",
"vite-plugin-pages": "^0.26.0",
"vite-plugin-vue-layouts": "^0.7.0",
"vitest": "^0.24.3",
"vue-tsc": "^1.0.7"
},
"lint-staged": {
"*.{js,vue,css}": [

2164
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@
"@/*": ["assets/*"]
},
"jsx": "preserve",
"types": ["vitest", "vite/client", "vue/ref-macros", "vite-plugin-pages/client", "vite-plugin-vue-layouts/client"]
},
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],

View File

@@ -3,7 +3,11 @@ import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Icons from "unplugin-icons/vite";
import Components from "unplugin-vue-components/vite";
import AutoImport from "unplugin-auto-import/vite";
import IconsResolver from "unplugin-icons/resolver";
import Pages from "vite-plugin-pages";
import Layouts from "vite-plugin-vue-layouts";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
export default defineConfig(({ mode }) => ({
resolve: {
@@ -13,10 +17,19 @@ export default defineConfig(({ mode }) => ({
},
base: mode === "production" ? "/{{ .Base }}/" : "/",
plugins: [
vue(),
vue({
reactivityTransform: true,
}),
Icons({
autoInstall: true,
}),
Pages({
dirs: "assets/pages",
importMode: "sync",
}),
Layouts({
layoutsDirs: "assets/layouts",
}),
Components({
dirs: ["assets/components"],
resolvers: [
@@ -27,6 +40,17 @@ export default defineConfig(({ mode }) => ({
dts: "assets/components.d.ts",
}),
AutoImport({
imports: ["vue", "vue-router", "vue-i18n", "vue/macros", "pinia", "@vueuse/head", "@vueuse/core"],
dts: "assets/auto-imports.d.ts",
dirs: ["assets/composables", "assets/stores", "assets/utils"],
vueTemplate: true,
}),
VueI18n({
runtimeOnly: true,
compositionOnly: true,
include: [path.resolve(__dirname, "locales/**")],
}),
htmlPlugin(mode),
],
server: {
@@ -41,7 +65,7 @@ export default defineConfig(({ mode }) => ({
const htmlPlugin = (mode) => {
return {
name: "html-transform",
enforce: "post",
enforce: "post" as const,
transformIndexHtml(html) {
return mode === "production" ? html.replaceAll("/{{ .Base }}/", "{{ .Base }}/") : html;
},

View File

@@ -63,10 +63,6 @@ func createRouter(h *handler) *mux.Router {
s.Handle("/version", authorizationRequired(h.version))
s.HandleFunc("/healthcheck", h.healthcheck)
if log.IsLevelEnabled(log.DebugLevel) {
s.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)
}
if base != "/" {
s.PathPrefix("/").Handler(http.StripPrefix(base+"/", http.HandlerFunc(h.index)))
} else {