ExpandedChart.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170
  1. import React, {
  2. useCallback,
  3. useContext,
  4. useEffect,
  5. useMemo,
  6. useState,
  7. } from "react";
  8. import styled from "styled-components";
  9. import yaml from "js-yaml";
  10. import backArrow from "assets/back_arrow.png";
  11. import _ from "lodash";
  12. import loadingSrc from "assets/loading.gif";
  13. import { ChartType, ClusterType, ResourceType } from "shared/types";
  14. import { Context } from "shared/Context";
  15. import api from "shared/api";
  16. import StatusIndicator from "components/StatusIndicator";
  17. import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
  18. import RevisionSection from "./RevisionSection";
  19. import ValuesYaml from "./ValuesYaml";
  20. import GraphSection from "./GraphSection";
  21. import MetricsSection from "./metrics/MetricsSection";
  22. import ListSection from "./ListSection";
  23. import StatusSection from "./status/StatusSection";
  24. import SettingsSection from "./SettingsSection";
  25. import Loading from "components/Loading";
  26. import { useWebsockets } from "shared/hooks/useWebsockets";
  27. import useAuth from "shared/auth/useAuth";
  28. import TitleSection from "components/TitleSection";
  29. import DeploymentType from "./DeploymentType";
  30. import EventsTab from "./events/EventsTab";
  31. import { PopulatedEnvGroup } from "components/porter-form/types";
  32. import { onlyInLeft } from "shared/array_utils";
  33. type Props = {
  34. namespace: string;
  35. currentChart: ChartType;
  36. currentCluster: ClusterType;
  37. closeChart: () => void;
  38. setSidebar: (x: boolean) => void;
  39. isMetricsInstalled: boolean;
  40. };
  41. const getReadableDate = (s: string) => {
  42. let ts = new Date(s);
  43. let date = ts.toLocaleDateString();
  44. let time = ts.toLocaleTimeString([], {
  45. hour: "numeric",
  46. minute: "2-digit",
  47. });
  48. return `${time} on ${date}`;
  49. };
  50. const ExpandedChart: React.FC<Props> = (props) => {
  51. const [currentChart, setCurrentChart] = useState<ChartType>(
  52. props.currentChart
  53. );
  54. const [showRevisions, setShowRevisions] = useState<boolean>(false);
  55. const [loading, setLoading] = useState<boolean>(false);
  56. const [components, setComponents] = useState<ResourceType[]>([]);
  57. const [isPreview, setIsPreview] = useState<boolean>(false);
  58. const [devOpsMode, setDevOpsMode] = useState<boolean>(
  59. localStorage.getItem("devOpsMode") === "true"
  60. );
  61. const [rightTabOptions, setRightTabOptions] = useState<any[]>([]);
  62. const [leftTabOptions, setLeftTabOptions] = useState<any[]>([]);
  63. const [saveValuesStatus, setSaveValueStatus] = useState<string>(null);
  64. const [forceRefreshRevisions, setForceRefreshRevisions] = useState<boolean>(
  65. false
  66. );
  67. const [controllers, setControllers] = useState<
  68. Record<string, Record<string, any>>
  69. >({});
  70. const [url, setUrl] = useState<string>(null);
  71. const [deleting, setDeleting] = useState<boolean>(false);
  72. const [imageIsPlaceholder, setImageIsPlaceholer] = useState<boolean>(false);
  73. const [newestImage, setNewestImage] = useState<string>(null);
  74. const [isLoadingChartData, setIsLoadingChartData] = useState<boolean>(true);
  75. const [showRepoTooltip, setShowRepoTooltip] = useState(false);
  76. const [isAuthorized] = useAuth();
  77. const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
  78. const {
  79. newWebsocket,
  80. openWebsocket,
  81. closeAllWebsockets,
  82. closeWebsocket,
  83. } = useWebsockets();
  84. const {
  85. currentCluster,
  86. currentProject,
  87. setCurrentError,
  88. setCurrentOverlay,
  89. } = useContext(Context);
  90. // Retrieve full chart data (includes form and values)
  91. const getChartData = async (chart: ChartType) => {
  92. setIsLoadingChartData(true);
  93. const res = await api.getChart(
  94. "<token>",
  95. {},
  96. {
  97. name: chart.name,
  98. namespace: chart.namespace,
  99. cluster_id: currentCluster.id,
  100. revision: chart.version,
  101. id: currentProject.id,
  102. }
  103. );
  104. const image = res.data?.config?.image?.repository;
  105. const tag = res.data?.config?.image?.tag?.toString();
  106. const newNewestImage = tag ? image + ":" + tag : image;
  107. let imageIsPlaceholder = false;
  108. if (
  109. (image === "porterdev/hello-porter" ||
  110. image === "public.ecr.aws/o1j4x7p4/hello-porter") &&
  111. !newestImage
  112. ) {
  113. imageIsPlaceholder = true;
  114. }
  115. setImageIsPlaceholer(imageIsPlaceholder);
  116. setNewestImage(newNewestImage);
  117. const updatedChart = res.data;
  118. setCurrentChart(updatedChart);
  119. updateComponents(updatedChart).finally(() => setIsLoadingChartData(false));
  120. };
  121. const getControllers = async (chart: ChartType) => {
  122. // don't retrieve controllers for chart that failed to even deploy.
  123. if (chart.info.status == "failed") return;
  124. try {
  125. const { data: chartControllers } = await api.getChartControllers(
  126. "<token>",
  127. {},
  128. {
  129. name: chart.name,
  130. namespace: chart.namespace,
  131. cluster_id: currentCluster.id,
  132. revision: chart.version,
  133. id: currentProject.id,
  134. }
  135. );
  136. chartControllers.forEach((c: any) => {
  137. c.metadata.kind = c.kind;
  138. setControllers((oldControllers) => ({
  139. ...oldControllers,
  140. [c.metadata.uid]: c,
  141. }));
  142. });
  143. return;
  144. } catch (error) {
  145. if (typeof error !== "string") {
  146. setCurrentError(JSON.stringify(error));
  147. }
  148. setCurrentError(error);
  149. }
  150. };
  151. const setupWebsocket = (kind: string) => {
  152. const apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status`;
  153. const wsConfig = {
  154. onmessage(evt: MessageEvent) {
  155. const event = JSON.parse(evt.data);
  156. let object = event.Object;
  157. object.metadata.kind = event.Kind;
  158. if (event.event_type != "UPDATE") {
  159. return;
  160. }
  161. setControllers((oldControllers) => {
  162. if (
  163. oldControllers &&
  164. oldControllers[object.metadata.uid]?.status?.conditions ==
  165. object.status?.conditions
  166. ) {
  167. return oldControllers;
  168. }
  169. return {
  170. ...oldControllers,
  171. [object.metadata.uid]: object,
  172. };
  173. });
  174. },
  175. onerror() {
  176. closeWebsocket(kind);
  177. },
  178. };
  179. newWebsocket(kind, apiEndpoint, wsConfig);
  180. };
  181. const updateComponents = async (currentChart: ChartType) => {
  182. setLoading(true);
  183. try {
  184. const res = await api.getChartComponents(
  185. "<token>",
  186. {},
  187. {
  188. id: currentProject.id,
  189. name: currentChart.name,
  190. namespace: currentChart.namespace,
  191. cluster_id: currentCluster.id,
  192. revision: currentChart.version,
  193. }
  194. );
  195. setComponents(res.data.Objects);
  196. setLoading(false);
  197. } catch (error) {
  198. console.log(error);
  199. setLoading(false);
  200. }
  201. };
  202. const onSubmit = async (rawValues: any) => {
  203. console.log("raw", rawValues);
  204. // Convert dotted keys to nested objects
  205. let values: any = {};
  206. // Weave in preexisting values and convert to yaml
  207. if (props.currentChart.config) {
  208. values = props.currentChart.config;
  209. }
  210. // Override config from currentChart prop if we have it on the current state
  211. if (currentChart.config) {
  212. values = currentChart.config;
  213. }
  214. for (let key in rawValues) {
  215. _.set(values, key, rawValues[key]);
  216. }
  217. let valuesYaml = yaml.dump({
  218. ...values,
  219. });
  220. const oldSyncedEnvGroups =
  221. props.currentChart.config?.container?.env?.synced || [];
  222. const newSyncedEnvGroups = values?.container?.env?.synced || [];
  223. const deletedEnvGroups = onlyInLeft<{
  224. keys: Array<any>;
  225. name: string;
  226. version: number;
  227. }>(
  228. oldSyncedEnvGroups,
  229. newSyncedEnvGroups,
  230. (oldVal, newVal) => oldVal.name === newVal.name
  231. );
  232. const addedEnvGroups = onlyInLeft<{
  233. keys: Array<any>;
  234. name: string;
  235. version: number;
  236. }>(
  237. newSyncedEnvGroups,
  238. oldSyncedEnvGroups,
  239. (oldVal, newVal) => oldVal.name === newVal.name
  240. );
  241. const addApplicationToEnvGroupPromises = addedEnvGroups.map(
  242. (envGroup: any) => {
  243. return api.addApplicationToEnvGroup(
  244. "<token>",
  245. {
  246. name: envGroup?.name,
  247. app_name: currentChart.name,
  248. },
  249. {
  250. project_id: currentProject.id,
  251. cluster_id: currentCluster.id,
  252. namespace: currentChart.namespace,
  253. }
  254. );
  255. }
  256. );
  257. try {
  258. await Promise.all(addApplicationToEnvGroupPromises);
  259. } catch (error) {
  260. setCurrentError(
  261. "We coudln't sync the env group to the application, please try again."
  262. );
  263. }
  264. const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
  265. (envGroup: any) => {
  266. return api.removeApplicationFromEnvGroup(
  267. "<token>",
  268. {
  269. name: envGroup?.name,
  270. app_name: currentChart.name,
  271. },
  272. {
  273. project_id: currentProject.id,
  274. cluster_id: currentCluster.id,
  275. namespace: currentChart.namespace,
  276. }
  277. );
  278. }
  279. );
  280. try {
  281. await Promise.all(removeApplicationToEnvGroupPromises);
  282. } catch (error) {
  283. setCurrentError(
  284. "We coudln't remove the synced env group from the application, please try again."
  285. );
  286. }
  287. setSaveValueStatus("loading");
  288. console.log("valuesYaml", valuesYaml);
  289. try {
  290. await api.upgradeChartValues(
  291. "<token>",
  292. {
  293. values: valuesYaml,
  294. },
  295. {
  296. id: currentProject.id,
  297. namespace: currentChart.namespace,
  298. name: currentChart.name,
  299. cluster_id: currentCluster.id,
  300. }
  301. );
  302. getChartData(currentChart);
  303. setSaveValueStatus("successful");
  304. setForceRefreshRevisions(true);
  305. window.analytics.track("Chart Upgraded", {
  306. chart: currentChart.name,
  307. values: valuesYaml,
  308. });
  309. } catch (err) {
  310. const parsedErr = err?.response?.data?.error;
  311. if (parsedErr) {
  312. err = parsedErr;
  313. }
  314. setSaveValueStatus(err);
  315. setCurrentError(parsedErr);
  316. window.analytics.track("Failed to Upgrade Chart", {
  317. chart: currentChart.name,
  318. values: valuesYaml,
  319. error: err,
  320. });
  321. return;
  322. }
  323. };
  324. const handleUpgradeVersion = useCallback(
  325. async (version: string, cb: () => void) => {
  326. // convert current values to yaml
  327. let values = currentChart.config;
  328. let valuesYaml = yaml.dump({
  329. ...values,
  330. });
  331. setSaveValueStatus("loading");
  332. getChartData(currentChart);
  333. try {
  334. await api.upgradeChartValues(
  335. "<token>",
  336. {
  337. values: valuesYaml,
  338. version: version,
  339. },
  340. {
  341. id: currentProject.id,
  342. namespace: currentChart.namespace,
  343. name: currentChart.name,
  344. cluster_id: currentCluster.id,
  345. }
  346. );
  347. setSaveValueStatus("successful");
  348. setForceRefreshRevisions(true);
  349. window.analytics.track("Chart Upgraded", {
  350. chart: currentChart.name,
  351. values: valuesYaml,
  352. });
  353. cb && cb();
  354. } catch (err) {
  355. let parsedErr = err?.response?.data?.error;
  356. if (parsedErr) {
  357. err = parsedErr;
  358. }
  359. setSaveValueStatus(err);
  360. setCurrentError(parsedErr);
  361. window.analytics.track("Failed to Upgrade Chart", {
  362. chart: currentChart.name,
  363. values: valuesYaml,
  364. error: err,
  365. });
  366. }
  367. },
  368. [currentChart]
  369. );
  370. const renderTabContents = (currentTab: string) => {
  371. let { setSidebar } = props;
  372. let chart = currentChart;
  373. console.log("CONTROLLERS", controllers);
  374. switch (currentTab) {
  375. case "metrics":
  376. return <MetricsSection currentChart={chart} />;
  377. case "events":
  378. return <EventsTab controllers={controllers} />;
  379. case "status":
  380. if (isLoadingChartData) {
  381. return (
  382. <Placeholder>
  383. <Loading />
  384. </Placeholder>
  385. );
  386. }
  387. if (imageIsPlaceholder) {
  388. return (
  389. <Placeholder>
  390. <TextWrap>
  391. <Header>
  392. <Spinner src={loadingSrc} /> This application is currently
  393. being deployed
  394. </Header>
  395. Navigate to the{" "}
  396. <A
  397. href={
  398. props.currentChart.git_action_config &&
  399. `https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`
  400. }
  401. target={"_blank"}
  402. >
  403. Actions
  404. </A>{" "}
  405. tab of your GitHub repo to view live build logs.
  406. </TextWrap>
  407. </Placeholder>
  408. );
  409. } else {
  410. return (
  411. <StatusSection
  412. currentChart={chart}
  413. setFullScreenLogs={() => setFullScreenLogs(true)}
  414. />
  415. );
  416. }
  417. case "settings":
  418. return (
  419. <SettingsSection
  420. currentChart={chart}
  421. refreshChart={() => getChartData(currentChart)}
  422. setShowDeleteOverlay={(x: boolean) => {
  423. if (x) {
  424. setCurrentOverlay({
  425. message: `Are you sure you want to delete ${currentChart.name}?`,
  426. onYes: handleUninstallChart,
  427. onNo: () => setCurrentOverlay(null),
  428. });
  429. } else {
  430. setCurrentOverlay(null);
  431. }
  432. }}
  433. />
  434. );
  435. case "graph":
  436. return (
  437. <GraphSection
  438. components={components}
  439. currentChart={chart}
  440. setSidebar={setSidebar}
  441. // Handle resize YAML wrapper
  442. showRevisions={showRevisions}
  443. />
  444. );
  445. case "list":
  446. return (
  447. <ListSection
  448. currentChart={chart}
  449. components={components}
  450. // Handle resize YAML wrapper
  451. showRevisions={showRevisions}
  452. />
  453. );
  454. case "values":
  455. return (
  456. <ValuesYaml
  457. currentChart={chart}
  458. refreshChart={() => getChartData(currentChart)}
  459. disabled={!isAuthorized("application", "", ["get", "update"])}
  460. />
  461. );
  462. default:
  463. }
  464. };
  465. const updateTabs = () => {
  466. // Collate non-form tabs
  467. let rightTabOptions = [] as any[];
  468. let leftTabOptions = [] as any[];
  469. leftTabOptions.push({ label: "Status", value: "status" });
  470. leftTabOptions.push({ label: "Events", value: "events" });
  471. if (props.isMetricsInstalled) {
  472. leftTabOptions.push({ label: "Metrics", value: "metrics" });
  473. }
  474. rightTabOptions.push({ label: "Chart Overview", value: "graph" });
  475. if (devOpsMode) {
  476. rightTabOptions.push(
  477. { label: "Manifests", value: "list" },
  478. { label: "Helm Values", value: "values" }
  479. );
  480. }
  481. // Settings tab is always last
  482. if (isAuthorized("application", "", ["get", "delete"])) {
  483. rightTabOptions.push({ label: "Settings", value: "settings" });
  484. }
  485. // Filter tabs if previewing an old revision or updating the chart version
  486. if (isPreview) {
  487. let liveTabs = ["status", "events", "settings", "deploy", "metrics"];
  488. rightTabOptions = rightTabOptions.filter(
  489. (tab: any) => !liveTabs.includes(tab.value)
  490. );
  491. leftTabOptions = leftTabOptions.filter(
  492. (tab: any) => !liveTabs.includes(tab.value)
  493. );
  494. }
  495. setLeftTabOptions(leftTabOptions);
  496. setRightTabOptions(rightTabOptions);
  497. };
  498. const setRevision = (chart: ChartType, isCurrent?: boolean) => {
  499. setIsPreview(!isCurrent);
  500. getChartData(chart);
  501. };
  502. // TODO: consolidate with pop + push in refreshTabs
  503. const toggleDevOpsMode = () => {
  504. setDevOpsMode(!devOpsMode);
  505. };
  506. const renderIcon = () => {
  507. if (
  508. currentChart.chart.metadata.icon &&
  509. currentChart.chart.metadata.icon !== ""
  510. ) {
  511. return <Icon src={currentChart.chart.metadata.icon} />;
  512. } else {
  513. return <i className="material-icons">tonality</i>;
  514. }
  515. };
  516. const chartStatus = useMemo(() => {
  517. const getAvailability = (kind: string, c: any) => {
  518. switch (kind?.toLowerCase()) {
  519. case "deployment":
  520. case "replicaset":
  521. return c.status.availableReplicas == c.status.replicas;
  522. case "statefulset":
  523. return c.status.readyReplicas == c.status.replicas;
  524. case "daemonset":
  525. return c.status.numberAvailable == c.status.desiredNumberScheduled;
  526. }
  527. };
  528. const chartStatus = currentChart.info.status;
  529. if (chartStatus === "deployed") {
  530. for (var uid in controllers) {
  531. let value = controllers[uid];
  532. let available = getAvailability(value.metadata.kind, value);
  533. let progressing = true;
  534. controllers[uid]?.status?.conditions?.forEach((condition: any) => {
  535. if (
  536. condition.type == "Progressing" &&
  537. condition.status == "False" &&
  538. condition.reason == "ProgressDeadlineExceeded"
  539. ) {
  540. progressing = false;
  541. }
  542. });
  543. if (!available && progressing) {
  544. return "loading";
  545. } else if (!available && !progressing) {
  546. return "failed";
  547. }
  548. }
  549. return "deployed";
  550. }
  551. return chartStatus;
  552. }, [currentChart, controllers]);
  553. const renderUrl = () => {
  554. if (url) {
  555. return (
  556. <Url href={url} target="_blank">
  557. <i className="material-icons">link</i>
  558. {url}
  559. </Url>
  560. );
  561. }
  562. const service: any = components?.find((c) => {
  563. return c.Kind === "Service";
  564. });
  565. if (loading) {
  566. return (
  567. <Url>
  568. <Bolded>Loading...</Bolded>
  569. </Url>
  570. );
  571. }
  572. if (!service?.Name || !service?.Namespace) {
  573. return;
  574. }
  575. return (
  576. <Url>
  577. <Bolded>Internal URI:</Bolded>
  578. {`${service.Name}.${service.Namespace}.svc.cluster.local`}
  579. </Url>
  580. );
  581. };
  582. const handleUninstallChart = async () => {
  583. setDeleting(true);
  584. setCurrentOverlay(null);
  585. const syncedEnvGroups = currentChart.config?.container?.env?.synced || [];
  586. const removeApplicationToEnvGroupPromises = syncedEnvGroups.map(
  587. (envGroup: any) => {
  588. return api.removeApplicationFromEnvGroup(
  589. "<token>",
  590. {
  591. name: envGroup?.name,
  592. app_name: currentChart.name,
  593. },
  594. {
  595. project_id: currentProject.id,
  596. cluster_id: currentCluster.id,
  597. namespace: currentChart.namespace,
  598. }
  599. );
  600. }
  601. );
  602. try {
  603. await Promise.all(removeApplicationToEnvGroupPromises);
  604. } catch (error) {
  605. setCurrentError(
  606. "We coudln't remove the synced env group from the application, please remove it manually before uninstalling the chart, or try again."
  607. );
  608. return;
  609. }
  610. try {
  611. await api.uninstallTemplate(
  612. "<token>",
  613. {},
  614. {
  615. namespace: currentChart.namespace,
  616. name: currentChart.name,
  617. id: currentProject.id,
  618. cluster_id: currentCluster.id,
  619. }
  620. );
  621. props.closeChart();
  622. } catch (error) {
  623. console.log(error);
  624. setCurrentError("Couldn't uninstall chart, please try again");
  625. }
  626. };
  627. useEffect(() => {
  628. window.analytics.track("Opened Chart", {
  629. chart: currentChart.name,
  630. });
  631. getChartData(currentChart).then(() => {
  632. getControllers(currentChart).then(() => {
  633. ["deployment", "statefulset", "daemonset", "replicaset"]
  634. .map((kind) => {
  635. setupWebsocket(kind);
  636. return kind;
  637. })
  638. .forEach((kind) => {
  639. openWebsocket(kind);
  640. });
  641. });
  642. });
  643. return () => {
  644. closeAllWebsockets();
  645. };
  646. }, []);
  647. useEffect(() => {
  648. updateTabs();
  649. localStorage.setItem("devOpsMode", devOpsMode.toString());
  650. }, [devOpsMode, currentChart?.form, isPreview]);
  651. useEffect((): any => {
  652. let isSubscribed = true;
  653. const ingressComponent = components?.find((c) => c.Kind === "Ingress");
  654. const ingressName = ingressComponent?.Name;
  655. if (!ingressName) return;
  656. api
  657. .getIngress(
  658. "<token>",
  659. {},
  660. {
  661. id: currentProject.id,
  662. name: ingressName,
  663. cluster_id: currentCluster.id,
  664. namespace: `${currentChart.namespace}`,
  665. }
  666. )
  667. .then((res) => {
  668. if (!isSubscribed) {
  669. return;
  670. }
  671. if (res.data?.spec?.rules && res.data?.spec?.rules[0]?.host) {
  672. setUrl(`https://${res.data?.spec?.rules[0]?.host}`);
  673. return;
  674. }
  675. if (res.data?.status?.loadBalancer?.ingress) {
  676. setUrl(
  677. `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}`
  678. );
  679. return;
  680. }
  681. })
  682. .catch(console.log);
  683. return () => (isSubscribed = false);
  684. }, [components, currentCluster, currentProject, currentChart]);
  685. return (
  686. <>
  687. {fullScreenLogs ? (
  688. <StatusSection
  689. fullscreen={true}
  690. currentChart={currentChart}
  691. setFullScreenLogs={() => setFullScreenLogs(false)}
  692. />
  693. ) : (
  694. <StyledExpandedChart>
  695. <HeaderWrapper>
  696. <BackButton onClick={props.closeChart}>
  697. <BackButtonImg src={backArrow} />
  698. </BackButton>
  699. <TitleSection
  700. icon={currentChart.chart.metadata.icon}
  701. iconWidth="33px"
  702. >
  703. {currentChart.name}
  704. <DeploymentType currentChart={currentChart} />
  705. <TagWrapper>
  706. Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
  707. </TagWrapper>
  708. </TitleSection>
  709. {currentChart.chart.metadata.name != "worker" &&
  710. currentChart.chart.metadata.name != "job" &&
  711. renderUrl()}
  712. <InfoWrapper>
  713. <StatusIndicator
  714. controllers={controllers}
  715. status={currentChart.info.status}
  716. margin_left={"0px"}
  717. />
  718. <LastDeployed>
  719. <Dot>•</Dot>Last deployed
  720. {" " + getReadableDate(currentChart.info.last_deployed)}
  721. </LastDeployed>
  722. </InfoWrapper>
  723. </HeaderWrapper>
  724. {deleting ? (
  725. <>
  726. <LineBreak />
  727. <Placeholder>
  728. <TextWrap>
  729. <Header>
  730. <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
  731. </Header>
  732. You will be automatically redirected after deletion is
  733. complete.
  734. </TextWrap>
  735. </Placeholder>
  736. </>
  737. ) : (
  738. <>
  739. <RevisionSection
  740. showRevisions={showRevisions}
  741. toggleShowRevisions={() => {
  742. setShowRevisions(!showRevisions);
  743. }}
  744. chart={currentChart}
  745. refreshChart={() => getChartData(currentChart)}
  746. setRevision={setRevision}
  747. forceRefreshRevisions={forceRefreshRevisions}
  748. refreshRevisionsOff={() => setForceRefreshRevisions(false)}
  749. status={chartStatus}
  750. shouldUpdate={
  751. currentChart.latest_version &&
  752. currentChart.latest_version !==
  753. currentChart.chart.metadata.version
  754. }
  755. latestVersion={currentChart.latest_version}
  756. upgradeVersion={handleUpgradeVersion}
  757. />
  758. {(isPreview || leftTabOptions.length > 0) && (
  759. <BodyWrapper>
  760. <PorterFormWrapper
  761. formData={currentChart.form}
  762. valuesToOverride={{
  763. namespace: props.namespace,
  764. clusterId: currentCluster.id,
  765. }}
  766. renderTabContents={renderTabContents}
  767. isReadOnly={
  768. imageIsPlaceholder ||
  769. !isAuthorized("application", "", ["get", "update"])
  770. }
  771. onSubmit={onSubmit}
  772. rightTabOptions={rightTabOptions}
  773. leftTabOptions={leftTabOptions}
  774. color={isPreview ? "#f5cb42" : null}
  775. addendum={
  776. <TabButton
  777. onClick={toggleDevOpsMode}
  778. devOpsMode={devOpsMode}
  779. >
  780. <i className="material-icons">offline_bolt</i> DevOps
  781. Mode
  782. </TabButton>
  783. }
  784. saveValuesStatus={saveValuesStatus}
  785. />
  786. </BodyWrapper>
  787. )}
  788. </>
  789. )}
  790. </StyledExpandedChart>
  791. )}
  792. </>
  793. );
  794. };
  795. export default ExpandedChart;
  796. const RepositoryName = styled.div`
  797. white-space: nowrap;
  798. overflow: hidden;
  799. text-overflow: ellipsis;
  800. max-width: 390px;
  801. position: relative;
  802. margin-right: 3px;
  803. `;
  804. const Tooltip = styled.div`
  805. position: absolute;
  806. left: -40px;
  807. top: 28px;
  808. min-height: 18px;
  809. max-width: calc(700px);
  810. padding: 5px 7px;
  811. background: #272731;
  812. z-index: 999;
  813. color: white;
  814. font-size: 12px;
  815. font-family: "Work Sans", sans-serif;
  816. outline: 1px solid #ffffff55;
  817. opacity: 0;
  818. animation: faded-in 0.2s 0.15s;
  819. animation-fill-mode: forwards;
  820. @keyframes faded-in {
  821. from {
  822. opacity: 0;
  823. }
  824. to {
  825. opacity: 1;
  826. }
  827. }
  828. `;
  829. const TextWrap = styled.div``;
  830. const LoadingWrapper = styled.div`
  831. width: 100%;
  832. height: 200px;
  833. display: flex;
  834. align-items: center;
  835. justify-content: center;
  836. `;
  837. const LineBreak = styled.div`
  838. width: calc(100% - 0px);
  839. height: 2px;
  840. background: #ffffff20;
  841. margin: 35px 0px;
  842. `;
  843. const BodyWrapper = styled.div`
  844. position: relative;
  845. overflow: hidden;
  846. margin-bottom: 120px;
  847. `;
  848. const BackButton = styled.div`
  849. position: absolute;
  850. top: 0px;
  851. right: 0px;
  852. display: flex;
  853. width: 36px;
  854. cursor: pointer;
  855. height: 36px;
  856. align-items: center;
  857. justify-content: center;
  858. border: 1px solid #ffffff55;
  859. border-radius: 100px;
  860. background: #ffffff11;
  861. :hover {
  862. background: #ffffff22;
  863. > img {
  864. opacity: 1;
  865. }
  866. }
  867. `;
  868. const BackButtonImg = styled.img`
  869. width: 16px;
  870. opacity: 0.75;
  871. `;
  872. const Header = styled.div`
  873. font-weight: 500;
  874. color: #aaaabb;
  875. font-size: 16px;
  876. margin-bottom: 15px;
  877. `;
  878. const Placeholder = styled.div`
  879. width: 100%;
  880. min-height: 300px;
  881. height: 40vh;
  882. display: flex;
  883. align-items: center;
  884. justify-content: center;
  885. color: #ffffff44;
  886. font-size: 14px;
  887. > i {
  888. font-size: 18px;
  889. margin-right: 10px;
  890. }
  891. `;
  892. const Spinner = styled.img`
  893. width: 15px;
  894. height: 15px;
  895. margin-right: 12px;
  896. margin-bottom: -2px;
  897. `;
  898. const Bolded = styled.div`
  899. font-weight: 500;
  900. color: #ffffff44;
  901. margin-right: 6px;
  902. `;
  903. const Url = styled.a`
  904. display: block;
  905. margin-left: 2px;
  906. font-size: 13px;
  907. margin-top: 16px;
  908. user-select: all;
  909. margin-bottom: -5px;
  910. user-select: text;
  911. display: flex;
  912. align-items: center;
  913. > i {
  914. font-size: 15px;
  915. margin-right: 10px;
  916. }
  917. `;
  918. const TabButton = styled.div`
  919. position: absolute;
  920. right: 0px;
  921. height: 30px;
  922. background: linear-gradient(to right, #20222700, #202227 20%);
  923. padding-left: 30px;
  924. display: flex;
  925. align-items: center;
  926. justify-content: center;
  927. font-size: 13px;
  928. color: ${(props: { devOpsMode: boolean }) =>
  929. props.devOpsMode ? "#aaaabb" : "#aaaabb55"};
  930. margin-left: 35px;
  931. border-radius: 20px;
  932. text-shadow: 0px 0px 8px
  933. ${(props: { devOpsMode: boolean }) =>
  934. props.devOpsMode ? "#ffffff66" : "none"};
  935. cursor: pointer;
  936. :hover {
  937. color: ${(props: { devOpsMode: boolean }) =>
  938. props.devOpsMode ? "" : "#aaaabb99"};
  939. }
  940. > i {
  941. font-size: 17px;
  942. margin-right: 9px;
  943. }
  944. `;
  945. const HeaderWrapper = styled.div`
  946. position: relative;
  947. `;
  948. const Dot = styled.div`
  949. margin-right: 9px;
  950. `;
  951. const InfoWrapper = styled.div`
  952. display: flex;
  953. align-items: center;
  954. margin-left: 3px;
  955. margin-top: 22px;
  956. `;
  957. const LastDeployed = styled.div`
  958. font-size: 13px;
  959. margin-left: 10px;
  960. margin-top: -1px;
  961. display: flex;
  962. align-items: center;
  963. color: #aaaabb66;
  964. `;
  965. const TagWrapper = styled.div`
  966. height: 20px;
  967. font-size: 12px;
  968. display: flex;
  969. margin-left: 15px;
  970. margin-bottom: -3px;
  971. align-items: center;
  972. font-weight: 400;
  973. justify-content: center;
  974. color: #ffffff44;
  975. border: 1px solid #ffffff44;
  976. border-radius: 3px;
  977. padding-left: 5px;
  978. background: #26282e;
  979. `;
  980. const NamespaceTag = styled.div`
  981. height: 20px;
  982. margin-left: 6px;
  983. color: #aaaabb;
  984. background: #43454a;
  985. border-radius: 3px;
  986. font-size: 12px;
  987. display: flex;
  988. align-items: center;
  989. justify-content: center;
  990. padding: 0px 6px;
  991. padding-left: 7px;
  992. border-top-left-radius: 0px;
  993. border-bottom-left-radius: 0px;
  994. `;
  995. const Icon = styled.img`
  996. width: 100%;
  997. `;
  998. const IconWrapper = styled.div`
  999. color: #efefef;
  1000. font-size: 16px;
  1001. height: 20px;
  1002. width: 20px;
  1003. display: flex;
  1004. justify-content: center;
  1005. align-items: center;
  1006. border-radius: 3px;
  1007. margin-right: 12px;
  1008. > i {
  1009. font-size: 20px;
  1010. }
  1011. `;
  1012. const StyledExpandedChart = styled.div`
  1013. width: 100%;
  1014. overflow: hidden;
  1015. z-index: 0;
  1016. animation: fadeIn 0.3s;
  1017. animation-timing-function: ease-out;
  1018. animation-fill-mode: forwards;
  1019. display: flex;
  1020. flex-direction: column;
  1021. @keyframes fadeIn {
  1022. from {
  1023. opacity: 0;
  1024. }
  1025. to {
  1026. opacity: 1;
  1027. }
  1028. }
  1029. `;
  1030. const DeploymentImageContainer = styled.div`
  1031. height: 20px;
  1032. font-size: 13px;
  1033. position: relative;
  1034. display: flex;
  1035. margin-left: 15px;
  1036. margin-bottom: -3px;
  1037. align-items: center;
  1038. font-weight: 400;
  1039. justify-content: center;
  1040. color: #ffffff66;
  1041. padding-left: 5px;
  1042. `;
  1043. const DeploymentTypeIcon = styled(Icon)`
  1044. width: 20px;
  1045. margin-right: 10px;
  1046. `;
  1047. const A = styled.a`
  1048. color: #8590ff;
  1049. text-decoration: underline;
  1050. cursor: pointer;
  1051. `;