KeyValueArray.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  1. import React, { useContext, useEffect, useState } from "react";
  2. import {
  3. GetFinalVariablesFunction,
  4. GetMetadataFunction,
  5. KeyValueArrayField,
  6. KeyValueArrayFieldState,
  7. PopulatedEnvGroup,
  8. } from "../types";
  9. import sliders from "../../../assets/sliders.svg";
  10. import upload from "../../../assets/upload.svg";
  11. import styled, { keyframes } from "styled-components";
  12. import useFormField from "../hooks/useFormField";
  13. import Modal from "../../../main/home/modals/Modal";
  14. import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
  15. import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
  16. import { hasSetValue } from "../utils";
  17. import _, { isObject, differenceBy, omit } from "lodash";
  18. import Helper from "components/form-components/Helper";
  19. import Heading from "components/form-components/Heading";
  20. import Loading from "components/Loading";
  21. import api from "shared/api";
  22. import { Context } from "shared/Context";
  23. import { dotenv_parse } from "shared/string_utils";
  24. interface Props extends KeyValueArrayField {
  25. id: string;
  26. }
  27. const KeyValueArray: React.FC<Props> = (props) => {
  28. const { state, setState, variables } = useFormField<KeyValueArrayFieldState>(
  29. props.id,
  30. {
  31. initState: () => {
  32. let values = props.value[0];
  33. const normalValues = Object.entries(values?.normal || {});
  34. values = omit(values, ["normal", "synced", "build"]);
  35. return {
  36. values: hasSetValue(props)
  37. ? ([...Object.entries(values), ...normalValues]?.map(([k, v]) => {
  38. return { key: k, value: v };
  39. }) as any[])
  40. : [],
  41. showEnvModal: false,
  42. showEditorModal: false,
  43. synced_env_groups: props.settings?.options?.enable_synced_env_groups
  44. ? null
  45. : [],
  46. };
  47. },
  48. }
  49. );
  50. const { currentProject } = useContext(Context);
  51. // If the variable includes normal it means that the form corresponds to an old job template version
  52. // The "normal" keyword doesn't exist for applications as well as the enable_synced_env_groups setting.
  53. // This is why we have to check if the form corresponds to a job or not.
  54. const enableSyncedEnvGroups = props.variable.includes("normal")
  55. ? !!props.settings?.options?.enable_synced_env_groups
  56. : true;
  57. useEffect(() => {
  58. if (hasSetValue(props) && !Array.isArray(state?.synced_env_groups)) {
  59. const values = props.value[0];
  60. // console.log(values);
  61. const envGroups = values?.synced || [];
  62. const promises = Promise.all(
  63. envGroups.map(async (envGroup: any) => {
  64. const res = await api.getEnvGroup(
  65. "<token>",
  66. {},
  67. {
  68. id: currentProject.id,
  69. cluster_id: variables.clusterId,
  70. namespace: variables.namespace,
  71. name: envGroup?.name,
  72. version: envGroup.version,
  73. }
  74. );
  75. return res.data;
  76. })
  77. );
  78. promises.then((populatedEnvGroups) => {
  79. setState(() => ({
  80. synced_env_groups: Array.isArray(populatedEnvGroups)
  81. ? populatedEnvGroups
  82. : [],
  83. }));
  84. });
  85. }
  86. }, [
  87. props.value[0],
  88. variables?.clusterId,
  89. variables?.namespace,
  90. currentProject?.id,
  91. ]);
  92. if (state == undefined) return <></>;
  93. if (!Array.isArray(state.synced_env_groups) && enableSyncedEnvGroups) {
  94. return <Loading />;
  95. }
  96. const parseEnv = (src: any, options: any) => {
  97. return dotenv_parse(src);
  98. };
  99. const readFile = (env: string) => {
  100. let envObj = parseEnv(env, null);
  101. let push = true;
  102. for (let key in envObj) {
  103. for (var i = 0; i < state.values.length; i++) {
  104. let existingKey = state.values[i]["key"];
  105. if (key === existingKey) {
  106. state.values[i]["value"] = envObj[key];
  107. push = false;
  108. }
  109. }
  110. if (push) {
  111. setState((prev) => {
  112. return {
  113. values: [...prev.values, { key, value: envObj[key] }],
  114. };
  115. });
  116. }
  117. }
  118. };
  119. const renderEditorModal = () => {
  120. if (state.showEditorModal) {
  121. return (
  122. <Modal
  123. onRequestClose={() =>
  124. setState(() => {
  125. return { showEditorModal: false };
  126. })
  127. }
  128. width="60%"
  129. height="80%"
  130. >
  131. <EnvEditorModal
  132. closeModal={() =>
  133. setState(() => {
  134. return { showEditorModal: false };
  135. })
  136. }
  137. setEnvVariables={(envFile: string) => readFile(envFile)}
  138. />
  139. </Modal>
  140. );
  141. }
  142. };
  143. const getProcessedValues = (
  144. objectArray: { key: string; value: string }[]
  145. ): any => {
  146. let obj = {} as any;
  147. objectArray?.forEach(({ key, value }) => {
  148. obj[key] = value;
  149. });
  150. return obj;
  151. };
  152. const renderEnvModal = () => {
  153. if (state.showEnvModal) {
  154. return (
  155. <Modal
  156. onRequestClose={() =>
  157. setState(() => {
  158. return { showEnvModal: false };
  159. })
  160. }
  161. width="800px"
  162. height="542px"
  163. >
  164. <LoadEnvGroupModal
  165. existingValues={getProcessedValues(state.values)}
  166. enableSyncedEnvGroups={enableSyncedEnvGroups}
  167. syncedEnvGroups={state.synced_env_groups}
  168. namespace={variables.namespace}
  169. clusterId={variables.clusterId}
  170. closeModal={() =>
  171. setState(() => {
  172. return {
  173. showEnvModal: false,
  174. };
  175. })
  176. }
  177. setSyncedEnvGroups={(value) => {
  178. setState((prev) => {
  179. return {
  180. synced_env_groups: [...(prev.synced_env_groups || []), value],
  181. };
  182. });
  183. }}
  184. setValues={(values) => {
  185. setState((prev) => {
  186. // Transform array to object similar on what we receive from setValues
  187. const prevValues = prev.values.reduce((acc, currentValue) => {
  188. acc[currentValue.key] = currentValue.value;
  189. return acc;
  190. }, {} as Record<string, string>);
  191. // Deconstruct the two records/objects inside one to merge their values (this will override the old duped vars too)
  192. // and convert the new object back to an array usable for the component
  193. const newValues = Object.entries({
  194. ...prevValues,
  195. ...values,
  196. })?.map(([k, v]) => {
  197. return {
  198. key: k,
  199. value: v,
  200. };
  201. });
  202. return {
  203. values: [...newValues],
  204. };
  205. });
  206. }}
  207. />
  208. </Modal>
  209. );
  210. }
  211. };
  212. const renderDeleteButton = (i: number) => {
  213. if (!props.isReadOnly) {
  214. return (
  215. <DeleteButton
  216. onClick={() => {
  217. state.values.splice(i, 1);
  218. setState((prev) => {
  219. return {
  220. values: prev.values
  221. .slice(0, i + 1)
  222. .concat(prev.values.slice(i + 1, prev.values.length)),
  223. };
  224. });
  225. }}
  226. >
  227. <i className="material-icons">cancel</i>
  228. </DeleteButton>
  229. );
  230. }
  231. };
  232. const renderHiddenOption = (hidden: boolean, i: number) => {
  233. if (props.secretOption && hidden) {
  234. return (
  235. <HideButton>
  236. <i className="material-icons">lock</i>
  237. </HideButton>
  238. );
  239. }
  240. };
  241. const checkOverridedKey = (key: string) => {
  242. const env_group = state.synced_env_groups.find(
  243. (env) => env?.variables && env?.variables[key]
  244. );
  245. if (env_group) {
  246. return (
  247. <Wrapper>
  248. <Helper color="#f5cb42" style={{ marginLeft: "10px" }}>
  249. Overridden by the env group "{env_group?.name}"
  250. </Helper>
  251. </Wrapper>
  252. );
  253. }
  254. return null;
  255. };
  256. const renderInputList = () => {
  257. return (
  258. <>
  259. {state.values?.map((entry: any, i: number) => {
  260. // Preprocess non-string env values set via raw Helm values
  261. let { value } = entry;
  262. if (typeof value === "object") {
  263. value = JSON.stringify(value);
  264. } else if (typeof value === "number" || typeof value === "boolean") {
  265. value = value.toString();
  266. }
  267. return (
  268. <InputWrapper key={i}>
  269. <KeyInput
  270. placeholder="ex: key"
  271. width="270px"
  272. value={entry.key}
  273. onChange={(e: any) => {
  274. e.persist();
  275. setState((prev) => {
  276. return {
  277. values: prev.values?.map((t, j) => {
  278. if (j == i) {
  279. return {
  280. ...t,
  281. key: e.target.value,
  282. };
  283. }
  284. return t;
  285. }),
  286. };
  287. });
  288. }}
  289. disabled={props.isReadOnly || value.includes("PORTERSECRET")}
  290. spellCheck={false}
  291. borderColor={
  292. checkOverridedKey(entry.key) ? "#f5cb42" : undefined
  293. }
  294. />
  295. <Spacer />
  296. {value?.includes("PORTERSECRET") ? (
  297. <KeyInput
  298. placeholder="ex: value"
  299. width="270px"
  300. disabled
  301. type={"password"}
  302. spellCheck={false}
  303. />
  304. ) : (
  305. <MultiLineInput
  306. placeholder="ex: value"
  307. width="270px"
  308. value={value}
  309. onChange={(e: any) => {
  310. e.persist();
  311. setState((prev) => {
  312. return {
  313. values: prev.values?.map((t, j) => {
  314. if (j == i) {
  315. return {
  316. ...t,
  317. value: e.target.value,
  318. };
  319. }
  320. return t;
  321. }),
  322. };
  323. });
  324. }}
  325. disabled={props.isReadOnly}
  326. spellCheck={false}
  327. rows={value?.split("\n").length}
  328. />
  329. )}
  330. {renderDeleteButton(i)}
  331. {renderHiddenOption(value.includes("PORTERSECRET"), i)}
  332. {checkOverridedKey(entry.key)}
  333. </InputWrapper>
  334. );
  335. })}
  336. </>
  337. );
  338. };
  339. return (
  340. <>
  341. <StyledInputArray>
  342. <Label>{props.label}</Label>
  343. {state.values.length === 0 ? <></> : renderInputList()}
  344. {props.isReadOnly ? (
  345. <></>
  346. ) : (
  347. <InputWrapper>
  348. <AddRowButton
  349. onClick={() => {
  350. setState((prev) => {
  351. return {
  352. values: [...prev.values, { key: "", value: "" }],
  353. };
  354. });
  355. }}
  356. >
  357. <i className="material-icons">add</i> Add Row
  358. </AddRowButton>
  359. <Spacer />
  360. {variables.namespace && props.envLoader && (
  361. <LoadButton
  362. onClick={() =>
  363. setState((prev) => {
  364. return {
  365. showEnvModal: !prev.showEnvModal,
  366. };
  367. })
  368. }
  369. >
  370. <img src={sliders} /> Load from Env Group
  371. </LoadButton>
  372. )}
  373. {props.fileUpload && (
  374. <UploadButton
  375. onClick={() => {
  376. setState((prev) => {
  377. return {
  378. showEditorModal: true,
  379. };
  380. });
  381. }}
  382. >
  383. <img src={upload} /> Copy from File
  384. </UploadButton>
  385. )}
  386. </InputWrapper>
  387. )}
  388. {enableSyncedEnvGroups && !!state.synced_env_groups?.length && (
  389. <>
  390. <Heading>Synced Environment Groups</Heading>
  391. <Br />
  392. {state.synced_env_groups?.map((envGroup: any) => {
  393. return (
  394. <ExpandableEnvGroup
  395. key={envGroup?.name}
  396. envGroup={envGroup}
  397. onDelete={() => {
  398. setState((prev) => {
  399. const synced = prev.synced_env_groups?.filter(
  400. (env) => env.name !== envGroup.name
  401. );
  402. return {
  403. ...prev,
  404. synced_env_groups: synced,
  405. };
  406. });
  407. }}
  408. />
  409. );
  410. })}
  411. </>
  412. )}
  413. </StyledInputArray>
  414. {renderEnvModal()}
  415. {renderEditorModal()}
  416. </>
  417. );
  418. };
  419. export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
  420. vars,
  421. props: KeyValueArrayField,
  422. state: KeyValueArrayFieldState
  423. ) => {
  424. if (!state) {
  425. return {
  426. [props.variable]: hasSetValue(props) ? props.value[0] : [],
  427. };
  428. }
  429. const isNumber = (s: string) => {
  430. return !isNaN(!s ? NaN : Number(String(s).trim()));
  431. };
  432. const rg = /(?:^|[^\\])(\\n)/g;
  433. const fixNewlines = (s: string) => {
  434. while (rg.test(s)) {
  435. s = s.replace(rg, (str) => {
  436. if (str.length == 2) return "\n";
  437. if (str[0] != "\\") return str[0] + "\n";
  438. return "\\n";
  439. });
  440. }
  441. return s;
  442. };
  443. if (props.variable.includes("env")) {
  444. let obj = {
  445. normal: {},
  446. } as any;
  447. state.values.forEach((entry: any, i: number) => {
  448. if (isNumber(entry.value)) {
  449. obj.normal[entry.key] = entry.value;
  450. } else {
  451. obj.normal[entry.key] = fixNewlines(entry.value);
  452. }
  453. });
  454. if (Array.isArray(props.value) && props.value[0]?.build) {
  455. obj.build = props.value[0].build;
  456. }
  457. if (state.synced_env_groups?.length) {
  458. obj.synced = state.synced_env_groups.map((envGroup) => ({
  459. name: envGroup?.name,
  460. version: envGroup?.version,
  461. keys: Object.entries(envGroup?.variables || {}).map(([key, val]) => ({
  462. name: key,
  463. secret: val.includes("PORTERSECRET"),
  464. })),
  465. }));
  466. }
  467. const variableContent = props.variable.split(".");
  468. let variable = props.variable;
  469. if (variable.includes("normal")) {
  470. variable = `${variableContent[0]}.${variableContent[1]}`;
  471. }
  472. return {
  473. [variable]: obj,
  474. };
  475. } else {
  476. let obj = {} as any;
  477. state.values.forEach((entry: any, i: number) => {
  478. if (isNumber(entry.value)) {
  479. obj[entry.key] = entry.value;
  480. } else {
  481. obj[entry.key] = fixNewlines(entry.value);
  482. }
  483. });
  484. return {
  485. [props.variable]: obj,
  486. };
  487. }
  488. };
  489. type KeyValueArrayMetadata = {
  490. [variable: string]: {
  491. added: { name: string }[];
  492. deleted: { name: string }[];
  493. };
  494. };
  495. export const getMetadata: GetMetadataFunction<KeyValueArrayMetadata> = (
  496. vars,
  497. props: KeyValueArrayField,
  498. state: KeyValueArrayFieldState
  499. ) => {
  500. // We don't need any metadata for other key-value-array fields yet so we return null for that variable
  501. if (!state || !props?.variable?.includes("env")) {
  502. return {
  503. [props.variable]: null,
  504. };
  505. }
  506. const originalSyncedEnvGroups: { name: string }[] =
  507. props.value[0]?.synced || [];
  508. const currSynced = state?.synced_env_groups || [];
  509. let obj: KeyValueArrayMetadata[""] = {
  510. added: [],
  511. deleted: [],
  512. };
  513. obj.added = differenceBy(currSynced, originalSyncedEnvGroups, "name");
  514. obj.deleted = differenceBy(originalSyncedEnvGroups, currSynced, "name");
  515. // This will assure that the variable is always "container.env" and not "container.env.normal" as it is
  516. // for some old versions of the jobs chart.
  517. const variableContent = props.variable.split(".");
  518. let variable = props.variable;
  519. if (variable.includes("normal")) {
  520. variable = `${variableContent[0]}.${variableContent[1]}`;
  521. }
  522. return {
  523. [variable]: obj,
  524. };
  525. };
  526. export default KeyValueArray;
  527. const ExpandableEnvGroup: React.FC<{
  528. envGroup: PopulatedEnvGroup;
  529. onDelete: () => void;
  530. }> = ({ envGroup, onDelete }) => {
  531. const [isExpanded, setIsExpanded] = useState(false);
  532. return (
  533. <>
  534. <StyledCard>
  535. <Flex>
  536. <ContentContainer>
  537. <EventInformation>
  538. <EventName>{envGroup.name}</EventName>
  539. </EventInformation>
  540. </ContentContainer>
  541. <ActionContainer>
  542. <ActionButton onClick={() => onDelete()}>
  543. <span className="material-icons">delete</span>
  544. </ActionButton>
  545. <ActionButton onClick={() => setIsExpanded((prev) => !prev)}>
  546. <i className="material-icons">
  547. {isExpanded ? "arrow_drop_up" : "arrow_drop_down"}
  548. </i>
  549. </ActionButton>
  550. </ActionContainer>
  551. </Flex>
  552. {isExpanded && (
  553. <>
  554. <Buffer />
  555. {isObject(envGroup.variables) ? (
  556. <>
  557. {Object.entries(envGroup.variables || {})?.map(
  558. ([key, value], i: number) => {
  559. // Preprocess non-string env values set via raw Helm values
  560. if (typeof value === "object") {
  561. value = JSON.stringify(value);
  562. } else {
  563. value = String(value);
  564. }
  565. return (
  566. <InputWrapper key={i}>
  567. <KeyInput
  568. placeholder="ex: key"
  569. width="270px"
  570. value={key}
  571. disabled
  572. />
  573. <Spacer />
  574. {value?.includes("PORTERSECRET") ? (
  575. <KeyInput
  576. placeholder="ex: value"
  577. width="270px"
  578. value={value}
  579. disabled
  580. type={
  581. value.includes("PORTERSECRET")
  582. ? "password"
  583. : "text"
  584. }
  585. />
  586. ) : (
  587. <MultiLineInput
  588. placeholder="ex: value"
  589. width="270px"
  590. value={value}
  591. disabled
  592. rows={value?.split("\n").length}
  593. spellCheck={false}
  594. ></MultiLineInput>
  595. )}
  596. </InputWrapper>
  597. );
  598. }
  599. )}
  600. </>
  601. ) : (
  602. <NoVariablesTextWrapper>
  603. This env group has no variables yet
  604. </NoVariablesTextWrapper>
  605. )}
  606. <Br />
  607. </>
  608. )}
  609. </StyledCard>
  610. </>
  611. );
  612. };
  613. const Br = styled.div`
  614. width: 100%;
  615. height: 1px;
  616. `;
  617. const Buffer = styled.div`
  618. width: 100%;
  619. height: 10px;
  620. `;
  621. const Spacer = styled.div`
  622. width: 10px;
  623. height: 20px;
  624. `;
  625. const AddRowButton = styled.div`
  626. display: flex;
  627. align-items: center;
  628. width: 270px;
  629. font-size: 13px;
  630. color: #aaaabb;
  631. height: 32px;
  632. border-radius: 3px;
  633. cursor: pointer;
  634. background: #ffffff11;
  635. :hover {
  636. background: #ffffff22;
  637. }
  638. > i {
  639. color: #ffffff44;
  640. font-size: 16px;
  641. margin-left: 8px;
  642. margin-right: 10px;
  643. display: flex;
  644. align-items: center;
  645. justify-content: center;
  646. }
  647. `;
  648. const LoadButton = styled(AddRowButton)`
  649. background: none;
  650. border: 1px solid #ffffff55;
  651. > i {
  652. color: #ffffff44;
  653. font-size: 16px;
  654. margin-left: 8px;
  655. margin-right: 10px;
  656. display: flex;
  657. align-items: center;
  658. justify-content: center;
  659. }
  660. > img {
  661. width: 14px;
  662. margin-left: 10px;
  663. margin-right: 12px;
  664. }
  665. `;
  666. const UploadButton = styled(AddRowButton)`
  667. background: none;
  668. position: relative;
  669. margin-left: 10px;
  670. border: 1px solid #ffffff55;
  671. > i {
  672. color: #ffffff44;
  673. font-size: 16px;
  674. margin-left: 8px;
  675. margin-right: 10px;
  676. display: flex;
  677. align-items: center;
  678. justify-content: center;
  679. }
  680. > img {
  681. width: 14px;
  682. margin-left: 10px;
  683. margin-right: 12px;
  684. }
  685. `;
  686. const DeleteButton = styled.div`
  687. width: 15px;
  688. height: 15px;
  689. display: flex;
  690. align-items: center;
  691. margin-left: 8px;
  692. margin-top: -3px;
  693. justify-content: center;
  694. > i {
  695. font-size: 17px;
  696. color: #ffffff44;
  697. display: flex;
  698. align-items: center;
  699. justify-content: center;
  700. cursor: pointer;
  701. :hover {
  702. color: #ffffff88;
  703. }
  704. }
  705. `;
  706. const HideButton = styled(DeleteButton)`
  707. margin-top: -5px;
  708. > i {
  709. font-size: 19px;
  710. cursor: default;
  711. :hover {
  712. color: #ffffff44;
  713. }
  714. }
  715. `;
  716. const Wrapper = styled.div`
  717. margin-left: 5px;
  718. height: 20px;
  719. display: flex;
  720. align-items: center;
  721. margin-top: -7px;
  722. `;
  723. const InputWrapper = styled.div`
  724. display: flex;
  725. align-items: center;
  726. margin-top: 5px;
  727. `;
  728. type InputProps = {
  729. disabled?: boolean;
  730. width: string;
  731. borderColor?: string;
  732. };
  733. const KeyInput = styled.input<InputProps>`
  734. outline: none;
  735. border: none;
  736. margin-bottom: 5px;
  737. font-size: 13px;
  738. background: #ffffff11;
  739. border: 1px solid
  740. ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
  741. border-radius: 3px;
  742. width: ${(props) => (props.width ? props.width : "270px")};
  743. color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
  744. padding: 5px 10px;
  745. height: 35px;
  746. `;
  747. export const MultiLineInput = styled.textarea<InputProps>`
  748. outline: none;
  749. border: none;
  750. margin-bottom: 5px;
  751. font-size: 13px;
  752. background: #ffffff11;
  753. border: 1px solid
  754. ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
  755. border-radius: 3px;
  756. min-width: ${(props) => (props.width ? props.width : "270px")};
  757. max-width: ${(props) => (props.width ? props.width : "270px")};
  758. color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
  759. padding: 8px 10px 5px 10px;
  760. min-height: 35px;
  761. max-height: 100px;
  762. white-space: nowrap;
  763. ::-webkit-scrollbar {
  764. width: 8px;
  765. :horizontal {
  766. height: 8px;
  767. }
  768. }
  769. ::-webkit-scrollbar-corner {
  770. width: 10px;
  771. background: #ffffff11;
  772. color: white;
  773. }
  774. ::-webkit-scrollbar-track {
  775. width: 10px;
  776. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  777. box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  778. }
  779. ::-webkit-scrollbar-thumb {
  780. background-color: darkgrey;
  781. outline: 1px solid slategrey;
  782. }
  783. `;
  784. const Label = styled.div`
  785. color: #ffffff;
  786. margin-bottom: 10px;
  787. `;
  788. const StyledInputArray = styled.div`
  789. margin-bottom: 15px;
  790. margin-top: 22px;
  791. `;
  792. const fadeIn = keyframes`
  793. from {
  794. opacity: 0;
  795. }
  796. to {
  797. opacity: 1;
  798. }
  799. `;
  800. const StyledCard = styled.div`
  801. border: 1px solid #ffffff44;
  802. background: #ffffff11;
  803. margin-bottom: 5px;
  804. border-radius: 8px;
  805. margin-top: 15px;
  806. padding: 10px 14px;
  807. overflow: hidden;
  808. font-size: 13px;
  809. animation: ${fadeIn} 0.5s;
  810. `;
  811. const Flex = styled.div`
  812. display: flex;
  813. height: 25px;
  814. align-items: center;
  815. justify-content: space-between;
  816. `;
  817. const ContentContainer = styled.div`
  818. display: flex;
  819. height: 40px;
  820. width: 100%;
  821. align-items: center;
  822. `;
  823. const EventInformation = styled.div`
  824. display: flex;
  825. flex-direction: column;
  826. justify-content: space-around;
  827. height: 100%;
  828. `;
  829. const EventName = styled.div`
  830. font-family: "Work Sans", sans-serif;
  831. font-weight: 500;
  832. color: #ffffff;
  833. `;
  834. const ActionContainer = styled.div`
  835. display: flex;
  836. align-items: center;
  837. white-space: nowrap;
  838. height: 100%;
  839. `;
  840. const ActionButton = styled.button`
  841. position: relative;
  842. border: none;
  843. background: none;
  844. color: white;
  845. padding: 5px;
  846. width: 30px;
  847. height: 30px;
  848. margin-left: 5px;
  849. display: flex;
  850. justify-content: center;
  851. align-items: center;
  852. border-radius: 50%;
  853. cursor: pointer;
  854. color: #aaaabb;
  855. border: 1px solid #ffffff00;
  856. :hover {
  857. background: #ffffff11;
  858. border: 1px solid #ffffff44;
  859. }
  860. > span {
  861. font-size: 20px;
  862. }
  863. `;
  864. const NoVariablesTextWrapper = styled.div`
  865. display: flex;
  866. align-items: center;
  867. justify-content: center;
  868. color: #ffffff99;
  869. `;