Skip to content

Commit 9bea956

Browse files
Merge pull request #87 from ioBroker/feature/60-statistics-improvement
2 parents d0bb7c3 + 6a96909 commit 9bea956

File tree

13 files changed

+334
-109
lines changed

13 files changed

+334
-109
lines changed

express/backend/src/api/adapter.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,56 @@
11
import { Router } from "express";
22
import { dbConnect, unescapeObjectKeys } from "../db/utils";
3-
import { AdapterStats } from "../global/adapter-stats";
3+
import { AdapterStats, AdapterVersions } from "../global/adapter-stats";
44
import { Statistics } from "../global/iobroker";
55

66
const router = Router();
77

8-
router.get("/api/adapter/:name/stats", async function (req, res) {
8+
router.get("/api/adapter/:name/stats/now", async function (req, res) {
99
try {
1010
const { name } = req.params;
11+
if (!isValidAdapterName(name)) {
12+
res.status(404).send("Adapter not found");
13+
return;
14+
}
15+
16+
const db = await dbConnect();
17+
const rawStatistics = db.rawStatistics();
18+
19+
const stats = await rawStatistics
20+
.find()
21+
.project<Statistics>({
22+
adapters: { [name]: 1 },
23+
versions: { [name]: 1 },
24+
date: 1,
25+
_id: 0,
26+
})
27+
.sort({ date: -1 })
28+
.limit(1)
29+
.toArray();
30+
if (stats.length === 0) {
31+
res.status(404).send("Adapter not found");
32+
return;
33+
}
34+
35+
const stat = unescapeObjectKeys(stats[0]);
36+
const versions: AdapterVersions = {
37+
total: stat.adapters[name] ?? 0,
38+
versions: stat.versions[name] ?? {},
39+
};
40+
res.send(versions);
41+
} catch (error: any) {
42+
console.error(error);
43+
res.status(500).send("An unexpected error occurred");
44+
}
45+
});
46+
47+
router.get("/api/adapter/:name/stats/history", async function (req, res) {
48+
try {
49+
const { name } = req.params;
50+
if (!isValidAdapterName(name)) {
51+
res.status(404).send("Adapter not found");
52+
return;
53+
}
1154
const db = await dbConnect();
1255
const rawStatistics = db.rawStatistics();
1356
const repoAdapters = db.repoAdapters();
@@ -62,15 +105,25 @@ router.get("/api/adapter/:name/stats", async function (req, res) {
62105

63106
console.log(result);
64107
if (Object.keys(result.counts).length === 0) {
65-
res.status(404).send(`Adapter ${name} not found`);
108+
res.status(404).send("Adapter not found");
66109
return;
67110
}
68111

69112
res.send(result);
70113
} catch (error: any) {
71114
console.error(error);
72-
res.status(500).send(error.message || error);
115+
res.status(500).send("An unexpected error occurred");
73116
}
74117
});
75118

119+
function isValidAdapterName(name: string) {
120+
const forbiddenChars = /[^a-z0-9\-_]/g;
121+
if (forbiddenChars.test(name)) {
122+
return false;
123+
}
124+
125+
// the name must start with a letter
126+
return /^[a-z]/.test(name);
127+
}
128+
76129
export default router;

express/backend/src/cron.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ async function collectRepos(): Promise<void> {
6464
]);
6565
const collection = db.repoAdapters();
6666

67-
// remove "stale" entries
68-
const { deletedCount } = await collection.deleteMany({
69-
source: { $exists: false },
70-
});
71-
console.log(`Deleted ${deletedCount || 0} stale entries`);
72-
7367
await Promise.all([
7468
addRepoAdapters(collection, latest, "latest"),
7569
addRepoAdapters(collection, stable, "stable"),

express/frontend/src/Root.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Flag } from "@mui/icons-material";
12
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
23
import MenuIcon from "@mui/icons-material/Menu";
34
import {
@@ -138,6 +139,17 @@ export function Root() {
138139
ioBroker Developer Portal
139140
</Link>
140141
</Typography>
142+
<Button
143+
color="inherit"
144+
variant="outlined"
145+
size="small"
146+
startIcon={<Flag />}
147+
href="https://github.com/ioBroker/dev-portal/issues"
148+
target="_blank"
149+
style={{ opacity: 0.3 }}
150+
>
151+
Report Issue
152+
</Button>
141153
{!user && (
142154
<Button
143155
color="inherit"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useUserContext } from "../../contexts/UserContext";
22
import { CardButton } from "../CardButton";
33

4-
export function LoginButton() {
4+
export function LoginButton({ variant }: { variant?: string }) {
55
const { login } = useUserContext();
6-
return <CardButton text="Login" onClick={login} />;
6+
return <CardButton text="Login" onClick={login} variant={variant} />;
77
}

express/frontend/src/contexts/UserContext.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,17 @@ export function useUserContext() {
2727
return context;
2828
}
2929

30+
export class UserTokenMissingError extends Error {
31+
constructor() {
32+
super("User token missing");
33+
}
34+
}
35+
3036
export function useUserToken() {
3137
const { user } = useUserContext();
3238
const token = user?.token;
3339
if (!token) {
34-
throw new Error("User token missing");
40+
throw new UserTokenMissingError();
3541
}
3642
return token;
3743
}

express/frontend/src/lib/gitHub.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ export class GitHubRepoComm {
113113
return result.data;
114114
}
115115

116+
public readonly getBranches = AsyncCache.of(async () => {
117+
const result = await this.request(
118+
"GET /repos/{owner}/{repo}/branches",
119+
{
120+
...this.baseOptions,
121+
per_page: 100,
122+
},
123+
);
124+
return result.data;
125+
});
126+
116127
public readonly getTags = AsyncCache.of(async () => {
117128
const result = await this.request("GET /repos/{owner}/{repo}/tags", {
118129
...this.baseOptions,

express/frontend/src/lib/ioBroker.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import axios from "axios";
2-
import { AdapterStats } from "../../../backend/src/global/adapter-stats";
2+
import {
3+
AdapterStats,
4+
AdapterVersions,
5+
} from "../../../backend/src/global/adapter-stats";
36
import {
47
AdapterRatings,
58
AllRatings,
@@ -180,9 +183,16 @@ export const getWeblateAdapterComponents = AsyncCache.of(async () => {
180183
return components;
181184
});
182185

183-
export async function getStatistics(adapterName: string) {
186+
export async function getCurrentVersions(adapterName: string) {
187+
const result = await axios.get<AdapterVersions>(
188+
getApiUrl(`adapter/${uc(adapterName)}/stats/now`),
189+
);
190+
return result.data;
191+
}
192+
193+
export async function getStatisticsHistory(adapterName: string) {
184194
const result = await axios.get<AdapterStats>(
185-
getApiUrl(`adapter/${uc(adapterName)}/stats`),
195+
getApiUrl(`adapter/${uc(adapterName)}/stats/history`),
186196
);
187197
return result.data;
188198
}
@@ -237,9 +247,12 @@ export interface CheckResults {
237247
errors: CheckResult[];
238248
}
239249

240-
export async function checkAdapter(repoName: string) {
241-
const { data } = await axios.get<CheckResults>(
242-
`${getApiUrl("repochecker/")}?url=${uc(`https://github.com/${repoName}`)}`,
243-
);
250+
export async function checkAdapter(repoName: string, branchName?: string) {
251+
const url = new URL(getApiUrl("repochecker/"), window.location.origin);
252+
url.searchParams.set("url", `https://github.com/${repoName}`);
253+
if (branchName) {
254+
url.searchParams.set("branch", branchName);
255+
}
256+
const { data } = await axios.get<CheckResults>(url.toString());
244257
return data;
245258
}

express/frontend/src/router.tsx

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { createBrowserRouter } from "react-router-dom";
1+
import { Paper, Typography } from "@mui/material";
2+
import { createBrowserRouter, useRouteError } from "react-router-dom";
23
import { App } from "./App";
34
import { Dashboard } from "./components/dashboard/Dashboard";
4-
import { UserProvider } from "./contexts/UserContext";
5+
import { LoginButton } from "./components/dashboard/LoginButton";
6+
import { UserProvider, UserTokenMissingError } from "./contexts/UserContext";
57
import { AdapterDashboard } from "./tools/adapter/AdapterDashboard";
68
import { AdapterDetails } from "./tools/adapter/AdapterDetails";
79
import { AdapterRatings } from "./tools/adapter/AdapterRatings";
8-
import { AdapterStatistics } from "./tools/adapter/AdapterStatistics";
910
import { CreateReleaseDialog } from "./tools/adapter/releases/CreateReleaseDialog";
1011
import { Releases } from "./tools/adapter/releases/Releases";
1112
import { UpdateRepositoriesDialog } from "./tools/adapter/releases/UpdateRepositoriesDialog";
13+
import { Statistics } from "./tools/adapter/statistics/Statistics";
1214
import { AdapterCheck } from "./tools/AdapterCheck";
1315
import { StartCreateAdapter } from "./tools/create-adapter/StartCreateAdapter";
1416
import { Wizard } from "./tools/create-adapter/Wizard";
@@ -24,66 +26,89 @@ export const router = createBrowserRouter([
2426

2527
children: [
2628
{
27-
path: "/create-adapter",
29+
path: "/",
30+
errorElement: <ErrorBoundary />,
2831
children: [
2932
{
30-
index: true,
31-
element: <StartCreateAdapter />,
33+
path: "/create-adapter",
34+
children: [
35+
{
36+
index: true,
37+
element: <StartCreateAdapter />,
38+
},
39+
{
40+
path: "wizard",
41+
element: <Wizard />,
42+
},
43+
],
3244
},
3345
{
34-
path: "wizard",
35-
element: <Wizard />,
46+
path: "/adapter-check",
47+
element: <AdapterCheck />,
3648
},
37-
],
38-
},
39-
{
40-
path: "/adapter-check",
41-
element: <AdapterCheck />,
42-
},
43-
{
44-
path: "/adapter/:name",
45-
element: <AdapterDetails />,
46-
children: [
4749
{
48-
index: true,
49-
element: <AdapterDashboard />,
50-
},
51-
{
52-
path: "releases",
53-
element: <Releases />,
50+
path: "/adapter/:name",
51+
element: <AdapterDetails />,
5452
children: [
5553
{
56-
path: "~release",
57-
element: <CreateReleaseDialog />,
54+
index: true,
55+
element: <AdapterDashboard />,
5856
},
5957
{
60-
path: "~to-latest",
61-
element: (
62-
<UpdateRepositoriesDialog action="to-latest" />
63-
),
58+
path: "releases",
59+
element: <Releases />,
60+
children: [
61+
{
62+
path: "~release",
63+
element: <CreateReleaseDialog />,
64+
},
65+
{
66+
path: "~to-latest",
67+
element: (
68+
<UpdateRepositoriesDialog action="to-latest" />
69+
),
70+
},
71+
{
72+
path: "~to-stable/:version",
73+
element: (
74+
<UpdateRepositoriesDialog action="to-stable" />
75+
),
76+
},
77+
],
6478
},
6579
{
66-
path: "~to-stable/:version",
67-
element: (
68-
<UpdateRepositoriesDialog action="to-stable" />
69-
),
80+
path: "statistics",
81+
element: <Statistics />,
82+
},
83+
{
84+
path: "ratings",
85+
element: <AdapterRatings />,
7086
},
7187
],
7288
},
7389
{
74-
path: "statistics",
75-
element: <AdapterStatistics />,
76-
},
77-
{
78-
path: "ratings",
79-
element: <AdapterRatings />,
90+
index: true,
91+
element: <Dashboard />,
8092
},
8193
],
8294
},
83-
{
84-
index: true,
85-
element: <Dashboard />,
86-
},
8795
],
8896
},
8997
]);
98+
99+
function ErrorBoundary() {
100+
let error = useRouteError();
101+
if (error instanceof UserTokenMissingError) {
102+
return (
103+
<Paper sx={{ padding: 2 }}>
104+
<Typography variant="h4">Not logged in</Typography>
105+
<p>You need to be logged in to access this page.</p>
106+
<p>
107+
<LoginButton variant="contained" />
108+
</p>
109+
</Paper>
110+
);
111+
}
112+
113+
throw error;
114+
}

0 commit comments

Comments
 (0)