KeyValueArray.tsx 25 KB

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