mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-04-07 11:03:37 +00:00
upload ai slop
This commit is contained in:
205
frontend/src/components/package/BuildLogsTab.tsx
Normal file
205
frontend/src/components/package/BuildLogsTab.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, Button, Menu, MenuItem, Typography } from "@mui/material";
|
||||
import ListIcon from "@mui/icons-material/List";
|
||||
import { skipToken, useQuery } from "@tanstack/react-query";
|
||||
import hljs from "highlight.js/lib/core";
|
||||
import plaintext from "highlight.js/lib/languages/plaintext";
|
||||
import "highlight.js/styles/github.css";
|
||||
import { Client } from "api/client/AhrimanClient";
|
||||
import { QueryKeys } from "api/QueryKeys";
|
||||
import { formatTimestamp } from "components/common/formatTimestamp";
|
||||
import CopyButton from "components/common/CopyButton";
|
||||
import type { LogRecord } from "api/types/LogRecord";
|
||||
import type { RepositoryId } from "api/types/RepositoryId";
|
||||
|
||||
hljs.registerLanguage("plaintext", plaintext);
|
||||
|
||||
interface LogVersion {
|
||||
version: string;
|
||||
processId: string;
|
||||
created: number;
|
||||
logs: string;
|
||||
}
|
||||
|
||||
interface BuildLogsTabProps {
|
||||
packageBase: string;
|
||||
repo: RepositoryId;
|
||||
refetchInterval: number | false;
|
||||
}
|
||||
|
||||
function convertLogs(records: LogRecord[], filter?: (r: LogRecord) => boolean): string {
|
||||
return records
|
||||
.filter(filter || Boolean)
|
||||
.map((r) => `[${new Date(r.created * 1000).toISOString()}] ${r.message}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export default function BuildLogsTab({ packageBase, repo, refetchInterval }: BuildLogsTabProps): React.JSX.Element {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
const preRef = useRef<HTMLElement>(null);
|
||||
const initialScrollDone = useRef(false);
|
||||
const wasAtBottom = useRef(true);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (preRef.current) {
|
||||
const el = preRef.current;
|
||||
wasAtBottom.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { data: allLogs } = useQuery<LogRecord[]>({
|
||||
queryKey: QueryKeys.logs(packageBase, repo),
|
||||
queryFn: () => Client.fetchLogs(packageBase, repo),
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
// Build version selectors from all logs
|
||||
const versions = useMemo<LogVersion[]>(() => {
|
||||
if (!allLogs || allLogs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const grouped: Record<string, LogRecord & { minCreated: number }> = {};
|
||||
for (const record of allLogs) {
|
||||
const key = `${record.version}-${record.process_id}`;
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = { ...record, minCreated: record.created };
|
||||
} else {
|
||||
grouped[key].minCreated = Math.min(grouped[key].minCreated, record.created);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(grouped)
|
||||
.sort((a, b) => b.minCreated - a.minCreated)
|
||||
.map((v) => ({
|
||||
version: v.version,
|
||||
processId: v.process_id,
|
||||
created: v.minCreated,
|
||||
logs: convertLogs(
|
||||
allLogs,
|
||||
(r) => r.version === v.version && r.process_id === v.process_id,
|
||||
),
|
||||
}));
|
||||
}, [allLogs]);
|
||||
|
||||
// Reset active index when logs data changes (React "adjusting state from props" pattern)
|
||||
const [prevAllLogs, setPrevAllLogs] = useState(allLogs);
|
||||
if (prevAllLogs !== allLogs) {
|
||||
setPrevAllLogs(allLogs);
|
||||
setActiveIndex(0);
|
||||
}
|
||||
|
||||
// Reset scroll tracking when active version changes
|
||||
const activeVersionKey = versions[activeIndex] ? `${versions[activeIndex].version}-${versions[activeIndex].processId}` : null;
|
||||
useEffect(() => {
|
||||
initialScrollDone.current = false;
|
||||
}, [activeVersionKey]);
|
||||
|
||||
// Refresh active version logs when using auto-refresh
|
||||
const activeVersion = versions[activeIndex];
|
||||
const { data: versionLogs } = useQuery<LogRecord[]>({
|
||||
queryKey: activeVersion
|
||||
? QueryKeys.logsVersion(packageBase, repo, activeVersion.version, activeVersion.processId)
|
||||
: ["logs-none"],
|
||||
queryFn: activeVersion
|
||||
? () => Client.fetchLogs(packageBase, repo, activeVersion.version, activeVersion.processId)
|
||||
: skipToken,
|
||||
enabled: !!activeVersion && !!refetchInterval,
|
||||
refetchInterval,
|
||||
});
|
||||
|
||||
// Derive displayed logs: prefer fresh polled data when available
|
||||
const displayedLogs = useMemo(() => {
|
||||
if (versionLogs && versionLogs.length > 0) {
|
||||
return convertLogs(versionLogs);
|
||||
}
|
||||
return activeVersion?.logs ?? "";
|
||||
}, [versionLogs, activeVersion]);
|
||||
|
||||
// Highlight code
|
||||
useEffect(() => {
|
||||
if (codeRef.current && displayedLogs) {
|
||||
codeRef.current.textContent = displayedLogs;
|
||||
delete codeRef.current.dataset.highlighted;
|
||||
hljs.highlightElement(codeRef.current);
|
||||
}
|
||||
}, [displayedLogs]);
|
||||
|
||||
// Auto-scroll: always scroll to bottom on initial load, then only if already near bottom
|
||||
// and the user has no active text selection (to avoid disrupting copy workflows).
|
||||
// wasAtBottom is tracked via onScroll so it reflects position *before* new content arrives.
|
||||
useEffect(() => {
|
||||
if (preRef.current && displayedLogs) {
|
||||
const el = preRef.current;
|
||||
if (!initialScrollDone.current) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
initialScrollDone.current = true;
|
||||
} else {
|
||||
const hasSelection = !document.getSelection()?.isCollapsed;
|
||||
if (wasAtBottom.current && !hasSelection) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [displayedLogs]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 1 }}>
|
||||
<Box>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<ListIcon />}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
/>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
>
|
||||
{versions.map((v, idx) => (
|
||||
<MenuItem
|
||||
key={`${v.version}-${v.processId}`}
|
||||
selected={idx === activeIndex}
|
||||
onClick={() => {
|
||||
setActiveIndex(idx);
|
||||
setAnchorEl(null);
|
||||
initialScrollDone.current = false;
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">{formatTimestamp(v.created)}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
{versions.length === 0 && (
|
||||
<MenuItem disabled>No logs available</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, position: "relative" }}>
|
||||
<Box
|
||||
ref={preRef}
|
||||
component="pre"
|
||||
onScroll={handleScroll}
|
||||
sx={{
|
||||
backgroundColor: "grey.100",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
height: 400,
|
||||
fontSize: "0.8rem",
|
||||
fontFamily: "monospace",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
<code ref={codeRef} className="language-plaintext" />
|
||||
</Box>
|
||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<CopyButton getText={() => displayedLogs} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
60
frontend/src/components/package/ChangesTab.tsx
Normal file
60
frontend/src/components/package/ChangesTab.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import hljs from "highlight.js/lib/core";
|
||||
import diff from "highlight.js/lib/languages/diff";
|
||||
import "highlight.js/styles/github.css";
|
||||
import { Client } from "api/client/AhrimanClient";
|
||||
import { QueryKeys } from "api/QueryKeys";
|
||||
import CopyButton from "components/common/CopyButton";
|
||||
import type { Changes } from "api/types/Changes";
|
||||
import type { RepositoryId } from "api/types/RepositoryId";
|
||||
|
||||
hljs.registerLanguage("diff", diff);
|
||||
|
||||
interface ChangesTabProps {
|
||||
packageBase: string;
|
||||
repo: RepositoryId;
|
||||
}
|
||||
|
||||
export default function ChangesTab({ packageBase, repo }: ChangesTabProps): React.JSX.Element {
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
|
||||
const { data } = useQuery<Changes>({
|
||||
queryKey: QueryKeys.changes(packageBase, repo),
|
||||
queryFn: () => Client.fetchChanges(packageBase, repo),
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
const changesText = data?.changes ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
if (codeRef.current) {
|
||||
codeRef.current.textContent = changesText;
|
||||
delete codeRef.current.dataset.highlighted;
|
||||
hljs.highlightElement(codeRef.current);
|
||||
}
|
||||
}, [changesText]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: "relative", mt: 1 }}>
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
backgroundColor: "grey.100",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
maxHeight: 400,
|
||||
fontSize: "0.8rem",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
<code ref={codeRef} className="language-diff" />
|
||||
</Box>
|
||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<CopyButton getText={() => changesText} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
60
frontend/src/components/package/EventsTab.tsx
Normal file
60
frontend/src/components/package/EventsTab.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import type React from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import EventDurationLineChart from "components/charts/EventDurationLineChart";
|
||||
import { Client } from "api/client/AhrimanClient";
|
||||
import { QueryKeys } from "api/QueryKeys";
|
||||
import { formatTimestamp } from "components/common/formatTimestamp";
|
||||
import type { Event } from "api/types/Event";
|
||||
import type { RepositoryId } from "api/types/RepositoryId";
|
||||
|
||||
interface EventsTabProps {
|
||||
packageBase: string;
|
||||
repo: RepositoryId;
|
||||
}
|
||||
|
||||
interface EventRow {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
event: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const columns: GridColDef<EventRow>[] = [
|
||||
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
|
||||
{ field: "event", headerName: "event", flex: 1 },
|
||||
{ field: "message", headerName: "description", flex: 2 },
|
||||
];
|
||||
|
||||
export default function EventsTab({ packageBase, repo }: EventsTabProps): React.JSX.Element {
|
||||
const { data: events = [] } = useQuery<Event[]>({
|
||||
queryKey: QueryKeys.events(repo, packageBase),
|
||||
queryFn: () => Client.fetchEvents(repo, packageBase, 30),
|
||||
enabled: !!packageBase,
|
||||
});
|
||||
|
||||
const rows: EventRow[] = events.map((e, idx) => ({
|
||||
id: idx,
|
||||
timestamp: formatTimestamp(e.created),
|
||||
event: e.event,
|
||||
message: e.message ?? "",
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<EventDurationLineChart events={events} />
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
density="compact"
|
||||
initialState={{
|
||||
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
|
||||
}}
|
||||
pageSizeOptions={[10, 25]}
|
||||
sx={{ height: 300, mt: 1 }}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
96
frontend/src/components/package/PackageDetailsGrid.tsx
Normal file
96
frontend/src/components/package/PackageDetailsGrid.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from "react";
|
||||
import { Grid, Typography, Link } from "@mui/material";
|
||||
import type { Package } from "api/types/Package";
|
||||
import type { Dependencies } from "api/types/Dependencies";
|
||||
import type { PackageProperties } from "api/types/PackageProperties";
|
||||
|
||||
interface PackageDetailsGridProps {
|
||||
pkg: Package;
|
||||
dependencies?: Dependencies;
|
||||
}
|
||||
|
||||
function listToString(items: string[]): React.ReactNode {
|
||||
const unique = [...new Set(items)].sort();
|
||||
return unique.map((item, i) => (
|
||||
<React.Fragment key={item}>
|
||||
{item}
|
||||
{i < unique.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetailsGridProps): React.JSX.Element {
|
||||
const packagesList = Object.entries(pkg.packages)
|
||||
.map(([name, p]) => `${name}${p.description ? ` (${p.description})` : ""}`);
|
||||
|
||||
const groups = Object.values(pkg.packages)
|
||||
.flatMap((p: PackageProperties) => p.groups ?? []);
|
||||
|
||||
const licenses = Object.values(pkg.packages)
|
||||
.flatMap((p: PackageProperties) => p.licenses ?? []);
|
||||
|
||||
const upstreamUrls = [...new Set(
|
||||
Object.values(pkg.packages)
|
||||
.map((p: PackageProperties) => p.url)
|
||||
.filter((u): u is string => !!u),
|
||||
)].sort();
|
||||
|
||||
const aurUrl = pkg.remote.web_url;
|
||||
|
||||
const pkgNames = Object.keys(pkg.packages);
|
||||
const allDepends = Object.values(pkg.packages).flatMap((p: PackageProperties) => {
|
||||
const deps = (p.depends ?? []).filter((d) => !pkgNames.includes(d));
|
||||
const makeDeps = (p.make_depends ?? []).filter((d) => !pkgNames.includes(d)).map((d) => `${d} (make)`);
|
||||
const optDeps = (p.opt_depends ?? []).filter((d) => !pkgNames.includes(d)).map((d) => `${d} (optional)`);
|
||||
return [...deps, ...makeDeps, ...optDeps];
|
||||
});
|
||||
|
||||
const implicitDepends = dependencies
|
||||
? Object.values(dependencies.paths).flat()
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={1} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packages</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{listToString(packagesList)}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">version</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packager</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }} />
|
||||
<Grid size={{ xs: 8, md: 5 }} />
|
||||
</Grid>
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">groups</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{listToString(groups)}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">licenses</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{listToString(licenses)}</Typography></Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">upstream</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}>
|
||||
{upstreamUrls.map((url) => (
|
||||
<Link key={url} href={url} target="_blank" rel="noopener" underline="hover" display="block" variant="body2">
|
||||
{url}
|
||||
</Link>
|
||||
))}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">AUR</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}>
|
||||
{aurUrl && (
|
||||
<Link href={aurUrl} target="_blank" rel="noopener" underline="hover" variant="body2">AUR link</Link>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} sx={{ mt: 0.5 }}>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">depends</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{listToString(allDepends)}</Typography></Grid>
|
||||
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">implicitly depends</Typography></Grid>
|
||||
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{listToString(implicitDepends)}</Typography></Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
frontend/src/components/package/PackageInfoActions.tsx
Normal file
70
frontend/src/components/package/PackageInfoActions.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import type React from "react";
|
||||
import { DialogActions, Button, FormControlLabel, Checkbox } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||
import type { AutoRefreshInterval } from "api/types/AutoRefreshInterval";
|
||||
|
||||
interface PackageInfoActionsProps {
|
||||
hasAuth: boolean;
|
||||
refreshDb: boolean;
|
||||
onRefreshDbChange: (checked: boolean) => void;
|
||||
onUpdate: () => void;
|
||||
onRemove: () => void;
|
||||
onReload: () => void;
|
||||
onClose: () => void;
|
||||
autorefreshIntervals: AutoRefreshInterval[];
|
||||
autoRefreshEnabled: boolean;
|
||||
autoRefreshInterval: number;
|
||||
onAutoRefreshToggle: (enabled: boolean) => void;
|
||||
onAutoRefreshIntervalChange: (interval: number) => void;
|
||||
}
|
||||
|
||||
export default function PackageInfoActions({
|
||||
hasAuth,
|
||||
refreshDb,
|
||||
onRefreshDbChange,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
onReload,
|
||||
onClose,
|
||||
autorefreshIntervals,
|
||||
autoRefreshEnabled,
|
||||
autoRefreshInterval,
|
||||
onAutoRefreshToggle,
|
||||
onAutoRefreshIntervalChange,
|
||||
}: PackageInfoActionsProps): React.JSX.Element {
|
||||
return (
|
||||
<DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
|
||||
{hasAuth && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={refreshDb} onChange={(_, checked) => onRefreshDbChange(checked)} size="small" />}
|
||||
label="update pacman databases"
|
||||
/>
|
||||
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
||||
update
|
||||
</Button>
|
||||
<Button onClick={onRemove} variant="contained" color="error" startIcon={<DeleteIcon />} size="small">
|
||||
remove
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={onReload} variant="outlined" color="secondary" startIcon={<RefreshIcon />} size="small">
|
||||
reload
|
||||
</Button>
|
||||
<AutoRefreshControl
|
||||
intervals={autorefreshIntervals}
|
||||
enabled={autoRefreshEnabled}
|
||||
currentInterval={autoRefreshInterval}
|
||||
onToggle={onAutoRefreshToggle}
|
||||
onIntervalChange={onAutoRefreshIntervalChange}
|
||||
/>
|
||||
<Button onClick={onClose} variant="contained" startIcon={<CloseIcon />} size="small">
|
||||
close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
);
|
||||
}
|
||||
34
frontend/src/components/package/PackagePatchesList.tsx
Normal file
34
frontend/src/components/package/PackagePatchesList.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type React from "react";
|
||||
import { Box, Typography, Chip, IconButton } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import type { Patch } from "api/types/Patch";
|
||||
|
||||
interface PackagePatchesListProps {
|
||||
patches: Patch[];
|
||||
editable: boolean;
|
||||
onDelete: (key: string) => void;
|
||||
}
|
||||
|
||||
export default function PackagePatchesList({ patches, editable, onDelete }: PackagePatchesListProps): React.JSX.Element | null {
|
||||
if (patches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>Environment variables</Typography>
|
||||
{patches.map((patch) => (
|
||||
<Box key={patch.key} sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
|
||||
<Chip label={patch.key} size="small" />
|
||||
<Typography variant="body2">=</Typography>
|
||||
<Typography variant="body2" sx={{ fontFamily: "monospace" }}>{JSON.stringify(patch.value)}</Typography>
|
||||
{editable && (
|
||||
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user