KeyValueArray.tsx 12 KB

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