Compare commits

...

184 Commits

Author SHA1 Message Date
Amir Raminfar ce39d3837f Release 4.6.2 2023-01-02 10:06:09 -08:00
Amir Raminfar 884dd94242 Adds analytics for requests made (#1999) 2023-01-02 09:28:27 -08:00
Amir Raminfar df2ff662dd Removes outdated i18n plugin (#1998) 2022-12-29 08:27:54 -08:00
renovate[bot] f602d4e69d Update dependency vite to v4.0.3 (#1996) 2022-12-25 08:02:53 -08:00
renovate[bot] 27f898b696 Update cypress/included Docker tag to v12.2.0 (#1995) 2022-12-25 08:02:46 -08:00
renovate[bot] 400afddc45 Update dependency vitest to ^0.26.0 (#1994) 2022-12-20 07:06:03 -08:00
Amir Raminfar edf7edced3 Release 4.6.1 2022-12-18 14:44:29 -08:00
Amir Raminfar 0ef5c36900 Fixes #1990 by only coming list when relative fields change (#1993) 2022-12-18 14:38:29 -08:00
renovate[bot] a008ede927 Update dependency vite to v4.0.2 (#1992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-18 14:18:39 -08:00
renovate[bot] b4e88937cc Update module github.com/docker/docker to v20.10.22+incompatible (#1989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-18 14:18:32 -08:00
renovate[bot] fd3992cabd Update cypress/included Docker tag to v12.1.0 (#1984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-14 11:00:42 -08:00
renovate[bot] 47bed89fe2 Update dependency vite to v4.0.1 (#1979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-12 13:31:39 -08:00
Amir Raminfar 361fff7803 Release 4.6.0 2022-12-12 09:38:17 -08:00
Amir Raminfar a6ba94e03f Adds hostname config for setting UI parameter (#1983)
* Adds hostname config to setting a hostname in title

* Also updates title for hostname
2022-12-12 09:37:54 -08:00
renovate[bot] d39e2514b2 Update dependency vite-plugin-pages to ^0.28.0 (#1982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-12 09:14:35 -08:00
Amir Raminfar 155757dd74 Removes tail size (#1981)
* Removes tail size

* Removes tail from analytics
2022-12-12 09:14:29 -08:00
Amir Raminfar cc97e90a3e Release 4.5.0 2022-12-09 10:20:28 -08:00
Amir Raminfar 68160dd9c4 Updates vite to v4 (#1980) 2022-12-09 10:17:28 -08:00
renovate[bot] 7694c95e1a Update module github.com/magiconair/properties to v1.8.7 (#1977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 09:56:48 -08:00
renovate[bot] 5b91a4a9e8 Update cypress/included Docker tag to v12.0.2 (#1976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 09:56:41 -08:00
renovate[bot] 7c56449874 Update cypress/included Docker tag to v12.0.1 (#1975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-07 09:50:21 -08:00
renovate[bot] 3dbe83fd3e Update dependency vite to v3.2.5 (#1971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-06 15:01:01 -08:00
renovate[bot] 2a3bce8cd8 Update cypress/included Docker tag to v12 (#1972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-06 15:00:50 -08:00
renovate[bot] d3303b2436 Update dependency cypress to v12 (#1973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-06 15:00:43 -08:00
renovate[bot] 5d2717d584 Update golang Docker tag to v1.19.4 (#1974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-06 15:00:35 -08:00
Amir Raminfar e22fd87d45 Fixes #1963 2022-11-30 08:49:21 -08:00
Amir Raminfar 3a6f1bf92c Updates modules 2022-11-29 14:06:19 -08:00
IrwenXYZ ae3283adda Update readme for memory consumption on ARM devices (#1967) 2022-11-25 20:19:39 -08:00
renovate[bot] 145158a925 Update dependency unplugin-auto-import to ^0.12.0 (#1966) 2022-11-25 09:56:19 -08:00
renovate[bot] e46e1a0814 Update cypress/included Docker tag to v11.2.0 (#1961) 2022-11-23 06:34:10 -08:00
renovate[bot] 7a624cd065 Update dependency jest-serializer-vue to v3 (#1959) 2022-11-23 06:33:56 -08:00
Amir Raminfar fe7c8743b9 Removes depedenbot 2022-11-15 15:40:58 -08:00
renovate[bot] ccc1aae78e Update cypress/included Docker tag to v11.1.0 (#1958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-15 13:05:01 -08:00
renovate[bot] 935e17c200 Update dependency vite to v3.2.4 (#1957) 2022-11-15 07:11:03 -08:00
renovate[bot] 63bb1f24a3 Update module github.com/spf13/afero to v1.9.3 (#1954) 2022-11-14 20:11:08 -08:00
renovate[bot] cdc8189ad1 Update module go to 1.19 (#1955) 2022-11-14 20:09:59 -08:00
renovate[bot] f6daab2111 Add renovate.json (#1953) 2022-11-14 19:43:38 -08:00
Amir Raminfar 7ed461e6ef Removes npmrc and hositing is not needed anymore (#1952) 2022-11-14 09:42:01 -08:00
Amir Raminfar 15636348b5 Updates modules 2022-11-10 09:35:11 -08:00
kodiakhq[bot] a370ad48d5 Merge pull request #1950 from amir20/dependabot/docker/e2e/cypress/included-11.0.1
Bump cypress/included from 11.0.0 to 11.0.1 in /e2e
2022-11-10 09:11:52 +00:00
dependabot[bot] 8900631b20 Bump cypress/included from 11.0.0 to 11.0.1 in /e2e
Bumps cypress/included from 11.0.0 to 11.0.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-10 09:06:19 +00:00
Amir Raminfar fa3ef24999 Updates cypress to 11.x (#1949) 2022-11-09 09:11:02 -08:00
kodiakhq[bot] deaf58c47b Merge pull request #1948 from amir20/dependabot/docker/e2e/cypress/included-11.0.0
Bump cypress/included from 10.11.0 to 11.0.0 in /e2e
2022-11-09 09:11:24 +00:00
dependabot[bot] 1d4573a657 Bump cypress/included from 10.11.0 to 11.0.0 in /e2e
Bumps cypress/included from 10.11.0 to 11.0.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-09 09:07:24 +00:00
Amir Raminfar 84300fc734 Release 4.4.1 2022-11-08 15:10:14 -08:00
Dmitry Mazurov 2b9873e76b Update mobile menu (#1946) 2022-11-08 15:09:57 -08:00
Amir Raminfar c589c599fc Release 4.4.0 2022-11-08 12:48:48 -08:00
Amir Raminfar 2702e80314 Adds hover for buttons 2022-11-08 12:47:10 -08:00
Dmitry Mazurov bb1dda41ff Add save collapse to localstorage (#1945) 2022-11-08 12:40:13 -08:00
Amir Raminfar ddafd95b69 Fixes int tests 2022-11-08 11:54:37 -08:00
Amir Raminfar d6e87a7137 Fixes collapseNav 2022-11-08 11:49:40 -08:00
Dmitry Mazurov 54eca89c4e Moved logout button to sidebar and fix default width sidebar. (#1942)
* Moved logout button to sidebar and fix default width sidebar.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Fix logout url. Save collapse to localstorage.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Clear code.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Update logout button if collapse

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Fix dark mode for collapse button. Update toolbar. Remove localstorage.

* Fix collapse auto-import

* Remove collapse from settings

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-08 11:45:11 -08:00
Amir Raminfar fbe34d7377 Updates modules 2022-11-07 09:08:29 -08:00
Dmitry Mazurov e2b497f158 Fix search modal for mobile view (#1941)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-04 10:36:19 -07:00
Amir Raminfar ca9fcf8e1d Updates snapshots 2022-11-03 13:43:24 -07:00
Dmitry Mazurov 4fb012c0e4 Merge branch 'amir20:master' into responsible 2022-11-03 22:48:44 +03:00
Dmitry Mazurov b77448f2a9 Fix responsible list containers
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-03 22:09:56 +03:00
Amir Raminfar 3bc921c8d7 Updates auto import 2022-11-03 09:51:17 -07:00
Amir Raminfar 54c08cf79b Removes duplicate case sensistive files (#1939) 2022-11-03 09:41:59 -07:00
Amir Raminfar 199c51722c Also skip docker login 2022-11-02 10:43:08 -07:00
Amir Raminfar 379af164dd Skips only the push int tests (#1937) 2022-11-02 09:16:45 -07:00
Dmitry Mazurov dc4ba652b4 Skip Integration Tests and Push branches and PRs from fork (#1936)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-02 07:24:39 -07:00
kodiakhq[bot] 0e8561a1c5 Merge pull request #1935 from amir20/dependabot/docker/golang-1.19.3-alpine
Bump golang from 1.19.2-alpine to 1.19.3-alpine
2022-11-02 09:29:57 +00:00
dependabot[bot] 60ec928a8e Bump golang from 1.19.2-alpine to 1.19.3-alpine
Bumps golang from 1.19.2-alpine to 1.19.3-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-11-02 09:26:23 +00:00
Dmitry Mazurov 26f3ab65a7 Update ru locale (#1934) 2022-11-01 11:36:02 -07:00
Amir Raminfar 0cc209484d Use trim instead 2022-11-01 10:57:49 -07:00
Amir Raminfar f09a7815af Cleans up DockerSecret 2022-11-01 09:08:05 -07:00
Amir Raminfar 70b573cec0 Fixes spacing 2022-10-31 15:11:33 -07:00
Amir Raminfar 0d79196eda Updates cypress 2022-10-31 10:21:41 -07:00
Amir Raminfar 8eb4e05055 Release 4.3.0 2022-10-31 09:33:26 -07:00
Amir Raminfar cd793a9b53 Updates modules 2022-10-31 09:32:21 -07:00
Dmitry Mazurov fad75e2f5b Fix read username and password from file (#1930)
* Fix read username and password from file

* Update Readme

* Add ru locale

* Update read the file from UnmarshalText

* Fix if not set username and password

* Fix if content in file empty

* Update ru locale

* Fix errorMsg for auth

* Removed local ru

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Remove struct for username and password

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-10-31 09:31:27 -07:00
Dmitry Mazurov d6710453e5 Add ru locale (#1932)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-10-31 09:03:47 -07:00
Amir Raminfar 3a8c9cbcfa Release 4.2.3 2022-10-28 14:42:46 -07:00
Dmitry Mazurov 8586fee5ab Add docker secrets env (#1929) 2022-10-28 14:31:48 -07:00
Amir Raminfar 8ccae473ed Updates modules 2022-10-28 12:24:42 -07:00
kodiakhq[bot] 02434afef3 Merge pull request #1923 from amir20/dependabot/docker/e2e/cypress/included-10.11.0
Bump cypress/included from 10.10.0 to 10.11.0 in /e2e
2022-10-26 09:45:31 +00:00
dependabot[bot] 192ee98dd1 Bump cypress/included from 10.10.0 to 10.11.0 in /e2e
Bumps cypress/included from 10.10.0 to 10.11.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-26 09:41:20 +00:00
kodiakhq[bot] 79e9dc7be6 Merge pull request #1922 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.21incompatible
Bump github.com/docker/docker from 20.10.20+incompatible to 20.10.21+incompatible
2022-10-26 09:15:01 +00:00
dependabot[bot] 19fe7e8d18 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.20+incompatible to 20.10.21+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.20...v20.10.21)

---
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-26 09:11:08 +00:00
Amir Raminfar e4ecfca5be Updats modules 2022-10-24 16:08:59 -07:00
kodiakhq[bot] f37925b637 Merge pull request #1920 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.8.1
Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
2022-10-24 09:15:06 +00:00
dependabot[bot] 822b783063 Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 09:11:05 +00:00
Amir Raminfar 7dcf1d4f15 Release 4.2.2 2022-10-21 13:14:07 -07:00
Amir Raminfar d0af303d6f Updates colors (#1919)
* Updates colors

* Updates colors int tests
2022-10-21 13:13:48 -07:00
Amir Raminfar fd31d394a5 Fixes tooltips 2022-10-19 15:16:42 -07:00
Amir Raminfar 1c0af19c88 Release 4.2.1 2022-10-19 08:31:26 -07:00
Amir Raminfar 9d77613ee9 Updates node 2022-10-19 08:31:13 -07:00
kodiakhq[bot] 62f747797c Merge pull request #1917 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.20incompatible
Bump github.com/docker/docker from 20.10.19+incompatible to 20.10.20+incompatible
2022-10-19 09:18:28 +00:00
kodiakhq[bot] fccb7fc2d4 Merge pull request #1916 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.2.1
Bump docker/setup-buildx-action from 2.2.0 to 2.2.1
2022-10-19 09:17:37 +00:00
kodiakhq[bot] 7fecbdd000 Merge pull request #1915 from amir20/dependabot/docker/node-19-alpine
Bump node from 18-alpine to 19-alpine
2022-10-19 09:17:24 +00:00
dependabot[bot] 46a2f2b810 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.19+incompatible to 20.10.20+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.19...v20.10.20)

---
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-19 09:12:59 +00:00
dependabot[bot] e9dcdda64d Bump docker/setup-buildx-action from 2.2.0 to 2.2.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.2.0...v2.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 09:12:50 +00:00
dependabot[bot] 67a0644c37 Bump node from 18-alpine to 19-alpine
Bumps node from 18-alpine to 19-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 09:12:48 +00:00
Amir Raminfar 21fc2ce2fd Adds on mouse over effect for stats (#1914) 2022-10-18 15:29:19 -07:00
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
107 changed files with 4844 additions and 3013 deletions
-2
View File
@@ -1,2 +0,0 @@
version = 1
merge.notify_on_conflict = false
-47
View File
@@ -1,47 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: "/"
labels:
- "gomod"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: npm
directory: "/"
labels:
- "npm"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: npm
directory: "/e2e"
labels:
- "npm"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: "docker"
directory: "/e2e"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"
+6 -8
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
@@ -27,7 +27,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.18.x
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
@@ -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.1
- 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
+4 -6
View File
@@ -10,24 +10,22 @@ jobs:
buildx:
name: Push branches and PRs
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle' }}
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Docker meta
id: meta
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.1
- 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
+13 -6
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
@@ -30,7 +30,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.18.x
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
@@ -43,12 +43,19 @@ 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.1
- name: Login to DockerHub
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
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
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
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: |
+11 -8
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:18-alpine as node
FROM --platform=$BUILDPLATFORM node:19-alpine as node
RUN npm install -g pnpm
@@ -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.4-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
+35 -14
View File
@@ -58,7 +58,6 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
ports:
- 9999:8080
### Enabling health check
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
@@ -81,7 +80,6 @@ Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `health
retries: 5
start_period: 30s
#### Security
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
@@ -98,7 +96,7 @@ this would then only allow you to view containers with a name starting with "foo
#### Authentication
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`.
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
#### Changing base URL
@@ -119,17 +117,18 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| ---------------- | --------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| n/a | `DOCKER_API_VERSION` | not set |
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| Flag | Env Variable | Default |
| ---------------- | ---------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
## Troubleshooting and FAQs
@@ -181,6 +180,28 @@ Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets
</details>
<details>
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
_This is an issue specific to ARM devices_
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
You can verify this by running `docker info`, and you should see the following:
```
WARNING: No memory limit support
WARNING: No swap limit support
```
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
```
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```
</details>
## License
[MIT](LICENSE)
+19 -1
View File
@@ -21,7 +21,25 @@ func SendStartEvent(se StartEvent) error {
},
}
jsonValue, err := json.Marshal(postBody)
return doRequest(postBody)
}
func SendRequestEvent(re RequestEvent) error {
postBody := map[string]interface{}{
"client_id": re.ClientId,
"events": []map[string]interface{}{
{
"name": "request",
"params": re,
},
},
}
return doRequest(postBody)
}
func doRequest(body map[string]interface{}) error {
jsonValue, err := json.Marshal(body)
if err != nil {
return err
}
+7 -1
View File
@@ -6,6 +6,12 @@ type StartEvent struct {
FilterLength int `json:"filterLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
TailSize int `json:"tailSize"`
Protected bool `json:"protected"`
HasHostname bool `json:"hasHostname"`
}
type RequestEvent struct {
ClientId string `json:"-"`
TotalContainers int `json:"totalContainers"`
RunningContainers int `json:"runningContainers"`
}
+2 -131
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>
+628
View File
@@ -0,0 +1,628 @@
// 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 collapseNav: typeof import('./composables/settings')['collapseNav']
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 onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
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 resolveDirective: typeof import('vue')['resolveDirective']
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 useLink: typeof import('vue-router')['useLink']
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 useSorted: typeof import('@vueuse/core')['useSorted']
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' {
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 collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
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 onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly 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 resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
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 useLink: UnwrapRef<typeof import('vue-router')['useLink']>
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 useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
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']>
}
}
+17 -9
View File
@@ -10,23 +10,26 @@ 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']
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']
MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
MdiLightLogout: typeof import('~icons/mdi-light/logout')['default']
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
@@ -40,5 +43,10 @@ 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']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
-34
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>
+60 -49
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) {
@@ -103,6 +105,15 @@ function addColumn(container: Container) {
width: 580px;
}
@media screen and (max-width: 768px) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem!important;
margin-right: 0.25rem!important;
}
}
.running {
color: var(--primary-color);
}
+6 -8
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;
-55
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>
-203
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>
@@ -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>
@@ -0,0 +1,76 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<stat-monitor
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(container.stat.memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="container.stat.cpu + '%'"
></stat-monitor>
</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();
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.cpu,
value: stat.snapshot.cpu + "%",
}));
return points;
}
);
const memoryData = computedWithControl(
() => container.value.getLastStat(),
() => {
const history = container.value.getStatHistory();
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.memory,
value: formatBytes(stat.snapshot.memoryUsage),
}));
return points;
}
);
</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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -1,12 +1,12 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile has-boxshadow">
<div class="column is-clipped is-paddingless">
<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);
+19
View File
@@ -0,0 +1,19 @@
<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;
background-color: var(--scheme-main-ter);
color: #258ccd;
}
</style>
@@ -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();
});
@@ -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>
+106
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>
@@ -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,
@@ -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>
@@ -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>
@@ -0,0 +1,56 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">{{ label }}</span>
<span class="has-text-weight-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
let selectedPoint: Point<unknown> = $ref();
function onSelectedPoint(point: Point<unknown>) {
selectedPoint = point;
}
let mouseOver = $ref(false);
</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);
}
.host:hover span {
color: var(--secondary-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>
@@ -0,0 +1,62 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove">
<path :d="path" class="area" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
</svg>
</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<unknown>[]; width?: number; height?: number }>();
const x = d3.scaleLinear().range([width, 0]);
const y = d3.scaleLinear().range([height, 0]);
const emit = defineEmits<{
(event: "selected-point", value: Point<unknown>): void;
}>();
const shape = d3
.area<Point<unknown>>()
.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) ?? "";
});
let lineX = $ref(0);
function onMove(e: MouseEvent) {
const { offsetX } = e;
const xValue = x.invert(offsetX);
const index = Math.round(xValue);
lineX = x(index);
const point = data[index];
emit("selected-point", point);
}
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
:deep(.line) {
stroke: var(--secondary-color);
stroke-width: 2;
display: none;
}
svg:hover :deep(.line) {
display: unset;
}
</style>
+19
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>
@@ -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,
}
`;
+44 -10
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,44 @@
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
</div>
<div class="level-item has-text-centered" v-if="secured">
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
</span>
</a>
</div>
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in 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 +72,15 @@
</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 { base, secured } = config;
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">
@@ -64,6 +94,10 @@ aside {
max-height: 100vh;
overflow: auto;
.level.is-hidden-mobile.is-active {
display: flex !important;
}
.menu-label {
margin-top: 1em;
}
+4 -9
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,
});
}
+13 -36
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>
+5 -16
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] },
{
+24 -24
View File
@@ -16,7 +16,12 @@
<div class="is-scrollbar-notification">
<transition name="fade">
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
<button
class="button has-boxshadow"
:class="hasMore ? 'has-more' : ''"
@click="scrollToBottom()"
v-show="paused"
>
<mdi-light-chevron-double-down />
</button>
</transition>
@@ -25,34 +30,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 +63,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">
@@ -114,18 +114,18 @@ section {
button {
position: fixed;
bottom: 30px;
background-color: var(--secondary-color);
transition: background-color 1s ease-out;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
background-color: var(--primary-color);
transition: background-color 0.24s ease-out;
border: none !important;
color: #222;
color: #eee;
&.has-more {
background-color: var(--primary-color);
background-color: var(--secondary-color);
animation-name: bounce;
animation-duration: 1000ms;
animation-fill-mode: both;
color: #fff;
color: #222;
}
}
}
+5 -17
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>
+33 -17
View File
@@ -2,31 +2,49 @@
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<router-link :to="{ name: 'default' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
</div>
<div class="column is-narrow has-text-right px-1">
<button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
<div class="column is-narrow has-text-right px-0">
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded is-small">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
</span>
</a>
</div>
</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 +54,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,17 +64,15 @@
</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";
import { Container } from "@/models/Container";
const { base, secured, hostname } = config;
const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
@@ -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.",
}
`;
+51 -37
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 = messages.slice(-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 };
}
-2
View File
@@ -1,3 +1 @@
import { useMediaQuery } from "@vueuse/core";
export const isMobile = useMediaQuery("(max-width: 770px)");
+9 -10
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;
}
+33 -15
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;
@@ -13,6 +10,7 @@ export const DEFAULT_SETTINGS: {
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
} = {
search: true,
size: "medium",
@@ -23,51 +21,71 @@ export const DEFAULT_SETTINGS: {
lightTheme: "auto",
hourStyle: "auto",
softWrap: true,
collapseNav: false,
};
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),
});
const collapseNav = computed({
get: () => settings.value.collapseNav,
set: (value) => (settings.value.collapseNav = value),
});
export {
collapseNav,
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
smallerScrollbars,
menuWidth,
size,
search,
settings
};
+5 -8
View File
@@ -1,12 +1,9 @@
import { useTitle } from "@vueuse/core";
import { ref, computed } from "vue";
const { hostname } = config;
let subtitle = $ref("");
const title = $computed(() => `${subtitle} - Dozzle` + (hostname ? ` @ ${hostname}` : ""));
const subtitle = ref("");
const title = computed(() => `${subtitle.value} - Dozzle`);
useTitle(title);
useTitle($$(title));
export function setTitle(t: string) {
subtitle.value = t;
subtitle = t;
}
+10 -5
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;
}
});
});
}
+125
View File
@@ -0,0 +1,125 @@
<template>
<main v-if="!authorizationNeeded">
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
<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="collapse"
class="button is-small 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 { 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 collapse() {
collapseNav.value = !collapseNav.value;
}
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;
color: var(--text-strong-color);
background: var(--scheme-main);
&:hover {
left: -25px;
}
}
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<template>
<main>
<router-view></router-view>
</main>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>
+6 -64
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");
+31
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
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
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
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
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
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);
};
-35
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>
-23
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>
-176
View File
@@ -1,176 +0,0 @@
<template>
<div>
<section class="hero is-small mt-4">
<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>
</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">Total Containers</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">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>
</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>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">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">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"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
<search-icon />
</span>
</p>
</div>
<p class="panel-tabs" v-if="!search">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
</p>
<router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
v-for="item in results.slice(0, 10)"
:key="item.id"
class="panel-block"
>
<span class="name">{{ item.name }}</span>
<div class="subtitle is-7 status">
<past-time :date="new Date(item.created * 1000)"></past-time>
</div>
</router-link>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { formatBytes } from "@/utils";
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime.vue";
import config from "@/stores/config";
import { useIntervalFn } from "@vueuse/core";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = ref("running");
const search = ref();
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
}
switch (sort.value) {
case "all":
return mostRecentContainers.value;
case "running":
return runningContainers.value;
default:
throw `Invalid sort order: ${sort.value}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
const totalCpu = ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
const totalMem = ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
router.push({ name: "container", params: { id: item.id, name: item.name } });
}
}
</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>
@@ -4,19 +4,14 @@
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">This page does not exist.</small>
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
</div>
</div>
</div>
</template>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "PageNotFound",
setup() {
setTitle("Page not found");
},
};
<script lang="ts" setup>
const { t } = useI18n();
setTitle(t("title.page-not-found"));
</script>
+20
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>
-7
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";
+181
View File
@@ -0,0 +1,181 @@
<template>
<div>
<section class="level section pb-0-is-mobile">
<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 pt-0-is-mobile">
<div class="column is-12-mobile is-6-tablet is-5-desktop is-4-fullhd">
<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 { version } = 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 list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return {
id,
created,
name,
state,
};
});
});
const { results } = useFuse(query, list, {
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;
}
}
}
@media screen and (max-width: 768px) {
.pb-0-is-mobile {
padding-bottom: 0 !important;
}
.pt-0-is-mobile {
padding-top: 0 !important;
}
}
.icon {
padding: 10px 3px;
}
</style>
@@ -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>
@@ -2,35 +2,32 @@
<div>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">About</h2>
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
</div>
<div>
You are using Dozzle <i>{{ currentVersion }}</i
>.
<span v-if="hasUpdate">
New version is available! Update to
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
{{ nextRelease.name }}</a
>.
</span>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
></div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Display</h2>
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> Show timestamps </o-switch>
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> Soft wrap lines</o-switch>
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
</div>
<div class="item">
@@ -54,7 +51,7 @@
</o-field>
</div>
<div class="column">
By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
{{ $t("settings.12-24-format") }}
</div>
</div>
</div>
@@ -83,7 +80,7 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Font size to use for logs</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
</div>
<div class="item">
@@ -111,33 +108,30 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Color scheme</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Options</h2>
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
</div>
<div class="item">
<o-switch v-model="search">
Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
<span v-html="$t('settings.search')"></span>
</o-switch>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import gt from "semver/functions/gt";
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
import {
search,
lightTheme,
@@ -149,22 +143,28 @@ import {
softWrap,
} from "@/composables/settings";
setTitle("Settings");
const { t } = useI18n();
const currentVersion = config.version;
const nextRelease = ref({ html_url: "", name: "" });
const hasUpdate = ref(false);
setTitle(t("title.settings"));
const currentVersion = $ref(config.version);
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate.value = gt(release.tag_name, currentVersion);
nextRelease.value = release;
hasUpdate = gt(release.tag_name, currentVersion);
nextRelease = release;
}
} else {
hasUpdate.value = true;
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}
@@ -1,9 +1,4 @@
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
@@ -15,15 +10,16 @@ watch(visibleContainers, (newValue) => {
if (route.query.name) {
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
if (container) {
router.push({ name: "container", params: { id: container.id } });
router.push({ name: "container-id", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
}
});
</script>
<template></template>
+19 -2
View File
@@ -1,14 +1,31 @@
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;
hostname: string;
}
const pageConfig = JSON.parse(text);
const config: Config = {
maxLogs: 600,
...pageConfig,
};
if (config.version == "{{ .Version }}") {
config.version = "master";
config.base = "";
config.authorizationNeeded = false;
config.secured = false;
config.hostname = "localhost";
} else {
config.version = config.version.replace(/^v/, "");
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
}
export default config;
export default config as Config;
+33 -33
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);
+37 -2
View File
@@ -75,10 +75,10 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: #{$grey-darker};
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: #d8f0ca;
--secondary-color: rgb(249 115 22);
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
@@ -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,32 @@ html.has-custom-scrollbars {
.button .button-wrapper > span {
display: contents;
}
.has-dropshadow {
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
}
.has-boxshadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
mark {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
.button.is-rounded:hover {
color: var(--text-strong-color);
background: var(--scheme-main-ter);
}
+12 -11
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";
-16
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
View File
@@ -0,0 +1 @@
type Point<T> = { x: number; y: number; value?: T };
-54
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;
}
}
+1 -1
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
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)
}
+57
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)
})
}
}
+4 -4
View File
@@ -35,7 +35,7 @@ type dockerProxy interface {
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, int, string) (io.ReadCloser, error)
ContainerLogs(context.Context, string, string) (io.ReadCloser, error)
Events(context.Context) (<-chan ContainerEvent, <-chan error)
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
@@ -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)
)
@@ -169,7 +169,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
if since != "" {
@@ -184,7 +184,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
ShowStdout: true,
ShowStderr: true,
Follow: true,
Tail: strconv.Itoa(tailSize),
Tail: "300",
Timestamps: true,
Since: since,
}
+3 -3
View File
@@ -133,7 +133,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "since")
logReader, _ := client.ContainerLogs(context.Background(), id, "since")
actual, _ := ioutil.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
@@ -154,7 +154,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "")
logReader, _ := client.ContainerLogs(context.Background(), id, "")
actual, _ := ioutil.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
@@ -170,7 +170,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
client := &dockerClient{proxy, filters.NewArgs()}
reader, err := client.ContainerLogs(context.Background(), id, 300, "")
reader, err := client.ContainerLogs(context.Background(), id, "")
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
+1 -1
View File
@@ -1,4 +1,4 @@
FROM cypress/included:10.6.0
FROM cypress/included:12.2.0
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
+2 -1
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: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+14
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");
});
});
+8 -2
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");
});
});
+17
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");
});
});
+15
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");
});
});
+25 -5
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}
+6 -4
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": "^3.2.5",
"cypress": "^12.0.2",
"typescript": "^4.9.4"
}
}
+91 -55
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': ^3.2.5
cypress: ^12.0.2
typescript: ^4.9.4
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': 3.2.5_cypress@12.0.2
cypress: 12.0.2
typescript: 4.9.4
packages:
@@ -52,21 +52,28 @@ 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/base64/1.0.5:
resolution: {integrity: sha512-RIlQtKezeRhNuxIpIc2BuM4kNqAakIarKV/TCgx2EC+zgj1NxX3+Mc80AkJCCMO8dZUuGzbULG7295YeBQZlSA==}
dev: false
/@frsource/cypress-plugin-visual-regression-diff/3.2.5_cypress@12.0.2:
resolution: {integrity: sha512-mN0IpyqRZoj2xm6S63iRHz4WilDgR7vZcCBZpYEp4IjjLb33VKjtbmaErKpy501t78tqvlBIA/93hSMs3sZPLQ==}
engines: {node: '>=10'}
peerDependencies:
cypress: '>=4.5.0'
dependencies:
cypress: 10.6.0
'@frsource/base64': 1.0.5
cypress: 12.0.2
glob: 8.0.3
meta-png: 1.0.5
move-file: 2.1.0
pixelmatch: 5.3.0
pngjs: 6.0.0
sharp: 0.30.7
sharp: 0.31.2
dev: false
/@types/node/14.18.24:
resolution: {integrity: sha512-aJdn8XErcSrfr7k8ZDDfU6/2OgjZcB2Fu9d+ESK8D7Oa5mtsv8Fa8GpcwTA0v60kuZBaalKPzuzun4Ov1YWO/w==}
/@types/node/14.18.34:
resolution: {integrity: sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA==}
dev: false
/@types/sinonjs__fake-timers/8.1.1:
@@ -81,7 +88,7 @@ packages:
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
requiresBuild: true
dependencies:
'@types/node': 14.18.24
'@types/node': 14.18.34
dev: false
optional: true
@@ -195,6 +202,12 @@ packages:
concat-map: 0.0.1
dev: false
/brace-expansion/2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: false
/buffer-crc32/0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: false
@@ -232,8 +245,9 @@ packages:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/ci-info/3.3.2:
resolution: {integrity: sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==}
/ci-info/3.7.0:
resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==}
engines: {node: '>=8'}
dev: false
/clean-stack/2.2.0:
@@ -248,8 +262,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 +327,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 +343,15 @@ packages:
which: 2.0.2
dev: false
/cypress/10.6.0:
resolution: {integrity: sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==}
engines: {node: '>=12.0.0'}
/cypress/12.0.2:
resolution: {integrity: sha512-WnLx1DpnbF1vbpDBkgP14rK5yS3U+Gvxrv2fsB4Owma26oIyENj7DDRnsJbSZuTfG4mcuUJxAkRHJR2wBqBfMA==}
engines: {node: ^14.0.0 || ^16.0.0 || >=18.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.34
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.3
arch: 2.2.0
@@ -348,10 +362,10 @@ 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
dayjs: 1.11.7
debug: 4.3.4_supports-color@8.1.1
enquirer: 2.3.6
eventemitter2: 6.4.7
@@ -367,12 +381,12 @@ packages:
listr2: 3.14.0_enquirer@2.3.6
lodash: 4.17.21
log-symbols: 4.1.0
minimist: 1.2.6
minimist: 1.2.7
ospath: 1.2.2
pretty-bytes: 5.6.0
proxy-from-env: 1.0.0
request-progress: 3.0.0
semver: 7.3.7
semver: 7.3.8
supports-color: 8.1.1
tmp: 0.2.1
untildify: 4.0.0
@@ -386,8 +400,8 @@ packages:
assert-plus: 1.0.0
dev: false
/dayjs/1.11.5:
resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
/dayjs/1.11.7:
resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==}
dev: false
/debug/3.2.7_supports-color@8.1.1:
@@ -598,8 +612,19 @@ packages:
path-is-absolute: 1.0.1
dev: false
/global-dirs/3.0.0:
resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==}
/glob/8.0.3:
resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==}
engines: {node: '>=12'}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.1
once: 1.4.0
dev: false
/global-dirs/3.0.1:
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
engines: {node: '>=10'}
dependencies:
ini: 2.0.0
@@ -665,7 +690,7 @@ packages:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
hasBin: true
dependencies:
ci-info: 3.3.2
ci-info: 3.7.0
dev: false
/is-fullwidth-code-point/3.0.0:
@@ -677,7 +702,7 @@ packages:
resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
engines: {node: '>=10'}
dependencies:
global-dirs: 3.0.0
global-dirs: 3.0.1
is-path-inside: 3.0.3
dev: false
@@ -758,7 +783,7 @@ packages:
log-update: 4.0.0
p-map: 4.0.0
rfdc: 1.3.0
rxjs: 7.5.6
rxjs: 7.6.0
through: 2.3.8
wrap-ansi: 7.0.0
dev: false
@@ -800,6 +825,10 @@ packages:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: false
/meta-png/1.0.5:
resolution: {integrity: sha512-kR/npdqfnlB8HycaDImcpauYh/l1ldvuDoT/QhWazSD9jDHJ8v9tankLhjN5ea8OKV1Amp4v/xxNYG2WiQYEgg==}
dev: false
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -828,8 +857,15 @@ packages:
brace-expansion: 1.1.11
dev: false
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
/minimatch/5.1.1:
resolution: {integrity: sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: false
/minimist/1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
dev: false
/mkdirp-classic/0.5.3:
@@ -855,11 +891,11 @@ packages:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: false
/node-abi/3.24.0:
resolution: {integrity: sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==}
/node-abi/3.30.0:
resolution: {integrity: sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==}
engines: {node: '>=10'}
dependencies:
semver: 7.3.7
semver: 7.3.8
dev: false
/node-addon-api/5.0.0:
@@ -945,10 +981,10 @@ packages:
detect-libc: 2.0.1
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.6
minimist: 1.2.7
mkdirp-classic: 0.5.3
napi-build-utils: 1.0.2
node-abi: 3.24.0
node-abi: 3.30.0
pump: 3.0.0
rc: 1.2.8
simple-get: 4.0.1
@@ -992,7 +1028,7 @@ packages:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.6
minimist: 1.2.7
strip-json-comments: 2.0.1
dev: false
@@ -1030,10 +1066,10 @@ packages:
glob: 7.2.3
dev: false
/rxjs/7.5.6:
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
/rxjs/7.6.0:
resolution: {integrity: sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==}
dependencies:
tslib: 2.4.0
tslib: 2.4.1
dev: false
/safe-buffer/5.2.1:
@@ -1044,24 +1080,24 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/semver/7.3.7:
resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
/semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/sharp/0.30.7:
resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
engines: {node: '>=12.13.0'}
/sharp/0.31.2:
resolution: {integrity: sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==}
engines: {node: '>=14.15.0'}
requiresBuild: true
dependencies:
color: 4.2.3
detect-libc: 2.0.1
node-addon-api: 5.0.0
prebuild-install: 7.1.1
semver: 7.3.7
semver: 7.3.8
simple-get: 4.0.1
tar-fs: 2.1.1
tunnel-agent: 0.6.0
@@ -1224,8 +1260,8 @@ packages:
punycode: 2.1.1
dev: false
/tslib/2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
/tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: false
/tunnel-agent/0.6.0:
@@ -1243,8 +1279,8 @@ packages:
engines: {node: '>=10'}
dev: false
/typescript/4.7.4:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
/typescript/4.9.4:
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: false
@@ -1269,7 +1305,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
+6 -6
View File
@@ -5,23 +5,23 @@ 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.22+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
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
github.com/magiconair/properties v1.8.6
github.com/magiconair/properties v1.8.7
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.0
github.com/spf13/afero v1.9.2
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0
github.com/spf13/afero v1.9.3
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.1
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -40,4 +40,4 @@ require (
gotest.tools/v3 v3.0.3 // indirect
)
go 1.18
go 1.19
+12 -4
View File
@@ -62,8 +62,10 @@ 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.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+zkVJT9BEK8kQkk=
github.com/docker/docker v20.10.22+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=
@@ -160,6 +162,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -183,17 +187,21 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+2 -1
View File
@@ -9,7 +9,8 @@
"base": "{{ .Base }}",
"version": "{{ .Version }}",
"authorizationNeeded": "{{ .AuthorizationNeeded }}",
"secured": "{{ .Secured }}"
"secured": "{{ .Secured }}",
"hostname": "{{ .Hostname }}"
}
</script>
<link
+50
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
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
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>.
+49
View File
@@ -0,0 +1,49 @@
toolbar:
clear: Очистить
download: Скачать
search: Поиск
label:
containers: Контейнеры
total-containers: Всего Контейнеров
running: Запущенные
total-cpu-usage: Использование процессора
total-mem-usage: Использование памяти
dozzle-version: Версия Dozzle
all: Все
password: Пароль
username: Имя пользователя
tooltip:
search: Поиск контейнеров (⌘ + k, ⌃k)
pin-column: Закрепить столбец
error:
page-not-found: Эта страница не доступна.
invalid-auth: Имя пользователя или пароль неверны.
logs-skipped: Пропущено {total} записей
title:
page-not-found: Страница не найдена
login: Требуется авторизация
settings: Настройки
button:
logout: Выйти
login: Войти
placeholder:
search-containers: Поиск контейнеров (⌘ + k, ⌃k)
settings:
display: Вид
small-scrollbars: Уменьшенная полоса прокрутки
show-timesamps: Показывать временные метки
soft-wrap: Плавный перенос текста
12-24-format: >-
Формат времени. По умолчанию, Dozzle будет использовать локализацию вашего браузера.
font-size: Размер шрифта
color-scheme: Цветовая схема
options: Опции
show-stopped-containers: Показывать остановленные контейнеры
about: Информация
search: >-
Включить поиск с помощью Dozzle, используя <code>command+f</code> или
<code>ctrl+f</code>
using-version: Вы используете версию Dozzle {version}.
update-available: >-
Доступна новая версия! Обновить до <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
+24 -4
View File
@@ -5,7 +5,6 @@ import (
"embed"
"io/fs"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"strings"
@@ -25,13 +24,25 @@ var (
version = "head"
)
type DockerSecret struct {
Value string
}
func (s *DockerSecret) UnmarshalText(b []byte) error {
v, err := os.ReadFile(string(b))
s.Value = strings.Trim(string(v), "\r\n")
return err
}
type args struct {
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
TailSize int `arg:"env:DOZZLE_TAILSIZE" default:"300" help:"update the initial tail size when fetching logs."`
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
UsernameFile *DockerSecret `arg:"env:DOZZLE_USERNAME_FILE" help:"sets the secret path read username for auth."`
PasswordFile *DockerSecret `arg:"env:DOZZLE_PASSWORD_FILE" help:"sets the secret path read password for auth"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
@@ -94,6 +105,14 @@ func main() {
}
}
if args.Username == "" && args.UsernameFile != nil {
args.Username = args.UsernameFile.Value
}
if args.Password == "" && args.PasswordFile != nil {
args.Password = args.PasswordFile.Value
}
if args.Username != "" || args.Password != "" {
if args.Username == "" || args.Password == "" {
log.Fatalf("Username AND password are required for authentication")
@@ -104,9 +123,10 @@ func main() {
Addr: args.Addr,
Base: args.Base,
Version: version,
TailSize: args.TailSize,
Username: args.Username,
Password: args.Password,
Hostname: args.Hostname,
NoAnalytics: args.NoAnalytics,
}
assets, err := fs.Sub(content, "dist")
@@ -156,8 +176,8 @@ func doStartEvent(arg args) {
FilterLength: len(arg.Filter),
CustomAddress: arg.Addr != ":8080",
CustomBase: arg.Base != "/",
TailSize: arg.TailSize,
Protected: arg.Username != "",
HasHostname: arg.Hostname != "",
}
if err := analytics.SendStartEvent(event); err != nil {
+53 -38
View File
@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "4.0.2",
"version": "4.6.2",
"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/cil": "^1.1.2",
"@iconify-json/mdi": "^1.1.30",
"@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/carbon": "^1.1.11",
"@iconify-json/cil": "^1.1.3",
"@iconify-json/mdi": "^1.1.40",
"@iconify-json/mdi-light": "^1.1.5",
"@iconify-json/octicon": "^1.1.26",
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@oruga-ui/oruga-next": "^0.5.10",
"@oruga-ui/theme-bulma": "^0.2.8",
"@vueuse/core": "^9.9.0",
"@vueuse/integrations": "^9.9.0",
"@vueuse/router": "^9.9.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.1",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"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",
"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"
"pinia": "^2.0.28",
"semver": "^7.3.8",
"splitpanes": "^3.1.5",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@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/semver": "^7.3.12",
"@vue/test-utils": "^2.0.2",
"@types/node": "^18.11.18",
"@types/semver": "^7.3.13",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "^3.2.45",
"@vue/test-utils": "^2.2.6",
"c8": "^7.12.0",
"eventsourcemock": "^2.0.0",
"husky": "^8.0.1",
"jest-serializer-vue": "^2.0.2",
"jsdom": "^20.0.0",
"lint-staged": "^13.0.3",
"husky": "^8.0.2",
"jest-serializer-vue": "^3.0.0",
"jsdom": "^20.0.3",
"lint-staged": "^13.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"release-it": "^15.3.0",
"prettier": "^2.8.1",
"release-it": "^15.6.0",
"sass": "^1.57.1",
"ts-node": "^10.9.1",
"vitest": "^0.22.1",
"vue-tsc": "^0.40.1"
"typescript": "^4.9.4",
"unplugin-auto-import": "^0.12.1",
"unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.12",
"vite": "4.0.3",
"vite-plugin-pages": "^0.28.0",
"vite-plugin-vue-layouts": "^0.7.0",
"vitest": "^0.26.2",
"vue-tsc": "^1.0.18"
},
"lint-staged": {
"*.{js,vue,css}": [

Some files were not shown because too many files have changed in this diff Show More