Public Access
feat(settings): show the build version of each service
This commit is contained in:
1 parent
9d1c7bb40d
commit
cfceca5e5e
11 files changed
+213
-3
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Info} from 'lucide-react';
|
||||
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
import {commitUrl, fetchVersion, formatBuildTime, FRONTEND_VERSION, VERSION_SOURCES} from '../utils/version.js';
|
||||
|
||||
const SHORT_HASH = 7;
|
||||
|
||||
function VersionValue({repo, version}) {
|
||||
const {t, i18n} = useTranslation();
|
||||
if (version === undefined) return <span className="text-muted-foreground">…</span>;
|
||||
if (version === null) return <span className="text-muted-foreground">{t('SETTINGS.VERSION_UNKNOWN')}</span>;
|
||||
|
||||
const url = commitUrl(repo, version.commit);
|
||||
const builtAt = formatBuildTime(version.builtAt, i18n.language);
|
||||
return (
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{url
|
||||
? <a href={url} target="_blank" rel="noopener noreferrer"
|
||||
className="font-mono text-primary hover:underline">{version.commit.slice(0, SHORT_HASH)}</a>
|
||||
: <span className="font-mono">{version.commit}</span>}
|
||||
{builtAt && <span className="truncate text-muted-foreground">· {builtAt}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A backend version is undefined while loading and null when it is unknown.
|
||||
*/
|
||||
export default function BuildVersionCard() {
|
||||
const {t} = useTranslation();
|
||||
const [versions, setVersions] = useState({Frontend: FRONTEND_VERSION});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
VERSION_SOURCES.filter((source) => source.url).forEach(async (source) => {
|
||||
const version = await fetchVersion(source.url());
|
||||
if (!cancelled) setVersions((current) => ({...current, [source.name]: version}));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><Info className="size-4 text-primary"/>{t('SETTINGS.VERSION')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{VERSION_SOURCES.map(({name, repo}) => (
|
||||
<li key={name} className="flex items-center justify-between gap-4">
|
||||
<span>{name}</span>
|
||||
<VersionValue repo={repo} version={versions[name]}/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, render, screen, within} from '@testing-library/react';
|
||||
import BuildVersionCard from './BuildVersionCard.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
// vite.config.js reads GIT_SHA from the shell; the test must not depend on it.
|
||||
vi.mock('../utils/version.js', async (importOriginal) => ({
|
||||
...await importOriginal(),
|
||||
FRONTEND_VERSION: {commit: 'dev', builtAt: null},
|
||||
}));
|
||||
|
||||
const AUTH_SHA = '4ec47f1aa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5';
|
||||
|
||||
const row = (name) => screen.getByText(name).closest('li');
|
||||
|
||||
describe('BuildVersionCard', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('links the short hash of each backend and marks a service that does not answer', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => (url.startsWith('http://auth')
|
||||
? {ok: true, json: async () => ({commit: AUTH_SHA, builtAt: '2026-09-27T08:05:00Z'})}
|
||||
: {ok: false, status: 502}));
|
||||
vi.stubEnv('VITE_API_URL', 'http://auth');
|
||||
vi.stubEnv('VITE_CORE_API_URL', 'http://core');
|
||||
|
||||
render(<BuildVersionCard/>);
|
||||
|
||||
const link = await within(row('Auth')).findByRole('link', {name: '4ec47f1'});
|
||||
expect(link.getAttribute('href')).toBe(`https://git.andreas-dahm.eu/Lobo_Corp/ToBePlayed-Auth/commit/${AUTH_SHA}`);
|
||||
expect(await within(row('Core')).findByText('SETTINGS.VERSION_UNKNOWN')).toBeTruthy();
|
||||
expect(fetch).toHaveBeenCalledWith('http://auth/api/version');
|
||||
expect(fetch).toHaveBeenCalledWith('http://core/api/version');
|
||||
});
|
||||
|
||||
it('shows a local build as dev without a link', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({ok: true, json: async () => ({commit: 'dev', builtAt: null})});
|
||||
|
||||
render(<BuildVersionCard/>);
|
||||
|
||||
expect(within(row('Frontend')).getByText('dev')).toBeTruthy();
|
||||
expect(await within(row('Core')).findByText('dev')).toBeTruthy();
|
||||
expect(screen.queryByRole('link')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user