Skip to content

Commit a46a25b

Browse files
committed
feat: implement metric groups and update settings page to use groups
1 parent 19249d5 commit a46a25b

File tree

14 files changed

+237
-141
lines changed

14 files changed

+237
-141
lines changed

ckanext/better_stats/assets/css/styles.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/better_stats/assets/js/bstats-settings.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,27 @@ ckan.module("bstats-stats-settings", function ($) {
1818

1919
this.el.find("#btn-clear-all-caches").on("click", this._onClearAll);
2020
this.el.find("#btn-reset-all").on("click", this._onResetAll);
21-
this.el.find("#metrics-tbody").on("change", ".metric-field", this._onFieldChange);
22-
this.el.find("#metrics-tbody").on("click", ".btn-clear-cache", this._onClearMetricCache);
21+
this.el.on("change", ".metric-field", this._onFieldChange);
22+
this.el.on("click", ".btn-clear-cache", this._onClearMetricCache);
2323
},
2424

2525
_initSortable() {
26-
this._sortable = Sortable.create(this.el.find("#metrics-tbody")[0], {
27-
handle: ".sortable-handle",
28-
animation: 150,
29-
onEnd: this._onReorder,
26+
this._sortables = [];
27+
this.el.find("[data-group-tbody]").each((_, tbody) => {
28+
this._sortables.push(
29+
Sortable.create(tbody, {
30+
handle: ".sortable-handle",
31+
animation: 150,
32+
onEnd: this._onReorder,
33+
})
34+
);
3035
});
3136
},
3237

33-
_onReorder() {
34-
const rows = this.el.find("#metrics-tbody .metric-row").toArray();
38+
_onReorder(evt) {
39+
const rows = Array.from(evt.to.querySelectorAll(".metric-row"));
3540
const items = rows.map((row, idx) => ({
36-
metric_name: $(row).data("metric"),
41+
metric_name: row.dataset.metric,
3742
order: (idx + 1) * 10,
3843
}));
3944
this._setStatus(this._("Saving\u2026"));
@@ -56,7 +61,8 @@ ckan.module("bstats-stats-settings", function ($) {
5661
},
5762

5863
_saveRow(row) {
59-
const allRows = this.el.find("#metrics-tbody .metric-row").toArray();
64+
const tbody = row.closest("[data-group-tbody]");
65+
const allRows = Array.from(tbody.querySelectorAll(".metric-row"));
6066
const idx = allRows.indexOf(row);
6167
const payload = { order: (idx + 1) * 10 };
6268

@@ -127,15 +133,15 @@ ckan.module("bstats-stats-settings", function ($) {
127133
if (++this._pending === 1) {
128134
this.el.find(".metric-field").prop("disabled", true);
129135
this.el.find(".sortable-handle").css("cursor", "not-allowed");
130-
this._sortable.option("disabled", true);
136+
this._sortables.forEach((s) => s.option("disabled", true));
131137
}
132138
},
133139

134140
_unlock() {
135141
if (--this._pending === 0) {
136142
this.el.find(".metric-field").prop("disabled", false);
137143
this.el.find(".sortable-handle").css("cursor", "grab");
138-
this._sortable.option("disabled", false);
144+
this._sortables.forEach((s) => s.option("disabled", false));
139145
}
140146
},
141147

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
// ── Settings page ─────────────────────────────────────────────
22
// Styles specific to /better_stats/settings
33

4-
.table-header th { font-size: 0.8rem; }
4+
.table-header th {
5+
font-size: 0.8rem;
6+
}
7+
8+
#metrics-accordion {
9+
.accordion-item {
10+
border: 1px solid rgba(0, 0, 0, 0.125);
11+
border-radius: 0.25rem;
12+
13+
.accordion-button.collapsed {
14+
border-bottom-right-radius: calc(0.25rem - 1px);
15+
border-bottom-left-radius: calc(0.25rem - 1px);
16+
}
17+
}
18+
}

ckanext/better_stats/const.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from dataclasses import dataclass
12
from enum import Enum
23

4+
import ckan.plugins.toolkit as tk
5+
36

47
class AccessLevel(Enum):
58
PUBLIC = "public"
@@ -24,3 +27,17 @@ class ExportFormat(Enum):
2427
JSON = "json"
2528
XLSX = "xlsx"
2629
IMAGE = "image"
30+
31+
32+
@dataclass
33+
class MetricGroup:
34+
name: str
35+
label: str
36+
icon: str = ""
37+
description: str = ""
38+
39+
40+
DATASETS_GROUP = MetricGroup(name="datasets", label=tk._("Datasets"), icon="fa-solid fa-file-alt")
41+
ORGANIZATIONS_GROUP = MetricGroup(name="organizations", label=tk._("Organizations"), icon="fa-solid fa-building-user")
42+
OVERVIEW_GROUP = MetricGroup(name="overview", label=tk._("Overview"), icon="fa-solid fa-bolt-lightning")
43+
GENERAL_GROUP = MetricGroup(name="general", label=tk._("General"), icon="fa-solid fa-chart-bar")

ckanext/better_stats/metrics/base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC, abstractmethod
44
from collections.abc import Callable
5-
from typing import Any, ClassVar, cast
5+
from typing import Any, ClassVar
66

77
import ckan.plugins.toolkit as tk
88

@@ -41,6 +41,7 @@ class attributes alongside any visualization methods they support.
4141
icon: ClassVar[str] = "fa-solid fa-chart-bar"
4242
supported_export_formats: ClassVar[list[str]] = ["csv", "json", "xlsx", "image"]
4343
scope: ClassVar[const.MetricScope] = const.MetricScope.GLOBAL
44+
group: ClassVar[const.MetricGroup] = const.GENERAL_GROUP
4445

4546
def __init__( # noqa: PLR0913
4647
self,
@@ -199,6 +200,7 @@ def to_dict(self) -> dict[str, Any]:
199200
"title": self.title,
200201
"description": self.description,
201202
"icon": self.icon,
203+
"group": {"name": self.group.name, "label": self.group.label, "icon": self.group.icon},
202204
"col_span": self.col_span,
203205
"row_span": self.row_span,
204206
"order": self.order,
@@ -270,11 +272,11 @@ def get_metric(cls, name: str) -> MetricBase | None:
270272
if not cfg.enabled:
271273
return None
272274

273-
metric.order = cast(int, cfg.order)
274-
metric.col_span = cast(int, cfg.col_span)
275-
metric.row_span = cast(int, cfg.row_span)
276-
metric.cache_timeout = cast(int, cfg.cache_timeout)
277-
metric.access_level = str(cfg.access_level or metric.access_level)
275+
metric.order = cfg.order
276+
metric.col_span = cfg.col_span
277+
metric.row_span = cfg.row_span
278+
metric.cache_timeout = cfg.cache_timeout
279+
metric.access_level = cfg.access_level or metric.access_level
278280

279281
return metric
280282

ckanext/better_stats/metrics/dataset_metrics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class DatasetCountMetric(MetricBase):
2727
icon: ClassVar[str] = "fa-solid fa-database"
2828
supported_export_formats = ["csv", "xlsx"]
2929
scope: ClassVar[const.MetricScope] = const.MetricScope.USER
30+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
3031

3132
def __init__(self) -> None:
3233
super().__init__(
@@ -60,6 +61,7 @@ class DatasetsByOrganizationMetric(MetricBase):
6061
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
6162
icon: ClassVar[str] = "fa-solid fa-building"
6263
scope: ClassVar[const.MetricScope] = const.MetricScope.USER
64+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
6365

6466
def __init__(self) -> None:
6567
super().__init__(
@@ -119,6 +121,7 @@ class DatasetCreationHistoryMetric(MetricBase):
119121
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
120122
icon: ClassVar[str] = "fa-solid fa-calendar-days"
121123
scope: ClassVar[const.MetricScope] = const.MetricScope.USER
124+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
122125

123126
def __init__(self) -> None:
124127
super().__init__(
@@ -202,6 +205,7 @@ class ResourcesByFormatMetric(MetricBase):
202205
]
203206
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
204207
icon: ClassVar[str] = "fa-solid fa-file-code"
208+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
205209

206210
def __init__(self) -> None:
207211
super().__init__(
@@ -265,6 +269,7 @@ class TopTagsMetric(MetricBase):
265269
]
266270
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
267271
icon: ClassVar[str] = "fa-solid fa-tags"
272+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
268273

269274
def __init__(self) -> None:
270275
super().__init__(
@@ -324,6 +329,7 @@ class DatasetsWithoutResourcesMetric(MetricBase):
324329
]
325330
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.TABLE
326331
icon: ClassVar[str] = "fa-solid fa-file-circle-xmark"
332+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
327333

328334
def __init__(self) -> None:
329335
super().__init__(
@@ -404,6 +410,7 @@ class StaleDatasetsMetric(MetricBase):
404410
]
405411
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.TABLE
406412
icon: ClassVar[str] = "fa-solid fa-hourglass-end"
413+
group: ClassVar[const.MetricGroup] = const.DATASETS_GROUP
407414

408415
def __init__(self) -> None:
409416
super().__init__(

ckanext/better_stats/metrics/organization_metrics.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class OrganizationHierarchyMetric(MetricBase):
2323
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
2424
icon: ClassVar[str] = "fa-solid fa-sitemap"
2525
supported_export_formats: ClassVar[list[str]] = ["image"]
26+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
2627

2728
def __init__(self) -> None:
2829
super().__init__(
@@ -121,6 +122,7 @@ class OrganizationCountMetric(MetricBase):
121122
]
122123
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CARD
123124
icon: ClassVar[str] = "fa-solid fa-sitemap"
125+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
124126

125127
def __init__(self) -> None:
126128
super().__init__(
@@ -198,6 +200,7 @@ class OrganizationMembershipMetric(MetricBase):
198200
]
199201
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
200202
icon: ClassVar[str] = "fa-solid fa-user-group"
203+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
201204

202205
def __init__(self) -> None:
203206
super().__init__(
@@ -282,6 +285,7 @@ class OrganizationOverviewMetric(MetricBase):
282285
]
283286
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.TABLE
284287
icon: ClassVar[str] = "fa-solid fa-table-list"
288+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
285289

286290
def __init__(self) -> None:
287291
super().__init__(
@@ -405,6 +409,7 @@ class InactiveOrganizationsMetric(MetricBase):
405409
]
406410
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.TABLE
407411
icon: ClassVar[str] = "fa-solid fa-building-circle-xmark"
412+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
408413

409414
def __init__(self) -> None:
410415
super().__init__(
@@ -468,6 +473,7 @@ class OrganizationSizesMetric(MetricBase):
468473
]
469474
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
470475
icon: ClassVar[str] = "fa-solid fa-cubes"
476+
group: ClassVar[const.MetricGroup] = const.ORGANIZATIONS_GROUP
471477

472478
def __init__(self) -> None:
473479
super().__init__(

ckanext/better_stats/metrics/portal_metrics.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class UserCountMetric(MetricBase):
2121
]
2222
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CARD
2323
icon: ClassVar[str] = "fa-solid fa-users"
24+
group: ClassVar[const.MetricGroup] = const.OVERVIEW_GROUP
2425

2526
def __init__(self) -> None:
2627
super().__init__(
@@ -82,6 +83,7 @@ class DatasetCompletenessMetric(MetricBase):
8283
]
8384
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.PROGRESS
8485
icon: ClassVar[str] = "fa-solid fa-circle-check"
86+
group: ClassVar[const.MetricGroup] = const.OVERVIEW_GROUP
8587

8688
def __init__(self) -> None:
8789
super().__init__(

ckanext/better_stats/metrics/system_metrics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class MemoryMetric(MetricBase):
2020
]
2121
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.PROGRESS
2222
icon: ClassVar[str] = "fa-solid fa-memory"
23+
group: ClassVar[const.MetricGroup] = const.OVERVIEW_GROUP
2324

2425
def __init__(self) -> None:
2526
super().__init__(
@@ -90,6 +91,7 @@ class CPUMetric(MetricBase):
9091
]
9192
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.PROGRESS
9293
icon: ClassVar[str] = "fa-solid fa-microchip"
94+
group: ClassVar[const.MetricGroup] = const.OVERVIEW_GROUP
9395

9496
def __init__(self) -> None:
9597
super().__init__(
@@ -147,6 +149,7 @@ class DiskUsageMetric(MetricBase):
147149
]
148150
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.PROGRESS
149151
icon: ClassVar[str] = "fa-solid fa-hard-drive"
152+
group: ClassVar[const.MetricGroup] = const.OVERVIEW_GROUP
150153

151154
def __init__(self) -> None:
152155
super().__init__(

ckanext/better_stats/model.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import uuid
22
from datetime import datetime, timezone
3+
from typing import Any
34

4-
from sqlalchemy import Boolean, Column, DateTime, Integer, String
5+
import sqlalchemy as sa
56
from sqlalchemy.dialects.postgresql import JSONB
67
from sqlalchemy.ext.mutable import MutableDict
8+
from sqlalchemy.orm import Mapped
79

810
import ckan.plugins.toolkit as tk
911
from ckan import model
@@ -14,19 +16,33 @@ def _current_datetime():
1416

1517

1618
class MetricConfig(tk.BaseModel):
17-
__tablename__ = "better_stats_metric_config"
18-
19-
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
20-
metric_name = Column(String(100), unique=True, nullable=False, index=True)
21-
enabled = Column(Boolean, default=True, nullable=False)
22-
order = Column(Integer, default=100)
23-
col_span = Column(Integer, default=3)
24-
row_span = Column(Integer, default=1)
25-
access_level = Column(String(20))
26-
cache_timeout = Column(Integer, default=3600)
27-
extras = Column(MutableDict.as_mutable(JSONB), default={})
28-
created = Column(DateTime, default=_current_datetime)
29-
modified = Column(DateTime, default=_current_datetime, onupdate=_current_datetime)
19+
__table__ = sa.Table(
20+
"better_stats_metric_config",
21+
tk.BaseModel.metadata,
22+
sa.Column("id", sa.String, primary_key=True, default=lambda: str(uuid.uuid4())),
23+
sa.Column("metric_name", sa.String, nullable=False, index=True),
24+
sa.Column("enabled", sa.Boolean, default=True, nullable=False),
25+
sa.Column("order", sa.Integer, default=100),
26+
sa.Column("col_span", sa.Integer, default=3),
27+
sa.Column("row_span", sa.Integer, default=1),
28+
sa.Column("access_level", sa.String(20)),
29+
sa.Column("cache_timeout", sa.Integer, default=3600),
30+
sa.Column("extras", MutableDict.as_mutable(JSONB), default={}),
31+
sa.Column("created", sa.DateTime, default=_current_datetime),
32+
sa.Column("modified", sa.DateTime, default=_current_datetime, onupdate=_current_datetime),
33+
)
34+
35+
id: Mapped[str]
36+
metric_name: Mapped[str]
37+
enabled: Mapped[bool]
38+
order: Mapped[int]
39+
col_span: Mapped[int]
40+
row_span: Mapped[int]
41+
access_level: Mapped[str]
42+
cache_timeout: Mapped[int]
43+
extras: Mapped[dict[str, Any]]
44+
created: Mapped[datetime]
45+
modified: Mapped[datetime]
3046

3147
@classmethod
3248
def for_metric(cls, metric_name: str) -> "MetricConfig | None":

0 commit comments

Comments
 (0)