KeyValueArray.tsx 12 KB

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