MinionPoolEvents.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /*
  2. Copyright (C) 2020 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import * as React from "react";
  15. import styled from "styled-components";
  16. import {
  17. MinionPoolDetails,
  18. MinionPoolEventProgressUpdate,
  19. } from "@src/@types/MinionPool";
  20. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  21. import DropdownLink from "@src/components/ui/Dropdowns/DropdownLink";
  22. import InfoIcon from "@src/components/ui/InfoIcon";
  23. import Pagination from "@src/components/ui/Pagination";
  24. import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
  25. import configLoader from "@src/utils/Config";
  26. import DateUtils from "@src/utils/DateUtils";
  27. const Wrapper = styled.div``;
  28. const Filters = styled.div`
  29. margin-bottom: 24px;
  30. display: flex;
  31. `;
  32. const FilterDropdownWrapper = styled.div`
  33. margin-left: 24px;
  34. `;
  35. const EventsTable = styled.div`
  36. background: ${ThemePalette.grayscale[1]};
  37. border-radius: ${ThemeProps.borderRadius};
  38. margin-bottom: 16px;
  39. `;
  40. const Header = styled.div`
  41. display: flex;
  42. border-bottom: 1px solid ${ThemePalette.grayscale[5]};
  43. padding: 4px 8px;
  44. `;
  45. type DataDivProps = {
  46. width?: string;
  47. grow?: boolean;
  48. secondary?: boolean;
  49. };
  50. const HeaderData = styled.div<DataDivProps>`
  51. ${props => (props.width ? ThemeProps.exactWidth(props.width) : "")}
  52. ${props => (props.grow ? "flex-grow: 1;" : "")}
  53. font-size: 10px;
  54. color: ${ThemePalette.grayscale[5]};
  55. font-weight: ${ThemeProps.fontWeights.medium};
  56. text-transform: uppercase;
  57. `;
  58. const Body = styled.div``;
  59. const Row = styled.div`
  60. display: flex;
  61. padding: 8px;
  62. border-bottom: 1px solid white;
  63. `;
  64. const RowData = styled.div<DataDivProps>`
  65. ${props => (props.width ? ThemeProps.exactWidth(props.width) : "")}
  66. ${props => (props.grow ? "flex-grow: 1;" : "")}
  67. ${props => (props.secondary ? `color: ${ThemePalette.grayscale[4]};` : "")}
  68. `;
  69. const Message = styled.pre`
  70. font-family: inherit;
  71. white-space: pre-line;
  72. margin: inherit;
  73. `;
  74. const NoData = styled.div`
  75. text-align: center;
  76. `;
  77. type FilterType = "all" | "events" | "progress";
  78. type EventLevel = "DEBUG" | "INFO" | "ERROR";
  79. type OrderDir = "asc" | "desc";
  80. type Props = {
  81. item?: MinionPoolDetails | null;
  82. };
  83. type State = {
  84. allEvents: MinionPoolEventProgressUpdate[];
  85. prevLenghts: number[];
  86. currentPage: number;
  87. filterBy: FilterType;
  88. eventLevel: EventLevel;
  89. orderDir: OrderDir;
  90. };
  91. class MinionPoolEvents extends React.Component<Props, State> {
  92. state = {
  93. allEvents: [] as MinionPoolEventProgressUpdate[],
  94. prevLenghts: [0, 0],
  95. currentPage: 1,
  96. filterBy: "events" as FilterType,
  97. eventLevel: "INFO" as EventLevel,
  98. orderDir: "desc" as OrderDir,
  99. };
  100. get filteredEventsWithoutPagination() {
  101. const shouldFilterByEventType = (event: any): boolean => {
  102. if (this.state.filterBy === "events") {
  103. return event.level;
  104. }
  105. if (this.state.filterBy === "progress") {
  106. return event.current_step != null;
  107. }
  108. return true;
  109. };
  110. const shouldFilterByLevel = (event: any): boolean => {
  111. if (!event.level) {
  112. return true;
  113. }
  114. if (this.state.eventLevel === "INFO") {
  115. return event.level === "INFO" || event.level === "ERROR";
  116. }
  117. if (this.state.eventLevel === "ERROR") {
  118. return event.level === "ERROR";
  119. }
  120. return true;
  121. };
  122. return this.state.allEvents
  123. .filter(
  124. (event: any) =>
  125. shouldFilterByEventType(event) && shouldFilterByLevel(event),
  126. )
  127. .sort((a: any, b: any) => {
  128. if (a.index && b.index && this.state.filterBy !== "all") {
  129. return this.state.orderDir === "asc"
  130. ? a.index - b.index
  131. : b.index - a.index;
  132. }
  133. const aTime = new Date(a.created_at).getTime();
  134. const bTime = new Date(b.created_at).getTime();
  135. return this.state.orderDir === "asc" ? aTime - bTime : bTime - aTime;
  136. });
  137. }
  138. get filteredEvents() {
  139. return this.filteredEventsWithoutPagination.filter((_, i) => {
  140. const minI =
  141. configLoader.config.maxMinionPoolEventsPerPage *
  142. (this.state.currentPage - 1);
  143. const maxI = minI + configLoader.config.maxMinionPoolEventsPerPage;
  144. return i >= minI && i < maxI;
  145. });
  146. }
  147. static getDerivedStateFromProps(
  148. props: Props,
  149. state: State,
  150. ): Partial<State> | null {
  151. if (!props.item) {
  152. return null;
  153. }
  154. const events = props.item?.events || [];
  155. const progressUpdates = props.item?.progress_updates || [];
  156. if (
  157. events.length === state.prevLenghts[0] &&
  158. progressUpdates.length === state.prevLenghts[1]
  159. ) {
  160. return null;
  161. }
  162. return {
  163. allEvents: events.concat(progressUpdates as any),
  164. prevLenghts: [events.length, progressUpdates.length],
  165. };
  166. }
  167. setOrderDir(orderDir: OrderDir) {
  168. this.setState({ orderDir, currentPage: 1 });
  169. }
  170. filterByType(filterBy: FilterType) {
  171. this.setState({ filterBy, currentPage: 1 });
  172. }
  173. filterByLevel(eventLevel: EventLevel) {
  174. this.setState({ eventLevel, currentPage: 1 });
  175. }
  176. handlePreviousPageClick() {
  177. this.setState(state => ({ currentPage: state.currentPage - 1 }));
  178. }
  179. handleNextPageClick() {
  180. this.setState(state => ({ currentPage: state.currentPage + 1 }));
  181. }
  182. renderHeader() {
  183. return (
  184. <Header>
  185. <HeaderData grow>Event / Progress Update Message</HeaderData>
  186. <HeaderData width="192px">Timestamp</HeaderData>
  187. </Header>
  188. );
  189. }
  190. renderBody() {
  191. return (
  192. <Body>
  193. {this.filteredEvents.map((event: any) => {
  194. let status = "INFO";
  195. status = event.level || status;
  196. if (event.level === "DEBUG") {
  197. status = "WARNING";
  198. }
  199. const title = event.current_step ? "Progress Update" : "Event";
  200. return (
  201. <Row key={event.id}>
  202. <RowData
  203. grow
  204. style={{
  205. display: "flex",
  206. alignItems: "center",
  207. paddingRight: "8px",
  208. }}
  209. >
  210. <StatusIcon
  211. style={{ marginRight: "8px" }}
  212. status={status}
  213. title={title}
  214. hollow={event.current_step != null}
  215. />
  216. <Message>{event.message}</Message>
  217. </RowData>
  218. <RowData width="192px" secondary>
  219. {DateUtils.getLocalDate(event.created_at).toFormat(
  220. "yyyy-LL-dd HH:mm:ss",
  221. )}
  222. </RowData>
  223. </Row>
  224. );
  225. })}
  226. </Body>
  227. );
  228. }
  229. renderPagination() {
  230. if (
  231. this.filteredEventsWithoutPagination.length <=
  232. configLoader.config.maxMinionPoolEventsPerPage
  233. ) {
  234. return null;
  235. }
  236. const totalPages = Math.ceil(
  237. this.filteredEventsWithoutPagination.length /
  238. configLoader.config.maxMinionPoolEventsPerPage,
  239. );
  240. return (
  241. <Pagination
  242. previousDisabled={this.state.currentPage === 1}
  243. nextDisabled={this.state.currentPage === totalPages}
  244. onPreviousClick={() => {
  245. this.handlePreviousPageClick();
  246. }}
  247. onNextClick={() => {
  248. this.handleNextPageClick();
  249. }}
  250. currentPage={this.state.currentPage}
  251. totalPages={totalPages}
  252. />
  253. );
  254. }
  255. renderEventsTable() {
  256. return (
  257. <EventsTable>
  258. {this.renderHeader()}
  259. {this.renderBody()}
  260. </EventsTable>
  261. );
  262. }
  263. renderFilters() {
  264. return (
  265. <Filters>
  266. <FilterDropdownWrapper>
  267. <DropdownLink
  268. selectedItem={this.state.filterBy}
  269. items={[
  270. { label: "Events", value: "events" },
  271. { label: "Progress Updates", value: "progress" },
  272. { label: "Events & Progress Updates", value: "all" },
  273. ]}
  274. onChange={item => {
  275. this.filterByType(item.value as FilterType);
  276. }}
  277. />
  278. </FilterDropdownWrapper>
  279. <FilterDropdownWrapper
  280. style={{ opacity: this.state.filterBy === "progress" ? 0.5 : 1 }}
  281. >
  282. <DropdownLink
  283. disabled={this.state.filterBy === "progress"}
  284. selectedItem={this.state.eventLevel}
  285. items={[
  286. { label: "DEBUG Event Level", value: "DEBUG" },
  287. { label: "INFO Event Level", value: "INFO" },
  288. { label: "ERROR Event Level", value: "ERROR" },
  289. ]}
  290. onChange={item => {
  291. this.filterByLevel(item.value as EventLevel);
  292. }}
  293. />
  294. <InfoIcon text="The log level only applies to the events. The progress updates are not affected." />
  295. </FilterDropdownWrapper>
  296. <FilterDropdownWrapper>
  297. <DropdownLink
  298. selectedItem={this.state.orderDir}
  299. items={[
  300. { label: "Ascending Order", value: "asc" },
  301. { label: "Descending Order", value: "desc" },
  302. ]}
  303. onChange={item => {
  304. this.setOrderDir(item.value as OrderDir);
  305. }}
  306. />
  307. </FilterDropdownWrapper>
  308. </Filters>
  309. );
  310. }
  311. renderNoData() {
  312. return (
  313. <NoData>
  314. There are no events or progress updates associated with this minion
  315. pool.
  316. </NoData>
  317. );
  318. }
  319. renderNoDataFound() {
  320. return <NoData>No events found</NoData>;
  321. }
  322. render() {
  323. const isNoData = this.state.allEvents.length === 0;
  324. const isNoDataFound = this.filteredEvents.length === 0;
  325. return (
  326. <Wrapper>
  327. {!isNoData ? this.renderFilters() : null}
  328. {!isNoData && !isNoDataFound ? this.renderEventsTable() : null}
  329. {!isNoData && !isNoDataFound ? this.renderPagination() : null}
  330. {isNoData ? this.renderNoData() : null}
  331. {isNoDataFound && !isNoData ? this.renderNoDataFound() : null}
  332. </Wrapper>
  333. );
  334. }
  335. }
  336. export default MinionPoolEvents;