feat(admin): link version numbers in sidebar to GitHub release notes (#566)
Closes #566. The admin sidebar showed the running frontend + backend versions as plain text. Wraps each version (and the "update available" indicator) in an anchor pointing at the corresponding GitHub release tag, opening in a new tab so the admin session isn't disrupted. A small githubReleaseUrl helper (extracted to its own module for testability) does the version → URL mapping. Because release-please tags every release as `vX.Y.Z[-beta.N]`, the version string already carries the channel suffix and a pure template covers both stable and beta without branching. Three unit tests pin the URL template — stable, beta-with-suffix, and a defensive check that the leading `v` isn't double-prefixed if a caller accidentally passes a tag-shaped value.
This commit is contained in:
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Info, ArrowUpCircle } from 'lucide-react';
|
import { Info, ArrowUpCircle } from 'lucide-react';
|
||||||
import { api } from '../../config/api';
|
import { api } from '../../config/api';
|
||||||
|
import { githubReleaseUrl as releaseUrl } from '../../utils/githubReleaseUrl';
|
||||||
import packageJson from '../../../package.json';
|
import packageJson from '../../../package.json';
|
||||||
|
|
||||||
// Frontend version from package.json
|
// Frontend version from package.json
|
||||||
@@ -63,20 +64,48 @@ export const VersionInfo: React.FC = () => {
|
|||||||
{channelBadge}
|
{channelBadge}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 space-y-0.5 text-xs text-neutral-500">
|
<div className="mt-1 space-y-0.5 text-xs text-neutral-500">
|
||||||
<div>Frontend: v{FRONTEND_VERSION}</div>
|
<div>
|
||||||
|
Frontend:{' '}
|
||||||
|
<a
|
||||||
|
href={releaseUrl(FRONTEND_VERSION)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-neutral-500 hover:text-neutral-700 hover:underline"
|
||||||
|
title={t('admin.viewReleaseNotes', 'View release notes on GitHub')}
|
||||||
|
>
|
||||||
|
v{FRONTEND_VERSION}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{versionInfo && (
|
{versionInfo && (
|
||||||
<div>Backend: v{versionInfo.backend}</div>
|
<div>
|
||||||
|
Backend:{' '}
|
||||||
|
<a
|
||||||
|
href={releaseUrl(versionInfo.backend)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-neutral-500 hover:text-neutral-700 hover:underline"
|
||||||
|
title={t('admin.viewReleaseNotes', 'View release notes on GitHub')}
|
||||||
|
>
|
||||||
|
v{versionInfo.backend}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{updateInfo?.enabled && updateInfo?.updateAvailable && (
|
{updateInfo?.enabled && updateInfo?.updateAvailable && updateInfo.latest && (
|
||||||
<div className="mt-2 flex items-center gap-1 text-xs text-blue-600">
|
<a
|
||||||
|
href={releaseUrl(updateInfo.latest.forChannel)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-2 flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
title={t('admin.viewReleaseNotes', 'View release notes on GitHub')}
|
||||||
|
>
|
||||||
<ArrowUpCircle className="w-3 h-3" />
|
<ArrowUpCircle className="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{t('admin.updates.updateAvailableShort', 'v{{version}} available', {
|
{t('admin.updates.updateAvailableShort', 'v{{version}} available', {
|
||||||
version: updateInfo.latest?.forChannel
|
version: updateInfo.latest.forChannel
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Regression coverage for the version → release-page URL template (#566).
|
||||||
|
*
|
||||||
|
* release-please tags every release as `vX.Y.Z[-beta.N]`. If anyone
|
||||||
|
* later refactors the helper (e.g. switches the prefix, the repo path,
|
||||||
|
* or strips the channel suffix), these assertions will fail loud
|
||||||
|
* instead of silently producing dead links in the admin sidebar.
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { githubReleaseUrl } from '../githubReleaseUrl';
|
||||||
|
|
||||||
|
describe('githubReleaseUrl', () => {
|
||||||
|
it('maps a stable version to its tag page', () => {
|
||||||
|
expect(githubReleaseUrl('3.55.0')).toBe(
|
||||||
|
'https://github.com/the-luap/picpeak/releases/tag/v3.55.0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps a beta version (with channel suffix) to its tag page', () => {
|
||||||
|
expect(githubReleaseUrl('3.55.0-beta.0')).toBe(
|
||||||
|
'https://github.com/the-luap/picpeak/releases/tag/v3.55.0-beta.0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not double-prefix the v when given a bare version', () => {
|
||||||
|
// Defensive — the helper is the single source of truth for the
|
||||||
|
// leading "v". Callers must pass the bare version string.
|
||||||
|
expect(githubReleaseUrl('3.55.0')).not.toContain('vv');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Build the GitHub release page URL for a given version string.
|
||||||
|
*
|
||||||
|
* Version strings already carry their channel suffix (e.g. `3.55.0`
|
||||||
|
* for stable, `3.55.0-beta.0` for beta). release-please tags every
|
||||||
|
* release as `vX.Y.Z[-beta.N]`, so a pure string template covers
|
||||||
|
* both channels without branching on the suffix.
|
||||||
|
*
|
||||||
|
* Used by the admin sidebar version display to deep-link to release
|
||||||
|
* notes for the running version (#566) and by the update-available
|
||||||
|
* indicator to link to the upgrade target's notes.
|
||||||
|
*/
|
||||||
|
export const githubReleaseUrl = (version: string): string =>
|
||||||
|
`https://github.com/the-luap/picpeak/releases/tag/v${version}`;
|
||||||
Reference in New Issue
Block a user