ExpandedStack.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import Loading from "components/Loading";
  2. import Placeholder from "components/OldPlaceholder";
  3. import TabSelector from "components/TabSelector";
  4. import TitleSection from "components/TitleSection";
  5. import React, { useContext, useState } from "react";
  6. import leftArrow from "assets/left-arrow.svg";
  7. import { useParams, useRouteMatch } from "react-router";
  8. import api from "shared/api";
  9. import { Context } from "shared/Context";
  10. import { useRouting } from "shared/routing";
  11. import { readableDate } from "shared/string_utils";
  12. import styled from "styled-components";
  13. import ChartList from "../../chart/ChartList";
  14. import Status from "../components/Status";
  15. import {
  16. Action,
  17. Br,
  18. InfoWrapper,
  19. LastDeployed,
  20. NamespaceTag,
  21. SepDot,
  22. Text,
  23. } from "../components/styles";
  24. import { getStackStatus, getStackStatusMessage } from "../shared";
  25. import { FullStackRevision, Stack, StackRevision } from "../types";
  26. import EnvGroups from "./components/EnvGroups";
  27. import RevisionList from "./_RevisionList";
  28. import SourceConfig from "./_SourceConfig";
  29. import { NavLink } from "react-router-dom";
  30. import Settings from "./components/Settings";
  31. import { ExpandedStackStore } from "./Store";
  32. import DynamicLink from "components/DynamicLink";
  33. const ExpandedStack = () => {
  34. const { namespace } = useParams<{
  35. namespace: string;
  36. stack_id: string;
  37. }>();
  38. const { stack, refreshStack } = useContext(ExpandedStackStore);
  39. const { pushFiltered } = useRouting();
  40. const { currentProject, currentCluster, setCurrentError } = useContext(
  41. Context
  42. );
  43. const { url } = useRouteMatch();
  44. const [isDeleting, setIsDeleting] = useState(false);
  45. const [currentTab, setCurrentTab] = useState("apps");
  46. const [currentRevision, setCurrentRevision] = useState<FullStackRevision>(
  47. () => stack.latest_revision
  48. );
  49. const handleDelete = () => {
  50. setIsDeleting(true);
  51. api
  52. .deleteStack(
  53. "<token>",
  54. {},
  55. {
  56. namespace,
  57. project_id: currentProject.id,
  58. cluster_id: currentCluster.id,
  59. stack_id: stack.id,
  60. }
  61. )
  62. .then(() => {
  63. pushFiltered("/stacks", []);
  64. })
  65. .catch((err) => {
  66. setCurrentError(err);
  67. setIsDeleting(false);
  68. });
  69. };
  70. if (stack === null) {
  71. return null;
  72. }
  73. if (isDeleting) {
  74. return (
  75. <Placeholder height="400px">
  76. <div>
  77. <h1>Deleting Stack</h1>
  78. <p>This may take some time...</p>
  79. <Loading />
  80. </div>
  81. </Placeholder>
  82. );
  83. }
  84. return (
  85. <div>
  86. <BreadcrumbRow>
  87. <Breadcrumb to="/stacks">
  88. <ArrowIcon src={leftArrow} />
  89. <Wrap>Back</Wrap>
  90. </Breadcrumb>
  91. </BreadcrumbRow>
  92. <StackTitleWrapper>
  93. <TitleSection materialIconClass="material-icons-outlined" icon={"lan"}>
  94. {stack.name}
  95. </TitleSection>
  96. <NamespaceTag.Wrapper>
  97. Namespace
  98. <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
  99. </NamespaceTag.Wrapper>
  100. </StackTitleWrapper>
  101. {/* Stack error message */}
  102. {currentRevision &&
  103. currentRevision?.reason &&
  104. currentRevision?.message?.length > 0 ? (
  105. <StackErrorMessageStyles.Wrapper>
  106. <i className="material-icons">history</i>
  107. <StackErrorMessageStyles.Text color="#aaaabb">
  108. {currentRevision?.status === "failed" ? "Error: " : ""}
  109. {currentRevision?.message}
  110. </StackErrorMessageStyles.Text>
  111. </StackErrorMessageStyles.Wrapper>
  112. ) : null}
  113. <Break />
  114. <InfoWrapper>
  115. <LastDeployed>
  116. <Status
  117. status={getStackStatus(stack)}
  118. message={getStackStatusMessage(stack)}
  119. />
  120. <SepDot>•</SepDot>
  121. Last updated {readableDate(stack.updated_at)}
  122. </LastDeployed>
  123. </InfoWrapper>
  124. <RevisionList
  125. revisions={stack.revisions}
  126. currentRevision={currentRevision}
  127. latestRevision={stack.latest_revision}
  128. stackId={stack.id}
  129. stackNamespace={namespace}
  130. onRevisionClick={(revision) => setCurrentRevision(revision)}
  131. onRollback={() => refreshStack()}
  132. ></RevisionList>
  133. <Br />
  134. <TabSelector
  135. currentTab={currentTab}
  136. options={[
  137. {
  138. label: "Apps",
  139. value: "apps",
  140. component: (
  141. <>
  142. <Gap></Gap>
  143. <Action.Row>
  144. <Action.Button to={`${url}/new-app-resource`}>
  145. <i className="material-icons">add</i>
  146. Create app resource
  147. </Action.Button>
  148. </Action.Row>
  149. {currentRevision.id !== stack.latest_revision.id ? (
  150. <ChartListWrapper>
  151. <Placeholder>
  152. Not available when previewing revisions
  153. </Placeholder>
  154. </ChartListWrapper>
  155. ) : (
  156. <ChartListWrapper>
  157. <ChartList
  158. currentCluster={currentCluster}
  159. currentView="stacks"
  160. namespace={namespace}
  161. sortType="Alphabetical"
  162. appFilters={
  163. stack?.latest_revision?.resources?.map(
  164. (res) => res.name
  165. ) || []
  166. }
  167. closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
  168. />
  169. </ChartListWrapper>
  170. )}
  171. </>
  172. ),
  173. },
  174. {
  175. label: "Source config",
  176. value: "source_config",
  177. component: (
  178. <>
  179. <SourceConfig
  180. namespace={namespace}
  181. revision={currentRevision}
  182. readOnly={stack.latest_revision.id !== currentRevision.id}
  183. onSourceConfigUpdate={() => refreshStack()}
  184. ></SourceConfig>
  185. </>
  186. ),
  187. },
  188. {
  189. label: "Env groups",
  190. value: "env_groups",
  191. component: (
  192. <>
  193. <Gap></Gap>
  194. <Action.Row>
  195. <Action.Button to={`${url}/new-env-group`}>
  196. <i className="material-icons">add</i>
  197. Create env group
  198. </Action.Button>
  199. </Action.Row>
  200. <EnvGroups stack={stack} />
  201. </>
  202. ),
  203. },
  204. {
  205. label: "Settings",
  206. value: "settings",
  207. component: (
  208. <>
  209. <Gap></Gap>
  210. <Settings
  211. stack={stack}
  212. onDelete={handleDelete}
  213. onUpdate={refreshStack}
  214. />
  215. </>
  216. ),
  217. },
  218. ]}
  219. setCurrentTab={(tab) => {
  220. setCurrentTab(tab);
  221. }}
  222. ></TabSelector>
  223. <PaddingBottom />
  224. </div>
  225. );
  226. };
  227. export default ExpandedStack;
  228. const ArrowIcon = styled.img`
  229. width: 15px;
  230. margin-right: 8px;
  231. opacity: 50%;
  232. `;
  233. const BreadcrumbRow = styled.div`
  234. width: 100%;
  235. display: flex;
  236. justify-content: flex-start;
  237. `;
  238. const Breadcrumb = styled(DynamicLink)`
  239. color: #aaaabb88;
  240. font-size: 13px;
  241. margin-bottom: 15px;
  242. display: flex;
  243. align-items: center;
  244. margin-top: -10px;
  245. z-index: 999;
  246. padding: 5px;
  247. padding-right: 7px;
  248. border-radius: 5px;
  249. cursor: pointer;
  250. :hover {
  251. background: #ffffff11;
  252. }
  253. `;
  254. const Wrap = styled.div`
  255. z-index: 999;
  256. `;
  257. const PaddingBottom = styled.div`
  258. width: 100%;
  259. height: 150px;
  260. `;
  261. const Break = styled.div`
  262. width: 100%;
  263. height: 20px;
  264. `;
  265. const BackButton = styled(NavLink)`
  266. position: absolute;
  267. top: 0px;
  268. right: 0px;
  269. display: flex;
  270. width: 36px;
  271. cursor: pointer;
  272. height: 36px;
  273. align-items: center;
  274. justify-content: center;
  275. border: 1px solid #ffffff55;
  276. border-radius: 100px;
  277. background: #ffffff11;
  278. :hover {
  279. background: #ffffff22;
  280. > img {
  281. opacity: 1;
  282. }
  283. }
  284. `;
  285. const BackButtonImg = styled.img`
  286. width: 16px;
  287. opacity: 0.75;
  288. `;
  289. const ChartListWrapper = styled.div`
  290. width: 100%;
  291. margin: auto;
  292. padding-bottom: 125px;
  293. `;
  294. const Gap = styled.div`
  295. width: 100%;
  296. background: none;
  297. height: 30px;
  298. `;
  299. const StackErrorMessageStyles = {
  300. Text: styled(Text)`
  301. font-size: 13px;
  302. `,
  303. Wrapper: styled.div`
  304. display: flex;
  305. align-items: center;
  306. margin-top: 5px;
  307. > i {
  308. color: #ffffff44;
  309. margin-right: 8px;
  310. font-size: 20px;
  311. }
  312. `,
  313. Title: styled(Text)`
  314. font-size: 16px;
  315. font-weight: bold;
  316. `,
  317. };
  318. const StackTitleWrapper = styled.div`
  319. width: 100%;
  320. display: flex;
  321. position: relative;
  322. align-items: center;
  323. // Hotfix to make sure the title section and the namespace tag are aligned
  324. ${NamespaceTag.Wrapper} {
  325. margin-left: 17px;
  326. margin-bottom: 13px;
  327. }
  328. `;