profile-more.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import dayjs from "dayjs";
  2. import { useState } from "react";
  3. import { useTranslation } from "react-i18next";
  4. import { useLockFn } from "ahooks";
  5. import {
  6. Box,
  7. Badge,
  8. Chip,
  9. Typography,
  10. MenuItem,
  11. Menu,
  12. IconButton,
  13. } from "@mui/material";
  14. import { FeaturedPlayListRounded } from "@mui/icons-material";
  15. import { viewProfile } from "@/services/cmds";
  16. import { Notice } from "@/components/base";
  17. import { EditorViewer } from "./editor-viewer";
  18. import { ProfileBox } from "./profile-box";
  19. import { LogViewer } from "./log-viewer";
  20. import { ConfirmViewer } from "./confirm-viewer";
  21. interface Props {
  22. selected: boolean;
  23. itemData: IProfileItem;
  24. enableNum: number;
  25. logInfo?: [string, string][];
  26. onEnable: () => void;
  27. onDisable: () => void;
  28. onMoveTop: () => void;
  29. onMoveEnd: () => void;
  30. onDelete: () => void;
  31. onEdit: () => void;
  32. }
  33. // profile enhanced item
  34. export const ProfileMore = (props: Props) => {
  35. const {
  36. selected,
  37. itemData,
  38. enableNum,
  39. logInfo = [],
  40. onEnable,
  41. onDisable,
  42. onMoveTop,
  43. onMoveEnd,
  44. onDelete,
  45. onEdit,
  46. } = props;
  47. const { uid, type } = itemData;
  48. const { t, i18n } = useTranslation();
  49. const [anchorEl, setAnchorEl] = useState<any>(null);
  50. const [position, setPosition] = useState({ left: 0, top: 0 });
  51. const [fileOpen, setFileOpen] = useState(false);
  52. const [confirmOpen, setConfirmOpen] = useState(false);
  53. const [logOpen, setLogOpen] = useState(false);
  54. const onEditInfo = () => {
  55. setAnchorEl(null);
  56. onEdit();
  57. };
  58. const onEditFile = () => {
  59. setAnchorEl(null);
  60. setFileOpen(true);
  61. };
  62. const onOpenFile = useLockFn(async () => {
  63. setAnchorEl(null);
  64. try {
  65. await viewProfile(itemData.uid);
  66. } catch (err: any) {
  67. Notice.error(err?.message || err.toString());
  68. }
  69. });
  70. const fnWrapper = (fn: () => void) => () => {
  71. setAnchorEl(null);
  72. return fn();
  73. };
  74. const hasError = !!logInfo.find((e) => e[0] === "exception");
  75. const showMove = enableNum > 1 && !hasError;
  76. const enableMenu = [
  77. { label: "Disable", handler: fnWrapper(onDisable) },
  78. { label: "Edit Info", handler: onEditInfo },
  79. { label: "Edit File", handler: onEditFile },
  80. { label: "Open File", handler: onOpenFile },
  81. { label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) },
  82. { label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) },
  83. {
  84. label: "Delete",
  85. handler: () => {
  86. setConfirmOpen(true);
  87. },
  88. },
  89. ];
  90. const disableMenu = [
  91. { label: "Enable", handler: fnWrapper(onEnable) },
  92. { label: "Edit Info", handler: onEditInfo },
  93. { label: "Edit File", handler: onEditFile },
  94. { label: "Open File", handler: onOpenFile },
  95. {
  96. label: "Delete",
  97. handler: () => {
  98. setConfirmOpen(true);
  99. },
  100. },
  101. ];
  102. const boxStyle = {
  103. height: 26,
  104. display: "flex",
  105. alignItems: "center",
  106. justifyContent: "space-between",
  107. lineHeight: 1,
  108. };
  109. return (
  110. <>
  111. <ProfileBox
  112. aria-selected={selected}
  113. onDoubleClick={onEditFile}
  114. // onClick={() => onSelect(false)}
  115. onContextMenu={(event) => {
  116. const { clientX, clientY } = event;
  117. setPosition({ top: clientY, left: clientX });
  118. setAnchorEl(event.currentTarget);
  119. event.preventDefault();
  120. }}
  121. >
  122. <Box
  123. display="flex"
  124. justifyContent="space-between"
  125. alignItems="center"
  126. mb={0.5}
  127. >
  128. <Typography
  129. width="calc(100% - 52px)"
  130. variant="h6"
  131. component="h2"
  132. noWrap
  133. title={itemData.name}
  134. >
  135. {itemData.name}
  136. </Typography>
  137. <Chip
  138. label={type}
  139. color="primary"
  140. size="small"
  141. variant="outlined"
  142. sx={{ height: 20, textTransform: "capitalize" }}
  143. />
  144. </Box>
  145. <Box sx={boxStyle}>
  146. {selected && type === "script" ? (
  147. hasError ? (
  148. <Badge color="error" variant="dot" overlap="circular">
  149. <IconButton
  150. size="small"
  151. edge="start"
  152. color="error"
  153. title="Console"
  154. onClick={() => setLogOpen(true)}
  155. >
  156. <FeaturedPlayListRounded fontSize="inherit" />
  157. </IconButton>
  158. </Badge>
  159. ) : (
  160. <IconButton
  161. size="small"
  162. edge="start"
  163. color="inherit"
  164. title="Console"
  165. onClick={() => setLogOpen(true)}
  166. >
  167. <FeaturedPlayListRounded fontSize="inherit" />
  168. </IconButton>
  169. )
  170. ) : (
  171. <Typography
  172. noWrap
  173. title={itemData.desc}
  174. sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
  175. >
  176. {itemData.desc}
  177. </Typography>
  178. )}
  179. </Box>
  180. </ProfileBox>
  181. <Menu
  182. open={!!anchorEl}
  183. anchorEl={anchorEl}
  184. onClose={() => setAnchorEl(null)}
  185. anchorPosition={position}
  186. anchorReference="anchorPosition"
  187. transitionDuration={225}
  188. MenuListProps={{ sx: { py: 0.5 } }}
  189. onContextMenu={(e) => {
  190. setAnchorEl(null);
  191. e.preventDefault();
  192. }}
  193. >
  194. {(selected ? enableMenu : disableMenu)
  195. .filter((item: any) => item.show !== false)
  196. .map((item) => (
  197. <MenuItem
  198. key={item.label}
  199. onClick={item.handler}
  200. sx={[
  201. { minWidth: 120 },
  202. (theme) => {
  203. return {
  204. color:
  205. item.label === "Delete"
  206. ? theme.palette.error.main
  207. : undefined,
  208. };
  209. },
  210. ]}
  211. dense
  212. >
  213. {t(item.label)}
  214. </MenuItem>
  215. ))}
  216. </Menu>
  217. <EditorViewer
  218. uid={uid}
  219. open={fileOpen}
  220. mode={type === "merge" ? "yaml" : "javascript"}
  221. onClose={() => setFileOpen(false)}
  222. />
  223. <ConfirmViewer
  224. title="Confirm deletion"
  225. message="This operation is not reversible"
  226. open={confirmOpen}
  227. onClose={() => setConfirmOpen(false)}
  228. onConfirm={() => {
  229. onDelete();
  230. setConfirmOpen(false);
  231. }}
  232. />
  233. {selected && (
  234. <LogViewer
  235. open={logOpen}
  236. logInfo={logInfo}
  237. onClose={() => setLogOpen(false)}
  238. />
  239. )}
  240. </>
  241. );
  242. };
  243. function parseExpire(expire?: number) {
  244. if (!expire) return "-";
  245. return dayjs(expire * 1000).format("YYYY-MM-DD");
  246. }