ExpandedJobChart.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. import React, { useContext, useEffect, useRef, useState } from "react";
  2. import styled from "styled-components";
  3. import yaml from "js-yaml";
  4. import backArrow from "assets/back_arrow.png";
  5. import { merge, set } from "lodash";
  6. import loading from "assets/loading.gif";
  7. import { ChartType, ClusterType } from "shared/types";
  8. import { Context } from "shared/Context";
  9. import TitleSection from "components/TitleSection";
  10. import SettingsSection from "./SettingsSection";
  11. import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
  12. import ValuesYaml from "./ValuesYaml";
  13. import DeploymentType from "./DeploymentType";
  14. import RevisionSection from "./RevisionSection";
  15. import Loading from "components/Loading";
  16. import JobList from "./jobs/JobList";
  17. import SaveButton from "components/SaveButton";
  18. import useAuth from "shared/auth/useAuth";
  19. import ExpandedJobRun from "./jobs/ExpandedJobRun";
  20. import { useJobs } from "./jobs/useJobs";
  21. import { useChart } from "shared/hooks/useChart";
  22. import Modal from "main/home/modals/Modal";
  23. import ConnectToJobInstructionsModal from "./jobs/ConnectToJobInstructionsModal";
  24. import CommandLineIcon from "assets/command-line-icon";
  25. const readableDate = (s: string) => {
  26. let ts = new Date(s);
  27. let date = ts.toLocaleDateString();
  28. let time = ts.toLocaleTimeString([], {
  29. hour: "numeric",
  30. minute: "2-digit",
  31. });
  32. return `${time} on ${date}`;
  33. };
  34. export const ExpandedJobChartFC: React.FC<{
  35. namespace: string;
  36. currentChart: ChartType;
  37. currentCluster: ClusterType;
  38. closeChart: () => void;
  39. setSidebar: (x: boolean) => void;
  40. }> = ({ currentChart: oldChart, closeChart, currentCluster }) => {
  41. const { setCurrentOverlay } = useContext(Context);
  42. const [isAuthorized] = useAuth();
  43. const {
  44. chart,
  45. status,
  46. saveStatus,
  47. refreshChart,
  48. deleteChart,
  49. updateChart,
  50. upgradeChart,
  51. loadChartWithSpecificRevision,
  52. } = useChart(oldChart, closeChart);
  53. const {
  54. jobs,
  55. hasPorterImageTemplate,
  56. status: jobsStatus,
  57. triggerRunStatus,
  58. runJob,
  59. selectedJob,
  60. setSelectedJob,
  61. } = useJobs(chart);
  62. const [devOpsMode, setDevOpsMode] = useState(
  63. () => localStorage.getItem("devOpsMode") === "true"
  64. );
  65. const [showConnectionModal, setShowConnectionModal] = useState(false);
  66. let rightTabOptions = [] as any[];
  67. if (devOpsMode) {
  68. rightTabOptions.push({ label: "Helm Values", value: "values" });
  69. }
  70. if (isAuthorized("job", "", ["get", "delete"])) {
  71. rightTabOptions.push({ label: "Settings", value: "settings" });
  72. }
  73. const leftTabOptions = [{ label: "Jobs", value: "jobs" }];
  74. const processValuesToUpdateChart = (newConfig?: any) => (
  75. currentChart: ChartType
  76. ) => {
  77. // return "";
  78. let conf: string;
  79. let values = {} as any;
  80. if (!newConfig) {
  81. conf = yaml.dump({
  82. ...currentChart.config,
  83. });
  84. } else {
  85. // Convert dotted keys to nested objects
  86. values = {};
  87. for (let key in newConfig) {
  88. set(values, key, newConfig[key]);
  89. }
  90. // Weave in preexisting values and convert to yaml
  91. conf = yaml.dump(
  92. {
  93. ...merge(currentChart.config, values),
  94. },
  95. { forceQuotes: true }
  96. );
  97. }
  98. return conf;
  99. };
  100. const renderTabContents = (currentTab: string) => {
  101. if (currentTab === "jobs" && hasPorterImageTemplate) {
  102. return (
  103. <Placeholder>
  104. <TextWrap>
  105. <Header>
  106. <Spinner src={loading} /> This job is currently being deployed
  107. </Header>
  108. Navigate to the
  109. <A
  110. href={`https://github.com/${chart?.git_action_config?.git_repo}/actions`}
  111. target={"_blank"}
  112. >
  113. Actions tab
  114. </A>{" "}
  115. of your GitHub repo to view live build logs.
  116. </TextWrap>
  117. </Placeholder>
  118. );
  119. }
  120. if (currentTab === "jobs") {
  121. return (
  122. <TabWrapper>
  123. <ButtonWrapper>
  124. <SaveButton
  125. onClick={() => {
  126. runJob();
  127. }}
  128. status={triggerRunStatus}
  129. makeFlush={true}
  130. clearPosition={true}
  131. rounded={true}
  132. statusPosition="right"
  133. >
  134. <i className="material-icons">play_arrow</i> Run Job
  135. </SaveButton>
  136. <CLIModalIconWrapper
  137. onClick={(e) => {
  138. e.preventDefault();
  139. setShowConnectionModal(true);
  140. }}
  141. >
  142. <CLIModalIcon />
  143. Shell Access
  144. </CLIModalIconWrapper>
  145. </ButtonWrapper>
  146. {jobsStatus === "loading" ? (
  147. <Loading></Loading>
  148. ) : (
  149. <JobList
  150. jobs={jobs}
  151. setJobs={() => {}}
  152. expandJob={(job: any) => {
  153. setSelectedJob(job);
  154. }}
  155. isDeployedFromGithub={!!chart?.git_action_config?.git_repo}
  156. repositoryUrl={chart?.git_action_config?.git_repo}
  157. currentChartVersion={Number(chart.version)}
  158. latestChartVersion={Number(chart.latest_version)}
  159. />
  160. )}
  161. </TabWrapper>
  162. );
  163. }
  164. if (currentTab === "values") {
  165. return (
  166. <ValuesYaml
  167. currentChart={chart}
  168. refreshChart={() => refreshChart()}
  169. disabled={!isAuthorized("job", "", ["get", "update"])}
  170. />
  171. );
  172. }
  173. if (
  174. currentTab === "settings" &&
  175. isAuthorized("job", "", ["get", "delete"])
  176. ) {
  177. return (
  178. <SettingsSection
  179. currentChart={chart}
  180. refreshChart={() => refreshChart()}
  181. setShowDeleteOverlay={(showOverlay: boolean) => {
  182. if (showOverlay) {
  183. setCurrentOverlay({
  184. message: `Are you sure you want to delete ${chart.name}?`,
  185. onYes: deleteChart,
  186. onNo: () => setCurrentOverlay(null),
  187. });
  188. } else {
  189. setCurrentOverlay(null);
  190. }
  191. }}
  192. saveButtonText="Save Config"
  193. />
  194. );
  195. }
  196. return null;
  197. };
  198. if (status === "loading") {
  199. return <Loading />;
  200. }
  201. if (status === "deleting") {
  202. return (
  203. <StyledExpandedChart>
  204. <ExpandedJobHeader
  205. chart={chart}
  206. jobs={jobs}
  207. closeChart={closeChart}
  208. refreshChart={refreshChart}
  209. upgradeChart={upgradeChart}
  210. loadChartWithSpecificRevision={loadChartWithSpecificRevision}
  211. />
  212. <LineBreak />
  213. <Placeholder>
  214. <TextWrap>
  215. <Header>
  216. <Spinner src={loading} /> Deleting "{chart.name}"
  217. </Header>
  218. You will be automatically redirected after deletion is complete.
  219. </TextWrap>
  220. </Placeholder>
  221. </StyledExpandedChart>
  222. );
  223. }
  224. if (selectedJob !== null) {
  225. return (
  226. <ExpandedJobRun
  227. currentChart={chart}
  228. jobRun={selectedJob}
  229. onClose={() => setSelectedJob(null)}
  230. />
  231. );
  232. }
  233. const formData = { ...chart.form };
  234. return (
  235. <>
  236. <ConnectToJobInstructionsModal
  237. show={showConnectionModal}
  238. onClose={() => setShowConnectionModal(false)}
  239. chartName={chart?.name}
  240. />
  241. <StyledExpandedChart>
  242. <ExpandedJobHeader
  243. chart={chart}
  244. jobs={jobs}
  245. closeChart={closeChart}
  246. refreshChart={refreshChart}
  247. upgradeChart={upgradeChart}
  248. loadChartWithSpecificRevision={loadChartWithSpecificRevision}
  249. />
  250. <BodyWrapper>
  251. {(leftTabOptions?.length > 0 ||
  252. formData.tabs?.length > 0 ||
  253. rightTabOptions?.length > 0) && (
  254. <PorterFormWrapper
  255. formData={formData}
  256. valuesToOverride={{
  257. namespace: chart.namespace,
  258. clusterId: currentCluster?.id,
  259. }}
  260. renderTabContents={renderTabContents}
  261. isReadOnly={
  262. hasPorterImageTemplate ||
  263. !isAuthorized("job", "", ["get", "update"])
  264. }
  265. onSubmit={(formValues) =>
  266. updateChart(processValuesToUpdateChart(formValues))
  267. }
  268. leftTabOptions={leftTabOptions}
  269. rightTabOptions={rightTabOptions}
  270. saveValuesStatus={saveStatus}
  271. saveButtonText="Save Config"
  272. includeHiddenFields
  273. addendum={
  274. <TabButton
  275. onClick={() =>
  276. setDevOpsMode((prev) => {
  277. localStorage.setItem("devOpsMode", prev.toString());
  278. return !prev;
  279. })
  280. }
  281. devOpsMode={devOpsMode}
  282. >
  283. <i className="material-icons">offline_bolt</i> DevOps Mode
  284. </TabButton>
  285. }
  286. />
  287. )}
  288. </BodyWrapper>
  289. </StyledExpandedChart>
  290. </>
  291. );
  292. };
  293. const ExpandedJobHeader: React.FC<{
  294. chart: ChartType;
  295. jobs: any[];
  296. closeChart: () => void;
  297. refreshChart: () => void;
  298. upgradeChart: () => void;
  299. loadChartWithSpecificRevision: (revision: number) => void;
  300. }> = ({
  301. chart,
  302. closeChart,
  303. jobs,
  304. refreshChart,
  305. upgradeChart,
  306. loadChartWithSpecificRevision,
  307. }) => (
  308. <HeaderWrapper>
  309. <BackButton onClick={closeChart}>
  310. <BackButtonImg src={backArrow} />
  311. </BackButton>
  312. <TitleSection icon={chart.chart.metadata.icon} iconWidth="33px">
  313. {chart.name}
  314. <DeploymentType currentChart={chart} />
  315. <TagWrapper>
  316. Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
  317. </TagWrapper>
  318. </TitleSection>
  319. <InfoWrapper>
  320. <LastDeployed>
  321. Run {jobs?.length} times <Dot>•</Dot>Last template update at
  322. {" " + readableDate(chart.info.last_deployed)}
  323. </LastDeployed>
  324. </InfoWrapper>
  325. <RevisionSection
  326. chart={chart}
  327. refreshChart={() => refreshChart()}
  328. setRevision={(chart) => {
  329. loadChartWithSpecificRevision(chart?.version);
  330. }}
  331. forceRefreshRevisions={false}
  332. refreshRevisionsOff={() => {}}
  333. shouldUpdate={
  334. chart.latest_version &&
  335. chart.latest_version !== chart.chart.metadata.version
  336. }
  337. latestVersion={chart.latest_version}
  338. upgradeVersion={() => {
  339. upgradeChart();
  340. }}
  341. />
  342. </HeaderWrapper>
  343. );
  344. const CLIModalIconWrapper = styled.div`
  345. height: 35px;
  346. font-size: 13px;
  347. font-weight: 500;
  348. font-family: "Work Sans", sans-serif;
  349. display: flex;
  350. align-items: center;
  351. justify-content: space-between;
  352. padding: 6px 20px 6px 10px;
  353. text-align: left;
  354. border: 1px solid #ffffff55;
  355. border-radius: 8px;
  356. background: #ffffff11;
  357. color: #ffffffdd;
  358. cursor: pointer;
  359. :hover {
  360. cursor: pointer;
  361. background: #ffffff22;
  362. > path {
  363. fill: #ffffff77;
  364. }
  365. }
  366. > path {
  367. fill: #ffffff99;
  368. }
  369. `;
  370. const CLIModalIcon = styled(CommandLineIcon)`
  371. width: 32px;
  372. height: 32px;
  373. padding: 8px;
  374. > path {
  375. fill: #ffffff99;
  376. }
  377. `;
  378. const LineBreak = styled.div`
  379. width: calc(100% - 0px);
  380. height: 2px;
  381. background: #ffffff20;
  382. margin: 15px 0px 55px;
  383. `;
  384. const ButtonWrapper = styled.div`
  385. display: flex;
  386. margin: 5px 0 35px;
  387. justify-content: space-between;
  388. `;
  389. const BackButton = styled.div`
  390. position: absolute;
  391. top: 0px;
  392. right: 0px;
  393. display: flex;
  394. width: 36px;
  395. cursor: pointer;
  396. height: 36px;
  397. align-items: center;
  398. justify-content: center;
  399. border: 1px solid #ffffff55;
  400. border-radius: 100px;
  401. background: #ffffff11;
  402. :hover {
  403. background: #ffffff22;
  404. > img {
  405. opacity: 1;
  406. }
  407. }
  408. `;
  409. const BackButtonImg = styled.img`
  410. width: 16px;
  411. opacity: 0.75;
  412. `;
  413. const TextWrap = styled.div``;
  414. const Header = styled.div`
  415. font-weight: 500;
  416. color: #aaaabb;
  417. font-size: 16px;
  418. margin-bottom: 15px;
  419. `;
  420. const Placeholder = styled.div`
  421. min-height: 400px;
  422. height: 50vh;
  423. padding: 30px;
  424. padding-bottom: 70px;
  425. font-size: 13px;
  426. color: #ffffff44;
  427. width: 100%;
  428. display: flex;
  429. align-items: center;
  430. justify-content: center;
  431. `;
  432. const Spinner = styled.img`
  433. width: 15px;
  434. height: 15px;
  435. margin-right: 12px;
  436. margin-bottom: -2px;
  437. `;
  438. const BodyWrapper = styled.div`
  439. position: relative;
  440. overflow: hidden;
  441. `;
  442. const TabWrapper = styled.div`
  443. height: 100%;
  444. width: 100%;
  445. padding-bottom: 47px;
  446. overflow: hidden;
  447. `;
  448. const HeaderWrapper = styled.div`
  449. position: relative;
  450. `;
  451. const Dot = styled.div`
  452. margin-right: 9px;
  453. margin-left: 9px;
  454. `;
  455. const InfoWrapper = styled.div`
  456. display: flex;
  457. align-items: center;
  458. margin: 24px 0px 17px 0px;
  459. height: 20px;
  460. `;
  461. const LastDeployed = styled.div`
  462. font-size: 13px;
  463. margin-left: 0;
  464. margin-top: -1px;
  465. display: flex;
  466. align-items: center;
  467. color: #aaaabb66;
  468. `;
  469. const TagWrapper = styled.div`
  470. height: 25px;
  471. font-size: 12px;
  472. display: flex;
  473. margin-left: 20px;
  474. margin-bottom: -3px;
  475. align-items: center;
  476. font-weight: 400;
  477. justify-content: center;
  478. color: #ffffff44;
  479. border: 1px solid #ffffff44;
  480. border-radius: 3px;
  481. padding-left: 5px;
  482. background: #26282e;
  483. `;
  484. const NamespaceTag = styled.div`
  485. height: 100%;
  486. margin-left: 6px;
  487. color: #aaaabb;
  488. background: #43454a;
  489. border-radius: 3px;
  490. font-size: 12px;
  491. display: flex;
  492. align-items: center;
  493. justify-content: center;
  494. padding: 0px 6px;
  495. padding-left: 7px;
  496. border-top-left-radius: 0px;
  497. border-bottom-left-radius: 0px;
  498. `;
  499. const StyledExpandedChart = styled.div`
  500. width: 100%;
  501. z-index: 0;
  502. animation: fadeIn 0.3s;
  503. animation-timing-function: ease-out;
  504. animation-fill-mode: forwards;
  505. display: flex;
  506. overflow-y: auto;
  507. padding-bottom: 120px;
  508. flex-direction: column;
  509. overflow: visible;
  510. @keyframes fadeIn {
  511. from {
  512. opacity: 0;
  513. }
  514. to {
  515. opacity: 1;
  516. }
  517. }
  518. `;
  519. const TabButton = styled.div`
  520. position: absolute;
  521. right: 0px;
  522. height: 30px;
  523. background: linear-gradient(to right, #20222700, #202227 20%);
  524. padding-left: 30px;
  525. display: flex;
  526. align-items: center;
  527. justify-content: center;
  528. font-size: 13px;
  529. color: ${(props: { devOpsMode: boolean }) =>
  530. props.devOpsMode ? "#aaaabb" : "#aaaabb55"};
  531. margin-left: 35px;
  532. border-radius: 20px;
  533. text-shadow: 0px 0px 8px
  534. ${(props: { devOpsMode: boolean }) =>
  535. props.devOpsMode ? "#ffffff66" : "none"};
  536. cursor: pointer;
  537. :hover {
  538. color: ${(props: { devOpsMode: boolean }) =>
  539. props.devOpsMode ? "" : "#aaaabb99"};
  540. }
  541. > i {
  542. font-size: 17px;
  543. margin-right: 9px;
  544. }
  545. `;
  546. const A = styled.a`
  547. color: #8590ff;
  548. text-decoration: underline;
  549. margin-left: 5px;
  550. cursor: pointer;
  551. `;