mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
552 lines
16 KiB
TypeScript
552 lines
16 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { Storage } from "../network/models.ts";
|
||
import { network } from "../network/network.ts";
|
||
import showToast from "../components/toast.ts";
|
||
import Loading from "../components/loading.tsx";
|
||
import { MdAdd, MdMoreHoriz } from "react-icons/md";
|
||
import { ErrorAlert } from "../components/alert.tsx";
|
||
import { useTranslation } from "../utils/i18n";
|
||
import { app } from "../app.ts";
|
||
import showPopup, { PopupMenuItem } from "../components/popup.tsx";
|
||
import Badge from "../components/badge.tsx";
|
||
|
||
export default function StorageView() {
|
||
const { t } = useTranslation();
|
||
const [storages, setStorages] = useState<Storage[] | null>(null);
|
||
const [loadingId, setLoadingId] = useState<number | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (app.user == null || !app.user.is_admin) {
|
||
return;
|
||
}
|
||
network.listStorages().then((response) => {
|
||
if (response.success) {
|
||
setStorages(response.data!);
|
||
} else {
|
||
showToast({
|
||
message: response.message,
|
||
type: "error",
|
||
});
|
||
}
|
||
});
|
||
}, []);
|
||
|
||
if (!app.user) {
|
||
return (
|
||
<ErrorAlert
|
||
className={"m-4"}
|
||
message={t("You are not logged in. Please log in to access this page.")}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (!app.user?.is_admin) {
|
||
return (
|
||
<ErrorAlert
|
||
className={"m-4"}
|
||
message={t("You are not authorized to access this page.")}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (storages == null) {
|
||
return <Loading />;
|
||
}
|
||
|
||
const updateStorages = async () => {
|
||
setStorages(null);
|
||
const response = await network.listStorages();
|
||
if (response.success) {
|
||
setStorages(response.data!);
|
||
} else {
|
||
showToast({
|
||
message: response.message,
|
||
type: "error",
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: number) => {
|
||
if (loadingId != null) {
|
||
return;
|
||
}
|
||
setLoadingId(id);
|
||
const response = await network.deleteStorage(id);
|
||
if (response.success) {
|
||
showToast({
|
||
message: t("Storage deleted successfully"),
|
||
});
|
||
updateStorages();
|
||
} else {
|
||
showToast({
|
||
message: response.message,
|
||
type: "error",
|
||
});
|
||
}
|
||
setLoadingId(null);
|
||
};
|
||
|
||
const handleSetDefault = async (id: number) => {
|
||
if (loadingId != null) {
|
||
return;
|
||
}
|
||
setLoadingId(id);
|
||
const response = await network.setDefaultStorage(id);
|
||
if (response.success) {
|
||
showToast({
|
||
message: t("Storage set as default successfully"),
|
||
});
|
||
updateStorages();
|
||
} else {
|
||
showToast({
|
||
message: response.message,
|
||
type: "error",
|
||
});
|
||
}
|
||
setLoadingId(null);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
role="alert"
|
||
className={`alert alert-info alert-outline ${storages.length !== 0 && "hidden"} mx-4 mb-4`}
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
className="h-6 w-6 shrink-0 stroke-current"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||
></path>
|
||
</svg>
|
||
<span>{t("No storage found. Please create a new storage.")}</span>
|
||
</div>
|
||
<div
|
||
className={`rounded-box border border-base-content/10 bg-base-100 mx-4 mb-4 overflow-x-auto ${storages.length === 0 ? "hidden" : ""}`}
|
||
>
|
||
<table className={"table"}>
|
||
<thead>
|
||
<tr>
|
||
<td>{t("Name")}</td>
|
||
<td>{t("Created At")}</td>
|
||
<td>{t("Space")}</td>
|
||
<td>{t("Action")}</td>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{storages.map((s) => {
|
||
return (
|
||
<tr key={s.id} className={"hover"}>
|
||
<td>
|
||
{s.name}
|
||
{s.isDefault && (
|
||
<Badge className={"ml-1"}>{t("Default")}</Badge>
|
||
)}
|
||
</td>
|
||
<td>{new Date(s.createdAt).toLocaleString()}</td>
|
||
<td>
|
||
{(s.currentSize / 1024 / 1024).toFixed(2)} /{" "}
|
||
{s.maxSize / 1024 / 1024} MB
|
||
</td>
|
||
<td>
|
||
<button
|
||
id={`set_default_button_${s.id}`}
|
||
className={"btn btn-square"}
|
||
type={"button"}
|
||
onClick={() => {
|
||
showPopup(
|
||
<ul className="menu bg-base-100 rounded-box z-1 w-64 p-2 shadow-sm">
|
||
<h4 className="text-sm font-bold px-3 py-1 text-primary">
|
||
{t("Actions")}
|
||
</h4>
|
||
<PopupMenuItem
|
||
onClick={() => {
|
||
const dialog = document.getElementById(
|
||
`confirm_delete_dialog_${s.id}`,
|
||
) as HTMLDialogElement;
|
||
dialog.showModal();
|
||
}}
|
||
>
|
||
<a>{t("Delete")}</a>
|
||
</PopupMenuItem>
|
||
{!s.isDefault && (
|
||
<PopupMenuItem
|
||
onClick={() => {
|
||
handleSetDefault(s.id);
|
||
}}
|
||
>
|
||
<a>t("Set as Default")</a>
|
||
</PopupMenuItem>
|
||
)}
|
||
</ul>,
|
||
document.getElementById(
|
||
`set_default_button_${s.id}`,
|
||
)!,
|
||
);
|
||
}}
|
||
>
|
||
{loadingId === s.id ? (
|
||
<span
|
||
className={"loading loading-spinner loading-sm"}
|
||
></span>
|
||
) : (
|
||
<MdMoreHoriz size={24} />
|
||
)}
|
||
</button>
|
||
<dialog
|
||
id={`confirm_delete_dialog_${s.id}`}
|
||
className="modal"
|
||
>
|
||
<div className="modal-box">
|
||
<h3 className="text-lg font-bold">
|
||
{t("Delete Storage")}
|
||
</h3>
|
||
<p className="py-4">
|
||
{t(
|
||
"Are you sure you want to delete this storage? This action cannot be undone.",
|
||
)}
|
||
</p>
|
||
<div className="modal-action">
|
||
<form method="dialog">
|
||
<button className="btn">{t("Cancel")}</button>
|
||
</form>
|
||
<button
|
||
className="btn btn-error"
|
||
onClick={() => {
|
||
handleDelete(s.id);
|
||
}}
|
||
>
|
||
{t("Delete")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div className={"flex flex-row-reverse px-4"}>
|
||
<NewStorageDialog onAdded={updateStorages} />
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
enum StorageType {
|
||
local,
|
||
s3,
|
||
}
|
||
|
||
function NewStorageDialog({ onAdded }: { onAdded: () => void }) {
|
||
const { t } = useTranslation();
|
||
const [storageType, setStorageType] = useState<StorageType | null>(null);
|
||
|
||
const [params, setParams] = useState({
|
||
name: "",
|
||
path: "",
|
||
endPoint: "",
|
||
accessKeyID: "",
|
||
secretAccessKey: "",
|
||
bucketName: "",
|
||
maxSizeInMB: 0,
|
||
domain: "",
|
||
});
|
||
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
const [error, setError] = useState("");
|
||
|
||
const handleSubmit = async () => {
|
||
if (storageType == null) {
|
||
return;
|
||
}
|
||
setIsLoading(true);
|
||
|
||
let response;
|
||
if (storageType === StorageType.local) {
|
||
if (params.path === "" || params.name === "" || params.maxSizeInMB <= 0) {
|
||
setError(t("All fields are required"));
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
response = await network.createLocalStorage(
|
||
params.name,
|
||
params.path,
|
||
params.maxSizeInMB,
|
||
);
|
||
} else if (storageType === StorageType.s3) {
|
||
if (
|
||
params.endPoint === "" ||
|
||
params.accessKeyID === "" ||
|
||
params.secretAccessKey === "" ||
|
||
params.bucketName === "" ||
|
||
params.name === "" ||
|
||
params.maxSizeInMB <= 0
|
||
) {
|
||
setError(t("All fields are required"));
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
response = await network.createS3Storage(
|
||
params.name,
|
||
params.endPoint,
|
||
params.accessKeyID,
|
||
params.secretAccessKey,
|
||
params.bucketName,
|
||
params.maxSizeInMB,
|
||
params.domain,
|
||
);
|
||
}
|
||
|
||
if (response!.success) {
|
||
showToast({
|
||
message: t("Storage created successfully"),
|
||
});
|
||
onAdded();
|
||
const dialog = document.getElementById(
|
||
"new_storage_dialog",
|
||
) as HTMLDialogElement;
|
||
dialog.close();
|
||
} else {
|
||
setError(response!.message);
|
||
}
|
||
setIsLoading(false);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<button
|
||
className="btn"
|
||
onClick={() => {
|
||
const dialog = document.getElementById(
|
||
"new_storage_dialog",
|
||
) as HTMLDialogElement;
|
||
dialog.showModal();
|
||
}}
|
||
>
|
||
<MdAdd />
|
||
{t("New Storage")}
|
||
</button>
|
||
<dialog id="new_storage_dialog" className="modal">
|
||
<div className="modal-box">
|
||
<h3 className="font-bold text-lg pb-4">{t("New Storage")}</h3>
|
||
|
||
<p className={"text-sm font-bold p-2"}>{t("Type")}</p>
|
||
<form className="filter mb-2">
|
||
<input
|
||
className="btn btn-square"
|
||
type="reset"
|
||
value="×"
|
||
onClick={() => {
|
||
setStorageType(null);
|
||
}}
|
||
/>
|
||
<input
|
||
className="btn"
|
||
type="radio"
|
||
name="type"
|
||
aria-label={t("Local")}
|
||
onInput={() => {
|
||
setStorageType(StorageType.local);
|
||
}}
|
||
/>
|
||
<input
|
||
className="btn"
|
||
type="radio"
|
||
name="type"
|
||
aria-label={t("S3")}
|
||
onInput={() => {
|
||
setStorageType(StorageType.s3);
|
||
}}
|
||
/>
|
||
</form>
|
||
|
||
{storageType === StorageType.local && (
|
||
<>
|
||
<label className="input w-full my-2">
|
||
{t("Name")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.name}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
name: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Path")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.path}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
path: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Max Size (MB)")}
|
||
<input
|
||
type="number"
|
||
className="validator"
|
||
required
|
||
min="0"
|
||
value={params.maxSizeInMB.toString()}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
maxSizeInMB: parseInt(e.target.value),
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
</>
|
||
)}
|
||
|
||
{storageType === StorageType.s3 && (
|
||
<>
|
||
<label className="input w-full my-2">
|
||
{t("Name")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.name}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
name: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Endpoint")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.endPoint}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
endPoint: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Access Key ID")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.accessKeyID}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
accessKeyID: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Secret Access Key")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.secretAccessKey}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
secretAccessKey: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Bucket Name")}
|
||
<input
|
||
type="text"
|
||
className="w-full"
|
||
value={params.bucketName}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
bucketName: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Domain")}
|
||
<input
|
||
type="text"
|
||
placeholder={t("Optional")}
|
||
className="w-full"
|
||
value={params.domain}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
domain: e.target.value,
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
<label className="input w-full my-2">
|
||
{t("Max Size (MB)")}
|
||
<input
|
||
type="number"
|
||
className="validator"
|
||
required
|
||
min="0"
|
||
value={params.maxSizeInMB.toString()}
|
||
onChange={(e) => {
|
||
setParams({
|
||
...params,
|
||
maxSizeInMB: parseInt(e.target.value),
|
||
});
|
||
}}
|
||
/>
|
||
</label>
|
||
</>
|
||
)}
|
||
|
||
{error !== "" && <ErrorAlert message={error} className={"my-2"} />}
|
||
|
||
<div className="modal-action">
|
||
<form method="dialog">
|
||
<button className="btn btn-ghost">{t("Close")}</button>
|
||
</form>
|
||
<button
|
||
className={"btn btn-primary"}
|
||
onClick={handleSubmit}
|
||
type={"button"}
|
||
>
|
||
{isLoading && (
|
||
<span
|
||
className={"loading loading-spinner loading-sm mr-2"}
|
||
></span>
|
||
)}
|
||
{t("Submit")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</dialog>
|
||
</>
|
||
);
|
||
}
|