Management.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import { useEffect, useRef, useState } from "react";
  2. import styled from "styled-components";
  3. import {
  4. Button,
  5. Col,
  6. Collapse,
  7. Icon,
  8. Menu,
  9. Modal,
  10. PopConfirm,
  11. Row,
  12. ScrollIndicator,
  13. Slider,
  14. SplitButton,
  15. Transfer
  16. } from "tiny-ui";
  17. import WLED from "./WLED.js";
  18. const { Panel } = Collapse;
  19. const to255 = (v) => Math.ceil((v / 100) * 255);
  20. const from255 = (v) => Math.ceil((v / 255) * 100);
  21. const download = (filename, data, type = "application/json") => {
  22. const blob = new Blob([data], { type: type });
  23. const href = URL.createObjectURL(blob);
  24. const link = document.createElement("a");
  25. link.href = href;
  26. link.download = filename;
  27. document.body.appendChild(link);
  28. link.click();
  29. document.body.removeChild(link);
  30. };
  31. const StyledRow = styled((props) => <Row {...props} />)`
  32. margin: 0.25rem 0.5rem;
  33. & .action {
  34. text-align: right;
  35. & .ty-icon {
  36. margin-right: 1rem;
  37. }
  38. }
  39. `;
  40. const StyledMenu = styled((props) => <Menu {...props} />)`
  41. & .ty-icon {
  42. margin-right: 1rem;
  43. }
  44. `;
  45. const ScrollingDiv = (props) => {
  46. const targetRef = useRef(null);
  47. return (
  48. <>
  49. <ScrollIndicator fixed={false} target={() => targetRef?.current} />
  50. <div
  51. ref={targetRef}
  52. style={{ overflowY: "scroll", height: 200 }}
  53. {...props}
  54. />
  55. </>
  56. );
  57. };
  58. const UploadModal = ({ data, client, onConfirm, ...rest }) => {
  59. const [payload, setPayload] = useState({});
  60. useEffect(() => {
  61. if (data) setPayload({ ...data });
  62. }, [data]);
  63. const handleConfirm = (_) => {
  64. onConfirm(payload);
  65. };
  66. const handleChange = (e) => {
  67. const fileReader = new FileReader();
  68. fileReader.readAsText(e?.target?.files?.[0], "UTF-8");
  69. fileReader.onload = (e) => {
  70. if (e?.target?.result) {
  71. setPayload({ ...payload, value: JSON.parse(e?.target?.result) });
  72. }
  73. };
  74. };
  75. if (!data) return null;
  76. return (
  77. <Modal
  78. header="Upload preset"
  79. visible={true}
  80. onConfirm={handleConfirm}
  81. {...rest}
  82. >
  83. <h5>Are you sure you want to upload and override presets?</h5>
  84. {payload?.value ? (
  85. <ScrollingDiv>
  86. <pre>{JSON.stringify(payload?.value, null, 2)}</pre>
  87. </ScrollingDiv>
  88. ) : null}
  89. <input type="file" onChange={handleChange} />
  90. </Modal>
  91. );
  92. };
  93. const PurgeModal = ({ data, client, onConfirm, ...rest }) => {
  94. const [payload, setPayload] = useState({});
  95. useEffect(() => {
  96. if (data) setPayload({ ...data });
  97. }, [data]);
  98. const handleConfirm = (_) => {
  99. onConfirm(payload);
  100. };
  101. if (!data) return null;
  102. return (
  103. <Modal
  104. header="Delete preset"
  105. visible={true}
  106. onConfirm={handleConfirm}
  107. {...rest}
  108. >
  109. <h5>Are you sure you want to delete these presets?</h5>
  110. <ScrollingDiv>
  111. <pre>{JSON.stringify(data?.value, null, 2)}</pre>
  112. </ScrollingDiv>
  113. </Modal>
  114. );
  115. };
  116. const CopyModal = ({ data, client, clients, onConfirm, ...rest }) => {
  117. const [transfer, setTransfer] = useState({});
  118. const [payload, setPayload] = useState({});
  119. useEffect(() => {
  120. setPayload({ ...payload, value: data });
  121. }, [data]);
  122. useEffect(() => {
  123. if (clients)
  124. setTransfer(
  125. clients
  126. ?.filter((o) => o !== client)
  127. ?.map((o, i) => ({ key: o, label: o }))
  128. );
  129. }, [clients]);
  130. const handleConfirm = (_) => {
  131. onConfirm(payload);
  132. };
  133. if (!data) return null;
  134. return (
  135. <Modal
  136. header="Copy preset"
  137. visible={true}
  138. onConfirm={handleConfirm}
  139. {...rest}
  140. >
  141. <ScrollingDiv>
  142. <pre>{JSON.stringify(data, null, 2)}</pre>
  143. </ScrollingDiv>
  144. <Row>
  145. <Col>
  146. <Transfer
  147. dataSource={transfer}
  148. defaultValue={[]}
  149. onChange={(value) => {
  150. setPayload({ ...payload, clients: value });
  151. }}
  152. />
  153. </Col>
  154. </Row>
  155. </Modal>
  156. );
  157. };
  158. const InstanceManagement = ({
  159. client,
  160. presets,
  161. onCopy,
  162. onPreset,
  163. onDelete
  164. }) => {
  165. if (!presets) return null;
  166. return Object.keys(presets)?.map((o, i) => (
  167. <StyledRow key={i}>
  168. <Col span={16}>{presets[o]?.name || ""}</Col>
  169. <Col span={8} className="action">
  170. <SplitButton
  171. btnType="primary"
  172. overlay={
  173. <StyledMenu>
  174. <Menu.Item
  175. onClick={(e) => {
  176. download(
  177. `preset_${o}.json`,
  178. JSON.stringify(presets[o], null, 2)
  179. );
  180. }}
  181. >
  182. <Icon name="download" />
  183. Save
  184. </Menu.Item>
  185. <Menu.Item
  186. onClick={(e) => {
  187. onCopy({ [o]: presets[o] });
  188. }}
  189. >
  190. <Icon name="inspection" />
  191. Copy
  192. </Menu.Item>
  193. <Menu.Divider />
  194. <Menu.Item
  195. onClick={(e) => onDelete({ clients: [client], value: [o] })}
  196. >
  197. <Icon name="trash" />
  198. Delete
  199. </Menu.Item>
  200. </StyledMenu>
  201. }
  202. onClick={(e) => {
  203. onPreset({ clients: [client], value: o });
  204. }}
  205. >
  206. <Icon name="checkmark" />
  207. Apply
  208. </SplitButton>
  209. </Col>
  210. </StyledRow>
  211. ));
  212. };
  213. const StateMangement = ({ client, onPower, onSync, onBrightness, current }) => {
  214. if (!current) return null;
  215. return (
  216. <Row>
  217. <Col>
  218. <Button.Group round>
  219. <Button
  220. btnType="bordered"
  221. icon={<Icon name="switch" />}
  222. round
  223. onClick={() =>
  224. onPower({ clients: [client], value: current?.on ? "off" : "on" })
  225. }
  226. >
  227. {current?.on ? "Lights off" : "Lights on"}
  228. </Button>
  229. <PopConfirm
  230. title="Are you sure you want to reboot?"
  231. onConfirm={() => onPower({ clients: [client], value: "reboot" })}
  232. >
  233. <Button icon={<Icon name="loader" />} btnType="bordered" round>
  234. Reboot
  235. </Button>
  236. </PopConfirm>
  237. </Button.Group>
  238. <Button.Group round>
  239. <PopConfirm
  240. title="Are you sure you want to make enable sync?"
  241. onConfirm={() => onSync({ clients: [client], value: "enable" })}
  242. >
  243. <Button btnType="bordered" round disabled={current?.udpSync?.send}>
  244. Enable sync
  245. </Button>
  246. </PopConfirm>
  247. <PopConfirm
  248. title="Are you sure you want to disable sync?"
  249. onConfirm={() => onSync({ clients: [client], value: "disable" })}
  250. >
  251. <Button btnType="bordered" round disabled={!current?.udpSync?.send}>
  252. Disable sync
  253. </Button>
  254. </PopConfirm>
  255. </Button.Group>
  256. </Col>
  257. <Col>
  258. <Slider
  259. defaultValue={from255(current?.brightness)}
  260. onChange={(v) => {
  261. onBrightness({ clients: [client], value: to255(v) });
  262. }}
  263. />
  264. </Col>
  265. </Row>
  266. );
  267. };
  268. const Management = ({
  269. key,
  270. itemKey,
  271. clientName,
  272. onCopy,
  273. onDelete,
  274. onSync,
  275. ...props
  276. }) => {
  277. const [copy, setCopy] = useState(null);
  278. const [purge, setPurge] = useState(null);
  279. const [upload, setUpload] = useState(null);
  280. return (
  281. <Panel
  282. key={key}
  283. itemKey={itemKey}
  284. header={
  285. <>
  286. {clientName} ({props?.client})
  287. </>
  288. }
  289. extra={
  290. <SplitButton
  291. onClick={(e) => {
  292. e.preventDefault();
  293. e.stopPropagation();
  294. }}
  295. overlay={
  296. <StyledMenu>
  297. <Menu.Item
  298. onClick={(e) => {
  299. e.preventDefault();
  300. e.stopPropagation();
  301. window.open(`http://${props?.client}`);
  302. }}
  303. >
  304. <Icon name="eye" />
  305. Open
  306. </Menu.Item>
  307. <Menu.Item
  308. onClick={(e) => {
  309. e.preventDefault();
  310. e.stopPropagation();
  311. download(
  312. `presets.json`,
  313. JSON.stringify(props?.presets, null, 2)
  314. );
  315. }}
  316. >
  317. <Icon name="download" />
  318. Save all
  319. </Menu.Item>
  320. <Menu.Item
  321. onClick={(e) => {
  322. e.preventDefault();
  323. e.stopPropagation();
  324. setUpload({ clients: [props?.client] });
  325. }}
  326. >
  327. <Icon name="upload" />
  328. Restore all
  329. </Menu.Item>
  330. <Menu.Item
  331. onClick={(e) => {
  332. e.preventDefault();
  333. e.stopPropagation();
  334. setCopy(props?.presets);
  335. }}
  336. >
  337. <Icon name="inspection" />
  338. Copy all
  339. </Menu.Item>
  340. <Menu.Divider />
  341. <Menu.Item
  342. onClick={(e) => {
  343. e.preventDefault();
  344. e.stopPropagation();
  345. setPurge({
  346. clients: [props?.client],
  347. value: Object.keys(props?.presets)
  348. });
  349. }}
  350. >
  351. <Icon name="trash" />
  352. Delete all
  353. </Menu.Item>
  354. </StyledMenu>
  355. }
  356. >
  357. <Icon name="setting" />
  358. </SplitButton>
  359. }
  360. >
  361. <StateMangement onSync={onSync} {...props} />
  362. <InstanceManagement
  363. {...props}
  364. onDelete={(e) => {
  365. setPurge(e);
  366. }}
  367. onCopy={(e) => {
  368. setCopy(e);
  369. }}
  370. />
  371. <CopyModal
  372. data={copy}
  373. onConfirm={(e) => {
  374. onCopy(e);
  375. setCopy(null);
  376. }}
  377. onCancel={(e) => setCopy(null)}
  378. {...props}
  379. />
  380. <PurgeModal
  381. data={purge}
  382. onConfirm={(e) => {
  383. onDelete(e);
  384. setPurge(null);
  385. }}
  386. onCancel={(e) => setPurge(null)}
  387. {...props}
  388. />
  389. <UploadModal
  390. data={upload}
  391. onConfirm={(e) => {
  392. onCopy(e);
  393. setUpload(null);
  394. }}
  395. onCancel={(e) => setUpload(null)}
  396. {...props}
  397. />
  398. </Panel>
  399. );
  400. };
  401. export default WLED(Management);