2
0

KeyValueArray.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import React from "react";
  2. import { GetFinalVariablesFunction, KeyValueArrayField, KeyValueArrayFieldState } from "../types";
  3. import sliders from "../../../assets/sliders.svg";
  4. import upload from "../../../assets/upload.svg";
  5. import styled from "styled-components";
  6. import useFormField from "../hooks/useFormField";
  7. import Modal from "../../../main/home/modals/Modal";
  8. import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
  9. import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
  10. interface Props extends KeyValueArrayField {
  11. id: string;
  12. }
  13. const KeyValueArray: React.FC<Props> = (props) => {
  14. const { state, setState, variables } = useFormField<KeyValueArrayFieldState>(
  15. props.id,
  16. {
  17. initState: {
  18. values:
  19. props.value && props.value[0]
  20. ? (Object.entries(props.value[0])?.map(([k, v]) => {
  21. return { key: k, value: v };
  22. }) as any[])
  23. : [],
  24. showEnvModal: false,
  25. showEditorModal: false,
  26. },
  27. }
  28. );
  29. if (state == undefined) return <></>;
  30. const parseEnv = (src: any, options: any) => {
  31. const debug = Boolean(options && options.debug);
  32. const obj = {} as Record<string, string>;
  33. const NEWLINE = "\n";
  34. const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/;
  35. const RE_NEWLINES = /\\n/g;
  36. const NEWLINES_MATCH = /\n|\r|\r\n/;
  37. // convert Buffers before splitting into lines and processing
  38. src
  39. .toString()
  40. .split(NEWLINES_MATCH)
  41. .forEach(function (line: any, idx: any) {
  42. // matching "KEY' and 'VAL' in 'KEY=VAL'
  43. const keyValueArr = line.match(RE_INI_KEY_VAL);
  44. // matched?
  45. if (keyValueArr != null) {
  46. const key = keyValueArr[1];
  47. // default undefined or missing values to empty string
  48. let val = keyValueArr[2] || "";
  49. const end = val.length - 1;
  50. const isDoubleQuoted = val[0] === '"' && val[end] === '"';
  51. const isSingleQuoted = val[0] === "'" && val[end] === "'";
  52. // if single or double quoted, remove quotes
  53. if (isSingleQuoted || isDoubleQuoted) {
  54. val = val.substring(1, end);
  55. // if double quoted, expand newlines
  56. if (isDoubleQuoted) {
  57. val = val.replace(RE_NEWLINES, NEWLINE);
  58. }
  59. } else {
  60. // remove surrounding whitespace
  61. val = val.trim();
  62. }
  63. obj[key] = val;
  64. } else if (debug) {
  65. console.log(
  66. `did not match key and value when parsing line ${idx + 1}: ${line}`
  67. );
  68. }
  69. });
  70. return obj;
  71. };
  72. const readFile = (env: string) => {
  73. let envObj = parseEnv(env, null);
  74. let push = true;
  75. for (let key in envObj) {
  76. for (var i = 0; i < state.values.length; i++) {
  77. let existingKey = state.values[i]["key"];
  78. if (key === existingKey) {
  79. state.values[i]["value"] = envObj[key];
  80. push = false;
  81. }
  82. }
  83. if (push) {
  84. setState((prev) => {
  85. return {
  86. values: [...prev.values, { key, value: envObj[key] }],
  87. };
  88. });
  89. }
  90. }
  91. };
  92. const renderEditorModal = () => {
  93. if (state.showEditorModal) {
  94. return (
  95. <Modal
  96. onRequestClose={() =>
  97. setState(() => {
  98. return { showEditorModal: false };
  99. })
  100. }
  101. width="60%"
  102. height="80%"
  103. >
  104. <EnvEditorModal
  105. closeModal={() =>
  106. setState(() => {
  107. return { showEditorModal: false };
  108. })
  109. }
  110. setEnvVariables={(envFile: string) => readFile(envFile)}
  111. />
  112. </Modal>
  113. );
  114. }
  115. };
  116. const getProcessedValues = (
  117. objectArray: { key: string; value: string }[]
  118. ): any => {
  119. let obj = {} as any;
  120. objectArray?.forEach(({ key, value }) => {
  121. obj[key] = value;
  122. });
  123. return obj;
  124. };
  125. const renderEnvModal = () => {
  126. if (state.showEnvModal) {
  127. return (
  128. <Modal
  129. onRequestClose={() =>
  130. setState(() => {
  131. return { showEnvModal: false };
  132. })
  133. }
  134. width="765px"
  135. height="542px"
  136. >
  137. <LoadEnvGroupModal
  138. existingValues={getProcessedValues(state.values)}
  139. namespace={variables.namespace}
  140. clusterId={variables.clusterId}
  141. closeModal={() =>
  142. setState(() => {
  143. return {
  144. showEnvModal: false,
  145. };
  146. })
  147. }
  148. setValues={(values) => {
  149. setState((prev) => {
  150. return {
  151. // might be broken
  152. values: [
  153. ...prev.values,
  154. ...Object.entries(values)?.map(([k, v]) => {
  155. return {
  156. key: k,
  157. value: v,
  158. };
  159. }),
  160. ],
  161. };
  162. });
  163. }}
  164. />
  165. </Modal>
  166. );
  167. }
  168. };
  169. const renderDeleteButton = (i: number) => {
  170. if (!props.isReadOnly) {
  171. return (
  172. <DeleteButton
  173. onClick={() => {
  174. state.values.splice(i, 1);
  175. setState((prev) => {
  176. return {
  177. values: prev.values
  178. .slice(0, i + 1)
  179. .concat(prev.values.slice(i + 1, prev.values.length)),
  180. };
  181. });
  182. }}
  183. >
  184. <i className="material-icons">cancel</i>
  185. </DeleteButton>
  186. );
  187. }
  188. };
  189. const renderHiddenOption = (hidden: boolean, i: number) => {
  190. if (props.secretOption && hidden) {
  191. return (
  192. <HideButton>
  193. <i className="material-icons">lock</i>
  194. </HideButton>
  195. );
  196. }
  197. };
  198. const renderInputList = () => {
  199. return (
  200. <>
  201. {state.values?.map((entry: any, i: number) => {
  202. // Preprocess non-string env values set via raw Helm values
  203. let { value } = entry;
  204. if (typeof value === "object") {
  205. value = JSON.stringify(value);
  206. } else if (typeof value === "number" || typeof value === "boolean") {
  207. value = value.toString();
  208. }
  209. return (
  210. <InputWrapper key={i}>
  211. <Input
  212. placeholder="ex: key"
  213. width="270px"
  214. value={entry.key}
  215. onChange={(e: any) => {
  216. e.persist();
  217. setState((prev) => {
  218. return {
  219. values: prev.values?.map((t, j) => {
  220. if (j == i) {
  221. return {
  222. ...t,
  223. key: e.target.value,
  224. };
  225. }
  226. return t;
  227. }),
  228. };
  229. });
  230. }}
  231. disabled={props.isReadOnly || value.includes("PORTERSECRET")}
  232. spellCheck={false}
  233. />
  234. <Spacer />
  235. <Input
  236. placeholder="ex: value"
  237. width="270px"
  238. value={value}
  239. onChange={(e: any) => {
  240. e.persist();
  241. setState((prev) => {
  242. return {
  243. values: prev.values?.map((t, j) => {
  244. if (j == i) {
  245. return {
  246. ...t,
  247. value: e.target.value,
  248. };
  249. }
  250. return t;
  251. }),
  252. };
  253. });
  254. }}
  255. disabled={props.isReadOnly || value.includes("PORTERSECRET")}
  256. type={value.includes("PORTERSECRET") ? "password" : "text"}
  257. spellCheck={false}
  258. />
  259. {renderDeleteButton(i)}
  260. {renderHiddenOption(value.includes("PORTERSECRET"), i)}
  261. </InputWrapper>
  262. );
  263. })}
  264. </>
  265. );
  266. };
  267. return (
  268. <>
  269. <StyledInputArray>
  270. <Label>{props.label}</Label>
  271. {state.values.length === 0 ? <></> : renderInputList()}
  272. {props.isReadOnly ? (
  273. <></>
  274. ) : (
  275. <InputWrapper>
  276. <AddRowButton
  277. onClick={() => {
  278. setState((prev) => {
  279. return {
  280. values: [...prev.values, { key: "", value: "" }],
  281. };
  282. });
  283. }}
  284. >
  285. <i className="material-icons">add</i> Add Row
  286. </AddRowButton>
  287. <Spacer />
  288. {variables.namespace && props.envLoader && (
  289. <LoadButton
  290. onClick={() =>
  291. setState((prev) => {
  292. return {
  293. showEnvModal: !prev.showEnvModal,
  294. };
  295. })
  296. }
  297. >
  298. <img src={sliders} /> Load from Env Group
  299. </LoadButton>
  300. )}
  301. {props.fileUpload && (
  302. <UploadButton
  303. onClick={() => {
  304. setState((prev) => {
  305. return {
  306. showEditorModal: true,
  307. };
  308. });
  309. }}
  310. >
  311. <img src={upload} /> Copy from File
  312. </UploadButton>
  313. )}
  314. </InputWrapper>
  315. )}
  316. </StyledInputArray>
  317. {renderEnvModal()}
  318. {renderEditorModal()}
  319. </>
  320. );
  321. };
  322. export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
  323. vars,
  324. props: KeyValueArrayField,
  325. state: KeyValueArrayFieldState
  326. ) => {
  327. if (!state) {
  328. return {
  329. [props.variable]: props.value ? props.value[0] : [],
  330. };
  331. }
  332. let obj = {} as any;
  333. const rg = /(?:^|[^\\])(\\n)/g;
  334. const fixNewlines = (s: string) => {
  335. while (rg.test(s)) {
  336. s = s.replace(rg, (str) => {
  337. if (str.length == 2) return "\n";
  338. if (str[0] != "\\") return str[0] + "\n";
  339. return "\\n";
  340. });
  341. }
  342. return s;
  343. };
  344. const isNumber = (s: string) => {
  345. return !isNaN(!s ? NaN : Number(String(s).trim()));
  346. };
  347. state.values.forEach((entry: any, i: number) => {
  348. if (isNumber(entry.value)) {
  349. obj[entry.key] = entry.value;
  350. } else {
  351. obj[entry.key] = fixNewlines(entry.value);
  352. }
  353. });
  354. return {
  355. [props.variable]: obj,
  356. };
  357. };
  358. export default KeyValueArray;
  359. const Spacer = styled.div`
  360. width: 10px;
  361. height: 20px;
  362. `;
  363. const AddRowButton = styled.div`
  364. display: flex;
  365. align-items: center;
  366. width: 270px;
  367. font-size: 13px;
  368. color: #aaaabb;
  369. height: 32px;
  370. border-radius: 3px;
  371. cursor: pointer;
  372. background: #ffffff11;
  373. :hover {
  374. background: #ffffff22;
  375. }
  376. > i {
  377. color: #ffffff44;
  378. font-size: 16px;
  379. margin-left: 8px;
  380. margin-right: 10px;
  381. display: flex;
  382. align-items: center;
  383. justify-content: center;
  384. }
  385. `;
  386. const LoadButton = styled(AddRowButton)`
  387. background: none;
  388. border: 1px solid #ffffff55;
  389. > i {
  390. color: #ffffff44;
  391. font-size: 16px;
  392. margin-left: 8px;
  393. margin-right: 10px;
  394. display: flex;
  395. align-items: center;
  396. justify-content: center;
  397. }
  398. > img {
  399. width: 14px;
  400. margin-left: 10px;
  401. margin-right: 12px;
  402. }
  403. `;
  404. const UploadButton = styled(AddRowButton)`
  405. background: none;
  406. position: relative;
  407. margin-left: 10px;
  408. border: 1px solid #ffffff55;
  409. > i {
  410. color: #ffffff44;
  411. font-size: 16px;
  412. margin-left: 8px;
  413. margin-right: 10px;
  414. display: flex;
  415. align-items: center;
  416. justify-content: center;
  417. }
  418. > img {
  419. width: 14px;
  420. margin-left: 10px;
  421. margin-right: 12px;
  422. }
  423. `;
  424. const DeleteButton = styled.div`
  425. width: 15px;
  426. height: 15px;
  427. display: flex;
  428. align-items: center;
  429. margin-left: 8px;
  430. margin-top: -3px;
  431. justify-content: center;
  432. > i {
  433. font-size: 17px;
  434. color: #ffffff44;
  435. display: flex;
  436. align-items: center;
  437. justify-content: center;
  438. cursor: pointer;
  439. :hover {
  440. color: #ffffff88;
  441. }
  442. }
  443. `;
  444. const HideButton = styled(DeleteButton)`
  445. margin-top: -5px;
  446. > i {
  447. font-size: 19px;
  448. cursor: default;
  449. :hover {
  450. color: #ffffff44;
  451. }
  452. }
  453. `;
  454. const InputWrapper = styled.div`
  455. display: flex;
  456. align-items: center;
  457. margin-top: 5px;
  458. `;
  459. const Input = styled.input`
  460. outline: none;
  461. border: none;
  462. margin-bottom: 5px;
  463. font-size: 13px;
  464. background: #ffffff11;
  465. border: 1px solid #ffffff55;
  466. border-radius: 3px;
  467. width: ${(props: { disabled?: boolean; width: string }) =>
  468. props.width ? props.width : "270px"};
  469. color: ${(props: { disabled?: boolean; width: string }) =>
  470. props.disabled ? "#ffffff44" : "white"};
  471. padding: 5px 10px;
  472. height: 35px;
  473. `;
  474. const Label = styled.div`
  475. color: #ffffff;
  476. margin-bottom: 10px;
  477. `;
  478. const StyledInputArray = styled.div`
  479. margin-bottom: 15px;
  480. margin-top: 22px;
  481. `;