refactor(admin): use inline pixel heights for overview charts
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m40s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m40s
Refactors the admin overview dashboard charts to use inline pixel heights (up to 150px) instead of CSS variables and percentage-based heights. This provides more robust rendering and layout control. Changes include: - Replacing `Height` with `HeightPx` in chart bar structures. - Rendering inline styles for height and width on charts and status bars. - Adding fallback data attributes (`data-height-px`, `data-chart-value`, etc.) and loading a new fallback script (`25-admin-charts.js`). - Updating and expanding test coverage to assert correct scaling and HTML rendering.
This commit is contained in:
@@ -707,24 +707,60 @@ func TestAdminOverviewChartsUseZeroAndFullHeights(t *testing.T) {
|
||||
for i, bar := range overview.UploadDays {
|
||||
want := 0
|
||||
if i == len(overview.UploadDays)-1 {
|
||||
want = 100
|
||||
want = 150
|
||||
}
|
||||
if bar.Height != want {
|
||||
t.Fatalf("upload bar %d height = %d, want %d", i, bar.Height, want)
|
||||
if bar.HeightPx != want {
|
||||
t.Fatalf("upload bar %d height = %d, want %d", i, bar.HeightPx, want)
|
||||
}
|
||||
}
|
||||
for i, bar := range overview.StorageDays {
|
||||
want := 0
|
||||
if i == len(overview.StorageDays)-1 {
|
||||
want = 100
|
||||
want = 150
|
||||
}
|
||||
if bar.Height != want {
|
||||
t.Fatalf("storage bar %d height = %d, want %d", i, bar.Height, want)
|
||||
if bar.HeightPx != want {
|
||||
t.Fatalf("storage bar %d height = %d, want %d", i, bar.HeightPx, want)
|
||||
}
|
||||
}
|
||||
if overview.StatusBars[0].WidthPercent != 100 {
|
||||
t.Fatalf("active status width = %d, want 100", overview.StatusBars[0].WidthPercent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOverviewRendersBarHeightVariables(t *testing.T) {
|
||||
func TestAdminOverviewChartsScaleRelativeToVisibleRange(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, time.UTC)
|
||||
yesterday := today.AddDate(0, 0, -1)
|
||||
twoDaysAgo := today.AddDate(0, 0, -2)
|
||||
boxes := []services.AdminBox{
|
||||
{ID: "today-1", CreatedAt: today, TotalSize: 30},
|
||||
{ID: "today-2", CreatedAt: today, TotalSize: 30},
|
||||
{ID: "today-3", CreatedAt: today, TotalSize: 30},
|
||||
{ID: "yesterday-1", CreatedAt: yesterday, TotalSize: 20},
|
||||
{ID: "yesterday-2", CreatedAt: yesterday, TotalSize: 20},
|
||||
{ID: "two-days-ago", CreatedAt: twoDaysAgo, TotalSize: 10},
|
||||
}
|
||||
overview := buildAdminOverview(boxes, services.AdminStats{TotalBoxes: 6, ExpiredBoxes: 2, ProtectedBoxes: 1})
|
||||
|
||||
last := len(overview.UploadDays) - 1
|
||||
if overview.UploadDays[last].HeightPx != 150 {
|
||||
t.Fatalf("3-upload day height = %d, want 150", overview.UploadDays[last].HeightPx)
|
||||
}
|
||||
if overview.UploadDays[last-1].HeightPx != 100 {
|
||||
t.Fatalf("2-upload day height = %d, want 100", overview.UploadDays[last-1].HeightPx)
|
||||
}
|
||||
if overview.UploadDays[last-2].HeightPx != 50 {
|
||||
t.Fatalf("1-upload day height = %d, want 50", overview.UploadDays[last-2].HeightPx)
|
||||
}
|
||||
if overview.StorageDays[last].HeightPx != 150 || overview.StorageDays[last-1].HeightPx != 66 || overview.StorageDays[last-2].HeightPx != 16 {
|
||||
t.Fatalf("storage heights = %d/%d/%d, want 150/66/16", overview.StorageDays[last].HeightPx, overview.StorageDays[last-1].HeightPx, overview.StorageDays[last-2].HeightPx)
|
||||
}
|
||||
if overview.StatusBars[0].WidthPercent != 100 || overview.StatusBars[1].WidthPercent != 50 || overview.StatusBars[2].WidthPercent != 25 {
|
||||
t.Fatalf("status widths = %d/%d/%d, want 100/50/25", overview.StatusBars[0].WidthPercent, overview.StatusBars[1].WidthPercent, overview.StatusBars[2].WidthPercent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminOverviewRendersInlineBarDimensions(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
adminToken := createAdminSession(t, app)
|
||||
@@ -738,11 +774,26 @@ func TestAdminOverviewRendersBarHeightVariables(t *testing.T) {
|
||||
t.Fatalf("AdminDashboard status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
body := response.Body.String()
|
||||
if !strings.Contains(body, "--bar-height: 100%") {
|
||||
t.Fatalf("admin overview did not render a full-height bar: %s", body)
|
||||
if !strings.Contains(body, `style="height: 150px"`) {
|
||||
t.Fatalf("admin overview did not render a full-height pixel bar: %s", body)
|
||||
}
|
||||
if strings.Contains(body, `style="height:`) {
|
||||
t.Fatalf("admin overview still uses fragile percent height styles: %s", body)
|
||||
if !strings.Contains(body, `data-height-px="150"`) || !strings.Contains(body, `data-chart-value=`) {
|
||||
t.Fatalf("admin overview did not render chart fallback data attributes: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `style="height: 0px"`) {
|
||||
t.Fatalf("admin overview did not render zero pixel bars: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `style="width: 100%"`) {
|
||||
t.Fatalf("admin overview did not render a full-width status bar: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `data-width-percent="100"`) || !strings.Contains(body, `data-stat-value=`) {
|
||||
t.Fatalf("admin overview did not render status fallback data attributes: %s", body)
|
||||
}
|
||||
if strings.Contains(body, "--bar-height") {
|
||||
t.Fatalf("admin overview still uses css variable bar heights: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "/static/js/25-admin-charts.js?version=test") {
|
||||
t.Fatalf("admin overview did not load chart fallback script: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,15 +159,17 @@ type adminOverview struct {
|
||||
}
|
||||
|
||||
type adminChartBar struct {
|
||||
Label string
|
||||
Value string
|
||||
Height int // 0-100, percent of the tallest bar
|
||||
Label string
|
||||
Value string
|
||||
HeightPx int
|
||||
RawValue int64
|
||||
}
|
||||
|
||||
type adminStatBar struct {
|
||||
Label string
|
||||
Value string
|
||||
Percent int
|
||||
Label string
|
||||
Value string
|
||||
RawValue int
|
||||
WidthPercent int
|
||||
}
|
||||
|
||||
type adminBoxView struct {
|
||||
@@ -336,6 +338,7 @@ func (a *App) recentBoxViews(boxes []services.AdminBox, limit int) []adminBoxVie
|
||||
// status distributions for the overview dashboard.
|
||||
func buildAdminOverview(boxes []services.AdminBox, stats services.AdminStats) adminOverview {
|
||||
const days = 14
|
||||
const chartMaxHeightPx = 150
|
||||
now := time.Now().UTC()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -374,14 +377,16 @@ func buildAdminOverview(boxes []services.AdminBox, stats services.AdminStats) ad
|
||||
storageDays := make([]adminChartBar, days)
|
||||
for i := 0; i < days; i++ {
|
||||
uploadDays[i] = adminChartBar{
|
||||
Label: labels[i],
|
||||
Value: strconv.Itoa(counts[i]),
|
||||
Height: scaleHeight(int64(counts[i]), int64(maxCount)),
|
||||
Label: labels[i],
|
||||
Value: strconv.Itoa(counts[i]),
|
||||
HeightPx: scaleHeightPx(int64(counts[i]), int64(maxCount), chartMaxHeightPx),
|
||||
RawValue: int64(counts[i]),
|
||||
}
|
||||
storageDays[i] = adminChartBar{
|
||||
Label: labels[i],
|
||||
Value: helpers.FormatBytes(bytes[i]),
|
||||
Height: scaleHeight(bytes[i], maxBytes),
|
||||
Label: labels[i],
|
||||
Value: helpers.FormatBytes(bytes[i]),
|
||||
HeightPx: scaleHeightPx(bytes[i], maxBytes, chartMaxHeightPx),
|
||||
RawValue: bytes[i],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,10 +394,11 @@ func buildAdminOverview(boxes []services.AdminBox, stats services.AdminStats) ad
|
||||
if activeBoxes < 0 {
|
||||
activeBoxes = 0
|
||||
}
|
||||
maxStatusValue := maxInt(activeBoxes, stats.ExpiredBoxes, stats.ProtectedBoxes)
|
||||
statusBars := []adminStatBar{
|
||||
{Label: "Active", Value: strconv.Itoa(activeBoxes), Percent: percentOf(activeBoxes, stats.TotalBoxes)},
|
||||
{Label: "Expired", Value: strconv.Itoa(stats.ExpiredBoxes), Percent: percentOf(stats.ExpiredBoxes, stats.TotalBoxes)},
|
||||
{Label: "Password-protected", Value: strconv.Itoa(stats.ProtectedBoxes), Percent: percentOf(stats.ProtectedBoxes, stats.TotalBoxes)},
|
||||
{Label: "Active", Value: strconv.Itoa(activeBoxes), RawValue: activeBoxes, WidthPercent: percentOf(activeBoxes, maxStatusValue)},
|
||||
{Label: "Expired", Value: strconv.Itoa(stats.ExpiredBoxes), RawValue: stats.ExpiredBoxes, WidthPercent: percentOf(stats.ExpiredBoxes, maxStatusValue)},
|
||||
{Label: "Password-protected", Value: strconv.Itoa(stats.ProtectedBoxes), RawValue: stats.ProtectedBoxes, WidthPercent: percentOf(stats.ProtectedBoxes, maxStatusValue)},
|
||||
}
|
||||
|
||||
return adminOverview{
|
||||
@@ -402,13 +408,16 @@ func buildAdminOverview(boxes []services.AdminBox, stats services.AdminStats) ad
|
||||
}
|
||||
}
|
||||
|
||||
func scaleHeight(value, max int64) int {
|
||||
func scaleHeightPx(value, max int64, maxHeightPx int) int {
|
||||
if max <= 0 || value <= 0 {
|
||||
return 0
|
||||
}
|
||||
height := int(value * 100 / max)
|
||||
if height < 4 {
|
||||
height = 4
|
||||
height := int(value * int64(maxHeightPx) / max)
|
||||
if height < 8 {
|
||||
height = 8
|
||||
}
|
||||
if height > maxHeightPx {
|
||||
return maxHeightPx
|
||||
}
|
||||
return height
|
||||
}
|
||||
@@ -420,6 +429,16 @@ func percentOf(value, total int) int {
|
||||
return value * 100 / total
|
||||
}
|
||||
|
||||
func maxInt(values ...int) int {
|
||||
max := 0
|
||||
for _, value := range values {
|
||||
if value > max {
|
||||
max = value
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func (a *App) AdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user