ExpandedChart.tsx 30 KB

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