Skip to content

Commit

Permalink
Merge pull request #87 from ioBroker/feature/60-statistics-improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleSamSwiss authored Jan 21, 2025
2 parents d0bb7c3 + 6a96909 commit 9bea956
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 109 deletions.
61 changes: 57 additions & 4 deletions express/backend/src/api/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,56 @@
import { Router } from "express";
import { dbConnect, unescapeObjectKeys } from "../db/utils";
import { AdapterStats } from "../global/adapter-stats";
import { AdapterStats, AdapterVersions } from "../global/adapter-stats";
import { Statistics } from "../global/iobroker";

const router = Router();

router.get("/api/adapter/:name/stats", async function (req, res) {
router.get("/api/adapter/:name/stats/now", async function (req, res) {
try {
const { name } = req.params;
if (!isValidAdapterName(name)) {
res.status(404).send("Adapter not found");
return;
}

const db = await dbConnect();
const rawStatistics = db.rawStatistics();

const stats = await rawStatistics
.find()
.project<Statistics>({
adapters: { [name]: 1 },
versions: { [name]: 1 },
date: 1,
_id: 0,
})
.sort({ date: -1 })
.limit(1)
.toArray();
if (stats.length === 0) {
res.status(404).send("Adapter not found");
return;
}

const stat = unescapeObjectKeys(stats[0]);
const versions: AdapterVersions = {
total: stat.adapters[name] ?? 0,
versions: stat.versions[name] ?? {},
};
res.send(versions);
} catch (error: any) {
console.error(error);
res.status(500).send("An unexpected error occurred");
}
});

router.get("/api/adapter/:name/stats/history", async function (req, res) {
try {
const { name } = req.params;
if (!isValidAdapterName(name)) {
res.status(404).send("Adapter not found");
return;
}
const db = await dbConnect();
const rawStatistics = db.rawStatistics();
const repoAdapters = db.repoAdapters();
Expand Down Expand Up @@ -62,15 +105,25 @@ router.get("/api/adapter/:name/stats", async function (req, res) {

console.log(result);
if (Object.keys(result.counts).length === 0) {
res.status(404).send(`Adapter ${name} not found`);
res.status(404).send("Adapter not found");
return;
}

res.send(result);
} catch (error: any) {
console.error(error);
res.status(500).send(error.message || error);
res.status(500).send("An unexpected error occurred");
}
});

function isValidAdapterName(name: string) {
const forbiddenChars = /[^a-z0-9\-_]/g;
if (forbiddenChars.test(name)) {
return false;
}

// the name must start with a letter
return /^[a-z]/.test(name);
}

export default router;
6 changes: 0 additions & 6 deletions express/backend/src/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,6 @@ async function collectRepos(): Promise<void> {
]);
const collection = db.repoAdapters();

// remove "stale" entries
const { deletedCount } = await collection.deleteMany({
source: { $exists: false },
});
console.log(`Deleted ${deletedCount || 0} stale entries`);

await Promise.all([
addRepoAdapters(collection, latest, "latest"),
addRepoAdapters(collection, stable, "stable"),
Expand Down
12 changes: 12 additions & 0 deletions express/frontend/src/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Flag } from "@mui/icons-material";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import MenuIcon from "@mui/icons-material/Menu";
import {
Expand Down Expand Up @@ -138,6 +139,17 @@ export function Root() {
ioBroker Developer Portal
</Link>
</Typography>
<Button
color="inherit"
variant="outlined"
size="small"
startIcon={<Flag />}
href="https://github.com/ioBroker/dev-portal/issues"
target="_blank"
style={{ opacity: 0.3 }}
>
Report Issue
</Button>
{!user && (
<Button
color="inherit"
Expand Down
4 changes: 2 additions & 2 deletions express/frontend/src/components/dashboard/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useUserContext } from "../../contexts/UserContext";
import { CardButton } from "../CardButton";

export function LoginButton() {
export function LoginButton({ variant }: { variant?: string }) {
const { login } = useUserContext();
return <CardButton text="Login" onClick={login} />;
return <CardButton text="Login" onClick={login} variant={variant} />;
}
8 changes: 7 additions & 1 deletion express/frontend/src/contexts/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ export function useUserContext() {
return context;
}

export class UserTokenMissingError extends Error {
constructor() {
super("User token missing");
}
}

export function useUserToken() {
const { user } = useUserContext();
const token = user?.token;
if (!token) {
throw new Error("User token missing");
throw new UserTokenMissingError();
}
return token;
}
Expand Down
11 changes: 11 additions & 0 deletions express/frontend/src/lib/gitHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ export class GitHubRepoComm {
return result.data;
}

public readonly getBranches = AsyncCache.of(async () => {
const result = await this.request(
"GET /repos/{owner}/{repo}/branches",
{
...this.baseOptions,
per_page: 100,
},
);
return result.data;
});

public readonly getTags = AsyncCache.of(async () => {
const result = await this.request("GET /repos/{owner}/{repo}/tags", {
...this.baseOptions,
Expand Down
27 changes: 20 additions & 7 deletions express/frontend/src/lib/ioBroker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import axios from "axios";
import { AdapterStats } from "../../../backend/src/global/adapter-stats";
import {
AdapterStats,
AdapterVersions,
} from "../../../backend/src/global/adapter-stats";
import {
AdapterRatings,
AllRatings,
Expand Down Expand Up @@ -180,9 +183,16 @@ export const getWeblateAdapterComponents = AsyncCache.of(async () => {
return components;
});

export async function getStatistics(adapterName: string) {
export async function getCurrentVersions(adapterName: string) {
const result = await axios.get<AdapterVersions>(
getApiUrl(`adapter/${uc(adapterName)}/stats/now`),
);
return result.data;
}

export async function getStatisticsHistory(adapterName: string) {
const result = await axios.get<AdapterStats>(
getApiUrl(`adapter/${uc(adapterName)}/stats`),
getApiUrl(`adapter/${uc(adapterName)}/stats/history`),
);
return result.data;
}
Expand Down Expand Up @@ -237,9 +247,12 @@ export interface CheckResults {
errors: CheckResult[];
}

export async function checkAdapter(repoName: string) {
const { data } = await axios.get<CheckResults>(
`${getApiUrl("repochecker/")}?url=${uc(`https://github.com/${repoName}`)}`,
);
export async function checkAdapter(repoName: string, branchName?: string) {
const url = new URL(getApiUrl("repochecker/"), window.location.origin);
url.searchParams.set("url", `https://github.com/${repoName}`);
if (branchName) {
url.searchParams.set("branch", branchName);
}
const { data } = await axios.get<CheckResults>(url.toString());
return data;
}
113 changes: 69 additions & 44 deletions express/frontend/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { createBrowserRouter } from "react-router-dom";
import { Paper, Typography } from "@mui/material";
import { createBrowserRouter, useRouteError } from "react-router-dom";
import { App } from "./App";
import { Dashboard } from "./components/dashboard/Dashboard";
import { UserProvider } from "./contexts/UserContext";
import { LoginButton } from "./components/dashboard/LoginButton";
import { UserProvider, UserTokenMissingError } from "./contexts/UserContext";
import { AdapterDashboard } from "./tools/adapter/AdapterDashboard";
import { AdapterDetails } from "./tools/adapter/AdapterDetails";
import { AdapterRatings } from "./tools/adapter/AdapterRatings";
import { AdapterStatistics } from "./tools/adapter/AdapterStatistics";
import { CreateReleaseDialog } from "./tools/adapter/releases/CreateReleaseDialog";
import { Releases } from "./tools/adapter/releases/Releases";
import { UpdateRepositoriesDialog } from "./tools/adapter/releases/UpdateRepositoriesDialog";
import { Statistics } from "./tools/adapter/statistics/Statistics";
import { AdapterCheck } from "./tools/AdapterCheck";
import { StartCreateAdapter } from "./tools/create-adapter/StartCreateAdapter";
import { Wizard } from "./tools/create-adapter/Wizard";
Expand All @@ -24,66 +26,89 @@ export const router = createBrowserRouter([

children: [
{
path: "/create-adapter",
path: "/",
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <StartCreateAdapter />,
path: "/create-adapter",
children: [
{
index: true,
element: <StartCreateAdapter />,
},
{
path: "wizard",
element: <Wizard />,
},
],
},
{
path: "wizard",
element: <Wizard />,
path: "/adapter-check",
element: <AdapterCheck />,
},
],
},
{
path: "/adapter-check",
element: <AdapterCheck />,
},
{
path: "/adapter/:name",
element: <AdapterDetails />,
children: [
{
index: true,
element: <AdapterDashboard />,
},
{
path: "releases",
element: <Releases />,
path: "/adapter/:name",
element: <AdapterDetails />,
children: [
{
path: "~release",
element: <CreateReleaseDialog />,
index: true,
element: <AdapterDashboard />,
},
{
path: "~to-latest",
element: (
<UpdateRepositoriesDialog action="to-latest" />
),
path: "releases",
element: <Releases />,
children: [
{
path: "~release",
element: <CreateReleaseDialog />,
},
{
path: "~to-latest",
element: (
<UpdateRepositoriesDialog action="to-latest" />
),
},
{
path: "~to-stable/:version",
element: (
<UpdateRepositoriesDialog action="to-stable" />
),
},
],
},
{
path: "~to-stable/:version",
element: (
<UpdateRepositoriesDialog action="to-stable" />
),
path: "statistics",
element: <Statistics />,
},
{
path: "ratings",
element: <AdapterRatings />,
},
],
},
{
path: "statistics",
element: <AdapterStatistics />,
},
{
path: "ratings",
element: <AdapterRatings />,
index: true,
element: <Dashboard />,
},
],
},
{
index: true,
element: <Dashboard />,
},
],
},
]);

function ErrorBoundary() {
let error = useRouteError();
if (error instanceof UserTokenMissingError) {
return (
<Paper sx={{ padding: 2 }}>
<Typography variant="h4">Not logged in</Typography>
<p>You need to be logged in to access this page.</p>
<p>
<LoginButton variant="contained" />
</p>
</Paper>
);
}

throw error;
}
Loading

0 comments on commit 9bea956

Please sign in to comment.