ProvisionerStatus.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import React, { Component } from 'react';
  2. import styled from 'styled-components';
  3. import posthog from 'posthog-js';
  4. import api from 'shared/api';
  5. import { Context } from 'shared/Context';
  6. import ansiparse from 'shared/ansiparser'
  7. import loading from 'assets/loading.gif';
  8. import warning from 'assets/warning.png';
  9. import { InfraType } from 'shared/types';
  10. import { filterOldInfras } from 'shared/common';
  11. import Helper from 'components/values-form/Helper';
  12. import InfraStatuses from './InfraStatuses';
  13. import { RouteComponentProps, withRouter } from "react-router";
  14. import { Link } from "react-router-dom";
  15. type PropsType = RouteComponentProps & {};
  16. type StateType = {
  17. error: boolean,
  18. logs: string[],
  19. websockets: any[],
  20. maxStep : Record<string, number>,
  21. currentStep: Record<string, number>,
  22. triggerEnd: boolean,
  23. infras: InfraType[],
  24. };
  25. const dummyInfras = [
  26. { kind: 'ecr', status: 'creating', id: 5, project_id: 1 },
  27. { kind: 'eks', status: 'error', id: 3, project_id: 1 },
  28. { kind: 'eks', status: 'error', id: 1, project_id: 1 },
  29. { kind: 'eks', status: 'error', id: 4, project_id: 1 },
  30. { kind: 'ecr', status: 'created', id: 2, project_id: 1 },
  31. ];
  32. class ProvisionerStatus extends Component<PropsType, StateType> {
  33. state = {
  34. error: false,
  35. logs: [] as string[],
  36. websockets : [] as any[],
  37. maxStep: {} as Record<string, any>,
  38. currentStep: {} as Record<string, number>,
  39. triggerEnd: false,
  40. infras: [] as InfraType[],
  41. }
  42. parentRef = React.createRef<HTMLDivElement>()
  43. scrollToBottom = (smooth: boolean) => {
  44. if (smooth) {
  45. this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
  46. } else {
  47. this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
  48. }
  49. }
  50. componentDidMount() {
  51. console.log('mounting provisioner')
  52. let { currentProject } = this.context;
  53. let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
  54. // Check if current project is provisioning
  55. api.getInfra('<token>', {}, {
  56. project_id: currentProject.id
  57. }, (err: any, res: any) => {
  58. if (err) {
  59. console.log(err);
  60. }
  61. let infras = filterOldInfras(res.data);
  62. let error = false;
  63. let maxStep = {} as Record<string, number>
  64. infras.forEach((infra: InfraType, i: number) => {
  65. maxStep[infra.kind] = null;
  66. if (infra.status === 'error') {
  67. error = true;
  68. }
  69. });
  70. // Filter historical infras list for most current instances of each
  71. let websockets = infras.map((infra: any) => {
  72. let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.id}/logs`)
  73. return this.setupWebsocket(ws, infra)
  74. });
  75. this.setState({ error, infras, websockets, maxStep, logs: ["Provisioning resources..."] });
  76. });
  77. }
  78. componentWillUnmount() {
  79. if (this.state.websockets.length == 0) { return; }
  80. this.state.websockets.forEach((ws: any) => {
  81. ws.close()
  82. })
  83. }
  84. isJSON = (str: string) => {
  85. try {
  86. JSON.parse(str);
  87. } catch (e) {
  88. return false;
  89. }
  90. return true;
  91. }
  92. setupWebsocket = (ws: WebSocket, infra: any) => {
  93. ws.onopen = () => {
  94. console.log('connected to websocket')
  95. }
  96. ws.onmessage = (evt: MessageEvent) => {
  97. let event = JSON.parse(evt.data);
  98. let validEvents = [] as any[];
  99. let err = null;
  100. for (var i = 0; i < event.length; i++) {
  101. let msg = event[i];
  102. if (msg["Values"] && msg["Values"]["data"] && this.isJSON(msg["Values"]["data"])) {
  103. let d = JSON.parse(msg["Values"]["data"]);
  104. if (d["kind"] == "error") {
  105. err = d["log"];
  106. break;
  107. }
  108. // add only valid events
  109. if (d["log"] != null && d["created_resources"] != null && d["total_resources"] != null) {
  110. validEvents.push(d);
  111. }
  112. }
  113. }
  114. if (err) {
  115. posthog.capture('Provisioning Error', {error: err});
  116. let e = ansiparse(err).map((el: any) => {
  117. return el.text;
  118. })
  119. let index = this.state.infras.findIndex(el => el.kind === infra.kind)
  120. infra.status = "error"
  121. let infras = this.state.infras
  122. infras[index] = infra
  123. this.setState({ logs: [...this.state.logs, ...e], error: true, infras });
  124. return;
  125. }
  126. if (validEvents.length == 0) {
  127. return;
  128. }
  129. if (!this.state.maxStep[infra.kind] || !this.state.maxStep[infra.kind]["total_resources"]) {
  130. this.setState({
  131. maxStep: {
  132. ...this.state.maxStep,
  133. [infra.kind] : validEvents[validEvents.length - 1]["total_resources"]
  134. }
  135. })
  136. }
  137. let logs = [] as any[]
  138. validEvents.forEach((e: any) => {
  139. logs.push(...ansiparse(e["log"]))
  140. })
  141. logs = logs.map((log: any) => {
  142. return log.text
  143. })
  144. this.setState({
  145. logs: [...this.state.logs, ...logs],
  146. currentStep: {
  147. ...this.state.currentStep,
  148. [infra.kind] : validEvents[validEvents.length - 1]["created_resources"]
  149. },
  150. }, () => {
  151. this.scrollToBottom(false)
  152. })
  153. }
  154. ws.onerror = (err: ErrorEvent) => {
  155. console.log('websocket err', err)
  156. }
  157. ws.onclose = () => {
  158. console.log('closing provisioner websocket')
  159. }
  160. return ws
  161. }
  162. renderLogs = () => {
  163. return this.state.logs.map((log, i) => {
  164. return <Log key={i}>{log}</Log>;
  165. });
  166. }
  167. onEnd = () => {
  168. let myInterval = setInterval(() => {
  169. api.getClusters('<token>', {}, {
  170. id: this.context.currentProject.id
  171. }, (err: any, res: any) => {
  172. if (err) {
  173. console.log(err);
  174. } else if (res.data) {
  175. let clusters = res.data;
  176. if (clusters.length > 0) {
  177. this.props.history.push("dashboard");
  178. // console.log('provision end project: ', this.context.currentProject);
  179. // console.log('provision end cluster: ', this.context.currentCluster);
  180. clearInterval(myInterval);
  181. } else {
  182. // console.log('looped!')
  183. // console.log('response :', res.data);
  184. // console.log('provision end project: ', this.context.currentProject);
  185. // console.log('provision end cluster: ', this.context.currentCluster);
  186. }
  187. }
  188. });
  189. }, 1000);
  190. }
  191. refreshLogs = () => {
  192. if (this.state.websockets.length == 0) { return; }
  193. let { currentProject } = this.context;
  194. let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
  195. this.state.websockets.forEach((ws: any) => {
  196. ws.close()
  197. })
  198. this.setState({
  199. websockets: [],
  200. logs: []
  201. })
  202. let websockets = this.state.infras.map((infra: any) => {
  203. let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
  204. return this.setupWebsocket(ws, infra)
  205. });
  206. this.setState({ websockets, logs: ["Provisioning resources..."] });
  207. }
  208. render() {
  209. let { error, triggerEnd, infras } = this.state;
  210. let maxStep = 0;
  211. let currentStep = 0;
  212. let skip = false;
  213. for (let i = 0; i < infras.length; i++) {
  214. if (!this.state.maxStep[infras[i].kind]) {
  215. skip = true;
  216. }
  217. }
  218. if (!skip) {
  219. for (let key in this.state.maxStep) {
  220. maxStep += this.state.maxStep[key]
  221. currentStep += this.state.currentStep[key]
  222. }
  223. }
  224. if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
  225. posthog.capture('Provisioning complete!')
  226. this.onEnd()
  227. this.setState({ triggerEnd: true });
  228. }
  229. return (
  230. <StyledProvisioner>
  231. {error ? (
  232. <>
  233. <TitleSection>
  234. <Title>
  235. <img src={warning} /> Provisioning Error
  236. </Title>
  237. </TitleSection>
  238. <Helper>
  239. Porter encountered an error while provisioning.
  240. <Link to="dashboard">Exit to dashboard</Link>
  241. to try again with new credentials.
  242. </Helper>
  243. </>
  244. ) : (
  245. <>
  246. <TitleSection>
  247. <Title>
  248. <img src={loading} /> Setting Up Porter
  249. </Title>
  250. </TitleSection>
  251. <Helper>
  252. Porter is currently provisioning resources in your cloud provider:
  253. </Helper>
  254. </>
  255. )}
  256. <LoadingBar>
  257. <Loaded
  258. progress={
  259. error
  260. ? "0%"
  261. : (
  262. (currentStep / (maxStep == 0 ? 1 : maxStep)) *
  263. 100
  264. ).toString() + "%"
  265. }
  266. />
  267. </LoadingBar>
  268. <InfraStatuses infras={infras} />
  269. <LogStream>
  270. <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
  271. </LogStream>
  272. <Helper>(Provisioning usually takes around 15 minutes)</Helper>
  273. </StyledProvisioner>
  274. );
  275. }
  276. }
  277. ProvisionerStatus.contextType = Context;
  278. export default withRouter(ProvisionerStatus);
  279. const Options = styled.div`
  280. width: 100%;
  281. height: 25px;
  282. background: #397ae3;
  283. display: flex;
  284. flex-direction: row;
  285. align-items: center;
  286. justify-content: space-between;
  287. `
  288. const Refresh = styled.div`
  289. display: flex;
  290. align-items: center;
  291. width: 87px;
  292. user-select: none;
  293. cursor: pointer;
  294. height: 100%;
  295. > i {
  296. margin-left: 6px;
  297. font-size: 17px;
  298. margin-right: 6px;
  299. }
  300. :hover {
  301. background: #2468d6;
  302. }
  303. `
  304. // const Link = styled.a`
  305. // cursor: pointer;
  306. // margin-left: 5px;
  307. // margin-right: 5px;
  308. // `;
  309. const Warning = styled.span`
  310. color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
  311. margin-left: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.makeFlush ? '' : '5px'};
  312. margin-right: 5px;
  313. `;
  314. const Wrapper = styled.div`
  315. width: 100%;
  316. height: 100%;
  317. overflow: auto;
  318. padding: 20px 25px;
  319. `;
  320. const Log = styled.div`
  321. font-family: monospace;
  322. `;
  323. const LogStream = styled.div`
  324. height: 300px;
  325. margin-top: 20px;
  326. font-size: 13px;
  327. border: 2px solid #ffffff55;
  328. border-radius: 10px;
  329. width: 100%;
  330. background: #00000022;
  331. user-select: text;
  332. `;
  333. const Message = styled.div`
  334. display: flex;
  335. height: 100%;
  336. width: 100%;
  337. align-items: center;
  338. justify-content: center;
  339. color: #ffffff44;
  340. font-size: 13px;
  341. `;
  342. const Loaded = styled.div<{ progress: string }>`
  343. width: ${props => props.progress};
  344. height: 100%;
  345. background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
  346. background-size: 400% 400%;
  347. animation: linkLoad 2s infinite;
  348. @keyframes linkLoad {
  349. 0%{background-position:91% 100%}
  350. 100%{background-position:10% 0%}
  351. }
  352. `;
  353. const LoadingBar = styled.div`
  354. width: 100%;
  355. margin-top: 24px;
  356. overflow: hidden;
  357. height: 20px;
  358. background: #ffffff11;
  359. border-radius: 30px;
  360. `;
  361. const Title = styled.div`
  362. font-size: 24px;
  363. font-weight: 600;
  364. font-family: 'Work Sans', sans-serif;
  365. color: #ffffff;
  366. white-space: nowrap;
  367. overflow: hidden;
  368. text-overflow: ellipsis;
  369. > img {
  370. width: 20px;
  371. margin-right: 10px;
  372. margin-bottom: -2px;
  373. }
  374. `;
  375. const TitleSection = styled.div`
  376. margin-bottom: 20px;
  377. display: flex;
  378. flex-direction: row;
  379. align-items: center;
  380. > a {
  381. > i {
  382. display: flex;
  383. align-items: center;
  384. margin-bottom: -2px;
  385. font-size: 18px;
  386. margin-left: 18px;
  387. color: #858FAAaa;
  388. cursor: pointer;
  389. :hover {
  390. color: #aaaabb;
  391. }
  392. }
  393. }
  394. `;
  395. const StyledProvisioner = styled.div`
  396. width: calc(90% - 150px);
  397. min-width: 300px;
  398. height: 600px;
  399. position: relative;
  400. padding-top: 50px;
  401. margin-top: calc(50vh - 350px);
  402. `;