first commit
This commit is contained in:
70
src/component/Admin/Entity/EntityDeleteDialog.tsx
Executable file
70
src/component/Admin/Entity/EntityDeleteDialog.tsx
Executable file
@@ -0,0 +1,70 @@
|
||||
import { Checkbox, DialogContent, FormGroup, Stack, Tooltip } from "@mui/material";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { batchDeleteEntities } from "../../../api/api.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { SmallFormControlLabel } from "../../Common/StyledComponents.tsx";
|
||||
import DialogAccordion from "../../Dialogs/DialogAccordion.tsx";
|
||||
import DraggableDialog, { StyledDialogContentText } from "../../Dialogs/DraggableDialog.tsx";
|
||||
|
||||
export interface EntityDeleteDialogProps {
|
||||
entityID?: number[];
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const EntityDeleteDialog = ({ entityID, open, onDelete, onClose }: EntityDeleteDialogProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [force, setForce] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
if (entityID) {
|
||||
setDeleting?.(true);
|
||||
dispatch(batchDeleteEntities({ ids: entityID, force }))
|
||||
.then(() => {
|
||||
onDelete?.();
|
||||
onClose?.();
|
||||
})
|
||||
.finally(() => {
|
||||
setDeleting?.(false);
|
||||
});
|
||||
}
|
||||
}, [entityID, force, setDeleting]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("common:areYouSure")}
|
||||
showActions
|
||||
loading={deleting}
|
||||
showCancel
|
||||
onAccept={onAccept}
|
||||
dialogProps={{
|
||||
open: open ?? false,
|
||||
onClose: onClose,
|
||||
fullWidth: true,
|
||||
maxWidth: "xs",
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<StyledDialogContentText>{t("entity.confirmBatchDelete", { num: entityID?.length })}</StyledDialogContentText>
|
||||
<DialogAccordion defaultExpanded={force} title={t("application:modals.advanceOptions")}>
|
||||
<FormGroup>
|
||||
<Tooltip title={t("entity.forceDeleteDes")}>
|
||||
<SmallFormControlLabel
|
||||
control={<Checkbox size="small" onChange={(e) => setForce(e.target.checked)} checked={force} />}
|
||||
label={t("entity.forceDelete")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormGroup>
|
||||
</DialogAccordion>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
export default EntityDeleteDialog;
|
||||
84
src/component/Admin/Entity/EntityDialog/EntityDialog.tsx
Executable file
84
src/component/Admin/Entity/EntityDialog/EntityDialog.tsx
Executable file
@@ -0,0 +1,84 @@
|
||||
import { Box, DialogContent } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||
import { getEntityDetail } from "../../../../api/api.ts";
|
||||
import { Entity } from "../../../../api/dashboard.ts";
|
||||
import { useAppDispatch } from "../../../../redux/hooks.ts";
|
||||
import AutoHeight from "../../../Common/AutoHeight.tsx";
|
||||
import FacebookCircularProgress from "../../../Common/CircularProgress.tsx";
|
||||
import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx";
|
||||
import EntityForm from "./EntityForm.tsx";
|
||||
|
||||
export interface EntityDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
entityID?: number;
|
||||
}
|
||||
|
||||
const EntityDialog = ({ open, onClose, entityID }: EntityDialogProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [values, setValues] = useState<Entity>({ edges: {}, id: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!entityID || !open) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
dispatch(getEntityDetail(entityID))
|
||||
.then((res) => {
|
||||
setValues(res);
|
||||
})
|
||||
.catch(() => {
|
||||
onClose();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<DraggableDialog
|
||||
title={t("entity.entityDialogTitle")}
|
||||
dialogProps={{
|
||||
fullWidth: true,
|
||||
maxWidth: "md",
|
||||
open: open,
|
||||
onClose: onClose,
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<AutoHeight>
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
|
||||
classNames="fade"
|
||||
key={`${loading}`}
|
||||
>
|
||||
<Box>
|
||||
{loading && (
|
||||
<Box
|
||||
sx={{
|
||||
py: 15,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<FacebookCircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && <EntityForm values={values} />}
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</AutoHeight>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityDialog;
|
||||
119
src/component/Admin/Entity/EntityDialog/EntityFileList.tsx
Executable file
119
src/component/Admin/Entity/EntityDialog/EntityFileList.tsx
Executable file
@@ -0,0 +1,119 @@
|
||||
import { Box, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { File } from "../../../../api/dashboard";
|
||||
import { FileType } from "../../../../api/explorer";
|
||||
import { sizeToString } from "../../../../util";
|
||||
import {
|
||||
NoWrapCell,
|
||||
NoWrapTableCell,
|
||||
NoWrapTypography,
|
||||
StyledTableContainerPaper,
|
||||
} from "../../../Common/StyledComponents";
|
||||
import TimeBadge from "../../../Common/TimeBadge";
|
||||
import UserAvatar from "../../../Common/User/UserAvatar";
|
||||
import FileTypeIcon from "../../../FileManager/Explorer/FileTypeIcon";
|
||||
import FileDialog from "../../File/FileDialog/FileDialog";
|
||||
import UserDialog from "../../User/UserDialog/UserDialog";
|
||||
|
||||
const EntityFileList = ({ files, userHashIDMap }: { files: File[]; userHashIDMap: Record<number, string> }) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [userDialogOpen, setUserDialogOpen] = useState(false);
|
||||
const [userDialogID, setUserDialogID] = useState<number>(0);
|
||||
const [fileDialogOpen, setFileDialogOpen] = useState(false);
|
||||
const [fileDialogID, setFileDialogID] = useState<number>(0);
|
||||
|
||||
const userClicked = (uid: number) => (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setUserDialogOpen(true);
|
||||
setUserDialogID(uid);
|
||||
};
|
||||
|
||||
const fileClicked = (fid: number) => (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setFileDialogOpen(true);
|
||||
setFileDialogID(fid);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<UserDialog open={userDialogOpen} onClose={() => setUserDialogOpen(false)} userID={userDialogID} />
|
||||
<FileDialog open={fileDialogOpen} onClose={() => setFileDialogOpen(false)} fileID={fileDialogID} />
|
||||
<TableContainer component={StyledTableContainerPaper} sx={{ maxHeight: "300px" }}>
|
||||
<Table size="small" stickyHeader sx={{ width: "100%", tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<NoWrapTableCell width={90}>{t("group.#")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("file.name")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("file.size")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("file.uploader")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={200}>{t("file.createdAt")}</NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{files?.map((option, index) => {
|
||||
return (
|
||||
<TableRow key={option.id} hover sx={{ cursor: "pointer" }} onClick={fileClicked(option.id ?? 0)}>
|
||||
<TableCell>
|
||||
<NoWrapTypography variant="inherit">{option.id}</NoWrapTypography>
|
||||
</TableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<FileTypeIcon name={option.name ?? ""} fileType={FileType.file} />
|
||||
<NoWrapTypography variant="inherit">{option.name}</NoWrapTypography>
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
<TableCell>
|
||||
<NoWrapTypography variant="inherit">{sizeToString(option.size ?? 0)}</NoWrapTypography>
|
||||
</TableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<UserAvatar
|
||||
sx={{ width: 24, height: 24 }}
|
||||
overwriteTextSize
|
||||
user={{
|
||||
id: userHashIDMap[option.owner_id ?? 0] ?? "",
|
||||
nickname: option.edges?.owner?.nick ?? "",
|
||||
created_at: option.edges?.owner?.created_at ?? "",
|
||||
}}
|
||||
/>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<Link
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
onClick={userClicked(option.owner_id ?? 0)}
|
||||
underline="hover"
|
||||
href="#/"
|
||||
>
|
||||
{option.edges?.owner?.nick}
|
||||
</Link>
|
||||
</NoWrapTypography>
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
<TableCell>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<TimeBadge datetime={option.created_at ?? ""} variant="inherit" timeAgoThreshold={0} />
|
||||
</NoWrapTypography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{!files?.length && (
|
||||
<TableRow>
|
||||
<NoWrapCell colSpan={5} align="center">
|
||||
{t("file.noRecords")}
|
||||
</NoWrapCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityFileList;
|
||||
114
src/component/Admin/Entity/EntityDialog/EntityForm.tsx
Executable file
114
src/component/Admin/Entity/EntityDialog/EntityForm.tsx
Executable file
@@ -0,0 +1,114 @@
|
||||
import { Box, Grid2 as Grid, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Entity } from "../../../../api/dashboard";
|
||||
import { EntityType } from "../../../../api/explorer";
|
||||
import { useAppDispatch } from "../../../../redux/hooks";
|
||||
import { sizeToString } from "../../../../util";
|
||||
import { NoWrapTypography } from "../../../Common/StyledComponents";
|
||||
import UserAvatar from "../../../Common/User/UserAvatar";
|
||||
import { EntityTypeText } from "../../../FileManager/Sidebar/Data";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm";
|
||||
import UserDialog from "../../User/UserDialog/UserDialog";
|
||||
import EntityFileList from "./EntityFileList";
|
||||
const EntityForm = ({ values }: { values: Entity }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [userDialogOpen, setUserDialogOpen] = useState(false);
|
||||
const [userDialogID, setUserDialogID] = useState<number>(0);
|
||||
|
||||
const userClicked = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
setUserDialogOpen(true);
|
||||
setUserDialogID(values?.edges?.user?.id ?? 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserDialog open={userDialogOpen} onClose={() => setUserDialogOpen(false)} userID={userDialogID} />
|
||||
<Box>
|
||||
<Grid container spacing={isMobile ? 2 : 3} alignItems={"stretch"}>
|
||||
<SettingForm title={t("file.id")} noContainer lgWidth={2}>
|
||||
<Typography variant={"body2"} color={"textSecondary"}>
|
||||
{values.id}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("file.size")} noContainer lgWidth={2}>
|
||||
<Typography variant={"body2"} color={"textSecondary"}>
|
||||
{sizeToString(values.size ?? 0)}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("file.blobType")} noContainer lgWidth={2}>
|
||||
<Typography variant={"body2"} color={"textSecondary"}>
|
||||
{t(EntityTypeText[values.type ?? EntityType.version])}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("entity.refenenceCount")} noContainer lgWidth={2}>
|
||||
<Typography variant={"body2"} color={"textSecondary"}>
|
||||
{values.reference_count ?? 0}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("file.creator")} noContainer lgWidth={4}>
|
||||
<NoWrapTypography variant={"body2"} color={"textSecondary"}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<UserAvatar
|
||||
sx={{ width: 24, height: 24 }}
|
||||
overwriteTextSize
|
||||
user={{
|
||||
id: values?.user_hash_id ?? "",
|
||||
nickname: values?.edges?.user?.nick ?? "",
|
||||
created_at: values?.edges?.user?.created_at ?? "",
|
||||
}}
|
||||
/>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<Link
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
onClick={userClicked}
|
||||
underline="hover"
|
||||
href="#/"
|
||||
>
|
||||
{values?.edges?.user?.nick}
|
||||
</Link>
|
||||
</NoWrapTypography>
|
||||
</Box>
|
||||
</NoWrapTypography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("entity.uploadSessionID")} noContainer lgWidth={4}>
|
||||
<Typography variant={"body2"} color={"textSecondary"} sx={{ wordBreak: "break-all" }}>
|
||||
{values.upload_session_id ?? "-"}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("file.source")} noContainer lgWidth={4}>
|
||||
<Typography variant={"body2"} color={"textSecondary"} sx={{ wordBreak: "break-all" }}>
|
||||
{values.source ?? "-"}
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("file.storagePolicy")} noContainer lgWidth={4}>
|
||||
<Typography variant={"body2"} color={"textSecondary"}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
underline="hover"
|
||||
to={`/admin/policy/${values.edges?.storage_policy?.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
{values.edges?.storage_policy?.name}
|
||||
</Link>
|
||||
</Typography>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("entity.referredFiles")} noContainer lgWidth={12}>
|
||||
<EntityFileList files={values.edges?.file ?? []} userHashIDMap={values.user_hash_id_map ?? {}} />
|
||||
</SettingForm>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityForm;
|
||||
153
src/component/Admin/Entity/EntityFilterPopover.tsx
Executable file
153
src/component/Admin/Entity/EntityFilterPopover.tsx
Executable file
@@ -0,0 +1,153 @@
|
||||
import { Box, Button, ListItemText, Popover, PopoverProps, Stack } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EntityType } from "../../../api/explorer";
|
||||
import { DenseFilledTextField, DenseSelect } from "../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu";
|
||||
import { EntityTypeText } from "../../FileManager/Sidebar/Data";
|
||||
import SettingForm from "../../Pages/Setting/SettingForm";
|
||||
import SinglePolicySelectionInput from "../Common/SinglePolicySelectionInput";
|
||||
export interface EntityFilterPopoverProps extends PopoverProps {
|
||||
storagePolicy: string;
|
||||
setStoragePolicy: (storagePolicy: string) => void;
|
||||
owner: string;
|
||||
setOwner: (owner: string) => void;
|
||||
type?: EntityType;
|
||||
setType: (type?: EntityType) => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
const EntityFilterPopover = ({
|
||||
storagePolicy,
|
||||
setStoragePolicy,
|
||||
owner,
|
||||
setOwner,
|
||||
type,
|
||||
setType,
|
||||
clearFilters,
|
||||
onClose,
|
||||
open,
|
||||
...rest
|
||||
}: EntityFilterPopoverProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
|
||||
// Create local state to track changes before applying
|
||||
const [localStoragePolicy, setLocalStoragePolicy] = useState(storagePolicy);
|
||||
const [localOwner, setLocalOwner] = useState(owner);
|
||||
const [localType, setLocalType] = useState(type);
|
||||
|
||||
// Initialize local state when popup opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLocalStoragePolicy(storagePolicy);
|
||||
setLocalOwner(owner);
|
||||
setLocalType(type);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Apply filters and close popover
|
||||
const handleApplyFilters = () => {
|
||||
setStoragePolicy(localStoragePolicy);
|
||||
setOwner(localOwner);
|
||||
setType(localType);
|
||||
onClose?.({}, "backdropClick");
|
||||
};
|
||||
|
||||
// Reset filters and close popover
|
||||
const handleResetFilters = () => {
|
||||
setLocalStoragePolicy("");
|
||||
setLocalOwner("");
|
||||
setLocalType(undefined);
|
||||
clearFilters();
|
||||
onClose?.({}, "backdropClick");
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
p: 2,
|
||||
width: 300,
|
||||
maxWidth: "100%",
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
{...rest}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<SettingForm title={t("file.uploaderID")} noContainer lgWidth={12}>
|
||||
<DenseFilledTextField
|
||||
fullWidth
|
||||
value={localOwner}
|
||||
onChange={(e) => setLocalOwner(e.target.value)}
|
||||
placeholder={t("user.emptyNoFilter")}
|
||||
size="small"
|
||||
/>
|
||||
</SettingForm>
|
||||
|
||||
<SettingForm title={t("file.blobType")} noContainer lgWidth={12}>
|
||||
<DenseSelect
|
||||
fullWidth
|
||||
displayEmpty
|
||||
value={localType != undefined ? localType : -1}
|
||||
onChange={(e) => setLocalType(e.target.value === -1 ? undefined : (e.target.value as EntityType))}
|
||||
>
|
||||
{[EntityType.version, EntityType.thumbnail, EntityType.live_photo].map((type) => (
|
||||
<SquareMenuItem key={type} value={type}>
|
||||
<ListItemText
|
||||
primary={t(EntityTypeText[type])}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: "body2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
<SquareMenuItem value={-1}>
|
||||
<ListItemText
|
||||
primary={<em>{t("user.all")}</em>}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: "body2",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
</DenseSelect>
|
||||
</SettingForm>
|
||||
|
||||
<SettingForm title={t("file.storagePolicy")} noContainer lgWidth={12}>
|
||||
<SinglePolicySelectionInput
|
||||
value={localStoragePolicy == "" ? -1 : parseInt(localStoragePolicy)}
|
||||
onChange={(value) => setLocalStoragePolicy(value.toString())}
|
||||
emptyValue={-1}
|
||||
emptyText={t("user.all")}
|
||||
/>
|
||||
</SettingForm>
|
||||
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Button variant="outlined" size="small" onClick={handleResetFilters}>
|
||||
{t("user.reset")}
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={handleApplyFilters}>
|
||||
{t("user.apply")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityFilterPopover;
|
||||
218
src/component/Admin/Entity/EntityRow.tsx
Executable file
218
src/component/Admin/Entity/EntityRow.tsx
Executable file
@@ -0,0 +1,218 @@
|
||||
import { Box, Checkbox, IconButton, Link, Skeleton, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { getEntityUrl } from "../../../api/api";
|
||||
import { Entity } from "../../../api/dashboard";
|
||||
import { EntityType } from "../../../api/explorer";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { sizeToString } from "../../../util";
|
||||
import { NoWrapTableCell, NoWrapTypography, SquareChip } from "../../Common/StyledComponents";
|
||||
import TimeBadge from "../../Common/TimeBadge";
|
||||
import UserAvatar from "../../Common/User/UserAvatar";
|
||||
import { EntityTypeText } from "../../FileManager/Sidebar/Data";
|
||||
import Delete from "../../Icons/Delete";
|
||||
import Download from "../../Icons/Download";
|
||||
|
||||
export interface EntityRowProps {
|
||||
entity?: Entity;
|
||||
loading?: boolean;
|
||||
selected?: boolean;
|
||||
onDelete?: (id: number) => void;
|
||||
onSelect?: (id: number) => void;
|
||||
openEntityDialog?: (id: number) => void;
|
||||
openUserDialog?: (id: number) => void;
|
||||
}
|
||||
|
||||
const EntityRow = ({
|
||||
entity,
|
||||
loading,
|
||||
selected,
|
||||
onDelete,
|
||||
onSelect,
|
||||
openUserDialog,
|
||||
openEntityDialog,
|
||||
}: EntityRowProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const dispatch = useAppDispatch();
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [openLoading, setOpenLoading] = useState(false);
|
||||
|
||||
const onSelectClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onSelect?.(entity?.id ?? 0);
|
||||
};
|
||||
|
||||
const onOpenClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setOpenLoading(true);
|
||||
|
||||
dispatch(getEntityUrl(entity?.id ?? 0))
|
||||
.then((url) => {
|
||||
// 直接下载文件:使用a标签的download属性强制下载
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `entity-${entity?.id}`;
|
||||
link.style.display = 'none';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
})
|
||||
.finally(() => {
|
||||
setOpenLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get entity URL:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const userClicked = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openUserDialog?.(entity?.edges?.user?.id ?? 0);
|
||||
};
|
||||
|
||||
const onDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onDelete?.(entity?.id ?? 0);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<TableRow sx={{ height: "43px" }}>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={30} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={80} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={200} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={50} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={100} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={30} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="text" width={100} />
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
<Skeleton variant="text" width={100} />
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</NoWrapTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
key={entity?.id}
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => openEntityDialog?.(entity?.id ?? 0)}
|
||||
selected={selected}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox size="small" disableRipple color="primary" onClick={onSelectClick} checked={selected} />
|
||||
</TableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">{entity?.id}</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">{t(EntityTypeText[entity?.type ?? EntityType.version])}</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Tooltip title={entity?.source || ""}>
|
||||
<NoWrapTypography variant="inherit">{entity?.source || "-"}</NoWrapTypography>
|
||||
</Tooltip>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
{!entity?.reference_count && <SquareChip size="small" label={t("entity.waitForRecycle")} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">{sizeToString(entity?.size ?? 0)}</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<Link
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
component={RouterLink}
|
||||
underline="hover"
|
||||
to={`/admin/policy/${entity?.edges?.storage_policy?.id}`}
|
||||
>
|
||||
{entity?.edges?.storage_policy?.name || "-"}
|
||||
</Link>
|
||||
</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">{entity?.reference_count ?? 0}</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<TimeBadge datetime={entity?.created_at ?? ""} variant="inherit" timeAgoThreshold={0} />
|
||||
</NoWrapTypography>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<UserAvatar
|
||||
sx={{ width: 24, height: 24 }}
|
||||
overwriteTextSize
|
||||
user={{
|
||||
id: entity?.user_hash_id ?? "",
|
||||
nickname: entity?.edges?.user?.nick ?? "",
|
||||
created_at: entity?.edges?.user?.created_at ?? "",
|
||||
}}
|
||||
/>
|
||||
<NoWrapTypography variant="inherit">
|
||||
<Link
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
onClick={userClicked}
|
||||
underline="hover"
|
||||
href="#/"
|
||||
>
|
||||
{entity?.edges?.user?.nick || "-"}
|
||||
</Link>
|
||||
</NoWrapTypography>
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton size="small" onClick={onOpenClick} disabled={openLoading}>
|
||||
<Download fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={onDeleteClick} disabled={deleteLoading}>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</NoWrapTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityRow;
|
||||
328
src/component/Admin/Entity/EntitySetting.tsx
Executable file
328
src/component/Admin/Entity/EntitySetting.tsx
Executable file
@@ -0,0 +1,328 @@
|
||||
import { Delete } from "@mui/icons-material";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
Divider,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getEntityList } from "../../../api/api";
|
||||
import { AdminListService, Entity } from "../../../api/dashboard";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents";
|
||||
import ArrowSync from "../../Icons/ArrowSync";
|
||||
import Filter from "../../Icons/Filter";
|
||||
import PageContainer from "../../Pages/PageContainer";
|
||||
import PageHeader from "../../Pages/PageHeader";
|
||||
import TablePagination from "../Common/TablePagination";
|
||||
import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting";
|
||||
import UserDialog from "../User/UserDialog/UserDialog";
|
||||
import EntityDeleteDialog from "./EntityDeleteDialog";
|
||||
import EntityDialog from "./EntityDialog/EntityDialog";
|
||||
import EntityFilterPopover from "./EntityFilterPopover";
|
||||
import EntityRow from "./EntityRow";
|
||||
export const StoragePolicyQuery = "storage_policy";
|
||||
export const UserQuery = "user";
|
||||
export const TypeQuery = "type";
|
||||
|
||||
const EntitySetting = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const dispatch = useAppDispatch();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [entities, setEntities] = useState<Entity[]>([]);
|
||||
const [page, setPage] = useQueryState(PageQuery, { defaultValue: "1" });
|
||||
const [pageSize, setPageSize] = useQueryState(PageSizeQuery, {
|
||||
defaultValue: "10",
|
||||
});
|
||||
const [orderBy, setOrderBy] = useQueryState(OrderByQuery, {
|
||||
defaultValue: "",
|
||||
});
|
||||
const [orderDirection, setOrderDirection] = useQueryState(OrderDirectionQuery, { defaultValue: "desc" });
|
||||
const [storagePolicy, setStoragePolicy] = useQueryState(StoragePolicyQuery, { defaultValue: "" });
|
||||
const [user, setUser] = useQueryState(UserQuery, { defaultValue: "" });
|
||||
const [type, setType] = useQueryState(TypeQuery, { defaultValue: "" });
|
||||
const [count, setCount] = useState(0);
|
||||
const [selected, setSelected] = useState<readonly number[]>([]);
|
||||
const filterPopupState = usePopupState({
|
||||
variant: "popover",
|
||||
popupId: "entityFilterPopover",
|
||||
});
|
||||
|
||||
const [userDialogOpen, setUserDialogOpen] = useState(false);
|
||||
const [userDialogID, setUserDialogID] = useState<number | undefined>(undefined);
|
||||
const [entityDialogOpen, setEntityDialogOpen] = useState(false);
|
||||
const [entityDialogID, setEntityDialogID] = useState<number | undefined>(undefined);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogID, setDeleteDialogID] = useState<number[] | undefined>(undefined);
|
||||
|
||||
const pageInt = parseInt(page) ?? 1;
|
||||
const pageSizeInt = parseInt(pageSize) ?? 10;
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setStoragePolicy("");
|
||||
setUser("");
|
||||
setType("");
|
||||
}, [setStoragePolicy, setUser, setType]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntities();
|
||||
}, [page, pageSize, orderBy, orderDirection, storagePolicy, user, type]);
|
||||
|
||||
const fetchEntities = () => {
|
||||
setLoading(true);
|
||||
setSelected([]);
|
||||
|
||||
const params: AdminListService = {
|
||||
page: pageInt,
|
||||
page_size: pageSizeInt,
|
||||
order_by: orderBy ?? "",
|
||||
order_direction: orderDirection ?? "desc",
|
||||
conditions: {},
|
||||
};
|
||||
|
||||
if (storagePolicy) {
|
||||
params.conditions!.entity_policy = storagePolicy;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
params.conditions!.entity_user = user;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
params.conditions!.entity_type = type;
|
||||
}
|
||||
|
||||
dispatch(getEntityList(params))
|
||||
.then((res) => {
|
||||
setEntities(res.entities);
|
||||
setPageSize(res.pagination.page_size.toString());
|
||||
setCount(res.pagination.total_items ?? 0);
|
||||
setLoading(false);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogID(Array.from(selected));
|
||||
};
|
||||
|
||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
const newSelected = entities.map((n) => n.id);
|
||||
setSelected(newSelected);
|
||||
return;
|
||||
}
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(id: number) => {
|
||||
const selectedIndex = selected.indexOf(id);
|
||||
let newSelected: readonly number[] = [];
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, id);
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1));
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1));
|
||||
}
|
||||
setSelected(newSelected);
|
||||
},
|
||||
[selected],
|
||||
);
|
||||
|
||||
const orderById = orderBy === "id" || orderBy === "";
|
||||
const direction = orderDirection as "asc" | "desc";
|
||||
const onSortClick = (field: string) => () => {
|
||||
const alreadySorted = orderBy === field || (field === "id" && orderById);
|
||||
setOrderBy(field);
|
||||
setOrderDirection(alreadySorted ? (direction === "asc" ? "desc" : "asc") : "asc");
|
||||
};
|
||||
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return !!(storagePolicy || user || type);
|
||||
}, [storagePolicy, user, type]);
|
||||
|
||||
const handleUserDialogOpen = (id: number) => {
|
||||
setUserDialogID(id);
|
||||
setUserDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEntityDialogOpen = (id: number) => {
|
||||
setEntityDialogID(id);
|
||||
setEntityDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSingleDelete = (id: number) => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogID([id]);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<EntityDialog open={entityDialogOpen} onClose={() => setEntityDialogOpen(false)} entityID={entityDialogID} />
|
||||
<UserDialog open={userDialogOpen} onClose={() => setUserDialogOpen(false)} userID={userDialogID} />
|
||||
<EntityDeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
entityID={deleteDialogID}
|
||||
onDelete={fetchEntities}
|
||||
/>
|
||||
<Container maxWidth="xl">
|
||||
<PageHeader title={t("dashboard:nav.entities")} />
|
||||
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
|
||||
<EntityFilterPopover
|
||||
{...bindPopover(filterPopupState)}
|
||||
storagePolicy={storagePolicy}
|
||||
setStoragePolicy={setStoragePolicy}
|
||||
owner={user}
|
||||
setOwner={setUser}
|
||||
type={type !== "" ? parseInt(type) : undefined}
|
||||
setType={(type) => setType(type !== undefined ? type.toString() : "")}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
|
||||
<SecondaryButton onClick={fetchEntities} disabled={loading} variant={"contained"} startIcon={<ArrowSync />}>
|
||||
{t("node.refresh")}
|
||||
</SecondaryButton>
|
||||
|
||||
<Badge color="primary" variant="dot" invisible={!hasActiveFilters}>
|
||||
<SecondaryButton startIcon={<Filter />} variant="contained" {...bindTrigger(filterPopupState)}>
|
||||
{t("user.filter")}
|
||||
</SecondaryButton>
|
||||
</Badge>
|
||||
|
||||
{selected.length > 0 && !isMobile && (
|
||||
<>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Button startIcon={<Delete />} variant="contained" color="error" onClick={handleDelete}>
|
||||
{t("entity.deleteXEntities", { num: selected.length })}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{isMobile && selected.length > 0 && (
|
||||
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
|
||||
<Button startIcon={<Delete />} variant="contained" color="error" onClick={handleDelete}>
|
||||
{t("entity.deleteXEntities", { num: selected.length })}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
<TableContainer component={StyledTableContainerPaper} sx={{ mt: 2 }}>
|
||||
<Table size="small" stickyHeader sx={{ width: "100%", tableLayout: "fixed" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox" sx={{ width: "36px!important" }} width={50}>
|
||||
<Checkbox
|
||||
size="small"
|
||||
indeterminate={selected.length > 0 && selected.length < entities.length}
|
||||
checked={entities.length > 0 && selected.length === entities.length}
|
||||
onChange={handleSelectAllClick}
|
||||
/>
|
||||
</TableCell>
|
||||
<NoWrapTableCell width={80}>
|
||||
<TableSortLabel
|
||||
active={orderById}
|
||||
direction={orderById ? direction : "asc"}
|
||||
onClick={onSortClick("id")}
|
||||
>
|
||||
{t("group.#")}
|
||||
</TableSortLabel>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>{t("file.blobType")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={250}>{t("file.source")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "size"}
|
||||
direction={orderBy === "size" ? direction : "asc"}
|
||||
onClick={onSortClick("size")}
|
||||
>
|
||||
{t("file.size")}
|
||||
</TableSortLabel>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell width={150}>{t("file.storagePolicy")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "reference_count"}
|
||||
direction={orderBy === "reference_count" ? direction : "asc"}
|
||||
onClick={onSortClick("reference_count")}
|
||||
>
|
||||
{t("entity.refenenceCount")}
|
||||
</TableSortLabel>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell width={180}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "created_at"}
|
||||
direction={orderBy === "created_at" ? direction : "asc"}
|
||||
onClick={onSortClick("created_at")}
|
||||
>
|
||||
{t("file.createdAt")}
|
||||
</TableSortLabel>
|
||||
</NoWrapTableCell>
|
||||
<NoWrapTableCell width={180}>{t("file.creator")}</NoWrapTableCell>
|
||||
<NoWrapTableCell width={100}></NoWrapTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!loading &&
|
||||
entities.map((entity) => (
|
||||
<EntityRow
|
||||
key={entity.id}
|
||||
entity={entity}
|
||||
onDelete={handleSingleDelete}
|
||||
selected={selected.indexOf(entity.id) !== -1}
|
||||
onSelect={handleSelect}
|
||||
openUserDialog={handleUserDialogOpen}
|
||||
openEntityDialog={handleEntityDialogOpen}
|
||||
/>
|
||||
))}
|
||||
{loading &&
|
||||
entities.length > 0 &&
|
||||
entities.slice(0, 10).map((entity) => <EntityRow key={`loading-${entity.id}`} loading={true} />)}
|
||||
{loading &&
|
||||
entities.length === 0 &&
|
||||
Array.from(Array(10)).map((_, index) => <EntityRow key={`loading-${index}`} loading={true} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{count > 0 && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<TablePagination
|
||||
page={pageInt}
|
||||
totalItems={count}
|
||||
rowsPerPage={pageSizeInt}
|
||||
rowsPerPageOptions={[10, 25, 50, 100, 200, 500]}
|
||||
onRowsPerPageChange={(value) => setPageSize(value.toString())}
|
||||
onChange={(_, value) => setPage(value.toString())}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntitySetting;
|
||||
Reference in New Issue
Block a user