mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-24 06:26:17 +01:00
Compare commits
5 Commits
806c4e51c5
...
feat/displ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d759112ee9 | ||
|
|
6576141f54 | ||
|
|
926e807020 | ||
|
|
d91847c6c5 | ||
|
|
0abd88270c |
47
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
47
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,19 @@
|
|||||||
name: 🐛 Bug report
|
name: 🐛 Bug report
|
||||||
description: Report a new bug or issue.
|
description: Report a new bug or issue.
|
||||||
title: '[Bug]: '
|
title: '[Bug]: '
|
||||||
labels: ['bug']
|
labels: ['bug', "needs confirmation"]
|
||||||
body:
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
id: component
|
||||||
|
attributes:
|
||||||
|
label: Component
|
||||||
|
description: Which part of Beszel is this about?
|
||||||
|
options:
|
||||||
|
- Hub
|
||||||
|
- Agent
|
||||||
|
- Hub & Agent
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -43,6 +54,39 @@ body:
|
|||||||
3. Pour it into a cup.
|
3. Pour it into a cup.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: category
|
||||||
|
attributes:
|
||||||
|
label: Category
|
||||||
|
description: Which category does this relate to most?
|
||||||
|
options:
|
||||||
|
- Metrics
|
||||||
|
- Charts & Visualization
|
||||||
|
- Settings & Configuration
|
||||||
|
- Notifications & Alerts
|
||||||
|
- Authentication
|
||||||
|
- Installation
|
||||||
|
- Performance
|
||||||
|
- UI / UX
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: metrics
|
||||||
|
attributes:
|
||||||
|
label: Affected Metrics
|
||||||
|
description: If applicable, which specific metric does this relate to most?
|
||||||
|
options:
|
||||||
|
- CPU
|
||||||
|
- Memory
|
||||||
|
- Storage
|
||||||
|
- Network
|
||||||
|
- Containers
|
||||||
|
- GPU
|
||||||
|
- Sensors
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: system
|
id: system
|
||||||
attributes:
|
attributes:
|
||||||
@@ -61,7 +105,6 @@ body:
|
|||||||
id: install-method
|
id: install-method
|
||||||
attributes:
|
attributes:
|
||||||
label: Installation method
|
label: Installation method
|
||||||
default: 0
|
|
||||||
options:
|
options:
|
||||||
- Docker
|
- Docker
|
||||||
- Binary
|
- Binary
|
||||||
|
|||||||
60
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
60
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,8 +1,19 @@
|
|||||||
name: 🚀 Feature request
|
name: 🚀 Feature request
|
||||||
description: Request a new feature or change.
|
description: Request a new feature or change.
|
||||||
title: "[Feature]: "
|
title: "[Feature]: "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement", "needs review"]
|
||||||
body:
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
id: component
|
||||||
|
attributes:
|
||||||
|
label: Component
|
||||||
|
description: Which part of Beszel is this about?
|
||||||
|
options:
|
||||||
|
- Hub
|
||||||
|
- Agent
|
||||||
|
- Hub & Agent
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: Before submitting, please search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
value: Before submitting, please search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||||
@@ -11,8 +22,55 @@ body:
|
|||||||
label: Describe the feature you would like to see
|
label: Describe the feature you would like to see
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: motivation
|
||||||
|
attributes:
|
||||||
|
label: Motivation / Use Case
|
||||||
|
description: Why do you want this feature? What problem does it solve?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe how you would like to see this feature implemented
|
label: Describe how you would like to see this feature implemented
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: Please attach any relevant screenshots, such as images from your current solution or similar implementations.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: category
|
||||||
|
attributes:
|
||||||
|
label: Category
|
||||||
|
description: Which category does this relate to most?
|
||||||
|
options:
|
||||||
|
- Metrics
|
||||||
|
- Charts & Visualization
|
||||||
|
- Settings & Configuration
|
||||||
|
- Notifications & Alerts
|
||||||
|
- Authentication
|
||||||
|
- Installation
|
||||||
|
- Performance
|
||||||
|
- UI / UX
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: metrics
|
||||||
|
attributes:
|
||||||
|
label: Affected Metrics
|
||||||
|
description: If applicable, which specific metric does this relate to most?
|
||||||
|
options:
|
||||||
|
- CPU
|
||||||
|
- Memory
|
||||||
|
- Storage
|
||||||
|
- Network
|
||||||
|
- Containers
|
||||||
|
- GPU
|
||||||
|
- Sensors
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
## 📃 Description
|
||||||
|
|
||||||
|
A short description of the pull request changes should go here and the sections below should list in detail all changes. You can remove the sections you don't need.
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
Add a link to the PR for [documentation](https://github.com/henrygd/beszel-docs) changes.
|
||||||
|
|
||||||
|
## 🪵 Changelog
|
||||||
|
|
||||||
|
### ➕ Added
|
||||||
|
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
|
||||||
|
### ✏️ Changed
|
||||||
|
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
|
||||||
|
### 🔧 Fixed
|
||||||
|
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
|
||||||
|
### 🗑️ Removed
|
||||||
|
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
|
||||||
|
## 📷 Screenshots
|
||||||
|
|
||||||
|
If this PR has any UI/UX changes it's strongly suggested you add screenshots here.
|
||||||
2
.github/workflows/docker-images.yml
vendored
2
.github/workflows/docker-images.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ matrix.image }}
|
images: ${{ matrix.image }}
|
||||||
tags: |
|
tags: |
|
||||||
type=edge,enable=true
|
type=raw,value=edge
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|||||||
43
.github/workflows/inactivity-actions.yml
vendored
Normal file
43
.github/workflows/inactivity-actions.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: 'Issue and PR Maintenance'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # runs at midnight UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-stale:
|
||||||
|
name: Close Stale Issues
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Close Stale Issues
|
||||||
|
uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Messaging
|
||||||
|
stale-issue-message: >
|
||||||
|
👋 This issue has been automatically marked as stale due to inactivity.
|
||||||
|
If this issue is still relevant, please comment to keep it open.
|
||||||
|
Without activity, it will be closed in 7 days.
|
||||||
|
|
||||||
|
close-issue-message: >
|
||||||
|
🔒 This issue has been automatically closed due to prolonged inactivity.
|
||||||
|
Feel free to open a new issue if you have further questions or concerns.
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
days-before-issue-stale: 14
|
||||||
|
days-before-issue-close: 7
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
stale-issue-label: 'stale'
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
only-issue-labels: 'awaiting-requester'
|
||||||
|
|
||||||
|
# Exemptions
|
||||||
|
exempt-assignees: true
|
||||||
|
exempt-milestones: true
|
||||||
82
.github/workflows/label-from-dropdown.yml
vendored
Normal file
82
.github/workflows/label-from-dropdown.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Label issues from dropdowns
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label_from_dropdown:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Apply labels based on dropdown choices
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
|
||||||
|
const issueNumber = context.issue.number;
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
// Get the issue body
|
||||||
|
const body = context.payload.issue.body;
|
||||||
|
|
||||||
|
// Helper to find dropdown value in the body (assuming markdown format)
|
||||||
|
function extractSectionValue(heading) {
|
||||||
|
const regex = new RegExp(`### ${heading}\\s+([\\s\\S]*?)(?:\\n###|$)`, 'i');
|
||||||
|
const match = body.match(regex);
|
||||||
|
if (match) {
|
||||||
|
// Get the first non-empty line after the heading
|
||||||
|
const lines = match[1].split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
|
return lines[0] || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract dropdown selections
|
||||||
|
const category = extractSectionValue('Category');
|
||||||
|
const metrics = extractSectionValue('Affected Metrics');
|
||||||
|
const component = extractSectionValue('Component');
|
||||||
|
|
||||||
|
// Build labels to add
|
||||||
|
let labelsToAdd = [];
|
||||||
|
if (category) labelsToAdd.push(category);
|
||||||
|
if (metrics) labelsToAdd.push(metrics);
|
||||||
|
if (component) labelsToAdd.push(component);
|
||||||
|
|
||||||
|
// Get existing labels in the repo
|
||||||
|
const { data: existingLabels } = await github.rest.issues.listLabelsForRepo({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: 100
|
||||||
|
});
|
||||||
|
const existingLabelNames = existingLabels.map(l => l.name);
|
||||||
|
|
||||||
|
// Find labels that need to be created
|
||||||
|
const labelsToCreate = labelsToAdd.filter(label => !existingLabelNames.includes(label));
|
||||||
|
|
||||||
|
// Create missing labels (with a default color)
|
||||||
|
for (const label of labelsToCreate) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name: label,
|
||||||
|
color: 'ededed' // light gray, you can pick any hex color
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if label already exists (race condition), otherwise rethrow
|
||||||
|
if (!e || e.status !== 422) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now apply all labels (they all exist now)
|
||||||
|
if (labelsToAdd.length > 0) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
labels: labelsToAdd
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,7 +17,9 @@ type UserSettings struct {
|
|||||||
ChartTime string `json:"chartTime"`
|
ChartTime string `json:"chartTime"`
|
||||||
NotificationEmails []string `json:"emails"`
|
NotificationEmails []string `json:"emails"`
|
||||||
NotificationWebhooks []string `json:"webhooks"`
|
NotificationWebhooks []string `json:"webhooks"`
|
||||||
// Language string `json:"lang"`
|
// TemperatureUnit int `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
|
||||||
|
// NetworkUnit int `json:"unitNet"` // 0 for bytes, 1 for bits
|
||||||
|
// DiskUnit int `json:"unitDisk"` // 0 for bytes, 1 for bits
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserManager(app core.App) *UserManager {
|
func NewUserManager(app core.App) *UserManager {
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ import { t } from "@lingui/core/macro"
|
|||||||
|
|
||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import {
|
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||||
useYAxisWidth,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
decimalString,
|
|
||||||
chartMargin,
|
|
||||||
} from "@/lib/utils"
|
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
@@ -30,7 +23,6 @@ const getNestedValue = (path: string, max = false, data: any): number | null =>
|
|||||||
|
|
||||||
export default memo(function AreaChartDefault({
|
export default memo(function AreaChartDefault({
|
||||||
maxToggled = false,
|
maxToggled = false,
|
||||||
unit = " MB/s",
|
|
||||||
chartName,
|
chartName,
|
||||||
chartData,
|
chartData,
|
||||||
max,
|
max,
|
||||||
@@ -38,12 +30,11 @@ export default memo(function AreaChartDefault({
|
|||||||
contentFormatter,
|
contentFormatter,
|
||||||
}: {
|
}: {
|
||||||
maxToggled?: boolean
|
maxToggled?: boolean
|
||||||
unit?: string
|
|
||||||
chartName: string
|
chartName: string
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
max?: number
|
max?: number
|
||||||
tickFormatter?: (value: number) => string
|
tickFormatter: (value: number) => string
|
||||||
contentFormatter?: (value: number) => string
|
contentFormatter: ({ value }: { value: number }) => string
|
||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { i18n } = useLingui()
|
const { i18n } = useLingui()
|
||||||
@@ -98,15 +89,7 @@ export default memo(function AreaChartDefault({
|
|||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
domain={[0, max ?? "auto"]}
|
domain={[0, max ?? "auto"]}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value) => updateYAxisWidth(tickFormatter(value))}
|
||||||
let val: string
|
|
||||||
if (tickFormatter) {
|
|
||||||
val = tickFormatter(value)
|
|
||||||
} else {
|
|
||||||
val = toFixedWithoutTrailingZeros(value, 2) + unit
|
|
||||||
}
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
@@ -117,12 +100,7 @@ export default memo(function AreaChartDefault({
|
|||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={({ value }) => {
|
contentFormatter={contentFormatter}
|
||||||
if (contentFormatter) {
|
|
||||||
return contentFormatter(value)
|
|
||||||
}
|
|
||||||
return decimalString(value) + unit
|
|
||||||
}}
|
|
||||||
// indicator="line"
|
// indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import {
|
|||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
decimalString,
|
|
||||||
chartMargin,
|
chartMargin,
|
||||||
toFixedFloat,
|
|
||||||
getSizeAndUnit,
|
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
formatBytes,
|
||||||
|
decimalString,
|
||||||
|
toFixedFloat,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $containerFilter } from "@/lib/stores"
|
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
import { ChartType } from "@/lib/enums"
|
import { ChartType, DataUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function ContainerChart({
|
export default memo(function ContainerChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
@@ -30,6 +30,7 @@ export default memo(function ContainerChart({
|
|||||||
unit?: string
|
unit?: string
|
||||||
}) {
|
}) {
|
||||||
const filter = useStore($containerFilter)
|
const filter = useStore($containerFilter)
|
||||||
|
const userSettings = useStore($userSettings)
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
const { containerData } = chartData
|
const { containerData } = chartData
|
||||||
@@ -88,9 +89,10 @@ export default memo(function ContainerChart({
|
|||||||
return updateYAxisWidth(val)
|
return updateYAxisWidth(val)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
obj.tickFormatter = (value) => {
|
const chartUnit = isNetChart ? userSettings.unitNet : DataUnit.Bytes
|
||||||
const { v, u } = getSizeAndUnit(value, false)
|
obj.tickFormatter = (val) => {
|
||||||
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? "/s" : ""}`)
|
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
||||||
|
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// tooltip formatter
|
// tooltip formatter
|
||||||
@@ -99,12 +101,14 @@ export default memo(function ContainerChart({
|
|||||||
try {
|
try {
|
||||||
const sent = item?.payload?.[key]?.ns ?? 0
|
const sent = item?.payload?.[key]?.ns ?? 0
|
||||||
const received = item?.payload?.[key]?.nr ?? 0
|
const received = item?.payload?.[key]?.nr ?? 0
|
||||||
|
const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true)
|
||||||
|
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true)
|
||||||
return (
|
return (
|
||||||
<span className="flex">
|
<span className="flex">
|
||||||
{decimalString(received)} MB/s
|
{decimalString(receivedValue)} {receivedUnit}
|
||||||
<span className="opacity-70 ms-0.5"> rx </span>
|
<span className="opacity-70 ms-0.5"> rx </span>
|
||||||
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
||||||
{decimalString(sent)} MB/s
|
{decimalString(sentValue)} {sentUnit}
|
||||||
<span className="opacity-70 ms-0.5"> tx</span>
|
<span className="opacity-70 ms-0.5"> tx</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -114,8 +118,8 @@ export default memo(function ContainerChart({
|
|||||||
}
|
}
|
||||||
} else if (chartType === ChartType.Memory) {
|
} else if (chartType === ChartType.Memory) {
|
||||||
obj.toolTipFormatter = (item: any) => {
|
obj.toolTipFormatter = (item: any) => {
|
||||||
const { v, u } = getSizeAndUnit(item.value, false)
|
const { value, unit } = formatBytes(item.value, false, DataUnit.Bytes, true)
|
||||||
return decimalString(v, 2) + u
|
return decimalString(value) + " " + unit
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import {
|
import { useYAxisWidth, cn, formatShortDate, decimalString, toFixedFloat, chartMargin, formatBytes } from "@/lib/utils"
|
||||||
useYAxisWidth,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
decimalString,
|
|
||||||
toFixedFloat,
|
|
||||||
chartMargin,
|
|
||||||
getSizeAndUnit,
|
|
||||||
} from "@/lib/utils"
|
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import { DataUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function DiskChart({
|
export default memo(function DiskChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
@@ -53,9 +46,9 @@ export default memo(function DiskChart({
|
|||||||
minTickGap={6}
|
minTickGap={6}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(val) => {
|
||||||
const { v, u } = getSizeAndUnit(value)
|
const { value, unit } = formatBytes(val * 1024, false, DataUnit.Bytes, true)
|
||||||
return updateYAxisWidth(toFixedFloat(v, 2) + u)
|
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{xAxis(chartData)}
|
{xAxis(chartData)}
|
||||||
@@ -66,8 +59,8 @@ export default memo(function DiskChart({
|
|||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={({ value }) => {
|
contentFormatter={({ value }) => {
|
||||||
const { v, u } = getSizeAndUnit(value)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
return decimalString(v) + u
|
return decimalString(convertedValue) + " " + unit
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils"
|
import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin, formatBytes } from "@/lib/utils"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
|
import { DataUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
@@ -39,8 +40,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value) => {
|
||||||
const val = toFixedFloat(value, 1)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
return updateYAxisWidth(val + " GB")
|
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -54,8 +55,11 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => a.order - b.order}
|
itemSorter={(a, b) => a.order - b.order}
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
contentFormatter={({ value }) => {
|
||||||
// indicator="line"
|
// mem values are supplied as GB
|
||||||
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,16 +13,18 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
decimalString,
|
|
||||||
chartMargin,
|
chartMargin,
|
||||||
|
convertTemperature,
|
||||||
|
decimalString,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { $temperatureFilter } from "@/lib/stores"
|
import { $temperatureFilter, $userSettings } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
|
|
||||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||||
const filter = useStore($temperatureFilter)
|
const filter = useStore($temperatureFilter)
|
||||||
|
const userSettings = useStore($userSettings)
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
if (chartData.systemStats.length === 0) {
|
if (chartData.systemStats.length === 0) {
|
||||||
@@ -72,9 +74,9 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
|||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
domain={[0, "auto"]}
|
domain={[0, "auto"]}
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(val) => {
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2)
|
const { value, unit } = convertTemperature(val, userSettings.unitTemp)
|
||||||
return updateYAxisWidth(val + " °C")
|
return updateYAxisWidth(toFixedWithoutTrailingZeros(value, 2) + " " + unit)
|
||||||
}}
|
}}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
@@ -88,7 +90,10 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
|||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={(item) => decimalString(item.value) + " °C"}
|
contentFormatter={(item) => {
|
||||||
|
const { value, unit } = convertTemperature(item.value, userSettings.unitTemp)
|
||||||
|
return decimalString(value) + " " + unit
|
||||||
|
}}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useState } from "react"
|
|||||||
import languages from "@/lib/languages"
|
import languages from "@/lib/languages"
|
||||||
import { dynamicActivate } from "@/lib/i18n"
|
import { dynamicActivate } from "@/lib/i18n"
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
// import { setLang } from "@/lib/i18n"
|
import { DataUnit, TemperatureUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -101,6 +101,75 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="mb-1 text-lg font-medium">
|
||||||
|
<Trans>Unit preferences</Trans>
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
<Trans>Change display units for metrics.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid sm:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="block" htmlFor="unitTemp">
|
||||||
|
<Trans>Temperature unit</Trans>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="unitTemp"
|
||||||
|
key={userSettings.unitTemp}
|
||||||
|
defaultValue={userSettings.unitTemp?.toString() || String(TemperatureUnit.Celsius)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="unitTemp">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={String(TemperatureUnit.Celsius)}>Celsius (°C)</SelectItem>
|
||||||
|
<SelectItem value={String(TemperatureUnit.Fahrenheit)}>Fahrenheit (°F)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="block" htmlFor="unitTemp">
|
||||||
|
<Trans>Network unit</Trans>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="unitNet"
|
||||||
|
key={userSettings.unitNet}
|
||||||
|
defaultValue={userSettings.unitNet?.toString() ?? String(DataUnit.Bytes)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="unitTemp">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={String(DataUnit.Bytes)}>Bytes (KB/s, MB/s, GB/s)</SelectItem>
|
||||||
|
<SelectItem value={String(DataUnit.Bits)}>Bits (kbps, Mbps, Gbps)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="block" htmlFor="unitDisk">
|
||||||
|
<Trans>Disk unit</Trans>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="unitDisk"
|
||||||
|
key={userSettings.unitDisk}
|
||||||
|
defaultValue={userSettings.unitDisk?.toString() ?? String(DataUnit.Bytes)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="unitDisk">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={String(DataUnit.Bytes)}>Bytes (KB/s, MB/s, GB/s)</SelectItem>
|
||||||
|
<SelectItem value={String(DataUnit.Bits)}>Bits (kbps, Mbps, Gbps)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||||
<Trans>Save Settings</Trans>
|
<Trans>Save Settings</Trans>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
$temperatureFilter,
|
$temperatureFilter,
|
||||||
} from "@/lib/stores"
|
} from "@/lib/stores"
|
||||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||||
import { ChartType, Os } from "@/lib/enums"
|
import { ChartType, DataUnit, Os } from "@/lib/enums"
|
||||||
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
@@ -21,11 +21,13 @@ import ChartTimeSelect from "../charts/chart-time-select"
|
|||||||
import {
|
import {
|
||||||
chartTimeData,
|
chartTimeData,
|
||||||
cn,
|
cn,
|
||||||
|
decimalString,
|
||||||
|
formatBytes,
|
||||||
getHostDisplayValue,
|
getHostDisplayValue,
|
||||||
getPbTimestamp,
|
getPbTimestamp,
|
||||||
getSizeAndUnit,
|
|
||||||
listen,
|
listen,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
@@ -131,6 +133,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||||
const [chartLoading, setChartLoading] = useState(true)
|
const [chartLoading, setChartLoading] = useState(true)
|
||||||
const isLongerChart = chartTime !== "1h"
|
const isLongerChart = chartTime !== "1h"
|
||||||
|
const userSettings = $userSettings.get()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${name} / Beszel`
|
document.title = `${name} / Beszel`
|
||||||
@@ -472,7 +475,13 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={t`Average system-wide CPU utilization`}
|
description={t`Average system-wide CPU utilization`}
|
||||||
cornerEl={maxValSelect}
|
cornerEl={maxValSelect}
|
||||||
>
|
>
|
||||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={maxValues} unit="%" />
|
<AreaChartDefault
|
||||||
|
chartData={chartData}
|
||||||
|
chartName="CPU Usage"
|
||||||
|
maxToggled={maxValues}
|
||||||
|
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
|
||||||
|
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{containerFilterBar && (
|
{containerFilterBar && (
|
||||||
@@ -519,7 +528,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={t`Throughput of root filesystem`}
|
description={t`Throughput of root filesystem`}
|
||||||
cornerEl={maxValSelect}
|
cornerEl={maxValSelect}
|
||||||
>
|
>
|
||||||
<AreaChartDefault chartData={chartData} chartName="dio" maxToggled={maxValues} />
|
<AreaChartDefault
|
||||||
|
chartData={chartData}
|
||||||
|
chartName="dio"
|
||||||
|
maxToggled={maxValues}
|
||||||
|
tickFormatter={(val) => {
|
||||||
|
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||||
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
|
}}
|
||||||
|
contentFormatter={({ value }) => {
|
||||||
|
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
|
||||||
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard
|
<ChartCard
|
||||||
@@ -529,7 +550,20 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
cornerEl={maxValSelect}
|
cornerEl={maxValSelect}
|
||||||
description={t`Network traffic of public interfaces`}
|
description={t`Network traffic of public interfaces`}
|
||||||
>
|
>
|
||||||
<AreaChartDefault chartData={chartData} chartName="bw" maxToggled={maxValues} />
|
<AreaChartDefault
|
||||||
|
chartData={chartData}
|
||||||
|
chartName="bw"
|
||||||
|
maxToggled={maxValues}
|
||||||
|
tickFormatter={(val) => {
|
||||||
|
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||||
|
// value = value >= 10 ? Math.ceil(value) : value
|
||||||
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
|
}}
|
||||||
|
contentFormatter={({ value }) => {
|
||||||
|
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, true)
|
||||||
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{containerFilterBar && containerData.length > 0 && (
|
{containerFilterBar && containerData.length > 0 && (
|
||||||
@@ -594,10 +628,10 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<div className="grid xl:grid-cols-2 gap-4">
|
<div className="grid xl:grid-cols-2 gap-4">
|
||||||
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
|
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
|
||||||
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
|
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
|
||||||
const sizeFormatter = (value: number, decimals?: number) => {
|
// const sizeFormatter = (value: number, decimals?: number) => {
|
||||||
const { v, u } = getSizeAndUnit(value, false)
|
// const { v, u } = getSizeAndUnit(value, false)
|
||||||
return toFixedFloat(v, decimals || 1) + u
|
// return toFixedFloat(v, decimals || 1) + u
|
||||||
}
|
// }
|
||||||
return (
|
return (
|
||||||
<div key={id} className="contents">
|
<div key={id} className="contents">
|
||||||
<ChartCard
|
<ChartCard
|
||||||
@@ -606,7 +640,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
title={`${gpu.n} ${t`Usage`}`}
|
title={`${gpu.n} ${t`Usage`}`}
|
||||||
description={t`Average utilization of ${gpu.n}`}
|
description={t`Average utilization of ${gpu.n}`}
|
||||||
>
|
>
|
||||||
<AreaChartDefault chartData={chartData} chartName={`g.${id}.u`} unit="%" />
|
<AreaChartDefault
|
||||||
|
chartData={chartData}
|
||||||
|
chartName={`g.${id}.u`}
|
||||||
|
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
|
||||||
|
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
@@ -618,8 +657,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
chartName={`g.${id}.mu`}
|
chartName={`g.${id}.mu`}
|
||||||
max={gpu.mt}
|
max={gpu.mt}
|
||||||
tickFormatter={sizeFormatter}
|
tickFormatter={(val) => {
|
||||||
contentFormatter={(value) => sizeFormatter(value, 2)}
|
const { value, unit } = formatBytes(val, false, DataUnit.Bytes, true)
|
||||||
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
|
}}
|
||||||
|
contentFormatter={({ value }) => {
|
||||||
|
const { value: convertedValue, unit } = formatBytes(value, false, DataUnit.Bytes, true)
|
||||||
|
return decimalString(convertedValue) + " " + unit
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -653,7 +698,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
description={t`Throughput of ${extraFsName}`}
|
description={t`Throughput of ${extraFsName}`}
|
||||||
cornerEl={maxValSelect}
|
cornerEl={maxValSelect}
|
||||||
>
|
>
|
||||||
<AreaChartDefault chartData={chartData} chartName={`efs.${extraFsName}`} maxToggled={maxValues} />
|
<AreaChartDefault
|
||||||
|
chartData={chartData}
|
||||||
|
chartName={`efs.${extraFsName}`}
|
||||||
|
maxToggled={maxValues}
|
||||||
|
tickFormatter={(val) => {
|
||||||
|
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||||
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
|
}}
|
||||||
|
contentFormatter={({ value }) => {
|
||||||
|
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
|
||||||
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,9 +63,17 @@ import {
|
|||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { $systems, pb } from "@/lib/stores"
|
import { $systems, $userSettings, pb } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
import {
|
||||||
|
cn,
|
||||||
|
copyToClipboard,
|
||||||
|
isReadOnlyUser,
|
||||||
|
useLocalStorage,
|
||||||
|
convertTemperature,
|
||||||
|
decimalString,
|
||||||
|
formatBytes,
|
||||||
|
} from "@/lib/utils"
|
||||||
import AlertsButton from "../alerts/alert-button"
|
import AlertsButton from "../alerts/alert-button"
|
||||||
import { $router, Link, navigate } from "../router"
|
import { $router, Link, navigate } from "../router"
|
||||||
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||||
@@ -127,6 +135,7 @@ export default function SystemsTable() {
|
|||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
|
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
|
||||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
|
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
|
||||||
|
const userSettings = useStore($userSettings)
|
||||||
|
|
||||||
const locale = i18n.locale
|
const locale = i18n.locale
|
||||||
|
|
||||||
@@ -217,12 +226,16 @@ export default function SystemsTable() {
|
|||||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||||
id: "net",
|
id: "net",
|
||||||
name: () => t`Net`,
|
name: () => t`Net`,
|
||||||
size: 50,
|
size: 0,
|
||||||
Icon: EthernetIcon,
|
Icon: EthernetIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
cell(info) {
|
cell(info) {
|
||||||
const val = info.getValue() as number
|
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true)
|
||||||
return <span className="tabular-nums whitespace-nowrap">{decimalString(val, val >= 100 ? 1 : 2)} MB/s</span>
|
return (
|
||||||
|
<span className="tabular-nums whitespace-nowrap">
|
||||||
|
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -278,9 +291,10 @@ export default function SystemsTable() {
|
|||||||
if (!val) {
|
if (!val) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
const { value, unit } = convertTemperature(val, userSettings.unitTemp)
|
||||||
return (
|
return (
|
||||||
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
|
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
|
||||||
{decimalString(val)} °C
|
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,3 +11,13 @@ export enum ChartType {
|
|||||||
Network,
|
Network,
|
||||||
CPU,
|
CPU,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DataUnit {
|
||||||
|
Bytes,
|
||||||
|
Bits,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TemperatureUnit {
|
||||||
|
Celsius,
|
||||||
|
Fahrenheit,
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export const $maxValues = atom(false)
|
|||||||
export const $userSettings = map<UserSettings>({
|
export const $userSettings = map<UserSettings>({
|
||||||
chartTime: "1h",
|
chartTime: "1h",
|
||||||
emails: [pb.authStore.record?.email || ""],
|
emails: [pb.authStore.record?.email || ""],
|
||||||
|
temperatureUnit: "celsius",
|
||||||
|
networkUnit: "mbps",
|
||||||
|
diskUnit: "mbps",
|
||||||
})
|
})
|
||||||
// update local storage on change
|
// update local storage on change
|
||||||
$userSettings.subscribe((value) => {
|
$userSettings.subscribe((value) => {
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ import { toast } from "@/components/ui/use-toast"
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
|
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
|
||||||
import { AlertInfo, AlertRecord, ChartTimeData, ChartTimes, FingerprintRecord, SystemRecord } from "@/types"
|
import {
|
||||||
|
AlertInfo,
|
||||||
|
AlertRecord,
|
||||||
|
ChartTimeData,
|
||||||
|
ChartTimes,
|
||||||
|
FingerprintRecord,
|
||||||
|
SystemRecord,
|
||||||
|
TemperatureConversion,
|
||||||
|
DataUnitConversion,
|
||||||
|
} from "@/types"
|
||||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
import { RecordModel, RecordSubscription } from "pocketbase"
|
||||||
import { WritableAtom } from "nanostores"
|
import { WritableAtom } from "nanostores"
|
||||||
import { timeDay, timeHour } from "d3-time"
|
import { timeDay, timeHour } from "d3-time"
|
||||||
@@ -11,6 +20,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||||
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
|
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||||
import { prependBasePath } from "@/components/router"
|
import { prependBasePath } from "@/components/router"
|
||||||
|
import { DataUnit, TemperatureUnit } from "./enums"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@@ -230,7 +240,7 @@ export function toFixedWithoutTrailingZeros(num: number, digits: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toFixedFloat(num: number, digits: number) {
|
export function toFixedFloat(num: number, digits: number) {
|
||||||
return parseFloat(num.toFixed(digits))
|
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
|
||||||
}
|
}
|
||||||
|
|
||||||
let decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
|
let decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
|
||||||
@@ -266,6 +276,72 @@ export function useLocalStorage<T>(key: string, defaultValue: T) {
|
|||||||
return [value, setValue]
|
return [value, setValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertTemperature(celsius: number, unit = TemperatureUnit.Celsius): TemperatureConversion {
|
||||||
|
const userSettings = $userSettings.get()
|
||||||
|
unit ||= userSettings.unitTemp || TemperatureUnit.Celsius
|
||||||
|
// need loose equality check due to form data being strings
|
||||||
|
if (unit == TemperatureUnit.Fahrenheit) {
|
||||||
|
return {
|
||||||
|
value: celsius * 1.8 + 32,
|
||||||
|
unit: "°F",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: celsius,
|
||||||
|
unit: "°C",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(
|
||||||
|
size: number,
|
||||||
|
perSecond = false,
|
||||||
|
unit = DataUnit.Bytes,
|
||||||
|
isMegabytes = false
|
||||||
|
): DataUnitConversion {
|
||||||
|
// Convert MB to bytes if isMegabytes is true
|
||||||
|
if (isMegabytes) size *= 1024 * 1024
|
||||||
|
|
||||||
|
// need loose equality check due to form data being strings
|
||||||
|
if (unit == DataUnit.Bits) {
|
||||||
|
const bits = size * 8
|
||||||
|
const suffix = perSecond ? "ps" : ""
|
||||||
|
if (bits < 1000) return { value: bits, unit: `b${suffix}` }
|
||||||
|
if (bits < 1_000_000) return { value: bits / 1_000, unit: `Kb${suffix}` }
|
||||||
|
if (bits < 1_000_000_000)
|
||||||
|
return {
|
||||||
|
value: bits / 1_000_000,
|
||||||
|
unit: `Mb${suffix}`,
|
||||||
|
}
|
||||||
|
if (bits < 1_000_000_000_000)
|
||||||
|
return {
|
||||||
|
value: bits / 1_000_000_000,
|
||||||
|
unit: `Gb${suffix}`,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: bits / 1_000_000_000_000,
|
||||||
|
unit: `Tb${suffix}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = perSecond ? "/s" : ""
|
||||||
|
if (size < 100) return { value: size, unit: `B${suffix}` }
|
||||||
|
if (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` }
|
||||||
|
if (size < 1000 * 1024 ** 2)
|
||||||
|
return {
|
||||||
|
value: size / 1024 ** 2,
|
||||||
|
unit: `MB${suffix}`,
|
||||||
|
}
|
||||||
|
if (size < 1000 * 1024 ** 3)
|
||||||
|
return {
|
||||||
|
value: size / 1024 ** 3,
|
||||||
|
unit: `GB${suffix}`,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: size / 1024 ** 4,
|
||||||
|
unit: `TB${suffix}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateUserSettings() {
|
export async function updateUserSettings() {
|
||||||
try {
|
try {
|
||||||
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
|
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
|
||||||
|
|||||||
16
beszel/site/src/types.d.ts
vendored
16
beszel/site/src/types.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import { RecordModel } from "pocketbase"
|
import { RecordModel } from "pocketbase"
|
||||||
import { Os } from "./lib/enums"
|
import { DataUnit, Os, TemperatureUnit } from "./lib/enums"
|
||||||
|
|
||||||
// global window properties
|
// global window properties
|
||||||
declare global {
|
declare global {
|
||||||
@@ -22,6 +22,17 @@ export interface FingerprintRecord extends RecordModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unit conversion result types
|
||||||
|
export interface TemperatureConversion {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataUnitConversion {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemRecord extends RecordModel {
|
export interface SystemRecord extends RecordModel {
|
||||||
name: string
|
name: string
|
||||||
host: string
|
host: string
|
||||||
@@ -205,6 +216,9 @@ export type UserSettings = {
|
|||||||
chartTime: ChartTimes
|
chartTime: ChartTimes
|
||||||
emails?: string[]
|
emails?: string[]
|
||||||
webhooks?: string[]
|
webhooks?: string[]
|
||||||
|
unitTemp?: TemperatureUnit
|
||||||
|
unitNet?: DataUnit
|
||||||
|
unitDisk?: DataUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataContainer = {
|
type ChartDataContainer = {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2021",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Reference in New Issue
Block a user