Management.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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 = ({
  214. client,
  215. setPurge,
  216. setUpload,
  217. setCopy,
  218. onPower,
  219. onSync,
  220. onBrightness,
  221. onPreset,
  222. current,
  223. ...props
  224. }) => {
  225. if (!current) return null;
  226. return (
  227. <Row>
  228. <Col span={12}>
  229. <Button
  230. btnType={current?.on ? "primary" : "outline"}
  231. icon={<Icon name="switch" />}
  232. round
  233. onClick={() =>
  234. onPower({ clients: [client], value: current?.on ? "off" : "on" })
  235. }
  236. >
  237. Lights {current?.on ? "on" : "off"}
  238. </Button>
  239. <PopConfirm
  240. title="Are you sure you want to make enable sync?"
  241. onConfirm={() => onSync({ clients: [client], value: "enable" })}
  242. >
  243. <Button btnType="outline" 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="outline" round disabled={!current?.udpSync?.send}>
  252. Disable sync
  253. </Button>
  254. </PopConfirm>
  255. <PopConfirm
  256. title="Are you sure you want to reboot?"
  257. onConfirm={() => onPower({ clients: [client], value: "reboot" })}
  258. >
  259. <Button icon={<Icon name="loader" />} btnType="danger" round>
  260. Reboot
  261. </Button>
  262. </PopConfirm>
  263. </Col>
  264. <Col span={12} style={{ textAlign: "right" }}>
  265. <Button.Group round>
  266. <Button
  267. btnType="secondary"
  268. icon={<Icon name="arrow-left" />}
  269. title="Previous preset"
  270. round
  271. onClick={() => {
  272. onPreset({ clients: [client], value: 249 });
  273. }}
  274. />
  275. <Button
  276. btnType="secondary"
  277. icon={<Icon name="arrow-right" />}
  278. title="Next preset"
  279. round
  280. onClick={() => {
  281. onPreset({ clients: [client], value: 250 });
  282. }}
  283. />
  284. </Button.Group>
  285. <SplitButton
  286. onClick={(e) => {
  287. e.preventDefault();
  288. e.stopPropagation();
  289. window.open(`http://${client}`);
  290. }}
  291. overlay={
  292. <StyledMenu>
  293. <Menu.Item
  294. onClick={(e) => {
  295. e.preventDefault();
  296. e.stopPropagation();
  297. window.open(`http://${client}`);
  298. }}
  299. >
  300. <Icon name="eye" />
  301. Open
  302. </Menu.Item>
  303. <Menu.Item
  304. onClick={(e) => {
  305. e.preventDefault();
  306. e.stopPropagation();
  307. download(
  308. `presets.json`,
  309. JSON.stringify(props?.presets, null, 2)
  310. );
  311. }}
  312. >
  313. <Icon name="download" />
  314. Save all
  315. </Menu.Item>
  316. <Menu.Item
  317. onClick={(e) => {
  318. e.preventDefault();
  319. e.stopPropagation();
  320. setUpload({ clients: [props?.client] });
  321. }}
  322. >
  323. <Icon name="upload" />
  324. Restore all
  325. </Menu.Item>
  326. <Menu.Item
  327. onClick={(e) => {
  328. e.preventDefault();
  329. e.stopPropagation();
  330. setCopy(props?.presets);
  331. }}
  332. >
  333. <Icon name="inspection" />
  334. Copy all
  335. </Menu.Item>
  336. <Menu.Divider />
  337. <Menu.Item
  338. onClick={(e) => {
  339. e.preventDefault();
  340. e.stopPropagation();
  341. setPurge({
  342. clients: [props?.client],
  343. value: Object.keys(props?.presets),
  344. });
  345. }}
  346. >
  347. <Icon name="trash" />
  348. Delete all
  349. </Menu.Item>
  350. </StyledMenu>
  351. }
  352. round
  353. >
  354. <Icon name="setting" />
  355. </SplitButton>
  356. </Col>
  357. <Col>
  358. <Slider
  359. defaultValue={from255(current?.brightness)}
  360. onChange={(v) => {
  361. onBrightness({ clients: [client], value: to255(v) });
  362. }}
  363. />
  364. </Col>
  365. </Row>
  366. );
  367. };
  368. const Management = ({
  369. key,
  370. itemKey,
  371. clientName,
  372. onCopy,
  373. onDelete,
  374. onSync,
  375. ...props
  376. }) => {
  377. const [copy, setCopy] = useState(null);
  378. const [purge, setPurge] = useState(null);
  379. const [upload, setUpload] = useState(null);
  380. return (
  381. <Panel
  382. key={key}
  383. itemKey={itemKey}
  384. header={
  385. <>
  386. {clientName} ({props?.client})
  387. </>
  388. }
  389. >
  390. <StateMangement
  391. setPurge={setPurge}
  392. setCopy={setCopy}
  393. setUpload={setUpload}
  394. onSync={onSync}
  395. {...props}
  396. />
  397. <InstanceManagement
  398. {...props}
  399. onDelete={(e) => {
  400. setPurge(e);
  401. }}
  402. onCopy={(e) => {
  403. setCopy(e);
  404. }}
  405. />
  406. <CopyModal
  407. data={copy}
  408. onConfirm={(e) => {
  409. onCopy(e);
  410. setCopy(null);
  411. }}
  412. onCancel={(e) => setCopy(null)}
  413. {...props}
  414. />
  415. <PurgeModal
  416. data={purge}
  417. onConfirm={(e) => {
  418. onDelete(e);
  419. setPurge(null);
  420. }}
  421. onCancel={(e) => setPurge(null)}
  422. {...props}
  423. />
  424. <UploadModal
  425. data={upload}
  426. onConfirm={(e) => {
  427. onCopy(e);
  428. setUpload(null);
  429. }}
  430. onCancel={(e) => setUpload(null)}
  431. {...props}
  432. />
  433. </Panel>
  434. );
  435. };
  436. export default WLED(Management);