mirror of
https://github.com/outline/outline.git
synced 2026-04-24 11:20:11 -05:00
Feat/installation info endpoint (#7744)
* feat: add installation.info endpoint using DockerHub API * feat: UI use an server-side API to show version info * fix: review fixes * test: installation.info endpoint * feat: filtering pre-releases in installation.info endpoint * fix: change fetch to ApiClient usage for getting version info * Undo translation change --------- Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
committed by
GitHub
parent
fe33871dfe
commit
2e1a827157
@@ -2,41 +2,29 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Badge from "~/components/Badge";
|
||||
import { version } from "../../../../package.json";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Logger from "~/utils/Logger";
|
||||
import { version as currentVersion } from "../../../../package.json";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
|
||||
export default function Version() {
|
||||
const [releasesBehind, setReleasesBehind] = React.useState(-1);
|
||||
const [versionsBehind, setVersionsBehind] = React.useState(-1);
|
||||
const { t } = useTranslation();
|
||||
|
||||
React.useEffect(() => {
|
||||
async function loadReleases() {
|
||||
const res = await fetch(
|
||||
"https://api.github.com/repos/outline/outline/releases"
|
||||
);
|
||||
const releases = await res.json();
|
||||
|
||||
if (Array.isArray(releases)) {
|
||||
const everyNewRelease = releases
|
||||
.map((release) => release.tag_name)
|
||||
.findIndex((tagName) => tagName === `v${version}`);
|
||||
|
||||
const onlyFullNewRelease = releases
|
||||
.filter((release) => !release.prerelease)
|
||||
.map((release) => release.tag_name)
|
||||
.findIndex((tagName) => tagName === `v${version}`);
|
||||
|
||||
const computedReleasesBehind = version.includes("pre")
|
||||
? everyNewRelease
|
||||
: onlyFullNewRelease;
|
||||
|
||||
if (computedReleasesBehind >= 0) {
|
||||
setReleasesBehind(computedReleasesBehind);
|
||||
async function loadVersionInfo() {
|
||||
try {
|
||||
// Fetch version info from the server-side proxy
|
||||
const res = await client.post("/installation.info");
|
||||
if (res.data && res.data.versionsBehind >= 0) {
|
||||
setVersionsBehind(res.data.versionsBehind);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Failed to load version info", error);
|
||||
}
|
||||
}
|
||||
|
||||
void loadReleases();
|
||||
void loadVersionInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -45,16 +33,16 @@ export default function Version() {
|
||||
href="https://github.com/outline/outline/releases"
|
||||
label={
|
||||
<>
|
||||
v{version}
|
||||
{releasesBehind >= 0 && (
|
||||
v{currentVersion}
|
||||
{versionsBehind >= 0 && (
|
||||
<>
|
||||
<br />
|
||||
<LilBadge>
|
||||
{releasesBehind === 0
|
||||
{versionsBehind === 0
|
||||
? t("Up to date")
|
||||
: t(`{{ releasesBehind }} versions behind`, {
|
||||
releasesBehind,
|
||||
count: releasesBehind,
|
||||
releasesBehind: versionsBehind,
|
||||
count: versionsBehind,
|
||||
})}
|
||||
</LilBadge>
|
||||
</>
|
||||
|
||||
@@ -20,6 +20,7 @@ import events from "./events";
|
||||
import fileOperationsRoute from "./fileOperations";
|
||||
import groupMemberships from "./groupMemberships";
|
||||
import groups from "./groups";
|
||||
import installation from "./installation";
|
||||
import integrations from "./integrations";
|
||||
import apiResponse from "./middlewares/apiResponse";
|
||||
import apiTracer from "./middlewares/apiTracer";
|
||||
@@ -91,6 +92,10 @@ router.use("/", fileOperationsRoute.routes());
|
||||
router.use("/", urls.routes());
|
||||
router.use("/", userMemberships.routes());
|
||||
|
||||
if (!env.isCloudHosted) {
|
||||
router.use("/", installation.routes());
|
||||
}
|
||||
|
||||
if (env.isDevelopment) {
|
||||
router.use("/", developer.routes());
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./installation";
|
||||
@@ -0,0 +1,31 @@
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
|
||||
const server = getTestServer();
|
||||
|
||||
describe("installation.info", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/installation.info", {
|
||||
body: {},
|
||||
});
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it("should return installation information", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/installation.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).not.toBeFalsy();
|
||||
expect(body.data.version).not.toBeFalsy();
|
||||
expect(body.data.latestVersion).not.toBeFalsy();
|
||||
expect(typeof body.data.versionsBehind).toBe("number");
|
||||
expect(body.policies).not.toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import Router from "koa-router";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { APIContext } from "@server/types";
|
||||
import { getVersion, getVersionInfo } from "@server/utils/getInstallationInfo";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("installation.info", auth(), async (ctx: APIContext) => {
|
||||
const currentVersion = getVersion();
|
||||
const { latestVersion, versionsBehind } = await getVersionInfo(
|
||||
currentVersion
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
version: currentVersion,
|
||||
latestVersion,
|
||||
versionsBehind,
|
||||
},
|
||||
policies: [],
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { version } from "../../package.json";
|
||||
import fetch from "./fetch";
|
||||
|
||||
const dockerhubLink =
|
||||
"https://hub.docker.com/v2/repositories/outlinewiki/outline";
|
||||
|
||||
function isFullReleaseVersion(versionName: string): boolean {
|
||||
const releaseRegex = /^(version-)?\d+\.\d+\.\d+$/; // Matches "N.N.N" or "version-N.N.N" for dockerhub releases before v0.56.0"
|
||||
return releaseRegex.test(versionName);
|
||||
}
|
||||
|
||||
export async function getVersionInfo(currentVersion: string): Promise<{
|
||||
latestVersion: string;
|
||||
versionsBehind: number;
|
||||
}> {
|
||||
let allVersions: string[] = [];
|
||||
let latestVersion: string | null = null;
|
||||
let nextUrl: string | null =
|
||||
dockerhubLink + "/tags?name=&ordering=last_updated&page_size=100";
|
||||
|
||||
// Continue fetching pages until the required versions are found or no more pages
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl);
|
||||
const data = await response.json();
|
||||
|
||||
// Map and filter the versions to keep only full releases
|
||||
const pageVersions = data.results
|
||||
.map((result: any) => result.name)
|
||||
.filter(isFullReleaseVersion);
|
||||
|
||||
allVersions = allVersions.concat(pageVersions);
|
||||
|
||||
// Set the latest version if not already set
|
||||
if (!latestVersion && pageVersions.length > 0) {
|
||||
latestVersion = pageVersions[0];
|
||||
}
|
||||
|
||||
// Check if the current version is found
|
||||
const currentIndex = allVersions.findIndex(
|
||||
(version: string) => version === currentVersion
|
||||
);
|
||||
|
||||
if (currentIndex !== -1) {
|
||||
const versionsBehind = currentIndex; // The number of versions behind
|
||||
return {
|
||||
latestVersion: latestVersion || currentVersion, // Fallback to current if no latest found
|
||||
versionsBehind,
|
||||
};
|
||||
}
|
||||
|
||||
nextUrl = data.next || null;
|
||||
}
|
||||
|
||||
return {
|
||||
latestVersion: latestVersion || currentVersion,
|
||||
versionsBehind: -1, // Return -1 if current version is not found
|
||||
};
|
||||
}
|
||||
|
||||
export function getVersion(): string {
|
||||
return version;
|
||||
}
|
||||
Reference in New Issue
Block a user