mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-11 04:23:38 +00:00
Compare commits
4 Commits
2.20.0rc7
...
feature/pk
| Author | SHA1 | Date | |
|---|---|---|---|
| 17e4807fcf | |||
| 9012ee7144 | |||
| 945ddb2942 | |||
| 9cd0926588 |
@@ -425,6 +425,15 @@ The PKGBUILD class also provides some additional functions on top of that:
|
|||||||
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
||||||
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
||||||
|
|
||||||
|
HTTP client
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``ahriman.core.http`` package provides a HTTP client built on top of the ``requests`` library.
|
||||||
|
|
||||||
|
The base class ``ahriman.core.http.SyncHttpClient`` wraps ``requests.Session`` and provides common features for all HTTP interactions: configurable timeouts, retry policies with exponential backoff (using ``urllib3.util.retry.Retry``), basic authentication, custom User-Agent header, error processing, and ``make_request`` method. The session is lazily created (via ``cached_property``).
|
||||||
|
|
||||||
|
On top of that, ``ahriman.core.http.SyncAhrimanClient`` extends the base client for communication with the ahriman web service specifically. It adds automatic login on session creation (using configured credentials), ``X-Request-ID`` header injection and Unix socket transport support (via ``requests-unixsocket2``) if required.
|
||||||
|
|
||||||
Additional features
|
Additional features
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ahriman-frontend",
|
"name": "ahriman-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.20.0-rc7",
|
"version": "2.20.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -17,16 +17,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import AppLayout from "components/layout/AppLayout";
|
import AppLayout from "components/layout/AppLayout";
|
||||||
import { AuthProvider } from "contexts/AuthProvider";
|
import { AuthProvider } from "contexts/AuthProvider";
|
||||||
import { ClientProvider } from "contexts/ClientProvider";
|
import { ClientProvider } from "contexts/ClientProvider";
|
||||||
import { NotificationProvider } from "contexts/NotificationProvider";
|
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||||
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||||
|
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import Theme from "theme/Theme";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -39,8 +37,7 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
return <QueryClientProvider client={queryClient}>
|
return <QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={Theme}>
|
<ThemeProvider>
|
||||||
<CssBaseline />
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import { blue } from "@mui/material/colors";
|
||||||
import type { Event } from "models/Event";
|
import type { Event } from "models/Event";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
@@ -33,6 +34,8 @@ export default function EventDurationLineChart({ events }: EventDurationLineChar
|
|||||||
{
|
{
|
||||||
label: "update duration, s",
|
label: "update duration, s",
|
||||||
data: updateEvents.map(event => event.data?.took ?? 0),
|
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||||
|
borderColor: blue[500],
|
||||||
|
backgroundColor: blue[200],
|
||||||
cubicInterpolationMode: "monotone" as const,
|
cubicInterpolationMode: "monotone" as const,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function AutoRefreshControl({
|
|||||||
<Tooltip title="Auto-refresh">
|
<Tooltip title="Auto-refresh">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label="Auto-refresh"
|
||||||
onClick={event => setAnchorEl(event.currentTarget)}
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
color={enabled ? "primary" : "default"}
|
color={enabled ? "primary" : "default"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,47 +17,57 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import "components/common/syntaxLanguages";
|
||||||
|
|
||||||
|
import { Box, useTheme } from "@mui/material";
|
||||||
import CopyButton from "components/common/CopyButton";
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import React, { type RefObject } from "react";
|
import React, { type RefObject } from "react";
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { githubGist, vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
preRef?: RefObject<HTMLElement | null>;
|
content: string;
|
||||||
getText: () => string;
|
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
language?: string;
|
||||||
onScroll?: () => void;
|
onScroll?: () => void;
|
||||||
wordBreak?: boolean;
|
preRef?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlock({
|
export default function CodeBlock({
|
||||||
preRef,
|
content,
|
||||||
getText,
|
|
||||||
height,
|
height,
|
||||||
|
language = "text",
|
||||||
onScroll,
|
onScroll,
|
||||||
wordBreak,
|
preRef,
|
||||||
}: CodeBlockProps): React.JSX.Element {
|
}: CodeBlockProps): React.JSX.Element {
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return <Box sx={{ position: "relative" }}>
|
return <Box sx={{ position: "relative" }}>
|
||||||
<Box
|
<Box
|
||||||
ref={preRef}
|
ref={preRef}
|
||||||
component="pre"
|
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
sx={{
|
sx={{ overflow: "auto", height }}
|
||||||
backgroundColor: "grey.100",
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
overflow: "auto",
|
|
||||||
height,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<code>
|
<SyntaxHighlighter
|
||||||
{getText()}
|
language={language}
|
||||||
</code>
|
style={mode === "dark" ? vs2015 : githubGist}
|
||||||
</Box>
|
wrapLongLines
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
customStyle={{
|
||||||
<CopyButton getText={getText} />
|
padding: theme.spacing(2),
|
||||||
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
margin: 0,
|
||||||
|
minHeight: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
|
{content && <Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
|
<CopyButton text={content} />
|
||||||
|
</Box>}
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ import { IconButton, Tooltip } from "@mui/material";
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
getText: () => string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
|
export default function CopyButton({ text }: CopyButtonProps): React.JSX.Element {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
useEffect(() => () => clearTimeout(timer.current), []);
|
useEffect(() => () => clearTimeout(timer.current), []);
|
||||||
|
|
||||||
const handleCopy: () => Promise<void> = async () => {
|
const handleCopy: () => Promise<void> = async () => {
|
||||||
await navigator.clipboard.writeText(getText());
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
clearTimeout(timer.current);
|
clearTimeout(timer.current);
|
||||||
timer.current = setTimeout(() => setCopied(false), 2000);
|
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import type React from "react";
|
|||||||
export default function RepositorySelect({
|
export default function RepositorySelect({
|
||||||
repositorySelect,
|
repositorySelect,
|
||||||
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
|
|
||||||
return <FormControl fullWidth margin="normal">
|
return <FormControl fullWidth margin="normal">
|
||||||
<InputLabel>repository</InputLabel>
|
<InputLabel>repository</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
value={repositorySelect.selectedKey || (currentRepository?.key ?? "")}
|
||||||
label="repository"
|
label="repository"
|
||||||
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
||||||
>
|
>
|
||||||
|
|||||||
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash";
|
||||||
|
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
||||||
|
import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintext";
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage("bash", bash);
|
||||||
|
SyntaxHighlighter.registerLanguage("diff", diff);
|
||||||
|
SyntaxHighlighter.registerLanguage("text", plaintext);
|
||||||
@@ -36,11 +36,11 @@ interface DashboardDialogProps {
|
|||||||
|
|
||||||
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
|
|
||||||
const { data: status } = useQuery<InternalStatus>({
|
const { data: status } = useQuery<InternalStatus>({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
/>
|
/>
|
||||||
{keyBody &&
|
{keyBody &&
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<CodeBlock getText={() => keyBody} height={300} />
|
<CodeBlock content={keyBody} height={300} />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import EventsTab from "components/package/EventsTab";
|
|||||||
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||||
import PackageInfoActions from "components/package/PackageInfoActions";
|
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||||
import PackagePatchesList from "components/package/PackagePatchesList";
|
import PackagePatchesList from "components/package/PackagePatchesList";
|
||||||
|
import PkgbuildTab from "components/package/PkgbuildTab";
|
||||||
|
import { type TabKey, tabs } from "components/package/TabKey";
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
@@ -55,7 +57,7 @@ export default function PackageInfoDialog({
|
|||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
}: PackageInfoDialogProps): React.JSX.Element {
|
}: PackageInfoDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -65,11 +67,11 @@ export default function PackageInfoDialog({
|
|||||||
setLocalPackageBase(packageBase);
|
setLocalPackageBase(packageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setTabIndex(0);
|
setActiveTab("logs");
|
||||||
setRefreshDatabase(true);
|
setRefreshDatabase(true);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -77,16 +79,17 @@ export default function PackageInfoDialog({
|
|||||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.package(localPackageBase, currentRepository) : ["packages"],
|
||||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
queryFn: localPackageBase && currentRepository ?
|
||||||
|
() => client.fetch.fetchPackage(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: dependencies } = useQuery<Dependencies>({
|
const { data: dependencies } = useQuery<Dependencies>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.dependencies(localPackageBase, currentRepository) : ["dependencies"],
|
||||||
queryFn: localPackageBase && current
|
queryFn: localPackageBase && currentRepository ?
|
||||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
() => client.fetch.fetchPackageDependencies(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,11 +105,12 @@ export default function PackageInfoDialog({
|
|||||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||||
|
|
||||||
const handleUpdate: () => Promise<void> = async () => {
|
const handleUpdate: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
await client.service.servicePackageAdd(
|
||||||
|
currentRepository, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||||
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -114,11 +118,11 @@ export default function PackageInfoDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove: () => Promise<void> = async () => {
|
const handleRemove: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
await client.service.servicePackageRemove(currentRepository, [localPackageBase]);
|
||||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
@@ -156,25 +160,26 @@ export default function PackageInfoDialog({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
<Tabs value={activeTab} onChange={(_, tab: TabKey) => setActiveTab(tab)}>
|
||||||
<Tab label="Build logs" />
|
{tabs.map(({ key, label }) => <Tab key={key} value={key} label={label} />)}
|
||||||
<Tab label="Changes" />
|
|
||||||
<Tab label="Events" />
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{tabIndex === 0 && localPackageBase && current &&
|
{activeTab === "logs" && localPackageBase && currentRepository &&
|
||||||
<BuildLogsTab
|
<BuildLogsTab
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={current}
|
repository={currentRepository}
|
||||||
refreshInterval={autoRefresh.interval}
|
refreshInterval={autoRefresh.interval}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{tabIndex === 1 && localPackageBase && current &&
|
{activeTab === "changes" && localPackageBase && currentRepository &&
|
||||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{tabIndex === 2 && localPackageBase && current &&
|
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
||||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
|
}
|
||||||
|
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||||
|
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box, Container } from "@mui/material";
|
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||||
|
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||||
|
import { Box, Container, IconButton, Tooltip } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import LoginDialog from "components/dialogs/LoginDialog";
|
import LoginDialog from "components/dialogs/LoginDialog";
|
||||||
import Footer from "components/layout/Footer";
|
import Footer from "components/layout/Footer";
|
||||||
@@ -27,6 +29,7 @@ import { QueryKeys } from "hooks/QueryKeys";
|
|||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
import { useRepository } from "hooks/useRepository";
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import type { InfoResponse } from "models/InfoResponse";
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { setAuthState } = useAuth();
|
const { setAuthState } = useAuth();
|
||||||
const { setRepositories } = useRepository();
|
const { setRepositories } = useRepository();
|
||||||
|
const { mode, toggleTheme } = useThemeMode();
|
||||||
const [loginOpen, setLoginOpen] = useState(false);
|
const [loginOpen, setLoginOpen] = useState(false);
|
||||||
|
|
||||||
const { data: info } = useQuery<InfoResponse>({
|
const { data: info } = useQuery<InfoResponse>({
|
||||||
@@ -58,6 +62,11 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Tooltip title="Toggle theme">
|
||||||
|
<IconButton aria-label="Toggle theme" onClick={toggleTheme}>
|
||||||
|
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<PackageTable
|
<PackageTable
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ import { useRepository } from "hooks/useRepository";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export default function Navbar(): React.JSX.Element | null {
|
export default function Navbar(): React.JSX.Element | null {
|
||||||
const { repositories, current, setCurrent } = useRepository();
|
const { repositories, currentRepository, setCurrentRepository } = useRepository();
|
||||||
|
|
||||||
if (repositories.length === 0 || !current) {
|
if (repositories.length === 0 || !currentRepository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = repositories.findIndex(repository =>
|
const currentIndex = repositories.findIndex(repository =>
|
||||||
repository.architecture === current.architecture && repository.repository === current.repository,
|
repository.architecture === currentRepository.architecture &&
|
||||||
|
repository.repository === currentRepository.repository,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
@@ -38,7 +39,7 @@ export default function Navbar(): React.JSX.Element | null {
|
|||||||
onChange={(_, newValue: number) => {
|
onChange={(_, newValue: number) => {
|
||||||
const repository = repositories[newValue];
|
const repository = repositories[newValue];
|
||||||
if (repository) {
|
if (repository) {
|
||||||
setCurrent(repository);
|
setCurrentRepository(repository);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
|
|||||||
@@ -175,10 +175,9 @@ export default function BuildLogsTab({
|
|||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
preRef={preRef}
|
preRef={preRef}
|
||||||
getText={() => displayedLogs}
|
content={displayedLogs}
|
||||||
height={400}
|
height={400}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
wordBreak
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|||||||
@@ -17,19 +17,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
import CopyButton from "components/common/CopyButton";
|
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
|
||||||
import { useClient } from "hooks/useClient";
|
|
||||||
import type { Changes } from "models/Changes";
|
|
||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
|
||||||
import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("diff", diff);
|
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
@@ -37,34 +28,7 @@ interface ChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
const { data } = useQuery<Changes>({
|
return <CodeBlock language="diff" content={data?.changes ?? ""} height={400} />;
|
||||||
queryKey: QueryKeys.changes(packageBase, repository),
|
|
||||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
|
||||||
enabled: !!packageBase,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changesText = data?.changes ?? "";
|
|
||||||
|
|
||||||
return <Box sx={{ position: "relative", mt: 1 }}>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language="diff"
|
|
||||||
style={githubGist}
|
|
||||||
customStyle={{
|
|
||||||
padding: "16px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "auto",
|
|
||||||
height: 400,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{changesText}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
|
||||||
<CopyButton getText={() => changesText} />
|
|
||||||
</Box>
|
|
||||||
</Box>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function PackagePatchesList({
|
|||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
{editable &&
|
{editable &&
|
||||||
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
<IconButton size="small" color="error" aria-label="Remove patch" onClick={() => onDelete(patch.key)}>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|||||||
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PkgbuildTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PkgbuildTab({ packageBase, repository }: PkgbuildTabProps): React.JSX.Element {
|
||||||
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
|
return <CodeBlock language="bash" content={data?.pkgbuild ?? ""} height={400} />;
|
||||||
|
}
|
||||||
27
frontend/src/components/package/TabKey.ts
Normal file
27
frontend/src/components/package/TabKey.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
export type TabKey = "logs" | "changes" | "pkgbuild" | "events";
|
||||||
|
|
||||||
|
export const tabs: { key: TabKey; label: string }[] = [
|
||||||
|
{ key: "logs", label: "Build logs" },
|
||||||
|
{ key: "changes", label: "Changes" },
|
||||||
|
{ key: "pkgbuild", label: "PKGBUILD" },
|
||||||
|
{ key: "events", label: "Events" },
|
||||||
|
];
|
||||||
@@ -22,9 +22,9 @@ import { createContext } from "react";
|
|||||||
|
|
||||||
export interface RepositoryContextValue {
|
export interface RepositoryContextValue {
|
||||||
repositories: RepositoryId[];
|
repositories: RepositoryId[];
|
||||||
current: RepositoryId | null;
|
currentRepository: RepositoryId | null;
|
||||||
setRepositories: (repositories: RepositoryId[]) => void;
|
setRepositories: (repositories: RepositoryId[]) => void;
|
||||||
setCurrent: (repository: RepositoryId) => void;
|
setCurrentRepository: (repository: RepositoryId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ export function RepositoryProvider({ children }: { children: ReactNode }): React
|
|||||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||||
|
|
||||||
const current = useMemo(() => {
|
const currentRepository = useMemo(() => {
|
||||||
if (repositories.length === 0) {
|
if (repositories.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||||
}, [repositories, hash]);
|
}, [repositories, hash]);
|
||||||
|
|
||||||
const setCurrent = useCallback((repository: RepositoryId) => {
|
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||||
window.location.hash = repository.key;
|
window.location.hash = repository.key;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
repositories, current, setRepositories, setCurrent,
|
repositories, currentRepository, setRepositories, setCurrentRepository,
|
||||||
}), [repositories, current, setCurrent]);
|
}), [repositories, currentRepository, setCurrentRepository]);
|
||||||
|
|
||||||
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/src/contexts/ThemeContext.ts
Normal file
27
frontend/src/contexts/ThemeContext.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface ThemeContextValue {
|
||||||
|
mode: "light" | "dark";
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
|
||||||
|
import { defaults as chartDefaults } from "chart.js";
|
||||||
|
import { ThemeContext } from "contexts/ThemeContext";
|
||||||
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { createAppTheme } from "theme/Theme";
|
||||||
|
|
||||||
|
function systemPreference(): "light" | "dark" {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||||
|
const [mode, setMode] = useLocalStorage<"light" | "dark">("theme-mode", systemPreference());
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setMode(prev => prev === "light" ? "dark" : "light");
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const theme = useMemo(() => createAppTheme(mode), [mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textColor = mode === "dark" ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
|
||||||
|
const gridColor = mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
||||||
|
chartDefaults.color = textColor;
|
||||||
|
chartDefaults.borderColor = gridColor;
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ mode, toggleTheme }), [mode, toggleTheme]);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</ThemeContext.Provider>;
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function usePackageActions(
|
|||||||
setSelectionModel: (model: string[]) => void,
|
setSelectionModel: (model: string[]) => void,
|
||||||
): UsePackageActionsResult {
|
): UsePackageActionsResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ export function usePackageActions(
|
|||||||
action: (repository: RepositoryId) => Promise<string>,
|
action: (repository: RepositoryId) => Promise<string>,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!current) {
|
if (!currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const successMessage = await action(current);
|
const successMessage = await action(currentRepository);
|
||||||
showSuccess("Success", successMessage);
|
showSuccess("Success", successMessage);
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
setSelectionModel([]);
|
setSelectionModel([]);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -64,8 +64,8 @@ export function usePackageActions(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReload: () => void = () => {
|
const handleReload: () => void = () => {
|
||||||
if (current !== null) {
|
if (currentRepository !== null) {
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
36
frontend/src/hooks/usePackageChanges.ts
Normal file
36
frontend/src/hooks/usePackageChanges.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export function usePackageChanges(packageBase: string, repository: RepositoryId): Changes | undefined {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data } = useQuery<Changes>({
|
||||||
|
queryKey: QueryKeys.changes(packageBase, repository),
|
||||||
|
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||||
|
enabled: !!packageBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -39,20 +39,20 @@ export interface UsePackageDataResult {
|
|||||||
|
|
||||||
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
|
|
||||||
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packages = [], isLoading } = useQuery({
|
const { data: packages = [], isLoading } = useQuery({
|
||||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
queryKey: currentRepository ? QueryKeys.packages(currentRepository) : ["packages"],
|
||||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchPackages(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: status } = useQuery({
|
const { data: status } = useQuery({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export interface SelectedRepositoryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectedRepository(): SelectedRepositoryResult {
|
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
const [selectedKey, setSelectedKey] = useState("");
|
const [selectedKey, setSelectedKey] = useState("");
|
||||||
|
|
||||||
let selectedRepository: RepositoryId | null = current;
|
let selectedRepository: RepositoryId | null = currentRepository;
|
||||||
if (selectedKey) {
|
if (selectedKey) {
|
||||||
const repository = repositories.find(repository => repository.key === selectedKey);
|
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||||
if (repository) {
|
if (repository) {
|
||||||
|
|||||||
25
frontend/src/hooks/useThemeMode.ts
Normal file
25
frontend/src/hooks/useThemeMode.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { ThemeContext, type ThemeContextValue } from "contexts/ThemeContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useThemeMode(): ThemeContextValue {
|
||||||
|
return useContextNotNull(ThemeContext);
|
||||||
|
}
|
||||||
@@ -20,4 +20,5 @@
|
|||||||
export interface Changes {
|
export interface Changes {
|
||||||
changes?: string;
|
changes?: string;
|
||||||
last_commit_sha?: string;
|
last_commit_sha?: string;
|
||||||
|
pkgbuild?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ import { amber, green, grey, orange, red } from "@mui/material/colors";
|
|||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
const base: Record<BuildStatus, string> = {
|
const base: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[900],
|
pending: amber[700],
|
||||||
building: orange[900],
|
building: orange[800],
|
||||||
failed: red[900],
|
failed: red[700],
|
||||||
success: green[800],
|
success: green[700],
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerBase: Record<BuildStatus, string> = {
|
const headerBase: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[700],
|
pending: amber[700],
|
||||||
building: orange[600],
|
building: orange[600],
|
||||||
failed: red[500],
|
failed: red[500],
|
||||||
|
|||||||
@@ -17,17 +17,20 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { createTheme } from "@mui/material/styles";
|
import { createTheme, type Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
const Theme = createTheme({
|
export function createAppTheme(mode: "light" | "dark"): Theme {
|
||||||
components: {
|
return createTheme({
|
||||||
MuiDialog: {
|
palette: {
|
||||||
defaultProps: {
|
mode,
|
||||||
maxWidth: "lg",
|
},
|
||||||
fullWidth: true,
|
components: {
|
||||||
|
MuiDialog: {
|
||||||
|
defaultProps: {
|
||||||
|
maxWidth: "lg",
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
export default Theme;
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pkgbase='ahriman'
|
pkgbase='ahriman'
|
||||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||||
pkgver=2.20.0rc7
|
pkgver=2.20.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0rc7" "ArcH linux ReposItory MANager"
|
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0" "ArcH linux ReposItory MANager"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ahriman \- ArcH linux ReposItory MANager
|
ahriman \- ArcH linux ReposItory MANager
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2.20.0rc7"
|
__version__ = "2.20.0"
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
if last_commit_sha is None:
|
if last_commit_sha is None:
|
||||||
continue # skip check in case if we can't calculate diff
|
continue # skip check in case if we can't calculate diff
|
||||||
|
|
||||||
changes = self.repository.package_changes(package, last_commit_sha)
|
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||||
self.repository.reporter.package_changes_update(package.base, changes)
|
self.repository.reporter.package_changes_update(package.base, changes)
|
||||||
|
|
||||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from typing import ClassVar
|
|||||||
from ahriman.core.exceptions import CalledProcessError
|
from ahriman.core.exceptions import CalledProcessError
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import check_output, utcnow, walk
|
from ahriman.core.utils import check_output, utcnow, walk
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild import Pkgbuild
|
from ahriman.models.pkgbuild import Pkgbuild
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@@ -51,24 +52,25 @@ class Sources(LazyLogging):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def changes(source_dir: Path, last_commit_sha: str | None) -> str | None:
|
def changes(source_dir: Path, last_commit_sha: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
extract changes from the last known commit if available
|
extract changes from the last known commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_dir(Path): local path to directory with source files
|
source_dir(Path): local path to directory with source files
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: changes from the last commit if available or ``None`` otherwise
|
Changes: changes from the last commit if available
|
||||||
"""
|
"""
|
||||||
if last_commit_sha is None:
|
|
||||||
return None # no previous reference found
|
|
||||||
|
|
||||||
instance = Sources()
|
instance = Sources()
|
||||||
|
|
||||||
|
diff = None
|
||||||
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
||||||
return instance.diff(source_dir, last_commit_sha)
|
diff = instance.diff(source_dir, last_commit_sha)
|
||||||
return None
|
pkgbuild = instance.read(source_dir, "HEAD", Path("PKGBUILD"))
|
||||||
|
|
||||||
|
return Changes(last_commit_sha, diff, pkgbuild)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||||
@@ -413,3 +415,17 @@ class Sources(LazyLogging):
|
|||||||
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||||
else:
|
else:
|
||||||
patch.write(sources_dir / "PKGBUILD")
|
patch.write(sources_dir / "PKGBUILD")
|
||||||
|
|
||||||
|
def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str:
|
||||||
|
"""
|
||||||
|
read file content from the specified commit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sources_dir(Path): local path to git repository
|
||||||
|
commit_sha(str): commit hash to read from
|
||||||
|
path(Path): path to file inside the repository
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: file content at specified commit
|
||||||
|
"""
|
||||||
|
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger)
|
||||||
|
|||||||
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""alter table package_changes add column pkgbuild text""",
|
||||||
|
]
|
||||||
@@ -45,10 +45,10 @@ class ChangesOperations(Operations):
|
|||||||
def run(connection: Connection) -> Changes:
|
def run(connection: Connection) -> Changes:
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
Changes(row["last_commit_sha"], row["changes"] or None)
|
Changes(row["last_commit_sha"], row["changes"] or None, row["pkgbuild"] or None)
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""
|
"""
|
||||||
select last_commit_sha, changes from package_changes
|
select last_commit_sha, changes, pkgbuild from package_changes
|
||||||
where package_base = :package_base and repository = :repository
|
where package_base = :package_base and repository = :repository
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
@@ -77,16 +77,17 @@ class ChangesOperations(Operations):
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
insert into package_changes
|
insert into package_changes
|
||||||
(package_base, last_commit_sha, changes, repository)
|
(package_base, last_commit_sha, changes, pkgbuild, repository)
|
||||||
values
|
values
|
||||||
(:package_base, :last_commit_sha, :changes ,:repository)
|
(:package_base, :last_commit_sha, :changes, :pkgbuild, :repository)
|
||||||
on conflict (package_base, repository) do update set
|
on conflict (package_base, repository) do update set
|
||||||
last_commit_sha = :last_commit_sha, changes = :changes
|
last_commit_sha = :last_commit_sha, changes = :changes, pkgbuild = :pkgbuild
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
"last_commit_sha": changes.last_commit_sha,
|
"last_commit_sha": changes.last_commit_sha,
|
||||||
"changes": changes.changes,
|
"changes": changes.changes,
|
||||||
|
"pkgbuild": changes.pkgbuild,
|
||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
|
|
||||||
# update commit hash for changes keeping current diff if there is any
|
# update commit hash for changes keeping current diff if there is any
|
||||||
changes = self.reporter.package_changes_get(single.base)
|
changes = self.reporter.package_changes_get(single.base)
|
||||||
self.reporter.package_changes_update(single.base, Changes(commit_sha, changes.changes))
|
self.reporter.package_changes_update(
|
||||||
|
single.base, Changes(commit_sha, changes.changes, changes.pkgbuild))
|
||||||
|
|
||||||
# update dependencies list
|
# update dependencies list
|
||||||
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
||||||
|
|||||||
@@ -102,27 +102,25 @@ class PackageInfo(RepositoryProperties):
|
|||||||
self.logger.exception("could not load package from %s", full_path)
|
self.logger.exception("could not load package from %s", full_path)
|
||||||
return list(result.values())
|
return list(result.values())
|
||||||
|
|
||||||
def package_changes(self, package: Package, last_commit_sha: str | None) -> Changes:
|
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
|
||||||
"""
|
"""
|
||||||
extract package change for the package since last commit if available
|
extract package change for the package since last commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package properties
|
package(Package): package properties
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Changes: changes if available
|
Changes | None: changes if available
|
||||||
"""
|
"""
|
||||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
dir_path = Path(dir_name)
|
dir_path = Path(dir_name)
|
||||||
patches = self.reporter.package_patches_get(package.base, None)
|
patches = self.reporter.package_patches_get(package.base, None)
|
||||||
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
||||||
|
|
||||||
changes: str | None = None
|
|
||||||
if current_commit_sha != last_commit_sha:
|
if current_commit_sha != last_commit_sha:
|
||||||
changes = Sources.changes(dir_path, last_commit_sha)
|
return Sources.changes(dir_path, last_commit_sha)
|
||||||
|
return None
|
||||||
return Changes(last_commit_sha, changes)
|
|
||||||
|
|
||||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ class Changes:
|
|||||||
Attributes:
|
Attributes:
|
||||||
last_commit_sha(str | None): last commit hash
|
last_commit_sha(str | None): last commit hash
|
||||||
changes(str | None): package change since the last commit if available
|
changes(str | None): package change since the last commit if available
|
||||||
|
pkgbuild(str | None): original PKGBUILD content if available
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_commit_sha: str | None = None
|
last_commit_sha: str | None = None
|
||||||
changes: str | None = None
|
changes: str | None = None
|
||||||
|
pkgbuild: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ class ChangesSchema(Schema):
|
|||||||
"description": "Last recorded commit hash",
|
"description": "Last recorded commit hash",
|
||||||
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
||||||
})
|
})
|
||||||
|
pkgbuild = fields.String(metadata={
|
||||||
|
"description": "Original PKGBUILD content",
|
||||||
|
})
|
||||||
|
|||||||
@@ -92,10 +92,11 @@ class ChangesView(StatusViewGuard, BaseView):
|
|||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
||||||
change = data.get("changes")
|
change = data.get("changes")
|
||||||
|
pkgbuild = data.get("pkgbuild")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
changes = Changes(last_commit_sha, change)
|
changes = Changes(last_commit_sha, change, pkgbuild)
|
||||||
self.service().package_changes_update(package_base, changes)
|
self.service().package_changes_update(package_base, changes)
|
||||||
|
|
||||||
raise HTTPNoContent
|
raise HTTPNoContent
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_changes(application_repository: ApplicationRepository, package_ahriman:
|
|||||||
"""
|
"""
|
||||||
must generate changes for the packages
|
must generate changes for the packages
|
||||||
"""
|
"""
|
||||||
changes = Changes("hash", "change")
|
changes = Changes("sha", "change")
|
||||||
hashes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
hashes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||||
changes_mock = mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=changes)
|
changes_mock = mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=changes)
|
||||||
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
@@ -42,6 +42,20 @@ def test_changes_skip(application_repository: ApplicationRepository, package_ahr
|
|||||||
report_mock.assert_not_called()
|
report_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_changes_no_update(application_repository: ApplicationRepository, package_ahriman: Package,
|
||||||
|
mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must skip update if package_changes returns None (no new commits)
|
||||||
|
"""
|
||||||
|
changes = Changes("sha", "change")
|
||||||
|
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||||
|
mocker.patch("ahriman.core.repository.Repository.package_changes", return_value=None)
|
||||||
|
report_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
|
|
||||||
|
application_repository.changes([package_ahriman])
|
||||||
|
report_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must clean cache directory
|
must clean cache directory
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from unittest.mock import call as MockCall
|
|||||||
|
|
||||||
from ahriman.core.build_tools.sources import Sources
|
from ahriman.core.build_tools.sources import Sources
|
||||||
from ahriman.core.exceptions import CalledProcessError
|
from ahriman.core.exceptions import CalledProcessError
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@@ -19,35 +20,27 @@ def test_changes(mocker: MockerFixture) -> None:
|
|||||||
"""
|
"""
|
||||||
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until")
|
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until")
|
||||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff", return_value="diff")
|
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff", return_value="diff")
|
||||||
|
read_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.read", return_value="pkgbuild")
|
||||||
local = Path("local")
|
local = Path("local")
|
||||||
last_commit_sha = "sha"
|
last_commit_sha = "sha"
|
||||||
|
|
||||||
assert Sources.changes(local, last_commit_sha) == "diff"
|
assert Sources.changes(local, last_commit_sha) == Changes(last_commit_sha, "diff", "pkgbuild")
|
||||||
fetch_mock.assert_called_once_with(local, commit_sha=last_commit_sha)
|
fetch_mock.assert_called_once_with(local, commit_sha=last_commit_sha)
|
||||||
diff_mock.assert_called_once_with(local, last_commit_sha)
|
diff_mock.assert_called_once_with(local, last_commit_sha)
|
||||||
|
read_mock.assert_called_once_with(local, "HEAD", Path("PKGBUILD"))
|
||||||
|
|
||||||
def test_changes_skip(mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must return none in case if commit sha is not available
|
|
||||||
"""
|
|
||||||
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until")
|
|
||||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
|
|
||||||
|
|
||||||
assert Sources.changes(Path("local"), None) is None
|
|
||||||
fetch_mock.assert_not_called()
|
|
||||||
diff_mock.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_changes_unknown_commit(mocker: MockerFixture) -> None:
|
def test_changes_unknown_commit(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must return none in case if commit sha wasn't found at the required depth
|
must return changes without diff in case if commit sha wasn't found at the required depth
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until", return_value=None)
|
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch_until", return_value=None)
|
||||||
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
|
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
|
||||||
|
read_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.read", return_value="pkgbuild")
|
||||||
|
|
||||||
assert Sources.changes(Path("local"), "sha") is None
|
assert Sources.changes(Path("local"), "sha") == Changes("sha", None, "pkgbuild")
|
||||||
diff_mock.assert_not_called()
|
diff_mock.assert_not_called()
|
||||||
|
read_mock.assert_called_once_with(Path("local"), "HEAD", Path("PKGBUILD"))
|
||||||
|
|
||||||
|
|
||||||
def test_extend_architectures(mocker: MockerFixture) -> None:
|
def test_extend_architectures(mocker: MockerFixture) -> None:
|
||||||
@@ -603,3 +596,12 @@ def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None:
|
|||||||
|
|
||||||
sources.patch_apply(local, patch)
|
sources.patch_apply(local, patch)
|
||||||
write_mock.assert_called_once_with(local / "PKGBUILD")
|
write_mock.assert_called_once_with(local / "PKGBUILD")
|
||||||
|
|
||||||
|
|
||||||
|
def test_read(sources: Sources, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must read file from commit
|
||||||
|
"""
|
||||||
|
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.check_output", return_value="content")
|
||||||
|
assert sources.read(Path("local"), "sha", Path("PKGBUILD")) == "content"
|
||||||
|
check_output_mock.assert_called_once()
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from ahriman.core.database.migrations.m017_pkgbuild import steps
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_pkgbuild() -> None:
|
||||||
|
"""
|
||||||
|
migration must not be empty
|
||||||
|
"""
|
||||||
|
assert steps
|
||||||
@@ -8,34 +8,35 @@ def test_changes_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
|||||||
"""
|
"""
|
||||||
must insert and get changes
|
must insert and get changes
|
||||||
"""
|
"""
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
changes1 = Changes("sha1", "change1", "pkgbuild1")
|
||||||
assert database.changes_get(package_ahriman.base).changes == "change1"
|
database.changes_insert(package_ahriman.base, changes1)
|
||||||
|
assert database.changes_get(package_ahriman.base) == changes1
|
||||||
|
changes2 = Changes("sha2", "change2", "pkgbuild2")
|
||||||
|
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
database.changes_insert(package_ahriman.base, changes2, RepositoryId("i686", database._repository_id.name))
|
||||||
RepositoryId("i686", database._repository_id.name))
|
assert database.changes_get(package_ahriman.base) == changes1
|
||||||
assert database.changes_get(package_ahriman.base).changes == "change1"
|
assert database.changes_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == changes2
|
||||||
assert database.changes_get(
|
|
||||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes == "change2"
|
|
||||||
|
|
||||||
|
|
||||||
def test_changes_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
def test_changes_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must remove changes for the package
|
must remove changes for the package
|
||||||
"""
|
"""
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
changes3 = Changes("sha3", "change3", "pkgbuild3")
|
||||||
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3"))
|
database.changes_insert(package_ahriman.base, Changes("sha1", "change1", "pkgbuild1"))
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
database.changes_insert(package_python_schedule.base, changes3)
|
||||||
|
database.changes_insert(package_ahriman.base, Changes("sha2", "change2", "pkgbuild2"),
|
||||||
RepositoryId("i686", database._repository_id.name))
|
RepositoryId("i686", database._repository_id.name))
|
||||||
|
|
||||||
database.changes_remove(package_ahriman.base)
|
database.changes_remove(package_ahriman.base)
|
||||||
assert database.changes_get(package_ahriman.base).changes is None
|
assert database.changes_get(package_ahriman.base).changes is None
|
||||||
assert database.changes_get(package_python_schedule.base).changes == "change3"
|
assert database.changes_get(package_python_schedule.base) == changes3
|
||||||
|
|
||||||
# insert null
|
# insert null
|
||||||
database.changes_insert(package_ahriman.base, Changes(), RepositoryId("i686", database._repository_id.name))
|
database.changes_insert(package_ahriman.base, Changes(), RepositoryId("i686", database._repository_id.name))
|
||||||
assert database.changes_get(
|
assert database.changes_get(
|
||||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes is None
|
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes is None
|
||||||
assert database.changes_get(package_python_schedule.base).changes == "change3"
|
assert database.changes_get(package_python_schedule.base) == changes3
|
||||||
|
|
||||||
|
|
||||||
def test_changes_insert_remove_full(database: SQLite, package_ahriman: Package,
|
def test_changes_insert_remove_full(database: SQLite, package_ahriman: Package,
|
||||||
@@ -43,13 +44,12 @@ def test_changes_insert_remove_full(database: SQLite, package_ahriman: Package,
|
|||||||
"""
|
"""
|
||||||
must remove all changes for the repository
|
must remove all changes for the repository
|
||||||
"""
|
"""
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha1", "change1"))
|
changes2 = Changes("sha2", "change2", "pkgbuild2")
|
||||||
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3"))
|
database.changes_insert(package_ahriman.base, Changes("sha1", "change1", "pkgbuild1"))
|
||||||
database.changes_insert(package_ahriman.base, Changes("sha2", "change2"),
|
database.changes_insert(package_python_schedule.base, Changes("sha3", "change3", "pkgbuild3"))
|
||||||
RepositoryId("i686", database._repository_id.name))
|
database.changes_insert(package_ahriman.base, changes2, RepositoryId("i686", database._repository_id.name))
|
||||||
|
|
||||||
database.changes_remove(None)
|
database.changes_remove(None)
|
||||||
assert database.changes_get(package_ahriman.base).changes is None
|
assert database.changes_get(package_ahriman.base).changes is None
|
||||||
assert database.changes_get(package_python_schedule.base).changes is None
|
assert database.changes_get(package_python_schedule.base).changes is None
|
||||||
assert database.changes_get(
|
assert database.changes_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == changes2
|
||||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)).changes == "change2"
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
|
|||||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
||||||
changes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
changes_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
||||||
return_value=Changes("commit", "change"))
|
return_value=Changes("commit", "change", "pkgbuild"))
|
||||||
commit_sha_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
commit_sha_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||||
depends_on_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on",
|
depends_on_mock = mocker.patch("ahriman.core.build_tools.package_archive.PackageArchive.depends_on",
|
||||||
return_value=Dependencies())
|
return_value=Dependencies())
|
||||||
@@ -231,7 +231,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
|
|||||||
build_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(Path, strict=True), None, None)
|
build_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(Path, strict=True), None, None)
|
||||||
depends_on_mock.assert_called_once_with()
|
depends_on_mock.assert_called_once_with()
|
||||||
dependencies_mock.assert_called_once_with(package_ahriman.base, Dependencies())
|
dependencies_mock.assert_called_once_with(package_ahriman.base, Dependencies())
|
||||||
commit_sha_mock.assert_called_once_with(package_ahriman.base, Changes("sha", "change"))
|
commit_sha_mock.assert_called_once_with(package_ahriman.base, Changes("sha", "change", "pkgbuild"))
|
||||||
|
|
||||||
|
|
||||||
def test_process_build_bump_pkgrel(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_process_build_bump_pkgrel(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def test_package_changes(package_info: PackageInfo, package_ahriman: Package, mo
|
|||||||
"""
|
"""
|
||||||
changes = Changes("sha", "change")
|
changes = Changes("sha", "change")
|
||||||
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
|
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
|
||||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes.changes)
|
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes)
|
||||||
|
|
||||||
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
||||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], package_info.paths)
|
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], package_info.paths)
|
||||||
@@ -108,11 +108,10 @@ def test_package_changes_skip(package_info: PackageInfo, package_ahriman: Packag
|
|||||||
"""
|
"""
|
||||||
must skip loading package changes if no new commits
|
must skip loading package changes if no new commits
|
||||||
"""
|
"""
|
||||||
changes = Changes("sha")
|
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
|
||||||
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value=changes.last_commit_sha)
|
|
||||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
|
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
|
||||||
|
|
||||||
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
assert package_info.package_changes(package_ahriman, "sha") is None
|
||||||
changes_mock.assert_not_called()
|
changes_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user