Files
ahriman/frontend/src/components/dialogs/PackageAddDialog.tsx
T

197 lines
7.6 KiB
TypeScript

/*
* 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 AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import {
Autocomplete,
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
FormControlLabel,
IconButton,
TextField,
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { ApiError } from "api/client/ApiError";
import DialogHeader from "components/common/DialogHeader";
import RepositorySelect from "components/common/RepositorySelect";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import { useDebounce } from "hooks/useDebounce";
import { useNotification } from "hooks/useNotification";
import { useSelectedRepository } from "hooks/useSelectedRepository";
import type { AURPackage } from "models/AURPackage";
import type { PackageActionRequest } from "models/PackageActionRequest";
import React, { useRef, useState } from "react";
interface EnvironmentVariable {
id: number;
key: string;
value: string;
}
interface PackageAddDialogProps {
onClose: () => void;
open: boolean;
}
export default function PackageAddDialog({ onClose, open }: PackageAddDialogProps): React.JSX.Element {
const client = useClient();
const { showSuccess, showError } = useNotification();
const repositorySelect = useSelectedRepository();
const [packageName, setPackageName] = useState("");
const [refreshDatabase, setRefreshDatabase] = useState(true);
const [environmentVariables, setEnvironmentVariables] = useState<EnvironmentVariable[]>([]);
const variableIdCounter = useRef(0);
const handleClose = (): void => {
setPackageName("");
repositorySelect.reset();
setRefreshDatabase(true);
setEnvironmentVariables([]);
onClose();
};
const debouncedSearch = useDebounce(packageName, 500);
const { data: searchResults = [] } = useQuery<AURPackage[]>({
enabled: debouncedSearch.length >= 3,
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
queryKey: QueryKeys.search(debouncedSearch),
});
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
if (!packageName) {
return;
}
const repository = repositorySelect.selectedRepository;
if (!repository) {
return;
}
try {
const patches = environmentVariables.filter(variable => variable.key);
const request: PackageActionRequest = { packages: [packageName], patches };
if (action === "add") {
request.refresh = refreshDatabase;
await client.service.servicePackageAdd(repository, request);
} else {
await client.service.servicePackageRequest(repository, request);
}
handleClose();
showSuccess("Success", `Packages ${packageName} have been ${action === "add" ? "added" : "requested"}`);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Action failed", `Package ${action} failed: ${detail}`);
}
};
return <Dialog fullWidth maxWidth="md" onClose={handleClose} open={open}>
<DialogHeader onClose={handleClose}>
Add new packages
</DialogHeader>
<DialogContent>
<RepositorySelect repositorySelect={repositorySelect} />
<Autocomplete
freeSolo
inputValue={packageName}
onInputChange={(_, value) => setPackageName(value)}
options={searchResults.map(pkg => pkg.package)}
renderInput={params =>
<TextField {...params} label="package" margin="normal" placeholder="AUR package" />
}
renderOption={(props, option) => {
const pkg = searchResults.find(pkg => pkg.package === option);
return <li {...props} key={option}>
{option}{pkg ? ` (${pkg.description})` : ""}
</li>;
}}
/>
<FormControlLabel
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => setRefreshDatabase(checked)} />}
label="update pacman databases"
/>
<Button
fullWidth
onClick={() => {
const id = variableIdCounter.current++;
setEnvironmentVariables(prev => [...prev, { id, key: "", value: "" }]);
}}
startIcon={<AddIcon />}
sx={{ mt: 1 }}
variant="outlined"
>
add environment variable
</Button>
{environmentVariables.map(variable =>
<Box key={variable.id} sx={{ alignItems: "center", display: "flex", gap: 1, mt: 1 }}>
<TextField
onChange={event => {
const newKey = event.target.value;
setEnvironmentVariables(prev =>
prev.map(entry => entry.id === variable.id ? { ...entry, key: newKey } : entry),
);
}}
placeholder="name"
size="small"
sx={{ flex: 1 }}
value={variable.key}
/>
<Box>=</Box>
<TextField
placeholder="value"
onChange={event => {
const newValue = event.target.value;
setEnvironmentVariables(prev =>
prev.map(entry => entry.id === variable.id ? { ...entry, value: newValue } : entry),
);
}}
size="small"
sx={{ flex: 1 }}
value={variable.value}
/>
<IconButton
aria-label="Remove variable"
color="error"
onClick={() => setEnvironmentVariables(prev => prev.filter(entry => entry.id !== variable.id))}
size="small"
>
<DeleteIcon />
</IconButton>
</Box>,
)}
</DialogContent>
<DialogActions>
<Button onClick={() => void handleSubmit("add")} startIcon={<PlayArrowIcon />} variant="contained">add</Button>
<Button color="success" onClick={() => void handleSubmit("request")} startIcon={<AddIcon />} variant="contained">request</Button>
</DialogActions>
</Dialog>;
}