Compare commits

..

2 Commits

Author SHA1 Message Date
Amir 2a94f035a2 1.17.1 2019-12-03 12:31:10 -08:00
Amir e8e2390776 Adds html title tag 2019-12-03 12:31:04 -08:00
399 changed files with 14578 additions and 41714 deletions
-57
View File
@@ -1,57 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/dozzle"
cmd = "GOEXPERIMENT=jsonv2 go build -race -o ./tmp/dozzle ."
delay = 1000
exclude_dir = [
"assets",
"tmp",
"data",
"node_modules",
"docs",
"test-results",
"dist",
"e2e",
"examples",
]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "2s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = true
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
+9
View File
@@ -0,0 +1,9 @@
{
"presets": [["@babel/preset-env", { "modules": false }]],
"plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]],
"env": {
"test": {
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
}
}
}
-9
View File
@@ -1,9 +0,0 @@
node_modules
.cache
.idea
.github
dist
.git
e2e
docs
internal/agent/pb/
-3
View File
@@ -13,8 +13,5 @@ max_line_length = 120
indent_style = tab
indent_size = 4
[Makefile]
indent_style = tab
[package.json]
indent_size = 1
-65
View File
@@ -1,65 +0,0 @@
name: 🐛 Bug Report
labels: ["bug"]
description: |
Use this template if something is not working as expected.
body:
- type: markdown
attributes:
value: |
I get an overwhelming of issues which I don't have time to investigate. Most often, they are due to configuration issues. For this reason, I ask you to help me by providing as much information as possible. For issues with `docker-socket-proxy` I may not be able to help as those are not related to Dozzle.
- type: checkboxes
attributes:
label: 🔍 Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: dropdown
attributes:
label: How is Dozzle deployed?
description: Select the components that you are using.
options:
- Standalone Deployment
- Agents
- Remote Hosts
- Swarm Mode
- Kubernetes
validations:
required: true
- type: input
attributes:
label: 📦 Dozzle version
description: Run `docker run amir20/dozzle --version` or `curl http://<host>/api/version`.
validations:
required: true
- type: textarea
attributes:
label: ✅ Command used to run Dozzle
description: Please provide the command used to run Dozzle. Provide docker-compose.yml if applicable.
validations:
required: true
- type: textarea
attributes:
label: 🐛 Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: 💻 Environment
description: Run `docker system info` and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: 📜 Dozzle logs are required for debugging purposes. You may need to enable debug mode. See https://dozzle.dev/guide/debugging.
description: Drag log files or paste the logs into the text input below
validations:
required: true
- type: textarea
attributes:
label: 📸 If applicable, add screenshots to help explain your bug
description: Drag screenshots into the text input below
validations:
required: false
@@ -1,20 +0,0 @@
name: 🚀 Feature Request
labels: ["enhancement"]
description: |
Use this template if you have a feature request or an idea for a new feature.
body:
- type: textarea
attributes:
label: Describe the feature you would like to see
validations:
required: true
- type: textarea
attributes:
label: Describe how you would like to see this feature implemented
validations:
required: false
- type: textarea
attributes:
label: Describe any alternatives you've considered
validations:
required: false
@@ -1,52 +0,0 @@
name: 🚤 Performance Issue
labels: ["performance"]
description: |
Use this template if you are seeing performance issues such as slow loading times or high CPU usage.
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: dropdown
attributes:
label: How is Dozzle deployed?
description: Select the components that you are using.
options:
- Standalone Deployment
- Agents
- Remote Hosts
- Swarm Mode
validations:
required: true
- type: input
attributes:
label: Dozzle version
description: Can be found in logs or settings page.
validations:
required: true
- type: textarea
attributes:
label: ✅ Command used to run Dozzle
description: Please provide the command used to run Dozzle. Provide docker-compose.yml if applicable.
validations:
required: true
- type: textarea
attributes:
label: Describe the performance issue you are seeing
validations:
required: true
- type: textarea
attributes:
label: How many containers, hosts, agents, etc. are you using?
description: Any details to reproduce the issue would be helpful. If related to volume of logs, please provide a guesstimate of how many logs are being generated.
validations:
required: true
- type: textarea
attributes:
label: Screenshots, videos, or logs
description: Drag issues into the text input below
validations:
required: false
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 💁🏼 Common Issues
url: https://dozzle.dev/guide/faq
about: Check the FAQ for common issues.
-1
View File
@@ -1 +0,0 @@
buy_me_a_coffee: amirraminfar
+6
View File
@@ -0,0 +1,6 @@
FROM golang:1.13
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD [""]
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -e
go test -cover ./...
+13
View File
@@ -0,0 +1,13 @@
FROM golang:1.13-alpine
RUN apk --no-cache add git nodejs-current nodejs-npm make g++ bash bzr curl docker rpm && \
npm i -g npm && \
wget https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz && \
tar -xvzf *.tar.gz -C /usr/local/bin && \
rm *.gz && \
go get -u github.com/gobuffalo/packr/packr
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD [""]
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
echo "Login to the docker..."
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY
fi
# Workaround for github actions when access to different repositories is needed.
# Github actions provides a GITHUB_TOKEN secret that can only access the current
# repository and you cannot configure it's value.
# Access to different repositories is needed by brew for example.
if [ -n "$GORELEASER_GITHUB_TOKEN" ] ; then
export GITHUB_TOKEN=$GORELEASER_GITHUB_TOKEN
fi
npm ci
goreleaser $@
-6
View File
@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"rangeStrategy": "bump",
"ignoreDeps": []
}
-18
View File
@@ -1,18 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 20
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 3
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- keep
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
-57
View File
@@ -1,57 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
-53
View File
@@ -1,53 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions:
id-token: write
contents: read
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
-154
View File
@@ -1,154 +0,0 @@
on:
push:
tags:
- "v*"
name: Test and Release
jobs:
npm-test:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: 24.12.0
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Tests
run: pnpm run test -- --outputTruncateLength=500
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: 1.25.5
check-latest: true
- name: Checkout code
uses: actions/checkout@v6
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Install gRPC and Go
run: |
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- name: Run Go Tests with Coverage
run: make test
int-test:
name: Integration Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: 24.12.0
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build
uses: docker/bake-action@v6
with:
source: .
load: true
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
buildx:
needs: [go-test, npm-test, int-test]
name: Release
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to DockerHub
uses: docker/login-action@v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the Container registry
uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
amir20/dozzle
ghcr.io/amir20/dozzle
tags: |
type=raw,value=latest
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
type=semver,pattern={{major}},prefix=v
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.18.0
with:
push: true
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
git-release:
needs: [buildx]
name: Github Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install Node
uses: actions/setup-node@v6
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx changelogithub
-51
View File
@@ -1,51 +0,0 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
name: Push container
jobs:
buildx:
name: Push branches and PRs
runs-on: ubuntu-latest
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: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to DockerHub
uses: docker/login-action@v3.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the Container registry
uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
amir20/dozzle
ghcr.io/amir20/dozzle
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.18.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
-60
View File
@@ -1,60 +0,0 @@
name: Deploy VitePress site to Pages
on:
push:
branches: [master]
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: pnpm/action-setup@v2
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: pnpm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install
- name: Build with VitePress
run: |
pnpm docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-20
View File
@@ -1,20 +0,0 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+23
View File
@@ -0,0 +1,23 @@
on: push
name: Build, Test and Release
jobs:
build:
name: npm test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: npm test
uses: actions/setup-node@v1
- run: npm it
- name: go test
uses: ./.github/golang/
- name: Release
uses: ./.github/goreleaser/
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ github. ref }}
with:
args: release
if: contains(github.ref, 'tags')
-128
View File
@@ -1,128 +0,0 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
name: Test
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: 24.12.0
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Typecheck
run: pnpm run typecheck
npm-test:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Tests
run: pnpm run test -- --outputTruncateLength=500
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: "1.25.5"
check-latest: true
- name: Checkout code
uses: actions/checkout@v6
- name: Install Protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install gRPC and Go
run: |
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- name: Run Go Tests with Coverage
run: make test
staticcheck:
runs-on: ubuntu-latest
name: Go Staticcheck
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25.5"
check-latest: true
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
- name: Stactic checker
uses: dominikh/staticcheck-action@v1.4.0
with:
install-go: false
int-test:
name: Integration Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: 24.12.0
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate certs
run: make shared_key.pem shared_cert.pem
- name: Build
uses: docker/bake-action@v6
with:
source: .
load: true
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
+3 -13
View File
@@ -1,18 +1,8 @@
certs
/data
dist
node_modules
.cache
static
a_main-packr.go
dozzle
coverage
.pnpm-debug.log
coverage.out
.netlify
/test-results/
/playwright-report/
/playwright/.cache/
*.pem
*.csr
tmp
.claude
gin-bin
+47
View File
@@ -0,0 +1,47 @@
before:
hooks:
- npm run clean
- npm run build
- packr
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm
- arm64
archives:
- replacements:
amd64: 64-bit
386: 32-bit
arm64: ARM_64-bit
arm: ARM_32-bit
linux: Linux
darwin: Darwin
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
dockers:
- image_templates:
- "amir20/dozzle:{{ .Tag }}"
- "amir20/dozzle:v{{ .Major }}.{{ .Minor }}"
- amir20/dozzle:latest
build_flag_templates:
- "--label=org.label-schema.schema-version=1.0"
- "--label=org.label-schema.build-date={{.Date}}"
- "--label=org.label-schema.vcs-ref={{.ShortCommit}}"
- "--label=org.label-schema.version={{.Version}}"
- "--label=org.label-schema.name=Dozzle"
- "--label=org.label-schema.url=https://dozzle.dev/"
- "--label=org.label-schema.vcs-url=https://github.com/amir20/dozzle"
- "--label=org.label-schema.description=Dozzle is a real-time log viewer for docker containers."
-4
View File
@@ -1,4 +0,0 @@
shared-workspace-lockfile=false
include-workspace-root=true
ignore-workspace-root-check=true
-6
View File
@@ -1,6 +0,0 @@
auto-imports.d.ts
components.d.ts
typed-router.d.ts
docs/.vitepress/cache
docs/.vitepress/dist
dist
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
printWidth: 120,
plugins: ["prettier-plugin-tailwindcss"],
};
+3
View File
@@ -0,0 +1,3 @@
{
"printWidth": 120
}
+1
View File
@@ -0,0 +1 @@
-r '\.go$' -R '^node_modules/' -R '^static/' -R '^.cache/' -G '*_test.go' -s -- go run main.go --level debug
-253
View File
@@ -1,253 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Comment Style
**Always use ultra-brief mode for all PR reviews and responses.**
Format:
- Critical issues only (bugs, security, blockers)
- Brief bullet points, no lengthy explanations
- Skip verbose sections (no "Strengths", "Summary", etc.)
- Include file:line references when relevant
- Maximum ~10-15 lines per response
## Project Overview
Dozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:
- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)
- **Frontend**: Vue 3 (SPA with Vite, TypeScript)
The application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).
## Development Commands
### Setup
```bash
# Install dependencies
pnpm install
# Install Go tools (protobuf, air hot-reloader)
make tools
# Generate certificates and protobuf files
make generate
```
### Development
```bash
# Run full development environment (backend + frontend with hot reload)
make dev
# Alternative: Run backend and frontend separately
pnpm run watch:backend # Go backend with air (port 3100)
pnpm run watch:frontend # Vite dev server (port 3100)
# Run in agent mode for development
pnpm run agent:dev
```
### Building
```bash
# Build frontend assets
pnpm build
# or
make dist
# Build entire application (includes frontend build)
make build
# Build Docker image
make docker
```
### Testing
```bash
# Run Go tests
make test
# Run frontend tests (Vitest)
pnpm test
# Run in watch mode
TZ=UTC pnpm test --watch
# Type checking
pnpm typecheck
```
### Preview & Other
```bash
# Preview production build locally
pnpm preview
# or
make preview
# Run integration tests (Playwright)
make int
```
## Architecture
### Backend (Go)
The Go backend is organized into these key packages:
- **`internal/web/`** - HTTP server and routing layer
- Routes defined in `routes.go` using chi router
- WebSocket/SSE handlers for log streaming (`logs.go`)
- Authentication middleware and token management (`auth.go`)
- Container action handlers (`actions.go`)
- **`internal/docker/`** - Docker API client implementation
- `client.go`: Main Docker client wrapper with container operations
- `log_reader.go`: Streaming container logs
- `stats_collector.go`: Real-time container stats collection
- **`internal/agent/`** - gRPC agent for multi-host support
- Uses Protocol Buffers (protos defined in `protos/`)
- Enables distributed log collection across Docker hosts
- **`internal/k8s/`** - Kubernetes client support
- Alternative to Docker client for k8s deployments
- **`internal/support/`** - Support utilities
- `cli/`: Command-line argument parsing and validation
- `docker/`: Multi-host Docker management and Swarm support
- `container/`: Container service abstractions
- `web/`: Web service utilities
- **`internal/auth/`** - Authentication providers
- Simple file-based auth (`simple.go`)
- Forward proxy auth (`proxy.go`)
- Role-based authorization (`roles.go`)
- **`internal/container/`** - Container domain models and interfaces
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s)
### Frontend (Vue 3)
The frontend uses file-based routing with these conventions:
- **`assets/pages/`** - File-based routes (unplugin-vue-router)
- `container/[id].vue`: Single container view
- `merged/[ids].vue`: Multi-container merged view
- `host/[id].vue`: Host-level logs
- `service/[name].vue`: Swarm service logs
- `stack/[name].vue`: Docker stack logs
- `group/[name].vue`: Custom grouped logs
- **`assets/components/`** - Vue components (auto-imported)
- `LogViewer/`: Core log viewing components
- `SimpleLogItem.vue`: Single-line log entries
- `ComplexLogItem.vue`: JSON/structured log entries
- `GroupedLogItem.vue`: Multi-line grouped log entries
- `ContainerEventLogItem.vue`: Container lifecycle events
- `SkippedEntriesLogItem.vue`: Placeholder for skipped logs
- `LoadMoreLogItem.vue`: Load more historical logs
- `ContainerViewer/`: Container-specific UI
- `common/`: Reusable UI components
- `BarChart.vue`: Lightweight bar chart with automatic downsampling
- `HostCard.vue`: Host overview card with metrics
- `MetricCard.vue`: Reusable metric display component
- `ContainerTable.vue`: Container table with historical stat visualization
- **`assets/stores/`** - Pinia stores (auto-imported)
- `config.ts`: App configuration and feature flags
- `container.ts`: Container state management
- `hosts.ts`: Multi-host state
- `settings.ts`: User preferences
- **`assets/composable/`** - Vue composables (auto-imported)
- `eventStreams.ts`: SSE connection management
- `historicalLogs.ts`: Historical log fetching
- `logContext.ts`: Log filtering and search context
- `storage.ts`: LocalStorage abstractions
- `visible.ts`: Log filtering by visible keys for complex logs
- **`assets/modules/`** - Vue plugins
- `router.ts`: Vue Router configuration
- `pinia.ts`: Pinia store setup
- `i18n.ts`: Internationalization
### Communication Flow
1. **Real-time Logs**: Frontend establishes SSE connections to `/api/hosts/{host}/containers/{id}/logs/stream`
2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events
3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events
4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)
5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`
### Build System
- **Frontend**: Vite builds to `dist/` with manifest
- **Backend**: Embeds `dist/` using Go embed directive
- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem
- **Makefile**: Orchestrates builds and dependency generation
## Important Development Notes
### Frontend
- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)
- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)
- Tailwind CSS with DaisyUI for styling
- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`
- **Log Entry Types**: Three types of log messages supported
- `SimpleLogEntry`: Single-line text logs (`string`)
- `ComplexLogEntry`: Structured JSON logs (`JSONObject`)
- `GroupedLogEntry`: Multi-line grouped logs (`string[]`)
- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages
- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)
- `BarChart.vue`: Self-contained bar chart with responsive downsampling
- Downsampling algorithm: Averages data into buckets based on available screen width
- All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)
### Backend
- The application uses Go 1.25+ with module support
- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)
- Protocol buffer generation happens via `go generate` directive in `main.go`
- Docker client uses API version negotiation for compatibility
### Authentication
- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)
- JWT tokens for simple auth with configurable TTL
- User file location: `./data/users.yml` or `./data/users.yaml`
### Testing
- Go tests use standard `testing` package with testify assertions
- Frontend uses Vitest with `@vue/test-utils`
- Integration tests with Playwright in `e2e/`
- Tests must run with `TZ=UTC` for consistent timestamps
### Container Stats & Metrics
- Stats are tracked using exponential moving average (EMA) with alpha=0.2
- History stored in rolling window (300 items max) via `useSimpleRefHistory`
- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)
- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)
- Stats visualization uses adaptive downsampling for performance
### Container Labels
- `dev.dozzle.name`: Custom container display name
- `dev.dozzle.group`: Group containers together
- Label-based filtering throughout the application
### Deployment Modes
- **Server mode**: Single or multi-host Docker monitoring
- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API
- **K8s mode**: Pod log monitoring in Kubernetes cluster
- **Agent mode**: Lightweight gRPC agent for remote log collection
+13 -13
View File
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
+6 -67
View File
@@ -1,70 +1,9 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS node
RUN corepack enable
WORKDIR /build
# Install dependencies from lock file
COPY pnpm-*.yaml ./
RUN pnpm fetch --ignore-scripts --no-optional
# Copy package.json and install dependencies
COPY package.json ./
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* *.config.ts *.config.js *.config.cjs ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
&& mkdir /dozzle \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
WORKDIR /dozzle
# Copy go mod files
COPY go.* ./
RUN go mod download
# Copy all other files
COPY internal ./internal
COPY types ./types
COPY main.go ./
COPY protos ./protos
COPY shared_key.pem shared_cert.pem ./
# Copy assets built with node
COPY --from=node /build/dist ./dist
# Args
ARG TAG=dev
ARG TARGETOS TARGETARCH
# Generate protos
RUN go generate
# Build binary
RUN GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
RUN mkdir /data
FROM alpine:latest as certs
RUN apk --update add ca-certificates
FROM scratch
COPY --from=builder /data /data
COPY --from=builder /tmp /tmp
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle
EXPOSE 8080
ENV PATH=/bin
ENV DOCKER_API_VERSION 1.38
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY dozzle /
ENTRYPOINT ["/dozzle"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Amir Raminfar
Copyright (c) 2018 Amir Raminfar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-70
View File
@@ -1,70 +0,0 @@
PROTO_DIR := protos
GEN_DIR := internal/agent/pb
PROTO_FILES := $(wildcard $(PROTO_DIR)/*.proto)
GEN_FILES := $(patsubst $(PROTO_DIR)/%.proto,$(GEN_DIR)/%.pb.go,$(PROTO_FILES))
.PHONY: clean
clean:
@rm -rf dist
@go clean -i
@rm -f shared_key.pem shared_cert.pem
@rm -f $(GEN_DIR)/*.pb.go
.PHONY: dist
dist:
@pnpm build
.PHONY: fake_assets
fake_assets:
@echo 'Skipping asset build'
@mkdir -p dist
@echo "assets build was skipped" > dist/index.html
.PHONY: test
test: fake_assets generate
go test -cover -race -count 1 -timeout 40s ./...
.PHONY: build
build: dist generate
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=local"
.PHONY: docker
docker: shared_key.pem shared_cert.pem
@docker build --build-arg TAG=local -t amir20/dozzle .
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
.PHONY: dev
dev: generate fake_assets
pnpm dev
.PHONY: int
int:
docker compose up --build --force-recreate --exit-code-from playwright
shared_key.pem:
@openssl genpkey -algorithm Ed25519 -out shared_key.pem
shared_cert.pem: shared_key.pem
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 1825
@rm shared_request.csr
$(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
@go generate
.PHONY: push
push: docker
@docker tag amir20/dozzle:latest amir20/dozzle:local-test
@docker push amir20/dozzle:local-test
tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/air-verse/air@latest
run: docker
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
preview: build
pnpm preview
+59 -103
View File
@@ -1,138 +1,91 @@
<p align="center">
<img src="assets/logo.svg" alt="Dozzle Logo" width="200"/>
</p>
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Size](https://images.microbadger.com/badges/image/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://images.microbadger.com/badges/version/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
# Dozzle - [dozzle.dev](https://dozzle.dev/)
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesnt store any log files. It is for live monitoring of your container logs only.
Dozzle is a real-time log viewer for Docker. It's free. It's small. And it's right in your browser.
https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
While dozzle should work for most, it is not meant to be a full logging solution. For enterprise use, I recommend you look at [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana).
[![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/amir20/dozzle)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://img.shields.io/docker/v/amir20/dozzle?sort=semver)](https://hub.docker.com/r/amir20/dozzle/)
![Test](https://github.com/amir20/dozzle/workflows/Test/badge.svg)
But if you don't want to pay for those services, then you are in luck! Dozzle will be able to capture all logs from your containers and send them in real-time to your browser. Installation is also very easy. Dozzle is not a database. It does not store or save any logs. You can only see live logs while using Dozzle.
> [!NOTE]
> If you like Dozzle, check out [`dtop`](https://github.com/amir20/dtop) which is a top like application for monitoring Docker containers. It integrates with Dozzle to allow for linking directly to container logs.
![Image](demo.gif)
## Features
## Getting dozzle
- Intelligent fuzzy search for container names 🤖
- Search logs using regex 🔦
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine) 📊
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Live stats with memory and CPU usage
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for proxy forward authorization 🚨
- [Swarm](https://dozzle.dev/guide/swarm-mode) mode support 🐳
- [Agent](https://dozzle.dev/guide/agent) mode for monitoring multiple Docker hosts 🕵️‍♂️
- Dark mode 🌙
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
## Getting Started
Dozzle is a small container (7 MB compressed). Pull the latest release with:
Dozzle is a very small Docker container (4 MB compressed). Pull the latest release from the index:
$ docker pull amir20/dozzle:latest
### Running Dozzle
## Using dozzle
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8080/](http://localhost:8080/).
dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
Here is the Docker Compose file:
### With Docker swarm
docker service create \
--name=dozzle \
--publish=8888:8080 \
--constraint=node.role==manager \
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
amir20/dozzle:latest
### With Docker compose
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
environment:
- DOZZLE_TAILSIZE=100
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
- 9999:8080
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
#### Security
## Swarm Mode
dozzle doesn't support authentication out of the box. You can control the device dozzle binds to by passing `--addr` parameter. For example,
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --addr localhost:1224
$ docker service create --name dozzle --env DOZZLE_MODE=swarm --mode global --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle.
See the [Swarm Mode](https://dozzle.dev/guide/swarm-mode) documentation for more details.
If you wish to restrict the containers shown you can pass the `--filter` parameter. For example,
## Agent Mode
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
this would then only allow you to view containers with a name starting with "foo"
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
#### Changing base URL
See the [Agent Mode](https://dozzle.dev/guide/agent) documentation for more details.
dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar",
then you can override by using `--base /foobar`. See env variables below for using `DOZZLE_BASE` to change this.
## Technical Details
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest --base /foobar
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
### Installation on podman
#### Environment variables and configuration
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or enviroment variables. The table below outlines all supported options and their respective env vars.
Verify first if your podman installation has enabled remote socket:
```
podman info
```
When you get under the key remote socket output like this, its already enabled:
```
remoteSocket:
exists: true
path: /run/user/1000/podman/podman.sock
```
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
Once you have the podman remote socket you can run Dozzle on podman.
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
```
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
For more details check [Podman Infos](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it)
## Security
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
## Analytics collected
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [ Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
If you do not want to be tracked at all, see the `--no-analytics` flag below.
## Environment variables and configuration
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. See documentation at [https://dozzle.dev/guide/supported-env-vars](https://dozzle.dev/guide/supported-env-vars) for more details.
## Support
There are many ways you can support Dozzle:
- Use it! Write about it! Star it! If you love Dozzle, drop me a line and tell me what you love.
- Blog about Dozzle to spread the word. If you are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
- Sponsor my work at https://www.buymeacoffee.com/amirraminfar
<a href="https://www.buymeacoffee.com/amirraminfar" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
| Flag | Env Variable | Default |
| ------------ | -------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--showAll` | `DOZZLE_SHOWALL` | `false` |
| n/a | `DOCKER_API_VERSION` | `1.38` |
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
| `--filter` | `DOZZLE_FILTER` | `""` |
## License
@@ -140,10 +93,13 @@ There are many ways you can support Dozzle:
## Building
To build and test locally:
To Build and test locally:
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install tools with `make tools`.
4. Install node modules `pnpm install`.
5. Run `make dev` to start a development server with hot reload.
1. Install NodeJs.
2. Install Go.
3. Globally install [packr utility](https://github.com/gobuffalo/packr) with `go get -u github.com/gobuffalo/packr/packr` outside of dozzle directory.
4. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle.
5. Install node modules with `npm install`.
6. Do `npm start`
Instructions for Github actions can be found [here](.github/goreleaser/Dockerfile) which build and tests Dozzle.
-5
View File
@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Dozzle is checked daily with https://docs.docker.com/scout/ and its dependencies are updated regularly. If you find a security bug, please update to the latest. For vulnerability on latest images please file an issue on Github.
+85
View File
@@ -0,0 +1,85 @@
/* snapshot: Test_createRoutes_foobar */
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
foo page
/* snapshot: Test_createRoutes_index */
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
index page
/* snapshot: Test_createRoutes_redirect */
HTTP/1.1 301 Moved Permanently
Connection: close
Content-Type: text/html; charset=utf-8
Location: /foobar/
<a href="/foobar/">Moved Permanently</a>.
/* snapshot: Test_createRoutes_version */
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
dev
none
unknown
/* snapshot: Test_handler_listContainers_happy */
HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
[{"id":"1234567890","names":null,"name":"test","image":"image","imageId":"image_id","command":"command","created":0,"state":"state","status":"status"}]
/* snapshot: Test_handler_streamEvents_error */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamEvents_error_request */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamEvents_happy */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
event: containers-changed
data: start
/* snapshot: Test_handler_streamLogs_error_finding_container */
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
error finding container
/* snapshot: Test_handler_streamLogs_error_reading */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
/* snapshot: Test_handler_streamLogs_happy */
HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
data: INFO Testing logs...
+50
View File
@@ -0,0 +1,50 @@
import EventSource from "eventsourcemock";
import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import App from "./App";
const localVue = createLocalVue();
localVue.use(Vuex);
describe("<App />", () => {
const stubs = { RouterLink: RouterLinkStub, "router-view": true };
let store;
beforeEach(() => {
global.BASE_PATH = "";
global.EventSource = EventSource;
const state = {
containers: [
{ id: "abc", name: "Test 1" },
{ id: "xyz", name: "Test 2" }
]
};
const actions = {
FETCH_CONTAINERS: () => Promise.resolve()
};
store = new Vuex.Store({
state,
actions
});
});
test("is a Vue instance", async () => {
const wrapper = shallowMount(App, { stubs, store, localVue });
expect(wrapper.isVueInstance()).toBeTruthy();
});
test("has right title", async () => {
const wrapper = shallowMount(App, { stubs, store, localVue });
await wrapper.vm.$nextTick();
expect(wrapper.vm.title).toContain("2 containers");
});
test("renders correctly", async () => {
const wrapper = shallowMount(App, { stubs, store, localVue });
await wrapper.vm.$nextTick();
expect(wrapper.element).toMatchSnapshot();
});
});
+80 -43
View File
@@ -1,52 +1,89 @@
<template>
<router-view></router-view>
<template lang="html">
<div class="columns is-marginless">
<side-menu></side-menu>
<div class="column is-offset-3-tablet is-offset-2-widescreen is-9-tablet is-10-widescreen is-paddingless">
<splitpanes>
<pane>
<router-view></router-view>
</pane>
<pane v-for="other in activeContainers" :key="other.id">
<scrollable-view>
<template v-slot:header>
<div class="name columns is-marginless">
<span class="column">{{ other.name }}</span>
<span class="column is-narrow">
<button class="delete is-medium" @click="removeActiveContainer(other)"></button>
</span>
</div>
</template>
<log-viewer-with-source :id="other.id"></log-viewer-with-source>
</scrollable-view>
</pane>
</splitpanes>
</div>
</div>
</template>
<script lang="ts" setup>
const mode = useColorMode();
watchEffect(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
} else {
document.documentElement.classList.remove("has-custom-scrollbars");
}
<script>
import { mapActions, mapGetters, mapState } from "vuex";
import { Splitpanes, Pane } from "splitpanes";
let theme = lightTheme.value;
if (theme === "auto") {
theme = mode.value;
import LogViewerWithSource from "./components/LogViewerWithSource";
import ScrollableView from "./components/ScrollableView";
import SideMenu from "./components/SideMenu";
export default {
name: "App",
components: {
LogViewerWithSource,
SideMenu,
ScrollableView,
Splitpanes,
Pane
},
data() {
return {
title: "",
showNav: false
};
},
metaInfo() {
return {
title: this.title,
titleTemplate: "%s - Dozzle"
};
},
async created() {
await this.fetchContainerList();
this.title = `${this.containers.length} containers`;
},
computed: {
...mapState(["containers", "activeContainers"])
},
methods: {
...mapActions({
fetchContainerList: "FETCH_CONTAINERS",
removeActiveContainer: "REMOVE_ACTIVE_CONTAINER"
})
},
watch: {
$route(to, from) {
this.showNav = false;
}
}
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", theme == "dark" ? "#121212" : "#F5F5F5");
document.documentElement.setAttribute("data-theme", theme);
});
};
</script>
<style>
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
<style scoped lang="scss">
.name {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(0, 0, 0, 0.1);
font-weight: bold;
font-family: monospace;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
::v-deep .splitpanes__splitter {
min-width: 4px;
background: #666;
}
</style>
+26
View File
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> renders correctly 1`] = `
<div
class="columns is-marginless"
>
<side-menu-stub />
<div
class="column is-offset-3-tablet is-offset-2-widescreen is-9-tablet is-10-widescreen is-paddingless"
>
<splitpanes-stub
dblclicksplitter="true"
pushotherpanes="true"
>
<pane-stub
maxsize="100"
minsize="0"
>
<router-view-stub />
</pane-stub>
</splitpanes-stub>
</div>
</div>
`;
-795
View File
@@ -1,795 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const DEFAULT_SETTINGS: typeof import('./stores/settings').DEFAULT_SETTINGS
const EffectScope: typeof import('vue').EffectScope
const K8sNamespace: typeof import('./stores/k8s').K8sNamespace
const K8sOwner: typeof import('./stores/k8s').K8sOwner
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const allLevels: typeof import('./composable/logContext').allLevels
const arrayEquals: typeof import('./utils/index').arrayEquals
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const automaticRedirect: typeof import('./stores/settings').automaticRedirect
const collapseNav: typeof import('./stores/settings').collapseNav
const compact: typeof import('./stores/settings').compact
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 createDrawer: typeof import('./composable/drawer').createDrawer
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 createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const dateLocale: typeof import('./stores/settings').dateLocale
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 drawerContext: typeof import('./composable/drawer').drawerContext
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 flattenJSONToMap: typeof import('./utils/index').flattenJSONToMap
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 getCurrentWatcher: typeof import('vue').getCurrentWatcher
const getDeep: typeof import('./utils/index').getDeep
const globalShowPopup: typeof import('./composable/popup').globalShowPopup
const groupContainers: typeof import('./stores/settings').groupContainers
const h: typeof import('vue').h
const hashCode: typeof import('./utils/index').hashCode
const hourStyle: typeof import('./stores/settings').hourStyle
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDefined: typeof import('@vueuse/core').isDefined
const isMobile: typeof import('./composable/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 isShallow: typeof import('vue').isShallow
const lightTheme: typeof import('./stores/settings').lightTheme
const loadBetween: typeof import('./composable/eventStreams').loadBetween
const locale: typeof import('./stores/settings').locale
const loggingContextKey: typeof import('./composable/logContext').loggingContextKey
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const manualResetRef: typeof import('@vueuse/core')['manualResetRef']
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('./stores/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 onElementRemoval: typeof import('@vueuse/core').onElementRemoval
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 onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const persistentVisibleKeysForContainer: typeof import('./composable/storage').persistentVisibleKeysForContainer
const pinnedContainers: typeof import('./composable/storage').pinnedContainers
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const provideLoggingContext: typeof import('./composable/logContext').provideLoggingContext
const provideScrollContext: typeof import('./composable/scrollContext').provideScrollContext
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 refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const scrollContextKey: typeof import('./composable/scrollContext').scrollContextKey
const search: typeof import('./stores/settings').search
const sessionHost: typeof import('./composable/storage').sessionHost
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const setTitle: typeof import('./composable/title').setTitle
const settings: typeof import('./stores/settings').settings
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const showAllContainers: typeof import('./stores/settings').showAllContainers
const showStd: typeof import('./stores/settings').showStd
const showTimestamp: typeof import('./stores/settings').showTimestamp
const size: typeof import('./stores/settings').size
const smallerScrollbars: typeof import('./stores/settings').smallerScrollbars
const softWrap: typeof import('./stores/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 toRelativeTime: typeof import('./utils/index').toRelativeTime
const toValue: typeof import('vue').toValue
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 useAnimate: typeof import('@vueuse/core').useAnimate
const useAnnouncements: typeof import('./stores/announcements').useAnnouncements
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
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 useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
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 useArrayUnique: typeof import('@vueuse/core').useArrayUnique
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 useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useContainerActions: typeof import('./composable/containerActions').useContainerActions
const useContainerStore: typeof import('./stores/container').useContainerStore
const useContainerStream: typeof import('./composable/eventStreams').useContainerStream
const useCountdown: typeof import('@vueuse/core').useCountdown
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 useDownloadUrl: typeof import('./composable/downloadUrl').useDownloadUrl
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDrawer: typeof import('./composable/drawer').useDrawer
const useDropZone: typeof import('@vueuse/core').useDropZone
const useDuckDB: typeof import('./composable/duckdb').useDuckDB
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 useExponentialMovingAverage: typeof import('./utils/index').useExponentialMovingAverage
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 useGroupedStream: typeof import('./composable/eventStreams').useGroupedStream
const useHead: typeof import('@vueuse/head').useHead
const useHistoricalContainerLog: typeof import('./composable/historicalLogs').useHistoricalContainerLog
const useHostStream: typeof import('./composable/eventStreams').useHostStream
const useHosts: typeof import('./stores/hosts').useHosts
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
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 useK8sStore: typeof import('./stores/k8s').useK8sStore
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useLoggingContext: typeof import('./composable/logContext').useLoggingContext
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 useMergedStream: typeof import('./composable/eventStreams').useMergedStream
const useModel: typeof import('vue').useModel
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 useNamespaceStream: typeof import('./composable/eventStreams').useNamespaceStream
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 useOwnerStream: typeof import('./composable/eventStreams').useOwnerStream
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePinnedLogsStore: typeof import('./stores/pinned').usePinnedLogsStore
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
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 usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useProfileStorage: typeof import('./composable/profileStorage').useProfileStorage
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 useSSRWidth: typeof import('@vueuse/core').useSSRWidth
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 useScrollContext: typeof import('./composable/scrollContext').useScrollContext
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSearchFilter: typeof import('./composable/search').useSearchFilter
const useSeoMeta: typeof import('@vueuse/head').useSeoMeta
const useServiceStream: typeof import('./composable/eventStreams').useServiceStream
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSimpleRefHistory: typeof import('./utils/index').useSimpleRefHistory
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 useStackStream: typeof import('./composable/eventStreams').useStackStream
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 useSwarmStore: typeof import('./stores/swarm').useSwarmStore
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
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 useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
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 useToast: typeof import('./composable/toast').useToast
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('./composable/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 watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
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
const withBase: typeof import('./stores/config').withBase
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { DrawerWidth } from './composable/drawer'
import('./composable/drawer')
// @ts-ignore
export type { LogStreamSource } from './composable/eventStreams'
import('./composable/eventStreams')
// @ts-ignore
export type { Config, Profile } from './stores/config'
import('./stores/config')
// @ts-ignore
export type { Host } from './stores/hosts'
import('./stores/hosts')
// @ts-ignore
export type { K8sNamespace, K8sOwner } from './stores/k8s'
import('./stores/k8s')
// @ts-ignore
export type { Settings } from './stores/settings'
import('./stores/settings')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly K8sNamespace: UnwrapRef<typeof import('./stores/k8s')['K8sNamespace']>
readonly K8sOwner: UnwrapRef<typeof import('./stores/k8s')['K8sOwner']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
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 createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
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 createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly dateLocale: UnwrapRef<typeof import('./stores/settings')['dateLocale']>
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 drawerContext: UnwrapRef<typeof import('./composable/drawer')['drawerContext']>
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 flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
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 getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly groupContainers: UnwrapRef<typeof import('./stores/settings')['groupContainers']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hashCode: UnwrapRef<typeof import('./utils/index')['hashCode']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composable/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 isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly loadBetween: UnwrapRef<typeof import('./composable/eventStreams')['loadBetween']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly loggingContextKey: UnwrapRef<typeof import('./composable/logContext')['loggingContextKey']>
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('./stores/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 onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
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 onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeysForContainer: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeysForContainer']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
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 refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/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('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/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 toRelativeTime: UnwrapRef<typeof import('./utils/index')['toRelativeTime']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
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 useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAnnouncements: UnwrapRef<typeof import('./stores/announcements')['useAnnouncements']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
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 useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
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 useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
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 useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
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 useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
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 useDownloadUrl: UnwrapRef<typeof import('./composable/downloadUrl')['useDownloadUrl']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useDuckDB: UnwrapRef<typeof import('./composable/duckdb')['useDuckDB']>
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 useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
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 useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHistoricalContainerLog: UnwrapRef<typeof import('./composable/historicalLogs')['useHistoricalContainerLog']>
readonly useHostStream: UnwrapRef<typeof import('./composable/eventStreams')['useHostStream']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
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 useK8sStore: UnwrapRef<typeof import('./stores/k8s')['useK8sStore']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
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 useMergedStream: UnwrapRef<typeof import('./composable/eventStreams')['useMergedStream']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
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 useNamespaceStream: UnwrapRef<typeof import('./composable/eventStreams')['useNamespaceStream']>
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 useOwnerStream: UnwrapRef<typeof import('./composable/eventStreams')['useOwnerStream']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePinnedLogsStore: UnwrapRef<typeof import('./stores/pinned')['usePinnedLogsStore']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
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 usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
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 useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
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 useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useServiceStream: UnwrapRef<typeof import('./composable/eventStreams')['useServiceStream']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
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 useStackStream: UnwrapRef<typeof import('./composable/eventStreams')['useStackStream']>
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 useSwarmStore: UnwrapRef<typeof import('./stores/swarm')['useSwarmStore']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
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 useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
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 useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
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('./composable/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 watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
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']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
-155
View File
@@ -1,155 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Announcements: typeof import('./components/Announcements.vue')['default']
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
Carousel: typeof import('./components/common/Carousel.vue')['default']
CarouselItem: typeof import('./components/common/CarouselItem.vue')['default']
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
'Cil:circle': typeof import('~icons/cil/circle')['default']
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
ContainerPopup: typeof import('./components/ContainerPopup.vue')['default']
ContainerStatCell: typeof import('./components/ContainerStatCell.vue')['default']
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
DateTime: typeof import('./components/common/DateTime.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
GroupedLogItem: typeof import('./components/LogViewer/GroupedLogItem.vue')['default']
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
HistoricalContainerLog: typeof import('./components/ContainerViewer/HistoricalContainerLog.vue')['default']
HostCard: typeof import('./components/HostCard.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
HostMenu: typeof import('./components/HostMenu.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default']
'Ion:ellipsisVertical': typeof import('~icons/ion/ellipsis-vertical')['default']
K8sMenu: typeof import('./components/K8sMenu.vue')['default']
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default']
LoadMoreLogItem: typeof import('./components/LogViewer/LoadMoreLogItem.vue')['default']
LogActions: typeof import('./components/LogViewer/LogActions.vue')['default']
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogItem: typeof import('./components/LogViewer/LogItem.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogList: typeof import('./components/LogViewer/LogList.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
'MaterialSymbols:codeBlocksRounded': typeof import('~icons/material-symbols/code-blocks-rounded')['default']
'MaterialSymbols:contentCopy': typeof import('~icons/material-symbols/content-copy')['default']
'MaterialSymbols:eyeTracking': typeof import('~icons/material-symbols/eye-tracking')['default']
'MaterialSymbols:link': typeof import('~icons/material-symbols/link')['default']
'MaterialSymbols:logout': typeof import('~icons/material-symbols/logout')['default']
'MaterialSymbols:person': typeof import('~icons/material-symbols/person')['default']
'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default']
'MaterialSymbolsLight:collapseAll': typeof import('~icons/material-symbols-light/collapse-all')['default']
'Mdi:account': typeof import('~icons/mdi/account')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
'Mdi:github': typeof import('~icons/mdi/github')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:key': typeof import('~icons/mdi/key')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
MetricCard: typeof import('./components/MetricCard.vue')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
NamespaceLog: typeof import('./components/K8sViewer/NamespaceLog.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
OwnerLog: typeof import('./components/K8sViewer/OwnerLog.vue')['default']
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
'Ph:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default']
'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default']
'Ph:circlesFour': typeof import('~icons/ph/circles-four')['default']
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
PhArrowDown: typeof import('~icons/ph/arrow-down')['default']
PhArrowUp: typeof import('~icons/ph/arrow-up')['default']
Popup: typeof import('./components/Popup.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default']
SideDrawer: typeof import('./components/common/SideDrawer.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
SidePanel: typeof import('./components/SidePanel.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
SlideTransition: typeof import('./components/common/SlideTransition.vue')['default']
SQLTable: typeof import('./components/LogViewer/SQLTable.vue')['default']
StackLog: typeof import('./components/StackViewer/StackLog.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
Terminal: typeof import('./components/Terminal.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
ToastModal: typeof import('./components/common/ToastModal.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
-119
View File
@@ -1,119 +0,0 @@
<template>
<Dropdown
class="dropdown-end"
@click="config.releaseCheckMode === 'manual' && fetchReleases()"
@closed="releaseSeen = mostRecent?.tag ?? config.version"
>
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
<span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span>
<span class="bg-red absolute top-0 right-px size-2 rounded-full"></span>
</template>
</template>
<template #content>
<div class="w-72">
<ul class="space-y-4 p-2">
<li v-for="release in announcements" v-if="announcements.length > 0">
<template v-if="release.announcement">
<div class="flex items-baseline gap-1">
<carbon:information class="text-info self-center" />
<a
:href="release.htmlUrl"
class="link-primary text-lg font-bold"
target="_blank"
rel="noreferrer noopener"
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
</div>
<div class="text-base-content/80 text-sm">
{{ release.body }}
</div>
</template>
<template v-else>
<div class="flex items-baseline gap-1">
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
<a
:href="release.htmlUrl"
class="link-primary text-lg font-bold"
target="_blank"
rel="noreferrer noopener"
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
{{ $t("releases.latest") }}
</Tag>
</div>
<div class="text-base-content/80 text-sm">
{{ summary(release) }}
</div>
</template>
</li>
<li v-else>
<div class="text-base-content/80 text-sm">
{{ $t("releases.no_releases") }}
</div>
</li>
</ul>
</div>
</template>
</Dropdown>
</template>
<script setup lang="ts">
import { useAnnouncements } from "@/stores/announcements";
const { announcements, mostRecent, fetchReleases } = useAnnouncements();
const { t } = useI18n();
const releaseSeen = useProfileStorage("releaseSeen", config.version);
function summary(release: { features: number; bugFixes: number; breaking: number }) {
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
return t("releases.three_parts", {
first: t("releases.breaking", { count: release.breaking }),
second: t("releases.features", { count: release.features }),
third: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.bugFixes > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.bugFixes > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.bugFixes", { count: release.bugFixes }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.features > 0) {
return t("releases.features", { count: release.features });
}
if (release.bugFixes > 0) {
return t("releases.bugFixes", { count: release.bugFixes });
}
if (release.breaking > 0) {
return t("releases.breaking", { count: release.breaking });
}
}
</script>
<style scoped></style>
-108
View File
@@ -1,108 +0,0 @@
<template>
<div ref="chartContainer" class="flex items-end gap-[2px]" @mousemove="onContainerHover">
<div
v-for="(dataPoint, i) in downsampledData"
:key="i"
class="bar min-h-px flex-1 rounded-t-sm"
:class="barClass"
:style="{ '--height': `${Math.min(dataPoint, 100)}%` }"
></div>
</div>
</template>
<style scoped>
.bar {
height: var(--height);
will-change: height;
contain: layout;
}
</style>
<script setup lang="ts">
const { chartData, barClass = "" } = defineProps<{
chartData: number[];
barClass?: string;
}>();
const hoverIndex = defineEmit<[startIndex: number, endIndex: number]>();
const chartContainer = ref<HTMLElement | null>(null);
const { width } = useElementSize(chartContainer);
const BAR_WIDTH = 3;
const GAP = 2;
const availableBars = computed(() => Math.floor(width.value / (BAR_WIDTH + GAP)));
const bucketSize = computed(() => Math.ceil(chartData.length / availableBars.value));
const downsampledData = ref<number[]>([]);
const changeCounter = ref(-1);
// Watch chartData changes
watch(
() => chartData,
() => {
// If changeCounter is -1, it means this is the first time the data is loaded
if (changeCounter.value === -1) {
recalculate();
}
changeCounter.value++;
if (changeCounter.value >= bucketSize.value) {
// Recalculate when counter reaches bucket size
recalculate();
changeCounter.value = 0;
}
},
);
// Recalculate when width changes
watch([availableBars, bucketSize], () => {
recalculate();
changeCounter.value = -1;
});
function recalculate() {
if (chartData.length <= availableBars.value || availableBars.value === 0) {
downsampledData.value = [...chartData];
return;
}
const size = bucketSize.value;
const result = [];
// Create complete buckets
const numCompleteBuckets = Math.floor(chartData.length / size);
for (let i = 0; i < numCompleteBuckets; i++) {
const start = i * size;
const end = start + size;
const bucket = chartData.slice(start, end);
const avg = bucket.reduce((sum, val) => sum + val, 0) / bucket.length;
result.push(avg);
}
// Show only the last N bars that fit on screen
downsampledData.value = result.slice(-availableBars.value);
}
function onContainerHover(event: MouseEvent) {
if (!chartContainer.value) return;
const rect = chartContainer.value.getBoundingClientRect();
const x = event.clientX - rect.left;
// Calculate which bar the mouse is over based on position
const barWidth = width.value / downsampledData.value.length;
const index = Math.floor(x / barWidth);
// Ensure index is within bounds
if (index < 0 || index >= downsampledData.value.length) return;
// Map downsampled index back to original data index range
const numCompleteBuckets = Math.floor(chartData.length / bucketSize.value);
const offset = Math.max(0, numCompleteBuckets - availableBars.value);
const startIndex = (offset + index) * bucketSize.value;
const endIndex = Math.min(startIndex + bucketSize.value - 1, chartData.length - 1);
hoverIndex(startIndex, endIndex);
}
</script>
-24
View File
@@ -1,24 +0,0 @@
<template>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm"><slot /> <carbon:caret-down /></button>
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm">
<li v-for="other in containers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }" class="text-nowrap">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
{{ other.name }}
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type Container } from "@/models/Container";
const { containers } = defineProps<{
containers: Container[];
}>();
</script>
-38
View File
@@ -1,38 +0,0 @@
<template>
<table class="w-full border-separate border-spacing-x-1">
<tbody>
<tr>
<th class="text-right font-light capitalize">STATE</th>
<td class="font-semibold uppercase">{{ container.state }}</td>
</tr>
<tr v-if="container.startedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">STARTED</th>
<td class="font-semibold">
<RelativeTime :date="container.startedAt" />
</td>
</tr>
<tr v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">FINISHED</th>
<td class="font-semibold">
<RelativeTime :date="container.finishedAt" />
</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">Load</th>
<td class="font-semibold">{{ container.stat.cpu.toFixed(2) }}%</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">MEM</th>
<td class="font-semibold">{{ formatBytes(container.stat.memoryUsage) }}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
const { container } = defineProps<{
container: Container;
}>();
</script>
-55
View File
@@ -1,55 +0,0 @@
<template>
<div class="flex flex-row items-center gap-2">
<BarChart class="h-4 flex-1" :chart-data="chartData" :bar-class="barClass" />
<span class="w-fit text-right text-sm">{{ displayValue }}</span>
</div>
</template>
<script setup lang="ts">
import type { Container } from "@/models/Container";
import type { Host } from "@/stores/hosts";
const { container, type, host } = defineProps<{
container: Container;
type: "cpu" | "mem";
host: Host;
}>();
function totalCores(): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
return host.nCPU ?? 1;
}
const chartData = computed(() => {
if (type === "cpu") {
const cores = totalCores();
return container.statsHistory.map((stat) => Math.min(stat.cpu / cores, 100));
}
return container.statsHistory.map((stat) => Math.min(stat.memory, 100));
});
const averageValue = computed(() => {
if (type === "cpu") {
const cores = totalCores();
return Math.min(container.movingAverage.cpu / cores, 100);
}
return container.movingAverage.memory;
});
const displayValue = computed(() => {
if (type === "cpu") {
return `${averageValue.value.toFixed(0)}%`;
}
return formatBytes(container.movingAverage.memoryUsage);
});
const barClass = computed(() => {
const value = averageValue.value;
if (value <= 50) return "bg-success";
if (value <= 70) return "bg-secondary";
if (value <= 90) return "bg-warning";
return "bg-error";
});
</script>
-240
View File
@@ -1,240 +0,0 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
v-model="selectedHost"
/>
</div>
<DropdownMenu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<div class="rounded-box border-base-content/10 overflow-x-auto border">
<table class="table-md md:table-lg table-zebra table">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="[value.customClass, { 'selected-sort': key === sortField }]"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody class="bg-base-300/30">
<tr v-for="container in paginated" :key="container.id" v-memo="[container.id]" class="hover:bg-base-100/80!">
<td v-if="isVisible('name')" class="max-w-80 truncate">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<RelativeTime :date="container.created" />
</td>
<td v-if="isVisible('cpu')">
<ContainerStatCell :container="container" type="cpu" :host="hosts[container.host]" />
</td>
<td v-if="isVisible('mem')">
<ContainerStatCell :container="container" type="mem" :host="hosts[container.host]" />
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated && totalPages <= 15">
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
/>
</nav>
<DropdownMenu
v-else-if="isPaginated"
class="btn-sm"
v-model="currentPage"
:options="Array.from({ length: totalPages }, (_, i) => ({ label: `${i + 1}`, value: i + 1 }))"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
import { toRefs } from "@vueuse/core";
const { hosts } = useHosts();
const selectedHost = ref(null);
const fields: Record<
string,
{
label: string;
sortFunc: (a: Container, b: Container) => number;
mobileVisible: boolean;
customClass?: string;
}
> = {
name: {
label: "label.container-name",
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
mobileVisible: true,
},
host: {
label: "label.host",
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
mobileVisible: false,
customClass: "w-1",
},
state: {
label: "label.status",
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
mobileVisible: false,
customClass: "w-1",
},
created: {
label: "label.created",
sortFunc: (a: Container, b: Container) => (a.created.getTime() - b.created.getTime()) * direction.value,
mobileVisible: true,
customClass: "w-1",
},
cpu: {
label: "label.avg-cpu",
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
mobileVisible: false,
customClass: "min-w-48",
},
mem: {
label: "label.avg-mem",
sortFunc: (a: Container, b: Container) =>
(a.movingAverage.memoryUsage - b.movingAverage.memoryUsage) * direction.value,
mobileVisible: false,
customClass: "min-w-48",
},
};
const { containers } = defineProps<{
containers: Container[];
}>();
type keys = keyof typeof fields;
const perPage = useStorage("DOZZLE_TABLE_PAGE_SIZE", 15);
const pageSizes = [15, 30, 50, 100];
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
column: "created" as keys,
direction: -1 as 1 | -1,
});
const { column: sortField, direction } = toRefs(storage.value);
const counter = useInterval(10000);
const filteredContainers = computed(() =>
containers.filter((c) => selectedHost.value === null || c.host === selectedHost.value),
);
const sortedContainers = computedWithControl(
() => [filteredContainers.value.length, sortField.value, direction.value, counter.value],
() => filteredContainers.value.sort((a, b) => fields[sortField.value].sortFunc(a, b)),
);
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage.value));
const isPaginated = computed(() => totalPages.value > 1);
const currentPage = ref(1);
watch(perPage, () => (currentPage.value = 1));
const paginated = computed(() => {
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
return sortedContainers.value.slice(start, end);
});
function sort(field: keys) {
if (sortField.value === field) {
direction.value *= -1;
} else {
sortField.value = field;
direction.value = 1;
}
}
function isVisible(field: keys) {
return fields[field].mobileVisible || !isMobile.value;
}
</script>
<style scoped>
@reference "@/main.css";
[data-icon] {
display: none;
transition: transform 0.2s ease-in-out;
[data-direction="desc"] & {
transform: rotate(180deg);
}
}
th {
@apply border-base-200 border-b-2;
&.selected-sort {
font-weight: bold;
@apply border-primary;
[data-icon] {
display: inline-block;
}
}
}
tbody td {
white-space: nowrap;
}
a {
@apply hover:text-primary;
}
</style>
@@ -1,249 +0,0 @@
<template>
<div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
@click="hideMenu"
>
<li v-if="!historical">
<a @click="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li v-if="enableDownload">
<a :href="downloadUrl" download>
<octicon:download-24 />
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
</a>
</li>
<li v-if="!historical">
<a @click="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li v-if="hasComplexLogs">
<a @click="showDrawer(LogAnalytics, { container }, 'lg')">
<ph:file-sql /> SQL Analytics
<KeyShortcut char="f" :modifiers="['shift', 'meta']" />
</a>
</li>
<li class="line"></li>
<li>
<details>
<summary>
<div class="flex w-4">
<carbon:circle-solid class="text-red w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2.5" v-if="streamConfig.stdout" />
</div>
Streams
</summary>
<ul class="menu">
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == true && streamConfig.stderr == true" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show-all") }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == true && streamConfig.stderr == false" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == false && streamConfig.stderr == true" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</details>
</li>
<li>
<details class="group/details">
<summary>
<mdi:gauge />
Levels
<Toggle
class="toggle-xs invisible group-open/details:visible"
v-model="toggleAllLevels"
title="Toggle all levels"
/>
</summary>
<ul class="menu">
<li v-for="level in allLevels">
<a class="capitalize" @click="levels.has(level) ? levels.delete(level) : levels.add(level)">
<mdi:check class="w-4" v-if="levels.has(level)" />
<div v-else class="w-4"></div>
<div class="flex">
<div class="badge" :data-level="level">{{ level }}</div>
</div>
</a>
</li>
</ul>
</details>
</li>
<!-- Container Actions (Enabled via config) -->
<template v-if="enableActions && !historical">
<li class="line"></li>
<li>
<button
@click="stop()"
:disabled="actionStates.stop || actionStates.restart"
v-if="container.state == 'running'"
>
<carbon:stop-filled-alt /> {{ $t("toolbar.stop") }}
</button>
<button
@click="start()"
:disabled="actionStates.start || actionStates.restart"
v-if="container.state != 'running'"
>
<carbon:play /> {{ $t("toolbar.start") }}
</button>
</li>
<li>
<button @click="restart()" :disabled="disableRestart">
<carbon:restart
:class="{
'animate-spin': actionStates.restart,
'text-secondary': actionStates.restart,
}"
/>
{{ $t("toolbar.restart") }}
</button>
</li>
</template>
<template v-if="enableShell && !historical">
<li class="line"></li>
<li>
<a @click="showDrawer(Terminal, { container, action: 'attach' }, 'lg')">
<ri:terminal-window-fill />
{{ $t("toolbar.attach") }}
<KeyShortcut char="a" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a @click="showDrawer(Terminal, { container, action: 'exec' }, 'lg')">
<material-symbols:terminal />
{{ $t("toolbar.shell") }}
<KeyShortcut char="e" :modifiers="['shift', 'meta']" />
</a>
</li>
</template>
</ul>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { allLevels } from "@/composable/logContext";
import LogAnalytics from "../LogViewer/LogAnalytics.vue";
import Terminal from "@/components/Terminal.vue";
const { showSearch } = useSearchFilter();
const { enableActions, enableShell, enableDownload } = config;
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const showDrawer = useDrawer();
const { container, historical = false } = defineProps<{ container: Container; historical?: boolean }>();
const clear = defineEmit();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
onKeyStroke("f", (e) => {
if (hasComplexLogs.value) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(LogAnalytics, { container }, "lg");
e.preventDefault();
}
}
});
if (enableShell) {
onKeyStroke("a", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "attach" }, "lg");
e.preventDefault();
}
});
onKeyStroke("e", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "exec" }, "lg");
e.preventDefault();
}
});
}
const containerRef = computed(() => [container]);
const { downloadUrl, isFiltered } = useDownloadUrl(containerRef, streamConfig, levels);
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
const toggleAllLevels = computed({
get: () => levels.value.size === allLevels.length,
set: (value) => {
if (value) {
allLevels.forEach((level) => levels.value.add(level));
} else {
levels.value.clear();
}
},
});
const hideMenu = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
};
</script>
<style scoped>
@reference "@/main.css";
li.line {
@apply bg-base-content/20 h-px;
}
a {
@apply whitespace-nowrap;
}
.menu li ul {
margin-inline-start: 0;
&:before {
display: none;
}
}
</style>
@@ -1,26 +0,0 @@
<template>
<div class="inline-flex size-4" :health="health" v-if="health" :title="health">
<cil:check-circle v-if="health == 'healthy'" />
<cil:x-circle v-else-if="health == 'unhealthy'" />
<cil:circle v-else />
</div>
</template>
<script lang="ts" setup>
import { ContainerHealth } from "@/types/Container";
defineProps<{
health: ContainerHealth | undefined;
}>();
</script>
<style scoped>
@reference "@/main.css";
[health="unhealthy"] {
@apply text-red;
}
[health="healthy"] {
@apply text-green;
}
</style>
@@ -1,56 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="@container mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
<ContainerTitle :container="container" />
<MultiContainerStat
class="ml-auto lg:hidden lg:@3xl:flex"
:containers="[container]"
v-if="container.state === 'running'"
/>
<ContainerActionsToolbar @clear="viewer?.clear()" :container="container" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useContainerStream"
:entity="container"
:visible-keys="visibleKeys"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const {
id,
showTitle = false,
scrollable = false,
closable = false,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer(toRef(() => id));
const visibleKeys = persistentVisibleKeysForContainer(container);
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(
toRef(() => [container.value]),
{ showContainerName: false, showHostname: false },
);
</script>
@@ -1,75 +0,0 @@
<template>
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<label class="swap swap-rotate size-4">
<input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" />
</label>
<div class="inline-flex items-center text-sm">
<div class="breadcrumbs overflow-x-visible p-0 font-mono">
<ul>
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
{{ container.hostLabel }}
</li>
<li>
<template v-if="otherContainers.length === 0">{{ container.name }}</template>
<div v-else>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm">
{{ container.name }} <carbon:caret-down />
</button>
<ul
tabindex="0"
class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm"
>
<li v-for="other in otherContainers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
<div v-if="other.isSwarm">{{ other.swarmId }}</div>
<div v-else>{{ other.name }}</div>
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</div>
<ContainerHealth :health="container.health" v-if="container.health" />
<Tag class="hidden! font-mono @xl:block!" size="small">
{{ container.image.replace(/@sha.*/, "") }}
</Tag>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
const { container } = defineProps<{ container: Container }>();
const pinned = computed({
get: () => pinnedContainers.value.has(container.name),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.name);
} else {
pinnedContainers.value.delete(container.name);
}
},
});
const store = useContainerStore();
const { containers: allContainers } = storeToRefs(store);
const otherContainers = computed(() =>
allContainers.value
.filter((c) => c.name === container.name && c.id !== container.id)
.sort((a, b) => +b.created - +a.created),
);
</script>
<style scoped></style>
@@ -1,63 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="@container mx-2 flex items-center gap-2 md:ml-4">
<ContainerTitle :container="container" />
<router-link
:to="{ name: '/container/[id]', params: { id: container.id } }"
class="btn btn-secondary btn-sm"
v-if="container.state === 'running'"
>
<mdi:lightning-bolt />
Live Logs
</router-link>
<ContainerActionsToolbar class="max-md:hidden" :container="container" historical />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useHistoricalContainerLog"
:entity="historicalContainer"
:visible-keys="visibleKeys"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { HistoricalContainer } from "@/models/Container";
import { ComponentExposed } from "vue-component-type-helpers";
const {
id,
showTitle = false,
scrollable = false,
closable = false,
date,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
date: Date;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer(toRef(() => id));
const historicalContainer = toRef(() => new HistoricalContainer(container.value, date));
const visibleKeys = persistentVisibleKeysForContainer(container);
useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(
toRef(() => [container.value]),
{ showContainerName: false, showHostname: false, historical: true },
);
</script>
-122
View File
@@ -1,122 +0,0 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import FuzzySearchModal from "./FuzzySearchModal.vue";
import { Container } from "@/models/Container";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { createI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { router } from "@/modules/router";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
vi.mock("vue-router");
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
function createFuzzySearchModal() {
global.EventSource = EventSource;
const wrapper = mount(FuzzySearchModal, {
global: {
plugins: [
createI18n({}),
createTestingPinia({
createSpy: vi.fn,
initialState: {
container: {
containers: [
new Container(
"123",
new Date(),
new Date(),
new Date(),
"image",
"test",
"command",
"host",
{},
"running",
0,
0,
[],
),
new Container(
"345",
new Date(),
new Date(),
new Date(),
"image",
"foo bar",
"command",
"host",
{},
"running",
0,
0,
[],
),
new Container(
"567",
new Date(),
new Date(),
new Date(),
"image",
"baz",
"command",
"host",
{},
"running",
0,
0,
[],
),
],
},
},
}),
],
},
});
return wrapper;
}
/**
* @vitest-environment jsdom
*/
describe("<FuzzySearchModal />", () => {
vi.mocked(useRouter).mockReturnValue({
...router,
push: vi.fn(),
});
beforeEach(() => {
vi.mocked(useRouter().push).mockReset();
});
test("shows none initially", async () => {
const wrapper = createFuzzySearchModal();
expect(wrapper.findAll("li").length).toBe(0);
});
test("search for foo", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("foo");
expect(wrapper.findAll("li").length).toBe(1);
expect(wrapper.find("ul [data-name]").html()).toMatchInlineSnapshot(
`"<span data-v-dc2e8c61="" data-name=""><mark>foo</mark> bar</span>"`,
);
});
test("choose baz", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("baz");
await wrapper.find("input").trigger("keydown.enter");
expect(useRouter().push).toHaveBeenCalledWith({ name: "/container/[id]", params: { id: "567" } });
});
});
-224
View File
@@ -1,224 +0,0 @@
<template>
<div class="dropdown dropdown-open w-full shadow-md">
<div class="input input-xl input-primary flex w-full items-center">
<mdi:magnify class="flex size-8" />
<input
tabindex="0"
class="input-ghost flex-1 px-1"
ref="input"
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keydown.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keydown.enter.exact="selected(data[selectedIndex].item)"
@keydown.alt.enter="addColumn(data[selectedIndex].item)"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<form method="dialog" class="flex">
<button v-if="isMobile">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
</div>
<div
class="dropdown-content bg-base-100 relative! mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent px-2"
tabindex="0"
v-if="results.length"
>
<ul class="menu w-auto">
<li v-for="(result, index) in data" ref="listItems">
<a
class="grid auto-cols-max grid-cols-[min-content_auto] gap-2 py-4"
@click.prevent="selected(result.item)"
:class="{ 'menu-focus': index === selectedIndex }"
>
<div :class="{ 'text-primary': result.item.state === 'running' }">
<template v-if="result.item.type === 'container'">
<octicon:container-24 />
</template>
<template v-else-if="result.item.type === 'service'">
<ph:stack-simple />
</template>
<template v-else-if="result.item.type === 'stack'">
<ph:stack />
</template>
</div>
<div class="truncate">
<template v-if="config.hosts.length > 1 && result.item.host">
<span class="font-light">{{ result.item.host }}</span> /
</template>
<span data-name v-html="matchedName(result)"></span>
</div>
<RelativeTime :date="result.item.created" class="text-xs font-light" />
<span
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
class="hover:text-secondary"
>
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else-if="result.item.type === 'container'" />
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContainerState } from "@/types/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
import { type FuseResult } from "fuse.js";
const close = defineEmit();
const query = ref("");
const input = ref<HTMLInputElement>();
const listItems = ref<HTMLInputElement[]>();
const selectedIndex = ref(0);
const router = useRouter();
const containerStore = useContainerStore();
const pinnedStore = usePinnedLogsStore();
const { visibleContainers } = storeToRefs(containerStore);
const swarmStore = useSwarmStore();
const { stacks, services } = storeToRefs(swarmStore);
onMounted(async () => {
const dialog = input.value?.closest("dialog");
if (dialog) {
const animations = dialog.getAnimations();
await Promise.all(animations.map((animation) => animation.finished));
input.value?.focus();
}
});
type Item = {
id: string;
created: Date;
name: string;
state?: ContainerState;
host?: string;
type: "container" | "service" | "stack";
};
const list = computed(() => {
const items: Item[] = [];
for (const container of visibleContainers.value) {
items.push({
id: container.id,
created: container.created,
name: container.name,
state: container.state,
host: container.hostLabel,
type: "container",
});
}
for (const service of services.value) {
items.push({
id: service.name,
created: service.updatedAt,
name: service.name,
state: "running",
type: "service",
});
}
for (const stack of stacks.value) {
items.push({
id: stack.name,
created: stack.updatedAt,
name: stack.name,
state: "running",
type: "stack",
});
}
return items;
});
const { results } = useFuse(query, list, {
fuseOptions: {
keys: ["name", "host"],
includeScore: true,
useExtendedSearch: true,
threshold: 0.3,
includeMatches: true,
},
});
const data = computed(() => {
return [...results.value].sort((a: FuseResult<Item>, b: FuseResult<Item>) => {
if (a.score === b.score) {
if (a.item.state === b.item.state) {
return b.item.created.getTime() - a.item.created.getTime();
} else if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else {
return (a.score ?? 0) - (b.score ?? 0);
}
});
});
watch(query, (data) => {
if (data.length > 0) {
selectedIndex.value = 0;
}
});
watch(selectedIndex, () => {
listItems.value?.[selectedIndex.value].scrollIntoView({ block: "end" });
});
function selected(item: Item) {
if (item.type === "container") {
router.push({ name: "/container/[id]", params: { id: item.id } });
} else if (item.type === "service") {
router.push({ name: "/service/[name]", params: { name: item.id } });
} else if (item.type === "stack") {
router.push({ name: "/stack/[name]", params: { name: item.id } });
}
close();
}
function addColumn(container: { id: string }) {
pinnedStore.pinContainer(container);
close();
}
function matchedName({ item, matches = [] }: FuseResult<Item>) {
const matched = matches.find((match) => match.key === "name");
if (matched) {
const { indices } = matched;
const result = [];
let lastIndex = 0;
for (const [start, end] of indices) {
if (lastIndex > start) continue;
result.push(item.name.slice(lastIndex, start));
result.push(`<mark>${item.name.slice(start, end + 1)}</mark>`);
lastIndex = end + 1;
}
result.push(item.name.slice(lastIndex));
return result.join("");
} else {
return item.name;
}
}
</script>
<style scoped>
@reference "@/main.css";
:deep(mark) {
@apply bg-transparent text-inherit underline underline-offset-2;
}
</style>
-29
View File
@@ -1,29 +0,0 @@
<template>
<ul class="menu w-full p-0 text-[0.95rem]">
<li v-if="customGroups.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:bounding-box-fill />
{{ $t("label.group-menu") }}
</summary>
<ul>
<li v-for="group in customGroups" :key="group.name">
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">
{{ group.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
<script lang="ts" setup>
const store = useSwarmStore();
const { customGroups } = storeToRefs(store);
</script>
<style scoped></style>
@@ -1,47 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="group.containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<ContainerDropdown :containers="group.containers">
{{ $t("label.container", group.containers.length) }}
</ContainerDropdown>
<MultiContainerStat class="ml-auto" :containers="group.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useGroupedStream"
:entity="group"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { GroupedContainers } from "@/models/Container";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
name: string;
scrollable?: boolean;
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { ready } = storeToRefs(containerStore);
const swarmStore = useSwarmStore();
const { customGroups } = storeToRefs(swarmStore);
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
provideLoggingContext(
toRef(() => group.value.containers),
{ showContainerName: true, showHostname: false },
);
</script>
-154
View File
@@ -1,154 +0,0 @@
<template>
<div class="card bg-base-100">
<div class="card-body flex gap-2 max-md:p-4">
<div class="flex flex-row gap-2 overflow-hidden">
<div class="flex items-center gap-1 truncate text-xl font-semibold">
<HostIcon :type="host.type" class="flex-none" />
<div class="truncate">
{{ host.name }}
</div>
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
<carbon:warning />
offline
</span>
<span
class="badge badge-success badge-xs gap-2 p-2"
:class="{ 'badge-warning': config.version != host.agentVersion }"
v-else-if="host.type == 'agent'"
title="Dozzle Agent"
>
{{ host.agentVersion }}
</span>
</div>
<ul class="ml-auto flex flex-row flex-wrap gap-x-2 text-sm max-md:text-xs md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" />
{{ $t("label.container", hostContainers.length) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
</div>
<div class="grid grid-cols-2 gap-2 md:gap-3" v-if="stats">
<MetricCard
:icon="PhCpu"
:value="stats.weighted.movingAverage.totalCPU"
:chartData="cpuHistory"
container-class="border-primary/40 bg-primary/20"
text-class="text-primary"
bar-class="bg-primary"
:formatValue="(value) => `${value.toFixed(1)}%`"
:label="`${host.nCPU} CPU`"
/>
<MetricCard
:icon="PhMemory"
:value="stats.weighted.movingAverage.totalMemUsage"
:chartData="memHistory"
container-class="border-secondary/40 bg-secondary/20"
text-class="text-secondary"
bar-class="bg-secondary"
:formatValue="(value) => formatBytes(value, { decimals: 1 })"
:label="formatBytes(host.memTotal, { decimals: 1 })"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Host } from "@/stores/hosts";
import { Container } from "@/models/Container";
// @ts-ignore
import PhCpu from "~icons/ph/cpu";
// @ts-ignore
import PhMemory from "~icons/ph/memory";
const props = defineProps<{
host: Host;
}>();
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
};
const hostContainers = computed(() =>
containers.value.filter((container) => container.host === props.host.id && container.state === "running"),
);
function toContainerCores(container: Container): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
return props.host.nCPU ?? 1;
}
type TotalStat = {
totalCPU: number;
totalMem: number;
totalMemUsage: number;
};
const totalStat = ref<TotalStat>({ totalCPU: 0, totalMem: 0, totalMemUsage: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const cpuHistory = computed(() =>
history.value.map((stat) => ({
percent: stat.totalCPU,
value: stat.totalCPU,
})),
);
const memHistory = computed(() =>
history.value.map((stat) => ({
percent: stat.totalMem,
value: stat.totalMemUsage,
})),
);
const stats = reactive({ mostRecent: totalStat, weighted: useExponentialMovingAverage(totalStat) });
watch(
() => hostContainers.value,
() => {
const initial: TotalStat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = hostContainers.value.reduce(
(acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
totalCPU: acc.totalCPU + item.cpu / cores,
totalMem: acc.totalMem + item.memory,
totalMemUsage: acc.totalMemUsage + item.memoryUsage,
};
},
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
);
initial.push(stat);
}
reset({ initial: initial.reverse() });
stats.weighted.reset(initial.at(-1)!);
},
{ immediate: true },
);
useIntervalFn(() => {
totalStat.value = hostContainers.value.reduce(
(acc, container) => {
const cores = toContainerCores(container);
return {
totalCPU: acc.totalCPU + container.stat.cpu / cores,
totalMem: acc.totalMem + container.stat.memory,
totalMemUsage: acc.totalMemUsage + container.stat.memoryUsage,
};
},
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
);
}, 1000);
</script>
-11
View File
@@ -1,11 +0,0 @@
<template>
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
<li v-for="host in hosts" :key="host.id">
<HostCard :host="host" />
</li>
</ul>
</template>
<script setup lang="ts">
const { hosts } = useHosts();
</script>
-251
View File
@@ -1,251 +0,0 @@
<template>
<div class="flex items-center">
<div class="breadcrumbs flex-1">
<ul>
<li>
<a @click.prevent="setHost(null)" class="link-primary">{{ $t("label.hosts") }}</a>
</li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
<router-link
:to="{
name: '/host/[id]',
params: { id: hosts[sessionHost].id },
}"
class="btn btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
{{ hosts[sessionHost].name }}
</router-link>
</li>
</ul>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
<ion:ellipsis-vertical />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
>
<li>
<a class="text-sm capitalize" @click="toggleShowAllContainers()">
<mdi:check class="w-4" v-if="showAllContainers" />
<div v-else class="w-4"></div>
{{ $t("label.show-all-containers") }}
</a>
<a class="text-sm capitalize" @click="collapseAll()">
<material-symbols-light:collapse-all class="w-4" />
{{ $t("label.collapse-all") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<SlideTransition :slide-right="!!sessionHost">
<template #left>
<ul class="menu p-0">
<li v-for="host in hosts" :key="host.id">
<a @click.prevent="setHost(host.id)" :class="{ 'text-base-content/50 pointer-events-none': !host.available }">
<HostIcon :type="host.type" />
{{ host.name }}
<span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
</a>
</li>
</ul>
</template>
<template #right>
<ul class="containers menu w-full p-0 [&_li.menu-title]:px-0">
<li v-for="{ label, containers, icon } in menuItems" :key="label">
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
<summary class="text-base-content/80 font-light">
<component :is="icon" />
{{ label.startsWith("label.") ? $t(label) : label }} ({{ containers.length }})
<router-link
:to="{
name: '/merged/[ids]',
params: { ids: containers.map(({ id }) => id).join(',') },
}"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
</router-link>
</summary>
<ul>
<li v-for="item in containers" :class="item.state" :key="item.id">
<Popup>
<router-link
:to="{ name: '/container/[id]', params: { id: item.id } }"
active-class="menu-active"
@click.alt.stop.prevent="pinnedStore.pinContainer(item)"
:title="item.name"
class="group auto-cols-[content_max_auto_max-content_max-content]"
>
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="item.state"
></div>
<div class="truncate">
{{ item.name }}
</div>
<ContainerHealth :health="item.health" />
<span
class="hover:text-secondary hidden group-hover:inline-block"
@click.stop.prevent="pinnedStore.pinContainer(item)"
v-show="!pinnedStore.isPinned(item)"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
</router-link>
<template #content>
<ContainerPopup :container="item" />
</template>
</Popup>
</li>
</ul>
</details>
</li>
</ul>
</template>
</SlideTransition>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage";
import { showAllContainers, groupContainers } from "@/stores/settings";
// @ts-ignore
import Pin from "~icons/ph/map-pin-simple";
// @ts-ignore
import Stack from "~icons/ph/stack";
// @ts-ignore
import Containers from "~icons/octicon/container-24";
const containerStore = useContainerStore();
const { visibleContainers } = storeToRefs(containerStore);
const pinnedStore = usePinnedLogsStore();
const { hosts } = useHosts();
const setHost = (host: string | null) => (sessionHost.value = host);
const collapsedGroups = useProfileStorage("collapsedGroups", new Set<string>());
const updateCollapsedGroups = (event: Event, label: string) => {
const details = event.target as HTMLDetailsElement;
if (details.open) {
collapsedGroups.value.delete(label);
} else {
collapsedGroups.value.add(label);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
const collapseAll = () => {
menuItems.value.forEach(({ label }) => {
collapsedGroups.value.add(label);
});
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
const sortedContainers = computed(() =>
visibleContainers.value.filter((c) => c.host === sessionHost.value).sort(sorter),
);
const sorter = (a: Container, b: Container) => {
if (a.state === "running" && b.state !== "running") {
return -1;
} else if (a.state !== "running" && b.state === "running") {
return 1;
} else {
return a.name.localeCompare(b.name);
}
};
const menuItems = computed(() => {
const namespaced: Record<string, Container[]> = {};
const pinned = [];
const singular = [];
for (const item of sortedContainers.value) {
const namespace = item.namespace;
if (debouncedPinnedContainers.value?.has(item.name)) {
pinned.push(item);
} else if (namespace) {
namespaced[namespace] ||= [];
namespaced[namespace].push(item);
} else {
singular.push(item);
}
}
const items = [];
if (pinned.length) {
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
}
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
const shouldGroup =
groupContainers.value === "always" || (groupContainers.value === "at-least-2" && containers.length > 1);
if (shouldGroup) {
items.push({ label, containers, icon: Stack });
} else {
for (const container of containers) {
singular.push(container);
}
}
}
singular.sort(sorter);
if (singular.length) {
items.push({
label: showAllContainers.value ? "label.all-containers" : "label.running-containers",
containers: singular,
icon: Containers,
});
}
return items;
});
const route = useRoute("/container/[id]");
watchEffect(() => {
if (route.name === "/container/[id]") {
const container = containerStore.findContainerById(route.params.id);
if (container) {
setHost(container.host);
}
}
});
const toggleShowAllContainers = () => (showAllContainers.value = !showAllContainers.value);
</script>
<style scoped>
.menu {
@apply text-[0.95rem];
}
li.exited {
@apply opacity-75;
}
li.deleted {
@apply hidden;
}
</style>
-43
View File
@@ -1,43 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="host">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 items-center gap-1.5 truncate md:gap-2">
<ph:computer-tower />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ host.name }}</div>
</div>
<Tag class="font-mono max-md:hidden" size="small">
{{ $t("label.container", containers.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useHostStream"
:entity="host"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { id, scrollable = false } = defineProps<{
id: string;
scrollable?: boolean;
}>();
const store = useContainerStore();
const { containersByHost } = storeToRefs(store);
const { hosts } = useHosts();
const host = computed(() => hosts.value[id]);
const containers = computed(() => containersByHost.value?.[id]?.filter((c) => c.state === "running") ?? []);
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(containers, { showContainerName: true, showHostname: false });
</script>
-142
View File
@@ -1,142 +0,0 @@
<template>
<div class="flex items-center">
<div class="breadcrumbs flex-1">
<ul>
<li>
<a @click.prevent="setNamespace(null)" class="link-primary">{{ $t("label.namespaces") }}</a>
</li>
<li v-if="selectedNamespace === 'all'">
{{ $t("label.all-namespaces") }}
</li>
<li v-else-if="selectedNamespace" class="cursor-default">
<router-link
:to="{
name: '/namespace/[name]',
params: { name: selectedNamespace },
}"
class="btn btn-outline btn-primary btn-xs"
active-class="btn-active"
>
<ph:arrows-merge />
{{ selectedNamespace }}
</router-link>
</li>
</ul>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
<ph:dots-three-vertical-bold />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
>
<li>
<a class="text-sm capitalize" @click="collapseAll()">
<material-symbols-light:collapse-all class="w-4" />
{{ $t("label.collapse-all") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<SlideTransition :slide-right="selectedNamespace !== null">
<template #left>
<ul class="menu p-0">
<li>
<a @click.prevent="setNamespace('all')">
<ph:circles-four />
{{ $t("label.all-namespaces") }}
</a>
</li>
<li v-for="ns in namespaces" :key="ns.name">
<a @click.prevent="setNamespace(ns.name)">
<ph:circles-four />
{{ ns.name }}
</a>
</li>
</ul>
</template>
<template #right>
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
<li v-for="{ name, owners } in filteredNamespaces" :key="name">
<details open>
<summary class="text-base-content/80 font-light">
<ph:stack />
{{ name }} ({{ owners.length }})
<router-link
:to="{ name: '/namespace/[name]', params: { name } }"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
</router-link>
</summary>
<ul>
<li v-for="owner in owners" :key="`${owner.kind}-${owner.name}`">
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
</router-link>
</li>
</ul>
</details>
</li>
<li v-if="ownersWithoutNamespace.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:circles-four />
{{ $t("label.owners") }} ({{ ownersWithoutNamespace.length }})
</summary>
<ul>
<li v-for="owner in ownersWithoutNamespace" :key="`${owner.kind}-${owner.name}`">
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
</SlideTransition>
</template>
<script lang="ts" setup>
const store = useK8sStore();
const { namespaces, owners } = storeToRefs(store);
const selectedNamespace = ref<string | null>("all");
const setNamespace = (namespace: string | null) => (selectedNamespace.value = namespace);
const filteredNamespaces = computed(() => {
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
return namespaces.value;
}
return namespaces.value.filter((ns) => ns.name === selectedNamespace.value);
});
const ownersWithoutNamespace = computed(() => {
const filtered = owners.value.filter((owner) => !owner.namespace);
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
return filtered;
}
return [];
});
const menu = useTemplateRef("menu");
const collapseAll = () => {
const details = menu.value?.querySelectorAll("details");
details?.forEach((detail) => (detail.open = false));
};
</script>
@@ -1,43 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="namespace.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<ph:stack />
<div class="font-mono text-sm font-semibold">{{ namespace.name }}</div>
<ContainerDropdown :containers="namespace.containers">
{{ $t("label.container", namespace.containers.length) }}
</ContainerDropdown>
</div>
<MultiContainerStat class="ml-auto" :containers="namespace.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useNamespaceStream"
:entity="namespace"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { K8sNamespace } from "@/stores/k8s";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { namespace, scrollable = false } = defineProps<{
scrollable?: boolean;
namespace: K8sNamespace;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
provideLoggingContext(
toRef(() => namespace.containers),
{ showContainerName: true, showHostname: false },
);
</script>
-43
View File
@@ -1,43 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="owner.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<ph:stack-simple />
<div class="font-mono text-sm font-semibold">{{ owner.kind }}/{{ owner.name }}</div>
<ContainerDropdown :containers="owner.containers">
{{ $t("label.container", owner.containers.length) }}
</ContainerDropdown>
</div>
<MultiContainerStat class="ml-auto" :containers="owner.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useOwnerStream"
:entity="owner"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { K8sOwner } from "@/stores/k8s";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { owner, scrollable = false } = defineProps<{
scrollable?: boolean;
owner: K8sOwner;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
provideLoggingContext(
toRef(() => owner.containers),
{ showContainerName: true, showHostname: false },
);
</script>
-62
View File
@@ -1,62 +0,0 @@
<template>
<div class="flex items-center justify-end gap-4">
<slot name="more-items"></slot>
<Announcements />
<router-link
:to="{ name: '/settings' }"
:aria-label="$t('title.settings')"
data-testid="settings"
class="btn btn-circle btn-sm"
>
<mdi:cog class="size-6" />
</router-link>
<dropdown class="dropdown-end" v-if="config.user">
<template #trigger>
<template v-if="config.disableAvatars || !config.user.email">
<material-symbols:person class="size-6" />
</template>
<template v-else>
<img
class="ring-base-content/60 size-6 max-w-none rounded-full p-px ring-1"
:src="withBase('/api/profile/avatar')"
/>
</template>
</template>
<template #content>
<div class="p-2">
<div class="font-bold">
{{ config.user.name }}
</div>
<div v-if="config.user.email" class="text-sm font-light">
{{ config.user.email }}
</div>
</div>
<ul v-if="config.authProvider === 'simple' || config.logoutUrl" class="menu mt-4 p-0">
<li>
<button @click.prevent="logout()" class="text-primary p-2">
<material-symbols:logout />
{{ $t("button.logout") }}
</button>
</li>
</ul>
</template>
</dropdown>
</div>
</template>
<script lang="ts" setup>
const { logoutUrl } = config;
async function logout() {
if (logoutUrl) {
location.href = logoutUrl;
} else {
await fetch(withBase("/api/token"), {
method: "DELETE",
});
location.reload();
}
}
</script>
+158
View File
@@ -0,0 +1,158 @@
import EventSource from "eventsourcemock";
import { sources } from "eventsourcemock";
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import MockDate from "mockdate";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
describe("<LogEventSource />", () => {
beforeEach(() => {
global.BASE_PATH = "";
global.EventSource = EventSource;
MockDate.set("6/12/2019", 0);
window.scrollTo = jest.fn();
});
afterEach(() => MockDate.reset());
function createLogEventSource(searchFilter = null) {
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.component("log-event-source", LogEventSource);
localVue.component("log-viewer", LogViewer);
const state = { searchFilter };
const store = new Vuex.Store({
state
});
return mount(LogEventSource, {
localVue,
store,
scopedSlots: {
default: `
<log-viewer :messages="props.messages"></log-viewer>
`
},
propsData: { id: "abc" }
});
}
test("is a Vue instance", async () => {
const wrapper = shallowMount(LogEventSource);
expect(wrapper.isVueInstance()).toBeTruthy();
});
test("renders correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.element).toMatchSnapshot();
});
test("should connect to EventSource", async () => {
shallowMount(LogEventSource);
sources["/api/logs/stream?id=abc"].emitOpen();
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(1);
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
wrapper.destroy();
expect(sources["/api/logs/stream?id=abc"].readyState).toBe(2);
});
test("should parse messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBeGreaterThanOrEqual(0);
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2019-06-12T10:55:42.459Z,
"message": " \\"This is a message.\\"",
}
`);
});
test("should pass messages to slot", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
const [message, _] = wrapper.find(LogViewer).vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBeGreaterThanOrEqual(0);
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2019-06-12T10:55:42.459Z,
"message": " \\"This is a message.\\"",
}
`);
});
test("should render messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({ data: `2019-06-12T10:55:42.459034602Z "This is a message."` });
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events">
<li><span class="date">today at 10:55 AM</span> <span class="text"> "This is a message."</span></li>
</ul>
`);
});
test("should render messages with color", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`
});
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events">
<li><span class="date">today at 10:55 AM</span> <span class="text"> <span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
</ul>
`);
});
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`
});
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events">
<li><span class="date">today at 10:55 AM</span> <span class="text"> &lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul>
`);
});
test("should render messages with filter", async () => {
const wrapper = createLogEventSource("test");
sources["/api/logs/stream?id=abc"].emitOpen();
sources["/api/logs/stream?id=abc"].emitMessage({
data: `2019-06-11T10:55:42.459034602Z Foo bar`
});
sources["/api/logs/stream?id=abc"].emitMessage({
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`
});
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events">
<li><span class="date">today at 10:55 AM</span> <span class="text"> This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li>
</ul>
`);
});
});
+55
View File
@@ -0,0 +1,55 @@
<template lang="html">
<div>
<slot v-bind:messages="messages"></slot>
</div>
</template>
<script>
let nextId = 0;
function parseMessage(data) {
const date = new Date(data.substring(0, 30));
const message = data.substring(30);
const key = nextId++;
return {
key,
date,
message
};
}
export default {
props: ["id"],
name: "LogEventSource",
data() {
return {
messages: []
};
},
created() {
this.es = null;
this.loadLogs(this.id);
},
methods: {
loadLogs(id) {
if (this.es) {
this.es.close();
this.messages = [];
this.es = null;
}
this.es = new EventSource(`${BASE_PATH}/api/logs/stream?id=${this.id}`);
this.es.onmessage = e => this.messages.push(parseMessage(e.data));
this.es.onerror = function(e) {
console.log("EventSource failed." + e);
};
this.$once("hook:beforeDestroy", () => this.es.close());
}
},
watch: {
id(newValue, oldValue) {
if (oldValue !== newValue) {
this.loadLogs(newValue);
}
}
}
};
</script>
+94
View File
@@ -0,0 +1,94 @@
<template lang="html">
<ul class="events">
<li v-for="item in filtered" :key="item.key">
<span class="date">{{ item.date | relativeTime }}</span>
<span class="text" v-html="colorize(item.message)"></span>
</li>
</ul>
</template>
<script>
import { mapActions, mapGetters, mapState } from "vuex";
import { formatRelative } from "date-fns";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
export default {
props: ["messages"],
name: "LogViewer",
components: {},
data() {
return {
showSearch: false
};
},
methods: {
colorize: function(value) {
return ansiConvertor
.toHtml(value)
.replace("&lt;mark&gt;", "<mark>")
.replace("&lt;/mark&gt;", "</mark>");
}
},
computed: {
...mapState(["searchFilter"]),
filtered() {
const { searchFilter, messages } = this;
if (searchFilter) {
const isSmartCase = searchFilter === searchFilter.toLowerCase();
const regex = isSmartCase ? new RegExp(searchFilter, "i") : new RegExp(searchFilter);
return messages
.filter(d => d.message.match(regex))
.map(d => ({
...d,
message: d.message.replace(regex, "<mark>$&</mark>")
}));
}
return messages;
}
},
filters: {
relativeTime(date) {
return formatRelative(date, new Date());
}
}
};
</script>
<style scoped lang="scss">
.events {
padding: 10px;
font-family: "Roboto Mono", monaco, monospace;
& > li {
font-size: 13px;
line-height: 16px;
word-wrap: break-word;
}
}
.date {
background-color: #262626;
color: #258ccd;
}
.text {
white-space: pre-wrap;
}
>>> mark {
border-radius: 2px;
background-color: #ffdd57;
animation: pops 0.2s ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
</style>
@@ -1,75 +0,0 @@
<template>
<DefineTemplate v-slot="{ data }">
<ul class="inline-flex flex-wrap space-x-4" @click="preventDefaultOnLinks">
<li v-for="(value, name) in data" :key="name" v-if="isObject(data)">
<span class="key">{{ name }}=</span>
<span class="value" v-if="value === null">&lt;null&gt;</span>
<ReuseTemplate :data="value" v-else-if="isObject(value) || Array.isArray(value)" />
<span v-else class="value" :class="typeof value" v-html="stripAnsi(value.toString())"></span>
</li>
<li v-else-if="Array.isArray(data)">
<ul class="array inline-flex flex-wrap space-x-1">
<li
v-for="(item, index) in data"
:key="index"
class="after:text-base-content/70 not-last:after:content-[',']"
>
<ReuseTemplate :data="item" v-if="isObject(item) || Array.isArray(item)" />
<span v-else class="value" :class="typeof item" v-html="stripAnsi(item.toString())"></span>
</li>
</ul>
</li>
<li class="key" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
</DefineTemplate>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div @click="containers.length > 0 && showDrawer(LogDetails, { entry: logEntry })" class="cursor-pointer">
<ReuseTemplate :data="validValues" />
</div>
</LogItem>
</template>
<script lang="ts" setup>
import stripAnsi from "strip-ansi";
import { type ComplexLogEntry } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
const { logEntry } = defineProps<{
logEntry: ComplexLogEntry;
showContainerName?: boolean;
}>();
const { containers } = useLoggingContext();
const [DefineTemplate, ReuseTemplate] = createReusableTemplate();
const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
const showDrawer = useDrawer();
function preventDefaultOnLinks(event: MouseEvent) {
if (event.target instanceof HTMLAnchorElement && event.target.rel?.includes("external")) {
event.stopImmediatePropagation();
}
}
</script>
<style scoped>
@reference "@/main.css";
.key {
@apply text-base-content/70 font-light;
}
.value {
@apply text-base-content font-bold;
}
.array {
@apply before:text-base-content/80 after:text-base-content/80 before:content-['['] after:content-[']'];
}
.string {
@apply before:content-['"'] after:content-['"'];
}
</style>
@@ -1,23 +0,0 @@
<template>
<LogItem :logEntry>
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
</LogItem>
</template>
<script lang="ts" setup>
import { ContainerEventLogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: ContainerEventLogEntry;
showContainerName?: boolean;
}>();
</script>
<style scoped>
@reference "@/main.css";
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
@@ -1,215 +0,0 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { useSearchFilter } from "@/composable/search";
import { settings } from "@/stores/settings";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { computed, nextTick } from "vue";
import { createI18n } from "vue-i18n";
import { createRouter, createWebHistory } from "vue-router";
import { default as Component } from "./EventSource.vue";
import LogViewer from "@/components/LogViewer/LogViewer.vue";
import { Container } from "@/models/Container";
import { Level } from "@/models/LogEntry";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
/**
* @vitest-environment jsdom
*/
describe("<ContainerEventSource />", () => {
const search = useSearchFilter();
beforeEach(() => {
global.EventSource = EventSource;
// @ts-ignore
window.scrollTo = vi.fn();
global.IntersectionObserver = class IntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
takeRecords = vi.fn();
root = null;
rootMargin = "";
thresholds = [];
} as any;
vi.useFakeTimers();
vi.setSystemTime(1560336942459);
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
function createLogEventSource(
{
searchFilter = "",
hourStyle = "auto",
}: { searchFilter?: string | undefined; hourStyle?: "auto" | "24" | "12" } = {
hourStyle: "auto",
},
) {
settings.value.hourStyle = hourStyle;
search.searchQueryFilter.value = searchFilter;
if (searchFilter) {
search.showSearch.value = true;
}
const router = createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/",
component: {
template: "Test from createLogEventSource",
},
},
{
name: "/container/[id].time.[datetime]",
path: "/container/:id/time/:datetime",
component: {
template: "Test from createLogEventSource",
},
},
],
});
return mount(Component, {
global: {
plugins: [
router,
createTestingPinia({
createSpy: vi.fn,
stubActions: false,
initialState: {
container: { containers: [{ id: "abc", image: "test:v123", host: "localhost" }] },
},
}),
createI18n({}),
],
components: {
LogViewer,
},
provide: {
[scrollContextKey as symbol]: {
paused: computed(() => false),
loading: computed(() => false),
},
[loggingContextKey as symbol]: {
containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]),
streamConfig: reactive({ stdout: true, stderr: true }),
hasComplexLogs: ref(false),
levels: new Set<Level>(["info"]),
historical: ref(false),
},
},
},
slots: {
default: `
<template #scoped="params"><LogViewer :messages="params.messages" :show-container-name="false" :visible-keys="[]" /></template>
`,
},
props: {
streamSource: useContainerStream,
entity: new Container(
"abc",
new Date(), // created
new Date(), // started
new Date(), // finished
"image",
"name",
"command",
"localhost",
{},
"created",
0,
0,
[],
),
},
});
}
const sourceUrl = "/api/hosts/localhost/containers/abc/logs/stream?stdout=1&stderr=1&levels=info";
test("renders loading correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.find("ul.animate-pulse").exists()).toBe(true);
});
test("should connect to EventSource", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
expect(sources[sourceUrl].readyState).toBe(1);
wrapper.unmount();
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
wrapper.unmount();
expect(sources[sourceUrl].readyState).toBe(2);
});
test("should parse messages", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
// @ts-ignore
const [message, _] = wrapper.vm.messages;
expect(message).toMatchSnapshot();
});
describe("render html correctly", () => {
test("should render messages", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "rm": "foo bar", "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
});
});
@@ -1,76 +0,0 @@
<template>
<ul class="flex animate-pulse flex-col gap-4 p-4" v-if="loading || (noLogs && waitingForMoreLog)">
<div class="flex flex-row gap-2" v-for="size in sizes">
<div class="bg-base-content/50 h-3 w-40 shrink-0 rounded-full opacity-50"></div>
<div class="bg-base-content/50 h-3 rounded-full opacity-50" :class="size"></div>
</div>
<span class="sr-only">Loading...</span>
</ul>
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">
{{ $t("label.no-logs") }}
</div>
<slot :messages="messages" v-else></slot>
<IndeterminateBar :color v-if="!historical" />
</template>
<script lang="ts" setup generic="T">
import { LogStreamSource } from "@/composable/eventStreams";
const route = useRoute();
const { entity, streamSource } = $defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
entity: T;
}>();
const { historical } = useLoggingContext();
const { messages, opened, loading, error } = streamSource(toRef(() => entity));
const color = computed(() => {
if (error.value) return "error";
if (loading.value) return "secondary";
if (opened.value) return "primary";
return "error";
});
const noLogs = computed(() => messages.value.length === 0);
const waitingForMoreLog = refAutoReset(false, 3000);
watchImmediate(loading, () => (waitingForMoreLog.value = true));
defineExpose({
clear: () => (messages.value = []),
});
if (historical.value && route.query.logId) {
watchOnce(messages, async () => {
await nextTick();
document.getElementById(route.query.logId as string)?.scrollIntoView({ behavior: "instant", block: "center" });
});
}
const sizes = ref<string[]>([]);
watch(
opened,
(value) => {
if (value) return;
const sizeOptions = [
"w-2/12",
"w-3/12",
"w-4/12",
"w-5/12",
"w-6/12",
"w-7/12",
"w-8/12",
"w-9/12",
"w-10/12",
"w-11/12",
"w-full",
];
sizes.value = Array.from({ length: 18 }, () => sizeOptions[Math.floor(Math.random() * sizeOptions.length)]);
},
{
flush: "sync",
immediate: true,
},
);
</script>
@@ -1,37 +0,0 @@
<template>
<LogItem :logEntry>
<div class="flex flex-col">
<div v-for="(msg, index) in logEntry.message" :key="index" class="flex items-start gap-x-2">
<LogLevel class="flex select-none" :level="logEntry.level" :position="getPosition(index)" />
<div
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(msg)"
:class="{ 'min-h-4': msg === '' }"
></div>
</div>
</div>
</LogItem>
</template>
<script lang="ts" setup>
import { GroupedLogEntry, type Position } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--color-base-content)",
bg: "var(--color-base-100)",
});
const { logEntry } = defineProps<{
logEntry: GroupedLogEntry;
}>();
const getPosition = (index: number): Position => {
const len = logEntry.message.length;
if (index === 0) return "start";
if (index === len - 1) return "end";
return "middle";
};
const colorize = (value: string) => ansiConvertor.toHtml(value);
</script>
@@ -1,31 +0,0 @@
<template>
<div ref="root" class="flex min-h-[1px] flex-1 content-center justify-center">
<span class="loading loading-bars loading-md text-primary m-2" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
import { LoadMoreLogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LoadMoreLogEntry;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
useIntersectionObserver(root, async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (isLoading.value) return;
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await logEntry.loadMore();
isLoading.value = false;
await nextTick();
if (logEntry.rememberScrollPosition) {
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
</script>
<style scoped></style>
-169
View File
@@ -1,169 +0,0 @@
<template>
<div
class="dropdown dropdown-hover absolute -left-2 z-10 font-sans"
:class="shouldShowBelow ? 'dropdown-right' : 'dropdown-right dropdown-end'"
v-show="container"
ref="dropdownRef"
@mouseenter="checkDropdownPosition"
>
<router-link
v-if="isSearching"
@click="resetSearch()"
tabindex="0"
class="btn btn-square btn-xs border-base-content/20 bg-base-100 pointer-events-auto! opacity-0 shadow-sm group-hover/entry:opacity-90"
:to="{
name: '/container/[id].time.[datetime]',
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}"
>
<material-symbols:eye-tracking />
</router-link>
<button
tabindex="0"
class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"
v-else
>
<ion:ellipsis-vertical />
</button>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm"
@click="hideMenu"
>
<li v-if="isSearching">
<router-link
@click="resetSearch()"
:to="{
name: '/container/[id].time.[datetime]',
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}"
>
<material-symbols:eye-tracking />
{{ $t("action.see-in-context") }}
</router-link>
</li>
<li>
<a
@click="copyLogMessage()"
:disabled="!isSupported"
:title="!isSupported ? $t('error.copy-not-supported') : ''"
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
>
<material-symbols:content-copy />
{{ $t("action.copy-log") }}
</a>
</li>
<li>
<a
@click="copyPermalink()"
:disabled="!isSupported"
:title="!isSupported ? $t('error.copy-not-supported') : ''"
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
>
<material-symbols:link />
{{ $t("action.copy-link") }}
</a>
</li>
<li v-if="logEntry instanceof ComplexLogEntry">
<a @click="showDrawer(LogDetails, { entry: logEntry })">
<material-symbols:code-blocks-rounded />
{{ $t("action.show-details") }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import stripAnsi from "strip-ansi";
import { Container } from "@/models/Container";
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
const { logEntry, container } = defineProps<{
logEntry: LogEntry<string | JSONObject>;
container: Container;
}>();
const { showToast } = useToast();
const showDrawer = useDrawer();
const router = useRouter();
const { isSearching, resetSearch } = useSearchFilter();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
async function copyLogMessage() {
if (!isSupported.value) {
return;
}
if (logEntry instanceof ComplexLogEntry) {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof SimpleLogEntry) {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof GroupedLogEntry) {
await copy(stripAnsi(logEntry.message.join("\n")));
}
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
async function copyPermalink() {
if (!isSupported.value) {
return;
}
const url = router.resolve({
name: "/container/[id].time.[datetime]",
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}).href;
const resolved = new URL(url, window.location.origin);
await copy(resolved.href);
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
function hideMenu(e: MouseEvent) {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
}
const dropdownRef = useTemplateRef<HTMLDivElement>("dropdownRef");
const shouldShowBelow = ref(false);
function checkDropdownPosition() {
if (!dropdownRef.value) return;
const rect = dropdownRef.value.getBoundingClientRect();
shouldShowBelow.value = rect.top < 150;
}
</script>
@@ -1,133 +0,0 @@
<template>
<aside>
<header class="flex items-center gap-4">
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
<h2 class="text-sm"><RelativeTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
<section>
<label class="form-control">
<textarea
v-model="query"
class="textarea textarea-primary w-full font-mono text-lg"
:class="{ 'textarea-error!': error }"
:disabled="state === 'downloading'"
></textarea>
<div class="mt-2">
<span class="text-error" v-if="error">{{ error }}</span>
<span v-else-if="state === 'initializing'">{{ $t("analytics.creating_table") }}</span>
<span v-else-if="state === 'downloading'">{{
$t("analytics.downloading", { size: formatBytes(bytes, { decimals: 1 }) })
}}</span>
<span v-else-if="evaluating">{{ $t("analytics.evaluating_query") }}</span>
<span v-else>
{{ $t("analytics.total_records", { count: results.numRows.toLocaleString() }) }}
<template v-if="results.numRows > pageLimit">{{
$t("analytics.showing_first", { count: page.numRows.toLocaleString() })
}}</template>
</span>
</div>
</label>
</section>
<SQLTable :table="page" :loading="evaluating || state !== 'ready'" />
</div>
</aside>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
import { type Table } from "@apache-arrow/esnext-esm";
const { container } = defineProps<{ container: Container }>();
const query = ref("SELECT * FROM logs LIMIT 100");
const error = ref<string | null>(null);
const debouncedQuery = debouncedRef(query, 500);
const evaluating = ref(false);
const pageLimit = 1000;
const state = ref<"downloading" | "ready" | "initializing">("downloading");
const bytes = ref(0);
const url = withBase(
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
);
const [{ useDuckDB }, response] = await Promise.all([import(`@/composable/duckdb`), fetch(url)]);
if (!response.ok) {
console.log("error fetching logs from", url);
throw new Error(`Failed to fetch logs: ${response.statusText}`);
}
const { db, conn } = await useDuckDB();
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
onMounted(async () => {
try {
state.value = "downloading";
const reader = response.body?.getReader();
if (!reader) throw new Error("No reader available from stream");
const chunks: Uint8Array[] = [];
bytes.value = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
bytes.value += value.length;
}
const arrayBuffer = new Uint8Array(bytes.value);
let position = 0;
for (const chunk of chunks) {
arrayBuffer.set(chunk, position);
position += chunk.length;
}
await db.registerFileBuffer("logs.json", arrayBuffer);
state.value = "initializing";
await conn.query(
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
);
state.value = "ready";
} catch (e) {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
}
});
const results = computedAsync(
async () => {
if (state.value === "ready") {
return await conn.query<Record<string, any>>(debouncedQuery.value);
} else {
return empty;
}
},
empty,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
evaluating,
},
);
whenever(evaluating, () => {
error.value = null;
state.value = "ready";
});
const page = computed(() =>
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
) as unknown as ComputedRef<Table<Record<string, any>>>;
</script>
<style scoped></style>
-10
View File
@@ -1,10 +0,0 @@
<template>
<Tag class="items-start!">
<DateTime :date="date" class="text-blue whitespace-nowrap" />
</Tag>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
-200
View File
@@ -1,200 +0,0 @@
<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="show-unknown text-white uppercase" v-if="entry.level">{{ entry.level }}</Tag>
<h1 class="text-lg max-md:hidden">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><RelativeTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
<section class="grid grid-cols-3 gap-2">
<div>
<div class="font-thin">Container Name</div>
<div class="truncate text-lg font-bold">{{ container.name }}</div>
</div>
<div>
<div class="font-thin">Host</div>
<div class="truncate text-lg font-bold">
{{ hosts[container.host].name }}
</div>
</div>
<div>
<div class="font-thin">Image</div>
<div class="truncate text-lg font-bold">{{ container.image }}</div>
</div>
</section>
<section class="flex flex-col gap-2">
<div class="flex gap-2">
Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
<button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" />
<material-symbols:content-copy class="swap-off" />
</button>
</UseClipboard>
</div>
<div class="bg-base-200 max-h-48 overflow-scroll rounded-sm border border-white/20 p-2">
<pre v-html="syntaxHighlight(entry.rawMessage)"></pre>
</div>
</section>
<table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry">
<caption class="caption-bottom">
Fields are sortable by dragging and dropping.
</caption>
<thead class="text-lg">
<tr>
<th class="w-60">Field</th>
<th class="max-md:hidden">Value</th>
<th class="w-20">
<input type="checkbox" class="toggle toggle-primary" v-model="toggleAllFields" title="Toggle all" />
</th>
</tr>
</thead>
<tbody ref="list">
<tr v-for="{ key, value, enabled } in fields" :key="key.join('.')" class="hover">
<td class="cursor-move font-mono break-all">
{{ key.join(".") }}
</td>
<td class="truncate max-md:hidden">
<code>{{ JSON.stringify(value) }}</code>
</td>
<td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ComplexLogEntry } from "@/models/LogEntry";
import { UseClipboard } from "@vueuse/components";
const { entry } = defineProps<{ entry: ComplexLogEntry }>();
const { currentContainer } = useContainerStore();
const list = ref<HTMLElement>();
const container = currentContainer(toRef(() => entry.containerID));
const visibleKeys = persistentVisibleKeysForContainer(container);
const { hosts } = useHosts();
const { useSortable } = await import("@vueuse/integrations/useSortable");
function toggleField(key: string[]) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = visibleKeys.value.get(key) ?? true;
visibleKeys.value.set(key, !enabled);
}
const fields = computed({
get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const rawFields = JSON.parse(entry.rawMessage);
const allFields = flattenJSONToMap(rawFields);
if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true });
}
} else {
for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(rawFields, key);
fieldsWithValue.push({ key, value, enabled });
}
for (const [key, value] of allFields) {
if ([...visibleKeys.value.keys()].findIndex((k) => arrayEquals(k, key)) === -1) {
fieldsWithValue.push({ key, value, enabled: true });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
const toggleAllFields = computed({
get: () => fields.value.every(({ enabled }) => enabled),
set(value) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
for (const key of visibleKeys.value.keys()) {
visibleKeys.value.set(key, value);
}
for (const field of fields.value) {
visibleKeys.value.set(field.key, value);
}
},
});
function syntaxHighlight(json: string) {
json = JSON.stringify(JSON.parse(json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")), null, 2);
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
function (match: string) {
var cls = "json-number";
if (match.startsWith('"')) {
if (match.endsWith(":")) {
cls = "json-key";
} else {
cls = "json-string";
}
} else if (/true|false/.test(match)) {
cls = "json-boolean";
} else if (/null/.test(match)) {
cls = "json-null";
}
return `<span class="${cls}">${match}</span>`;
},
);
}
useSortable(list, fields);
</script>
<style scoped>
@reference "@/main.css";
.font-mono {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
}
pre {
& :deep(.json-key) {
@apply text-blue;
}
& :deep(.json-string) {
@apply text-green;
}
& :deep(.json-number) {
@apply text-orange;
}
& :deep(.json-boolean) {
@apply text-purple;
}
& :deep(.json-null) {
@apply text-red;
}
}
</style>
-40
View File
@@ -1,40 +0,0 @@
<template>
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<LogActions :logEntry :container />
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<RandomColorTag class="w-30 shrink-0 select-none md:w-40" :value="host.name" v-if="showHostname" />
<RandomColorTag
v-if="showContainerName"
class="w-30 shrink-0 select-none group-[.compact]:flex-1 md:w-40"
:value="container.name"
truncateRight
/>
<LogDate
v-if="showTimestamp"
:date="logEntry.date"
class="shrink-0 select-none"
:class="{ 'bg-secondary': route.query.logId === logEntry.id.toString() }"
/>
</div>
<slot />
</div>
</template>
<script lang="ts" setup>
import { LogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LogEntry<any>;
}>();
const { showHostname, showContainerName } = useLoggingContext();
const { currentContainer } = useContainerStore();
const { hosts } = useHosts();
const container = currentContainer(toRef(() => logEntry.containerID));
const host = computed(() => hosts.value[container.value.host]);
const route = useRoute();
</script>
-69
View File
@@ -1,69 +0,0 @@
<template>
<div
:data-level="level"
:data-position="position"
class="mt-1.5 size-2.5 flex-none rounded-lg"
:class="{ showUnknown }"
></div>
</template>
<script lang="ts" setup>
import { Position, Level } from "@/models/LogEntry";
const {
level,
position,
showUnknown = false,
} = defineProps<{
level?: Level;
position?: Position;
showUnknown?: boolean;
}>();
</script>
<style scoped>
@reference "@/main.css";
[data-position="start"],
[data-position="middle"],
[data-position="end"] {
align-self: stretch;
height: auto;
}
[data-position="start"] {
border-radius: 0.375rem 0.375rem 0 0;
}
[data-position="middle"] {
border-radius: 0;
margin-top: 0;
}
[data-position="end"] {
border-radius: 0 0 0.375rem 0.375rem;
margin-top: 0;
}
</style>
<style>
@reference "@/main.css";
[data-level="debug"],
[data-level="trace"] {
@apply !bg-purple;
}
[data-level="info"] {
@apply !bg-green;
}
[data-level="error"],
[data-level="fatal"] {
@apply !bg-red;
}
[data-level="warn"] {
@apply !bg-orange;
}
[data-level="unknown"].show-unknown {
@apply !bg-base-300;
}
</style>
-116
View File
@@ -1,116 +0,0 @@
<template>
<ul class="group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }" data-logs>
<li
v-for="item in messages"
ref="list"
:key="item.id"
:id="item.id.toString()"
:data-time="item.date.getTime()"
class="group/entry"
>
<component :is="item.getComponent()" :log-entry="item" />
</li>
</ul>
</template>
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const { progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | string[] | JSONObject>[];
}>();
const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]);
let previousDate = new Date();
useIntersectionObserver(
list,
(entries) => {
if (containers.value.length != 1) return;
const container = containers.value[0];
for (const entry of entries) {
if (entry.isIntersecting) {
const time = entry.target.getAttribute("data-time");
if (time) {
const date = new Date(parseInt(time));
if (+date === +previousDate) break;
previousDate = date;
const diff = new Date().getTime() - container.created.getTime();
progress.value = (date.getTime() - container.created.getTime()) / diff;
currentDate.value = date;
break;
}
}
}
},
{
rootMargin: "-10% 0px -10% 0px",
threshold: 1,
},
);
</script>
<style scoped>
@reference "@/main.css";
ul {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
> li {
@apply flex px-2 py-1 break-words last:snap-end odd:bg-gray-400/[0.07] md:px-4;
&:last-child {
scroll-margin-block-end: 5rem;
}
}
&.small {
@apply text-[0.7em];
}
&.medium {
@apply text-[0.8em];
}
&.large {
@apply text-[1em];
}
&.compact {
> li {
@apply py-0;
}
:deep(.tag) {
@apply rounded-none;
}
}
:deep(mark) {
@apply bg-secondary inline-block rounded-xs;
animation: pops 200ms ease-out;
}
:deep(a[rel~="external"]) {
@apply text-primary underline-offset-4 hover:underline;
}
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
</style>
-24
View File
@@ -1,24 +0,0 @@
<template>
<Tag :std="std" class="items-start!">
{{ std }}
</Tag>
</template>
<script lang="ts" setup>
import { Std } from "@/models/LogEntry";
defineProps<{
std: Std;
}>();
</script>
<style scoped>
@reference "@/main.css";
[std="stdout"] {
@apply text-blue;
}
[std="stderr"] {
@apply text-red;
}
</style>
-18
View File
@@ -1,18 +0,0 @@
<template>
<LogList :messages="visibleMessages" />
</template>
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | string[] | JSONObject>[];
visibleKeys: Map<string[], boolean>;
}>();
const { messages, visibleKeys } = toRefs(props);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const visibleMessages = filteredPayload(messages);
</script>
<style scoped></style>
@@ -1,121 +0,0 @@
<template>
<div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
@click="hideMenu"
>
<li>
<a @click="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li v-if="enableDownload">
<a :href="downloadUrl" download>
<octicon:download-24 />
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
</a>
</li>
<li>
<a @click="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex size-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="text-red w-2" />
<carbon:circle-solid class="text-blue w-2" />
</template>
</div>
{{ $t("toolbar.show-all") }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="text-blue w-2" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="text-red w-2" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
<li class="line"></li>
<li>
<a @click="showHostname = !showHostname">
<mdi:check class="w-4" v-if="showHostname" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show-hostname") }}
</a>
</li>
<li>
<a @click="showContainerName = !showContainerName">
<mdi:check class="w-4" v-if="showContainerName" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show-container-name") }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
const { showSearch } = useSearchFilter();
const { enableDownload } = config;
const clear = defineEmit();
const { streamConfig, showHostname, showContainerName, containers, levels } = useLoggingContext();
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels);
const hideMenu = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
};
</script>
<style scoped>
@reference "@/main.css";
li.line {
@apply bg-base-content/20 h-px;
}
a {
@apply whitespace-nowrap;
}
</style>
@@ -1,176 +0,0 @@
<template>
<div class="flex gap-1 md:gap-4">
<div class="grid min-w-15 grid-cols-[auto_1fr] items-center gap-0.5 text-xs leading-none max-md:hidden">
<PhArrowUp class="text-primary" />
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
<PhArrowDown class="text-secondary" />
<span class="tabular-nums">{{ formatBytes(networkRate.rx, { short: true, decimals: 1 }) }}/s</span>
</div>
<StatMonitor
:data="cpuData"
:icon="PhCpu"
:stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'"
:limit="roundCPU(limits.cpu) + ' CPU'"
container-class="border-primary/40 bg-primary/20"
text-class="hover:text-primary"
bar-class="bg-primary"
:formatter="(value: number) => value.toFixed(2) + '%'"
/>
<StatMonitor
:data="memoryData"
:icon="PhMemory"
:stat-value="formatBytes(totalStat.memoryUsage)"
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
container-class="border-secondary/40 bg-secondary/20"
text-class="hover:text-secondary"
bar-class="bg-secondary"
:formatter="(value: number) => formatBytes(value)"
/>
</div>
</template>
<script lang="ts" setup>
import { Stat } from "@/models/Container";
import { Container } from "@/models/Container";
// @ts-ignore
import PhCpu from "~icons/ph/cpu";
// @ts-ignore
import PhMemory from "~icons/ph/memory";
const { containers } = defineProps<{
containers: Container[];
}>();
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const { hosts } = useHosts();
const networkRate = ref({ rx: 0, tx: 0 });
const roundCPU = (num: number) => (Number.isInteger(num) ? num.toFixed(0) : num.toFixed(1));
function toContainerCores(container: Container): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
const hostInfo = hosts.value[container.host];
return hostInfo?.nCPU ?? 1;
}
watch(
() => containers,
() => {
const initial: Stat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = containers.reduce(
(acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
cpu: acc.cpu + item.cpu / cores,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
initial.push(stat);
}
totalStat.value = initial[0];
reset({ initial: initial.reverse() });
},
{ immediate: true },
);
const limits = computed(() => {
// Group containers by host
const containersByHost = new Map<string, Container[]>();
containers.forEach((container) => {
if (!containersByHost.has(container.host)) {
containersByHost.set(container.host, []);
}
containersByHost.get(container.host)!.push(container);
});
let totalCpu = 0;
let totalMemory = 0;
// Process each host independently
containersByHost.forEach((hostContainers, hostId) => {
const hostInfo = hosts.value[hostId];
const hostTotalMemory = hostInfo?.memTotal || 0;
const hostTotalCpu = hostInfo?.nCPU || 0;
// Check if any container lacks limits
const hasUnlimitedCpu = hostContainers.some((c) => !c.cpuLimit || c.cpuLimit <= 0);
const hasUnlimitedMemory = hostContainers.some((c) => !c.memoryLimit);
// Calculate CPU for this host
if (hasUnlimitedCpu) {
// At least one container has no limit, use host total
totalCpu += hostTotalCpu;
} else {
// All containers have limits, sum them up (capped at host total)
const sumCpu = hostContainers.reduce((sum, c) => sum + 1, 0);
totalCpu += Math.min(sumCpu, hostTotalCpu);
}
// Calculate Memory for this host
if (hasUnlimitedMemory) {
// At least one container has no limit, use host total
totalMemory += hostTotalMemory;
} else {
// All containers have limits, sum them up (capped at host total)
const sumMemory = hostContainers.reduce((sum, c) => sum + (c.memoryLimit || 0), 0);
totalMemory += Math.min(sumMemory, hostTotalMemory);
}
});
return {
cpu: totalCpu,
memory: totalMemory,
};
});
useIntervalFn(() => {
const previousStat = totalStat.value;
totalStat.value = containers.reduce(
(acc, container) => {
const cores = toContainerCores(container);
return {
cpu: acc.cpu + container.stat.cpu / cores,
memory: acc.memory + container.stat.memory,
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
networkRate.value = {
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
tx: Math.max(0, totalStat.value.networkTxTotal - previousStat.networkTxTotal),
};
}, 1000);
const cpuData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu),
})),
);
const memoryData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: stat.memory,
value: stat.memoryUsage,
})),
);
</script>
@@ -1,46 +0,0 @@
<template>
<div class="tag grid overflow-hidden rounded-sm text-center text-sm text-white">
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
{{ value }}
</div>
</div>
</template>
<script lang="ts">
const colors = [
"hsl(200, 85%, 65%)", // Vibrant Sky Blue
"hsl(150, 85%, 65%)", // Vibrant Mint
"hsl(300, 85%, 65%)", // Vibrant Purple
"hsl(25, 85%, 65%)", // Vibrant Peach
"hsl(270, 85%, 65%)", // Vibrant Lavender
"hsl(340, 85%, 65%)", // Vibrant Rose
"hsl(170, 85%, 65%)", // Vibrant Aqua
"hsl(50, 85%, 65%)", // Vibrant Yellow
"hsl(235, 85%, 65%)", // Vibrant Periwinkle
"hsl(10, 85%, 65%)", // Vibrant Coral
"hsl(180, 85%, 65%)", // Vibrant Turquoise
"hsl(320, 85%, 65%)", // Vibrant Orchid
"hsl(90, 85%, 65%)", // Vibrant Lime
"hsl(260, 85%, 65%)", // Vibrant Amethyst
"hsl(30, 85%, 65%)", // Vibrant Orange
"hsl(210, 85%, 65%)", // Vibrant Ocean
"hsl(290, 85%, 65%)", // Vibrant Mauve
"hsl(120, 85%, 65%)", // Vibrant Spring
"hsl(350, 85%, 65%)", // Vibrant Strawberry
"hsl(190, 85%, 65%)", // Vibrant Azure
] as const;
</script>
<script lang="ts" setup>
const { value, truncateRight = false } = defineProps<{
value: string;
truncateRight?: boolean;
}>();
const color = computed(() => colors[Math.abs(hashCode(value)) % colors.length]);
</script>
<style scoped>
.random-color {
background-color: v-bind(color);
}
</style>
-40
View File
@@ -1,40 +0,0 @@
<template>
<table class="table-zebra table-pin-rows table-md table" v-if="!loading">
<thead>
<tr>
<th v-for="column in columns" :key="column">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table" :key="row">
<td v-for="column in columns" :key="column">{{ row[column] }}</td>
</tr>
</tbody>
</table>
<table class="table-md table animate-pulse" v-else>
<thead>
<tr>
<th v-for="_ in 3">
<div class="bg-base-content/50 h-4 w-20 animate-pulse opacity-50"></div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="_ in 9">
<td v-for="_ in 3">
<div class="bg-base-content/50 h-4 w-20 opacity-20"></div>
</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { type Table } from "@apache-arrow/esnext-esm";
const { loading, table } = defineProps<{
loading: boolean;
table: Table<Record<string, any>>;
}>();
const columns = computed(() => (table.numRows > 0 ? Object.keys(table.get(0) as Record<string, any>) : []));
</script>
@@ -1,25 +0,0 @@
<template>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(logEntry.message)"
></div>
</LogItem>
</template>
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--color-base-content)",
bg: "var(--color-base-100)",
});
defineProps<{
logEntry: SimpleLogEntry;
}>();
const colorize = (value: string) => ansiConvertor.toHtml(value);
</script>
@@ -1,17 +0,0 @@
<template>
<div class="my-4 flex-1 text-center">
<div class="relative">
<ZigZag class="absolute inset-0 mt-2" />
<button class="btn btn-primary btn-xs relative whitespace-pre-wrap" @click="logEntry.loadSkippedEntries()">
{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { SkippedLogsEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
@@ -1,66 +0,0 @@
<template>
<div class="relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false" :class="textClass">
<div class="overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden" :class="containerClass">
<BarChart
:chart-data="chartData"
:bar-class="`${barClass} opacity-70 hover:opacity-100`"
class="h-8 w-44"
@hover-index="(startIndex: number, endIndex: number) => onHoverIndexChange(startIndex, endIndex)"
/>
</div>
<div class="bg-base-200 flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
<component :is="icon" class="text-sm" />
<div class="font-bold tabular-nums select-none">
{{ displayValue }}
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Component } from "vue";
const {
data,
icon,
statValue,
limit = -1,
containerClass = "border-primary",
textClass = "",
barClass = "bg-primary",
formatter,
} = defineProps<{
data: Point<unknown>[];
icon: Component;
statValue: string | number;
limit?: string | number;
containerClass?: string;
textClass?: string;
barClass?: string;
formatter?: (value: number) => string;
}>();
const chartData = computed(() => data.map((point) => (point.y as number) ?? 0));
const mouseOver = ref(false);
const hoveredRange = ref<{ start: number; end: number } | null>(null);
function onHoverIndexChange(startIndex: number, endIndex: number) {
hoveredRange.value = { start: startIndex, end: endIndex };
}
const displayValue = computed(() => {
if (mouseOver.value && hoveredRange.value !== null) {
const { start, end } = hoveredRange.value;
const points = data.slice(start, end + 1);
const sum = points.reduce((acc, point) => acc + ((point.value as number) ?? (point.y as number) ?? 0), 0);
const avg = sum / points.length;
if (formatter) {
return formatter(avg);
}
return avg.toFixed(2);
}
return statValue;
});
</script>
@@ -1,30 +0,0 @@
<template>
<EventSource ref="source" #default="{ messages }" :stream-source="streamSource" :entity="entity">
<LogViewer :messages="messages" :visible-keys="visibleKeys" />
</EventSource>
</template>
<script lang="ts" setup generic="T">
import EventSource from "@/components/LogViewer/EventSource.vue";
import { LogStreamSource } from "@/composable/eventStreams";
import { ComponentExposed } from "vue-component-type-helpers";
const { streamSource, visibleKeys, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
visibleKeys: Map<string[], boolean>;
entity: T;
}>();
const source = useTemplateRef<ComponentExposed<typeof EventSource>>("source");
defineExpose({
clear: () => source.value?.clear(),
});
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
source.value?.clear();
e.preventDefault();
}
});
</script>
-11
View File
@@ -1,11 +0,0 @@
<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="stroke-primary stroke-1" />
<line x1="15" y1="8" x2="30" y2="0" class="stroke-primary stroke-1" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
@@ -1,129 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
</ul>
</div>
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
</ul>
</div>
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
</ul>
</div>
<!--v-if-->
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > should parse messages 1`] = `
LoadMoreLogEntry {
"_message": "",
"containerID": "",
"date": 2019-06-12T10:55:42.709Z,
"id": 1560336942709,
"level": undefined,
"loader": [Function],
"rawMessage": "info",
"rememberScrollPosition": true,
"std": "stderr",
}
`;
+19
View File
@@ -0,0 +1,19 @@
<template lang="html">
<log-event-source :id="id" v-slot="eventSource">
<log-viewer :messages="eventSource.messages"></log-viewer>
</log-event-source>
</template>
<script>
import LogEventSource from "./LogEventSource";
import LogViewer from "./LogViewer";
export default {
props: ["id"],
name: "LogViewerWithSource",
components: {
LogEventSource,
LogViewer
}
};
</script>
-56
View File
@@ -1,56 +0,0 @@
<template>
<div class="rounded-lg border p-2 md:p-3" :class="containerClass">
<div class="mb-2 flex items-center gap-1.5 text-sm font-medium" :class="textClass">
<component :is="icon" class="text-lg" />
<span>{{ label }}</span>
</div>
<div class="mb-1.5 text-lg font-semibold tabular-nums">{{ formattedValue }}</div>
<div class="text-base-content/60 mb-1 text-xs tabular-nums max-md:hidden">
avg {{ formatValue(average) }} pk {{ formatValue(peak) }}
</div>
<BarChart class="h-8" :chartData="percentData" :barClass="barClass" />
</div>
</template>
<script setup lang="ts">
import type { Component } from "vue";
export interface MetricDataPoint {
percent: number; // value 0 - 100
value: number;
}
const {
label,
icon,
value,
chartData,
containerClass = "",
textClass = "",
barClass = "",
formatValue = (v: number) => v.toString(),
} = defineProps<{
label: string;
icon: Component;
value: string | number;
chartData: MetricDataPoint[];
containerClass?: string;
textClass?: string;
barClass?: string;
formatValue?: (value: number) => string;
}>();
const percentData = computed(() => chartData.map((d) => d.percent));
const peak = computed(() => (chartData.length > 0 ? Math.max(...chartData.map((d) => d.value)) : 0));
const average = computed(() => {
if (chartData.length === 0) return 0;
return chartData.reduce((sum, d) => sum + d.value, 0) / chartData.length;
});
const formattedValue = computed(() => {
if (typeof value === "string") return value;
return formatValue(value);
});
</script>
@@ -1,37 +0,0 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
<octicon:container-24 />
<ContainerDropdown :containers="containers">{{ $t("label.container", containers.length) }}</ContainerDropdown>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useMergedStream"
:entity="containers"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { ids = [], scrollable = false } = defineProps<{
ids?: string[];
scrollable?: boolean;
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { allContainersById, ready } = storeToRefs(containerStore);
const containers = computed(() => ids.map((id) => allContainersById.value[id]));
provideLoggingContext(containers, { showContainerName: true, showHostname: false });
</script>
-12
View File
@@ -1,12 +0,0 @@
<template>
<div class="flex flex-col gap-5 px-4 py-4 md:px-8">
<section>
<Links>
<template #more-items>
<Tag>{{ config.version }}</Tag>
</template>
</Links>
</section>
<slot></slot>
</div>
</template>
-58
View File
@@ -1,58 +0,0 @@
<template>
<slot></slot>
<teleport to="body">
<transition name="fade">
<div
v-show="show && (delayedShow || globalShow)"
class="ring-base-content/20 bg-base-100 fixed z-50 rounded-sm p-3 shadow-sm ring"
ref="content"
>
<slot name="content"></slot>
</div>
</transition>
</teleport>
</template>
<script lang="ts" setup>
import { globalShowPopup } from "@/composable/popup";
const globalShow = globalShowPopup();
const show = ref(globalShow.value);
const delayedShow = refDebounced(show, 1000);
const content = ref<HTMLElement>();
const onMouseEnter = (e: Event) => {
show.value = true;
globalShow.value = true;
if (content.value && e.target instanceof HTMLElement) {
const { left, top, width } = e.target.getBoundingClientRect();
const x = left + width + 10;
const y = top;
content.value.style.left = `${x}px`;
content.value.style.top = `${y}px`;
}
};
const onMouseLeave = () => {
show.value = false;
globalShow.value = false;
};
const el: Ref<HTMLElement> = useCurrentElement();
useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter);
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
-102
View File
@@ -1,102 +0,0 @@
<template>
<transition name="fadeout">
<div class="inline-flex flex-col items-end gap-2" ref="root" v-show="!autoHide || show">
<div class="relative inline-block">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-300 stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<span class="text-4xl">
{{ Math.ceil(progress * 100) }}
</span>
<span> % </span>
</div>
</div>
<RelativeTime :date="date" class="text-sm whitespace-nowrap" />
</div>
</transition>
</template>
<script lang="ts" setup>
const {
indeterminate = false,
autoHide = false,
progress,
date = new Date(),
} = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
progress: number;
date?: Date;
}>();
const show = autoResetRef(false, 2000);
watch(
() => progress,
() => {
if (autoHide) {
show.value = true;
}
},
);
</script>
<style scoped>
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
circle {
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill-opacity: 0.75;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(progress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
}
}
@keyframes svg-animation {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
@keyframes circle-animation {
0%,
25% {
stroke-dashoffset: 275px;
transform: rotate(0);
}
50%,
75% {
stroke-dashoffset: 70px;
transform: rotate(45deg);
}
100% {
stroke-dashoffset: 275px;
transform: rotate(360deg);
}
}
.fadeout-leave-active {
@apply transition-opacity duration-400;
}
.fadeout-leave-to {
opacity: 0;
}
</style>

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