Kaynağa Gözat

Merge branch 'alerting-fe' of github.com-meehawk:porter-dev/porter into preview-env-v2-fe

Soham Parekh 3 yıl önce
ebeveyn
işleme
f89c395f46
34 değiştirilmiş dosya ile 3589 ekleme ve 14624 silme
  1. 1 14275
      dashboard/package-lock.json
  2. 3 0
      dashboard/package.json
  3. 4 0
      dashboard/src/assets/danger.svg
  4. 6 0
      dashboard/src/assets/document.svg
  5. 4 0
      dashboard/src/assets/down-arrow.svg
  6. 3 0
      dashboard/src/assets/filter-outline.svg
  7. 5 0
      dashboard/src/assets/info-circle.svg
  8. 5 0
      dashboard/src/assets/info-outlined.svg
  9. 4 0
      dashboard/src/assets/time.svg
  10. 3 1
      dashboard/src/components/Boilerplate.tsx
  11. 9 45
      dashboard/src/components/RadioFilter.tsx
  12. 76 0
      dashboard/src/components/date-time-picker/DateTimePicker.tsx
  13. 885 0
      dashboard/src/components/date-time-picker/react-datepicker.css
  14. 1 1
      dashboard/src/components/porter-form/PorterForm.tsx
  15. 1 3
      dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx
  16. 161 130
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  17. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  18. 378 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx
  19. 201 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx
  20. 147 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx
  21. 222 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx
  22. 316 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx
  23. 53 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts
  24. 461 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx
  25. 94 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventTable.tsx
  26. 32 154
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx
  27. 155 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts
  28. 0 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx
  29. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  30. 345 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx
  31. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  32. 3 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  33. 1 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  34. 7 1
      dashboard/webpack.config.js

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 14275
dashboard/package-lock.json


+ 3 - 0
dashboard/package.json

@@ -45,6 +45,7 @@
     "react": "^16.13.1",
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-ace": "^9.1.3",
     "react-color": "^2.19.3",
     "react-color": "^2.19.3",
+    "react-datepicker": "^4.8.0",
     "react-dom": "^16.13.1",
     "react-dom": "^16.13.1",
     "react-error-boundary": "^3.1.3",
     "react-error-boundary": "^3.1.3",
     "react-infinite-scroll-component": "^6.1.0",
     "react-infinite-scroll-component": "^6.1.0",
@@ -89,6 +90,7 @@
     "@types/random-words": "^1.1.0",
     "@types/random-words": "^1.1.0",
     "@types/react": "^16.14.14",
     "@types/react": "^16.14.14",
     "@types/react-color": "^3.0.6",
     "@types/react-color": "^3.0.6",
+    "@types/react-datepicker": "^4.4.2",
     "@types/react-dom": "^16.9.8",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
     "@types/react-router": "^5.1.8",
@@ -101,6 +103,7 @@
     "babel-loader": "^8.2.2",
     "babel-loader": "^8.2.2",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-styled-components": "^1.13.3",
     "babel-plugin-styled-components": "^1.13.3",
+    "css-loader": "^5.2.7",
     "file-loader": "^6.1.0",
     "file-loader": "^6.1.0",
     "html-webpack-plugin": "^4.5.0",
     "html-webpack-plugin": "^4.5.0",
     "prettier": "2.2.1",
     "prettier": "2.2.1",

+ 4 - 0
dashboard/src/assets/danger.svg

@@ -0,0 +1,4 @@
+<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.4" d="M17.3164 77.6135C17.2687 77.6135 17.2247 77.6135 17.1734 77.6098C16.0184 77.5511 14.8854 77.3018 13.8074 76.8655C8.50171 74.7095 5.94237 68.6485 8.09471 63.3465L34.9384 16.3178C35.8624 14.6458 37.263 13.2451 38.9717 12.2991C43.9767 9.52715 50.3054 11.3495 53.0737 16.3508L79.7414 63.0201C80.3354 64.4171 80.5884 65.5538 80.6507 66.7125C80.7937 69.4845 79.8477 72.1428 77.9924 74.1998C76.137 76.2568 73.5887 77.4705 70.8204 77.6098L17.5804 77.6135H17.3164Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M40.79 36.743C40.79 34.9757 42.231 33.5347 43.9984 33.5347C45.7657 33.5347 47.2067 34.9757 47.2067 36.743V47.1123C47.2067 48.8833 45.7657 50.3207 43.9984 50.3207C42.231 50.3207 40.79 48.8833 40.79 47.1123V36.743ZM40.79 59.6564C40.79 57.878 42.231 56.4297 43.9984 56.4297C45.7657 56.4297 47.2067 57.8597 47.2067 59.616C47.2067 61.4237 45.7657 62.8647 43.9984 62.8647C42.231 62.8647 40.79 61.4237 40.79 59.6564Z" fill="white"/>
+</svg>

+ 6 - 0
dashboard/src/assets/document.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.7162 16.2234H8.49622" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M15.7162 12.0369H8.49622" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.2513 7.86011H8.49631" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9086 2.74982C15.9086 2.74982 8.23161 2.75382 8.21961 2.75382C5.45961 2.77082 3.75061 4.58682 3.75061 7.35682V16.5528C3.75061 19.3368 5.47261 21.1598 8.25661 21.1598C8.25661 21.1598 15.9326 21.1568 15.9456 21.1568C18.7056 21.1398 20.4156 19.3228 20.4156 16.5528V7.35682C20.4156 4.57282 18.6926 2.74982 15.9086 2.74982Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/down-arrow.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.2743 19.75V4.75" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.2987 13.7002L12.2747 19.7502L6.24969 13.7002" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/filter-outline.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29332 22L14.0696 19.7519V13.8603L21.5593 6.26456C21.8416 5.97995 22 5.58933 22 5.18027V3.51754C22 2.67869 21.3417 2 20.5295 2H3.47049C2.65826 2 2 2.67869 2 3.51754V5.2183C2 5.60431 2.14169 5.97534 2.39719 6.2565L9.29332 13.8603V22Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
dashboard/src/assets/info-circle.svg

@@ -0,0 +1,5 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99988 0.750183C15.1089 0.750183 19.2499 4.89218 19.2499 10.0002C19.2499 15.1082 15.1089 19.2502 9.99988 19.2502C4.89188 19.2502 0.749878 15.1082 0.749878 10.0002C0.749878 4.89218 4.89188 0.750183 9.99988 0.750183Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.995 6.20428V10.6233" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.995 13.7961H10.005" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
dashboard/src/assets/info-outlined.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.9899 15.7961V11.3771" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.9899 8.20428H11.9999" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3346 2.75018H7.66561C4.64461 2.75018 2.75061 4.88918 2.75061 7.91618V16.0842C2.75061 19.1112 4.63561 21.2502 7.66561 21.2502H16.3336C19.3646 21.2502 21.2506 19.1112 21.2506 16.0842V7.91618C21.2506 4.88918 19.3646 2.75018 16.3346 2.75018Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/time.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.2498 12.0005C21.2498 17.1095 17.1088 21.2505 11.9998 21.2505C6.8908 21.2505 2.7498 17.1095 2.7498 12.0005C2.7498 6.89149 6.8908 2.75049 11.9998 2.75049C17.1088 2.75049 21.2498 6.89149 21.2498 12.0005Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M15.4314 14.9429L11.6614 12.6939V7.84686" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 1
dashboard/src/components/Boilerplate.tsx

@@ -4,10 +4,12 @@ import styled from "styled-components";
 
 
 type Props = {};
 type Props = {};
 
 
-export const Boilerplate: React.FC<Props> = (props) => {
+const Boilerplate: React.FC<Props> = (props) => {
   const [someState, setSomeState] = useState("");
   const [someState, setSomeState] = useState("");
 
 
   return <StyledBoilerplate></StyledBoilerplate>;
   return <StyledBoilerplate></StyledBoilerplate>;
 };
 };
 
 
+export default Boilerplate;
+
 const StyledBoilerplate = styled.div``;
 const StyledBoilerplate = styled.div``;

+ 9 - 45
dashboard/src/components/RadioFilter.tsx

@@ -90,7 +90,7 @@ const RadioFilter: React.FC<Props> = (props) => {
         noMargin={props.noMargin}
         noMargin={props.noMargin}
       >
       >
         {props.icon && <FilterIcon src={props.icon} />}
         {props.icon && <FilterIcon src={props.icon} />}
-        {props.name}
+        <TextAlt>{props.name}</TextAlt>
         <Bar />
         <Bar />
         <Selected>
         <Selected>
           {props.selected
           {props.selected
@@ -113,6 +113,7 @@ const Bar = styled.div`
   height: calc(18px);
   height: calc(18px);
   background: #494b4f;
   background: #494b4f;
   margin: 0 8px;
   margin: 0 8px;
+  margin-left: 0;
 `;
 `;
 
 
 const Selected = styled.div`
 const Selected = styled.div`
@@ -131,6 +132,13 @@ const Text = styled.div`
   margin-right: 10px;
   margin-right: 10px;
 `;
 `;
 
 
+const TextAlt = styled(Text)`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+`;
+
 const OptionRow = styled.div<{ isLast: boolean; selected?: boolean }>`
 const OptionRow = styled.div<{ isLast: boolean; selected?: boolean }>`
   width: 100%;
   width: 100%;
   height: 35px;
   height: 35px;
@@ -146,21 +154,6 @@ const OptionRow = styled.div<{ isLast: boolean; selected?: boolean }>`
   }
   }
 `;
 `;
 
 
-const FilterCount = styled.div`
-  padding: 5px;
-  color: #ffffff;
-  background: #ffffff11;
-  margin-left: 7px;
-  font-size: 12px;
-  border-radius: 50px;
-  margin-right: -5px;
-  height: 20px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  min-width: 20px;
-`;
-
 const Placeholder = styled.div`
 const Placeholder = styled.div`
   color: #aaaabb88;
   color: #aaaabb88;
   font-size: 12px;
   font-size: 12px;
@@ -176,35 +169,6 @@ const ScrollableWrapper = styled.div`
   max-height: 350px;
   max-height: 350px;
 `;
 `;
 
 
-const Label = styled.div`
-  height: 37px;
-  display: flex;
-  align-items: center;
-  margin-left: 10px;
-  font-size: 13px;
-`;
-
-const Option: any = styled.div`
-  width: 100%;
-  border-top: 1px solid #00000000;
-  height: 37px;
-  font-size: 13px;
-  align-items: center;
-  display: flex;
-  align-items: center;
-  padding-left: 15px;
-  cursor: pointer;
-  padding-right: 10px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  background: ${(props: any) => (props.selected ? "#ffffff11" : "")};
-
-  :hover {
-    background: #ffffff22;
-  }
-`;
-
 const Relative = styled.div`
 const Relative = styled.div`
   position: relative;
   position: relative;
 `;
 `;

+ 76 - 0
dashboard/src/components/date-time-picker/DateTimePicker.tsx

@@ -0,0 +1,76 @@
+import React, { useState } from "react";
+
+import DatePicker from "react-datepicker";
+import time from "assets/time.svg";
+
+import styled from "styled-components";
+
+type Props = {
+  startDate: any;
+  setStartDate: any;
+};
+
+const DateTimePicker: React.FC<Props> = ({ startDate, setStartDate }) => {
+  return (
+    <DateTimePickerWrapper>
+      <TimeIcon src={time} />
+      <link
+        rel="stylesheet"
+        href="https://cdnjs.cloudflare.com/ajax/libs/react-datepicker/2.14.1/react-datepicker.min.css"
+      />
+      <Bar />
+      <StyledDatePicker
+        selected={startDate}
+        onChange={(date: any) => setStartDate(date)}
+        showTimeSelect
+        dateFormat="MMMM d, yyyy h:mm aa"
+      />
+    </DateTimePickerWrapper>
+  );
+};
+
+export default DateTimePicker;
+
+const Bar = styled.div`
+  width: 1px;
+  height: calc(18px);
+  background: #494b4f;
+  margin-left: 8px;
+`;
+
+const TimeIcon = styled.img`
+  width: 16px;
+  height: 16px;
+  z-index: 999;
+`;
+
+const Div = styled.div`
+  display: block;
+`;
+
+const DateTimePickerWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding-right: 42px;
+  margin-right: 10px;
+  padding-left: 8px;
+  height: 30px;
+  border-radius: 5px;
+  border: 1px solid #494b4f;
+  height: 30px;
+  background: #26292e;
+`;
+
+const StyledDatePicker = styled(DatePicker)`
+  border: 0;
+  width: calc(100% + 42px);
+  position: relative;
+  border: none;
+  margin-bottom: 3px;
+  outline-width: 0;
+  background: transparent;
+  text-align: center;
+  padding: 0 15px;
+  font-size: 13px;
+`;

+ 885 - 0
dashboard/src/components/date-time-picker/react-datepicker.css

@@ -0,0 +1,885 @@
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  margin-left: -8px;
+  position: absolute;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow,
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  box-sizing: content-box;
+  position: absolute;
+  border: 8px solid transparent;
+  height: 0;
+  width: 1px;
+}
+
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  content: "";
+  z-index: -1;
+  border-width: 8px;
+  left: -8px;
+  border-bottom-color: #aeaeae;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle {
+  top: 0;
+  margin-top: -8px;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before {
+  border-top: none;
+  border-bottom-color: #f0f0f0;
+}
+
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before {
+  top: -1px;
+  border-bottom-color: #aeaeae;
+}
+
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  bottom: 0;
+  margin-bottom: -8px;
+}
+
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  border-bottom: none;
+  border-top-color: #fff;
+}
+
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  bottom: -1px;
+  border-top-color: #aeaeae;
+}
+
+.react-datepicker-wrapper {
+  display: inline-block;
+  padding: 0;
+  border: 0;
+}
+
+.react-datepicker {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 0.8rem;
+  background-color: #fff;
+  color: #000;
+  border: 1px solid #aeaeae;
+  border-radius: 0.3rem;
+  display: inline-block;
+  position: relative;
+}
+
+.react-datepicker--time-only .react-datepicker__triangle {
+  left: 35px;
+}
+
+.react-datepicker--time-only .react-datepicker__time-container {
+  border-left: 0;
+}
+
+.react-datepicker--time-only .react-datepicker__time {
+  border-radius: 0.3rem;
+}
+
+.react-datepicker--time-only .react-datepicker__time-box {
+  border-radius: 0.3rem;
+}
+
+.react-datepicker__triangle {
+  position: absolute;
+  left: 50px;
+}
+
+.react-datepicker-popper {
+  z-index: 1;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] {
+  margin-top: 10px;
+}
+
+.react-datepicker-popper[data-placement="bottom-end"]
+  .react-datepicker__triangle,
+.react-datepicker-popper[data-placement="top-end"] .react-datepicker__triangle {
+  left: auto;
+  right: 50px;
+}
+
+.react-datepicker-popper[data-placement^="top"] {
+  margin-bottom: 10px;
+}
+
+.react-datepicker-popper[data-placement^="right"] {
+  margin-left: 8px;
+}
+
+.react-datepicker-popper[data-placement^="right"] .react-datepicker__triangle {
+  left: auto;
+  right: 42px;
+}
+
+.react-datepicker-popper[data-placement^="left"] {
+  margin-right: 8px;
+}
+
+.react-datepicker-popper[data-placement^="left"] .react-datepicker__triangle {
+  left: 42px;
+  right: auto;
+}
+
+.react-datepicker__header {
+  text-align: center;
+  background-color: #f0f0f0;
+  border-bottom: 1px solid #aeaeae;
+  border-top-left-radius: 0.3rem;
+  border-top-right-radius: 0.3rem;
+  padding-top: 8px;
+  position: relative;
+}
+
+.react-datepicker__header--time {
+  padding-bottom: 8px;
+  padding-left: 5px;
+  padding-right: 5px;
+}
+
+.react-datepicker__year-dropdown-container--select,
+.react-datepicker__month-dropdown-container--select,
+.react-datepicker__month-year-dropdown-container--select,
+.react-datepicker__year-dropdown-container--scroll,
+.react-datepicker__month-dropdown-container--scroll,
+.react-datepicker__month-year-dropdown-container--scroll {
+  display: inline-block;
+  margin: 0 2px;
+}
+
+.react-datepicker__current-month,
+.react-datepicker-time__header,
+.react-datepicker-year-header {
+  margin-top: 0;
+  color: #000;
+  font-weight: bold;
+  font-size: 0.944rem;
+}
+
+.react-datepicker-time__header {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+.react-datepicker__navigation {
+  background: none;
+  line-height: 1.7rem;
+  text-align: center;
+  cursor: pointer;
+  position: absolute;
+  top: 10px;
+  width: 0;
+  padding: 0;
+  border: 0.45rem solid transparent;
+  z-index: 1;
+  height: 10px;
+  width: 10px;
+  text-indent: -999em;
+  overflow: hidden;
+}
+
+.react-datepicker__navigation--previous {
+  left: 10px;
+  border-right-color: #ccc;
+}
+
+.react-datepicker__navigation--previous:hover {
+  border-right-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--previous--disabled,
+.react-datepicker__navigation--previous--disabled:hover {
+  border-right-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__navigation--next {
+  right: 10px;
+  border-left-color: #ccc;
+}
+
+.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
+  right: 80px;
+}
+
+.react-datepicker__navigation--next:hover {
+  border-left-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--next--disabled,
+.react-datepicker__navigation--next--disabled:hover {
+  border-left-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__navigation--years {
+  position: relative;
+  top: 0;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.react-datepicker__navigation--years-previous {
+  top: 4px;
+  border-top-color: #ccc;
+}
+
+.react-datepicker__navigation--years-previous:hover {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--years-upcoming {
+  top: -4px;
+  border-bottom-color: #ccc;
+}
+
+.react-datepicker__navigation--years-upcoming:hover {
+  border-bottom-color: #b3b3b3;
+}
+
+.react-datepicker__month-container {
+  float: left;
+}
+
+.react-datepicker__month {
+  margin: 0.4rem;
+  text-align: center;
+}
+
+.react-datepicker__month .react-datepicker__month-text,
+.react-datepicker__month .react-datepicker__quarter-text {
+  display: inline-block;
+  width: 4rem;
+  margin: 2px;
+}
+
+.react-datepicker__input-time-container {
+  clear: both;
+  width: 100%;
+  float: left;
+  margin: 5px 0 10px 15px;
+  text-align: left;
+}
+
+.react-datepicker__input-time-container .react-datepicker-time__caption {
+  display: inline-block;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container {
+  display: inline-block;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input {
+  display: inline-block;
+  margin-left: 10px;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input {
+  width: 85px;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"]::-webkit-inner-spin-button,
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"]::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"] {
+  -moz-appearance: textfield;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__delimiter {
+  margin-left: 5px;
+  display: inline-block;
+}
+
+.react-datepicker__time-container {
+  float: right;
+  border-left: 1px solid #aeaeae;
+  width: 85px;
+}
+
+.react-datepicker__time-container--with-today-button {
+  display: inline;
+  border: 1px solid #aeaeae;
+  border-radius: 0.3rem;
+  position: absolute;
+  right: -72px;
+  top: 0;
+}
+
+.react-datepicker__time-container .react-datepicker__time {
+  position: relative;
+  background: white;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box {
+  width: 85px;
+  overflow-x: hidden;
+  margin: 0 auto;
+  text-align: center;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list {
+  list-style: none;
+  margin: 0;
+  height: calc(195px + (1.7rem / 2));
+  overflow-y: scroll;
+  padding-right: 0px;
+  padding-left: 0px;
+  width: 100%;
+  box-sizing: content-box;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item {
+  height: 30px;
+  padding: 5px 10px;
+  white-space: nowrap;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item:hover {
+  cursor: pointer;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--selected {
+  background-color: #216ba5;
+  color: white;
+  font-weight: bold;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--selected:hover {
+  background-color: #216ba5;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--disabled {
+  color: #ccc;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--disabled:hover {
+  cursor: default;
+  background-color: transparent;
+}
+
+.react-datepicker__week-number {
+  color: #ccc;
+  display: inline-block;
+  width: 1.7rem;
+  line-height: 1.7rem;
+  text-align: center;
+  margin: 0.166rem;
+}
+
+.react-datepicker__week-number.react-datepicker__week-number--clickable {
+  cursor: pointer;
+}
+
+.react-datepicker__week-number.react-datepicker__week-number--clickable:hover {
+  border-radius: 0.3rem;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__day-names,
+.react-datepicker__week {
+  white-space: nowrap;
+}
+
+.react-datepicker__day-name,
+.react-datepicker__day,
+.react-datepicker__time-name {
+  color: #000;
+  display: inline-block;
+  width: 1.7rem;
+  line-height: 1.7rem;
+  text-align: center;
+  margin: 0.166rem;
+}
+
+.react-datepicker__month--selected,
+.react-datepicker__month--in-selecting-range,
+.react-datepicker__month--in-range,
+.react-datepicker__quarter--selected,
+.react-datepicker__quarter--in-selecting-range,
+.react-datepicker__quarter--in-range {
+  border-radius: 0.3rem;
+  background-color: #216ba5;
+  color: #fff;
+}
+
+.react-datepicker__month--selected:hover,
+.react-datepicker__month--in-selecting-range:hover,
+.react-datepicker__month--in-range:hover,
+.react-datepicker__quarter--selected:hover,
+.react-datepicker__quarter--in-selecting-range:hover,
+.react-datepicker__quarter--in-range:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__month--disabled,
+.react-datepicker__quarter--disabled {
+  color: #ccc;
+  pointer-events: none;
+}
+
+.react-datepicker__month--disabled:hover,
+.react-datepicker__quarter--disabled:hover {
+  cursor: default;
+  background-color: transparent;
+}
+
+.react-datepicker__day,
+.react-datepicker__month-text,
+.react-datepicker__quarter-text {
+  cursor: pointer;
+}
+
+.react-datepicker__day:hover,
+.react-datepicker__month-text:hover,
+.react-datepicker__quarter-text:hover {
+  border-radius: 0.3rem;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__day--today,
+.react-datepicker__month-text--today,
+.react-datepicker__quarter-text--today {
+  font-weight: bold;
+}
+
+.react-datepicker__day--highlighted,
+.react-datepicker__month-text--highlighted,
+.react-datepicker__quarter-text--highlighted {
+  border-radius: 0.3rem;
+  background-color: #3dcc4a;
+  color: #fff;
+}
+
+.react-datepicker__day--highlighted:hover,
+.react-datepicker__month-text--highlighted:hover,
+.react-datepicker__quarter-text--highlighted:hover {
+  background-color: #32be3f;
+}
+
+.react-datepicker__day--highlighted-custom-1,
+.react-datepicker__month-text--highlighted-custom-1,
+.react-datepicker__quarter-text--highlighted-custom-1 {
+  color: magenta;
+}
+
+.react-datepicker__day--highlighted-custom-2,
+.react-datepicker__month-text--highlighted-custom-2,
+.react-datepicker__quarter-text--highlighted-custom-2 {
+  color: green;
+}
+
+.react-datepicker__day--selected,
+.react-datepicker__day--in-selecting-range,
+.react-datepicker__day--in-range,
+.react-datepicker__month-text--selected,
+.react-datepicker__month-text--in-selecting-range,
+.react-datepicker__month-text--in-range,
+.react-datepicker__quarter-text--selected,
+.react-datepicker__quarter-text--in-selecting-range,
+.react-datepicker__quarter-text--in-range {
+  border-radius: 0.3rem;
+  background-color: #216ba5;
+  color: #fff;
+}
+
+.react-datepicker__day--selected:hover,
+.react-datepicker__day--in-selecting-range:hover,
+.react-datepicker__day--in-range:hover,
+.react-datepicker__month-text--selected:hover,
+.react-datepicker__month-text--in-selecting-range:hover,
+.react-datepicker__month-text--in-range:hover,
+.react-datepicker__quarter-text--selected:hover,
+.react-datepicker__quarter-text--in-selecting-range:hover,
+.react-datepicker__quarter-text--in-range:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__day--keyboard-selected,
+.react-datepicker__month-text--keyboard-selected,
+.react-datepicker__quarter-text--keyboard-selected {
+  border-radius: 0.3rem;
+  background-color: #2a87d0;
+  color: #fff;
+}
+
+.react-datepicker__day--keyboard-selected:hover,
+.react-datepicker__month-text--keyboard-selected:hover,
+.react-datepicker__quarter-text--keyboard-selected:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__day--in-selecting-range,
+.react-datepicker__month-text--in-selecting-range,
+.react-datepicker__quarter-text--in-selecting-range {
+  background-color: rgba(33, 107, 165, 0.5);
+}
+
+.react-datepicker__month--selecting-range .react-datepicker__day--in-range,
+.react-datepicker__month--selecting-range
+  .react-datepicker__month-text--in-range,
+.react-datepicker__month--selecting-range
+  .react-datepicker__quarter-text--in-range {
+  background-color: #f0f0f0;
+  color: #000;
+}
+
+.react-datepicker__day--disabled,
+.react-datepicker__month-text--disabled,
+.react-datepicker__quarter-text--disabled {
+  cursor: default;
+  color: #ccc;
+}
+
+.react-datepicker__day--disabled:hover,
+.react-datepicker__month-text--disabled:hover,
+.react-datepicker__quarter-text--disabled:hover {
+  background-color: transparent;
+}
+
+.react-datepicker__month-text.react-datepicker__month--selected:hover,
+.react-datepicker__month-text.react-datepicker__month--in-range:hover,
+.react-datepicker__month-text.react-datepicker__quarter--selected:hover,
+.react-datepicker__month-text.react-datepicker__quarter--in-range:hover,
+.react-datepicker__quarter-text.react-datepicker__month--selected:hover,
+.react-datepicker__quarter-text.react-datepicker__month--in-range:hover,
+.react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,
+.react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover {
+  background-color: #216ba5;
+}
+
+.react-datepicker__month-text:hover,
+.react-datepicker__quarter-text:hover {
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__input-container {
+  position: relative;
+  display: inline-block;
+  width: 100%;
+}
+
+.react-datepicker__year-read-view,
+.react-datepicker__month-read-view,
+.react-datepicker__month-year-read-view {
+  border: 1px solid transparent;
+  border-radius: 0.3rem;
+}
+
+.react-datepicker__year-read-view:hover,
+.react-datepicker__month-read-view:hover,
+.react-datepicker__month-year-read-view:hover {
+  cursor: pointer;
+}
+
+.react-datepicker__year-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__year-read-view:hover
+  .react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view:hover
+  .react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-year-read-view:hover
+  .react-datepicker__month-read-view--down-arrow {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  border-top-color: #ccc;
+  float: right;
+  margin-left: 20px;
+  top: 8px;
+  position: relative;
+  border-width: 0.45rem;
+}
+
+.react-datepicker__year-dropdown,
+.react-datepicker__month-dropdown,
+.react-datepicker__month-year-dropdown {
+  background-color: #f0f0f0;
+  position: absolute;
+  width: 50%;
+  left: 25%;
+  top: 30px;
+  z-index: 1;
+  text-align: center;
+  border-radius: 0.3rem;
+  border: 1px solid #aeaeae;
+}
+
+.react-datepicker__year-dropdown:hover,
+.react-datepicker__month-dropdown:hover,
+.react-datepicker__month-year-dropdown:hover {
+  cursor: pointer;
+}
+
+.react-datepicker__year-dropdown--scrollable,
+.react-datepicker__month-dropdown--scrollable,
+.react-datepicker__month-year-dropdown--scrollable {
+  height: 150px;
+  overflow-y: scroll;
+}
+
+.react-datepicker__year-option,
+.react-datepicker__month-option,
+.react-datepicker__month-year-option {
+  line-height: 20px;
+  width: 100%;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.react-datepicker__year-option:first-of-type,
+.react-datepicker__month-option:first-of-type,
+.react-datepicker__month-year-option:first-of-type {
+  border-top-left-radius: 0.3rem;
+  border-top-right-radius: 0.3rem;
+}
+
+.react-datepicker__year-option:last-of-type,
+.react-datepicker__month-option:last-of-type,
+.react-datepicker__month-year-option:last-of-type {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  border-bottom-left-radius: 0.3rem;
+  border-bottom-right-radius: 0.3rem;
+}
+
+.react-datepicker__year-option:hover,
+.react-datepicker__month-option:hover,
+.react-datepicker__month-year-option:hover {
+  background-color: #ccc;
+}
+
+.react-datepicker__year-option:hover
+  .react-datepicker__navigation--years-upcoming,
+.react-datepicker__month-option:hover
+  .react-datepicker__navigation--years-upcoming,
+.react-datepicker__month-year-option:hover
+  .react-datepicker__navigation--years-upcoming {
+  border-bottom-color: #b3b3b3;
+}
+
+.react-datepicker__year-option:hover
+  .react-datepicker__navigation--years-previous,
+.react-datepicker__month-option:hover
+  .react-datepicker__navigation--years-previous,
+.react-datepicker__month-year-option:hover
+  .react-datepicker__navigation--years-previous {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__year-option--selected,
+.react-datepicker__month-option--selected,
+.react-datepicker__month-year-option--selected {
+  position: absolute;
+  left: 15px;
+}
+
+.react-datepicker__close-icon {
+  cursor: pointer;
+  background-color: transparent;
+  border: 0;
+  outline: 0;
+  padding: 0px 6px 0px 0px;
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.react-datepicker__close-icon::after {
+  cursor: pointer;
+  background-color: #216ba5;
+  color: #fff;
+  border-radius: 50%;
+  height: 16px;
+  width: 16px;
+  padding: 2px;
+  font-size: 12px;
+  line-height: 1;
+  text-align: center;
+  display: table-cell;
+  vertical-align: middle;
+  content: "\00d7";
+}
+
+.react-datepicker__today-button {
+  background: #f0f0f0;
+  border-top: 1px solid #aeaeae;
+  cursor: pointer;
+  text-align: center;
+  font-weight: bold;
+  padding: 5px 0;
+  clear: left;
+}
+
+.react-datepicker__portal {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.8);
+  left: 0;
+  top: 0;
+  justify-content: center;
+  align-items: center;
+  display: flex;
+  z-index: 2147483647;
+}
+
+.react-datepicker__portal .react-datepicker__day-name,
+.react-datepicker__portal .react-datepicker__day,
+.react-datepicker__portal .react-datepicker__time-name {
+  width: 3rem;
+  line-height: 3rem;
+}
+
+@media (max-width: 400px), (max-height: 550px) {
+  .react-datepicker__portal .react-datepicker__day-name,
+  .react-datepicker__portal .react-datepicker__day,
+  .react-datepicker__portal .react-datepicker__time-name {
+    width: 2rem;
+    line-height: 2rem;
+  }
+}
+
+.react-datepicker__portal .react-datepicker__current-month,
+.react-datepicker__portal .react-datepicker-time__header {
+  font-size: 1.44rem;
+}
+
+.react-datepicker__portal .react-datepicker__navigation {
+  border: 0.81rem solid transparent;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous {
+  border-right-color: #ccc;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous:hover {
+  border-right-color: #b3b3b3;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous--disabled,
+.react-datepicker__portal
+  .react-datepicker__navigation--previous--disabled:hover {
+  border-right-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next {
+  border-left-color: #ccc;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next:hover {
+  border-left-color: #b3b3b3;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next--disabled,
+.react-datepicker__portal .react-datepicker__navigation--next--disabled:hover {
+  border-left-color: #e6e6e6;
+  cursor: default;
+}

+ 1 - 1
dashboard/src/components/porter-form/PorterForm.tsx

@@ -254,7 +254,7 @@ const StyledPorterForm = styled.div<{ showSave?: boolean }>`
   height: ${(props) => (props.showSave ? "calc(100% - 50px)" : "100%")};
   height: ${(props) => (props.showSave ? "calc(100% - 50px)" : "100%")};
   background: #ffffff11;
   background: #ffffff11;
   color: #ffffff;
   color: #ffffff;
-  padding: 0px 35px 25px;
+  padding: 0px 35px 20px;
   position: relative;
   position: relative;
   border-radius: 8px;
   border-radius: 8px;
   font-size: 13px;
   font-size: 13px;

+ 1 - 3
dashboard/src/main/home/cluster-dashboard/dashboard/events/EventsTab.tsx

@@ -3,7 +3,6 @@ import styled from "styled-components";
 import EventCard from "components/events/EventCard";
 import EventCard from "components/events/EventCard";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import InfiniteScroll from "react-infinite-scroll-component";
 import InfiniteScroll from "react-infinite-scroll-component";
-import Dropdown from "components/Dropdown";
 import { useKubeEvents } from "components/events/useEvents";
 import { useKubeEvents } from "components/events/useEvents";
 import SubEventsList from "components/events/SubEventsList";
 import SubEventsList from "components/events/SubEventsList";
 
 
@@ -39,8 +38,7 @@ const EventsTab = () => {
       <Placeholder>
       <Placeholder>
         <div>
         <div>
           <Header>We couldn't detect the Porter agent on your cluster</Header>
           <Header>We couldn't detect the Porter agent on your cluster</Header>
-          In order to use the events tab, you need to install the Porter agent
-          on your cluster.
+          In order to use the events tab, you need to install the Porter agent.
           <InstallPorterAgentButton onClick={() => triggerInstall()}>
           <InstallPorterAgentButton onClick={() => triggerInstall()}>
             <i className="material-icons">add</i> Install Porter agent
             <i className="material-icons">add</i> Install Porter agent
           </InstallPorterAgentButton>
           </InstallPorterAgentButton>

+ 161 - 130
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -14,6 +14,7 @@ import RevisionSection from "./RevisionSection";
 import ValuesYaml from "./ValuesYaml";
 import ValuesYaml from "./ValuesYaml";
 import GraphSection from "./GraphSection";
 import GraphSection from "./GraphSection";
 import MetricsSection from "./metrics/MetricsSection";
 import MetricsSection from "./metrics/MetricsSection";
+import LogsSection from "./logs-section/LogsSection";
 import ListSection from "./ListSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
 import SettingsSection from "./SettingsSection";
@@ -22,10 +23,11 @@ import { useWebsockets } from "shared/hooks/useWebsockets";
 import useAuth from "shared/auth/useAuth";
 import useAuth from "shared/auth/useAuth";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import DeploymentType from "./DeploymentType";
 import DeploymentType from "./DeploymentType";
-import IncidentsTab from "./incidents/IncidentsTab";
+import EventsTab from "./events/EventsTab";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces";
 import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces";
 import { useStackEnvGroups } from "./useStackEnvGroups";
 import { useStackEnvGroups } from "./useStackEnvGroups";
+import DeployStatusSection from "./deploy-status-section/DeployStatusSection";
 
 
 type Props = {
 type Props = {
   namespace: string;
   namespace: string;
@@ -74,6 +76,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [isAuthorized] = useAuth();
   const [isAuthorized] = useAuth();
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
+  const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
 
 
   const {
   const {
     isStack,
     isStack,
@@ -131,6 +134,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   };
   };
 
 
   const getControllers = async (chart: ChartType) => {
   const getControllers = async (chart: ChartType) => {
+    
     // don't retrieve controllers for chart that failed to even deploy.
     // don't retrieve controllers for chart that failed to even deploy.
     if (chart.info.status == "failed") return;
     if (chart.info.status == "failed") return;
 
 
@@ -412,16 +416,23 @@ const ExpandedChart: React.FC<Props> = (props) => {
     let { setSidebar } = props;
     let { setSidebar } = props;
     let chart = currentChart;
     let chart = currentChart;
     switch (currentTab) {
     switch (currentTab) {
+      case "logs":
+        return (
+          <LogsSection 
+            currentChart={chart}
+            isFullscreen={isFullscreen}
+            setIsFullscreen={setIsFullscreen}
+          />
+        );
       case "metrics":
       case "metrics":
         return <MetricsSection currentChart={chart} />;
         return <MetricsSection currentChart={chart} />;
-      case "incidents":
+      case "events":
         if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
         if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
           return null;
           return null;
         }
         }
         return (
         return (
-          <IncidentsTab
-            releaseName={chart?.name}
-            namespace={chart?.namespace}
+          <EventsTab
+            controllers={controllers}
           />
           />
         );
         );
       case "status":
       case "status":
@@ -525,13 +536,18 @@ const ExpandedChart: React.FC<Props> = (props) => {
     // Collate non-form tabs
     // Collate non-form tabs
     let rightTabOptions = [] as any[];
     let rightTabOptions = [] as any[];
     let leftTabOptions = [] as any[];
     let leftTabOptions = [] as any[];
-    leftTabOptions.push({ label: "Status", value: "status" });
-
-    /* Temporarily disable incident detection
-    if (!DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
-      leftTabOptions.push({ label: "Incidents", value: "incidents" });
+    if (
+      currentChart.chart.metadata.home === "https://getporter.dev/" &&
+      (
+        currentChart.chart.metadata.name === "web" || 
+        currentChart.chart.metadata.name === "worker" ||
+        currentChart.chart.metadata.name === "job"
+      )
+    ) {
+      leftTabOptions.push({ label: "Events", value: "events" });
+      leftTabOptions.push({ label: "Logs", value: "logs" });
     }
     }
-    */
+    leftTabOptions.push({ label: "Status", value: "status" });
 
 
     if (props.isMetricsInstalled) {
     if (props.isMetricsInstalled) {
       leftTabOptions.push({ label: "Metrics", value: "metrics" });
       leftTabOptions.push({ label: "Metrics", value: "metrics" });
@@ -586,9 +602,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const renderUrl = () => {
   const renderUrl = () => {
     if (url) {
     if (url) {
       return (
       return (
-        <Url href={url} target="_blank">
+        <Url>
           <i className="material-icons">link</i>
           <i className="material-icons">link</i>
-          {url}
+          <a href={url} target="_blank">{url}</a>
         </Url>
         </Url>
       );
       );
     }
     }
@@ -756,134 +772,147 @@ const ExpandedChart: React.FC<Props> = (props) => {
           setFullScreenLogs={() => setFullScreenLogs(false)}
           setFullScreenLogs={() => setFullScreenLogs(false)}
         />
         />
       ) : (
       ) : (
-        <StyledExpandedChart>
-          <BreadcrumbRow>
-            <Breadcrumb onClick={props.closeChart}>
-              <ArrowIcon src={leftArrow} />
-              <Wrap>Back</Wrap>
-            </Breadcrumb>
-          </BreadcrumbRow>
-          <HeaderWrapper>
-            <TitleSection
-              icon={currentChart.chart.metadata.icon}
-              iconWidth="33px"
-            >
-              {currentChart.name}
-              <DeploymentType currentChart={currentChart} />
-              <TagWrapper>
-                Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
-              </TagWrapper>
-            </TitleSection>
-
-            {currentChart.chart.metadata.name != "worker" &&
-              currentChart.chart.metadata.name != "job" &&
-              renderUrl()}
-            <InfoWrapper>
-              <StatusIndicator
-                controllers={controllers}
-                status={currentChart.info.status}
-                margin_left={"0px"}
-              />
-              <LastDeployed>
-                <Dot>•</Dot>Last deployed
-                {" " + getReadableDate(currentChart.info.last_deployed)}
-              </LastDeployed>
-            </InfoWrapper>
-          </HeaderWrapper>
-          {deleting ? (
-            <>
-              <LineBreak />
-              <Placeholder>
-                <TextWrap>
-                  <Header>
-                    <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
-                  </Header>
-                  You will be automatically redirected after deletion is
-                  complete.
-                </TextWrap>
-              </Placeholder>
-            </>
+        <>
+          {isFullscreen ? (
+            <LogsSection 
+              isFullscreen={true}
+              setIsFullscreen={setIsFullscreen}
+              currentChart={currentChart} 
+            />
           ) : (
           ) : (
-            <>
-              <RevisionSection
-                showRevisions={showRevisions}
-                toggleShowRevisions={() => {
-                  setShowRevisions(!showRevisions);
-                }}
-                chart={currentChart}
-                refreshChart={() => getChartData(currentChart)}
-                setRevision={setRevision}
-                forceRefreshRevisions={forceRefreshRevisions}
-                refreshRevisionsOff={() => setForceRefreshRevisions(false)}
-                shouldUpdate={
-                  currentChart.latest_version &&
-                  currentChart.latest_version !==
-                    currentChart.chart.metadata.version
-                }
-                latestVersion={currentChart.latest_version}
-                upgradeVersion={handleUpgradeVersion}
-              />
-              {isStack && isLoadingStackEnvGroups ? (
+            <StyledExpandedChart>
+              <BreadcrumbRow>
+                <Breadcrumb onClick={props.closeChart}>
+                  <ArrowIcon src={leftArrow} />
+                  <Wrap>Back</Wrap>
+                </Breadcrumb>
+              </BreadcrumbRow>
+              <HeaderWrapper>
+                <TitleSection
+                  icon={currentChart.chart.metadata.icon}
+                  iconWidth="33px"
+                >
+                  {currentChart.name}
+                  <DeploymentType currentChart={currentChart} />
+                  <TagWrapper>
+                    Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
+                  </TagWrapper>
+                </TitleSection>
+
+                {currentChart.chart.metadata.name != "worker" &&
+                  currentChart.chart.metadata.name != "job" &&
+                  renderUrl()}
+                <InfoWrapper>
+                  {/*
+                  <StatusIndicator
+                    controllers={controllers}
+                    status={currentChart.info.status}
+                    margin_left={"0px"}
+                  />
+                  */}
+                  <DeployStatusSection chart={currentChart} />
+                  <LastDeployed>
+                    <Dot>•</Dot>Last deployed
+                    {" " + getReadableDate(currentChart.info.last_deployed)}
+                  </LastDeployed>
+                </InfoWrapper>
+              </HeaderWrapper>
+              {deleting ? (
                 <>
                 <>
                   <LineBreak />
                   <LineBreak />
                   <Placeholder>
                   <Placeholder>
                     <TextWrap>
                     <TextWrap>
                       <Header>
                       <Header>
-                        <Spinner src={loadingSrc} />
+                        <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
                       </Header>
                       </Header>
+                      You will be automatically redirected after deletion is
+                      complete.
                     </TextWrap>
                     </TextWrap>
                   </Placeholder>
                   </Placeholder>
                 </>
                 </>
               ) : (
               ) : (
                 <>
                 <>
-                  {(isPreview || leftTabOptions.length > 0) && (
-                    <BodyWrapper>
-                      <PorterFormWrapper
-                        formData={cloneDeep(currentChart.form)}
-                        valuesToOverride={{
-                          namespace: props.namespace,
-                          clusterId: currentCluster.id,
-                        }}
-                        renderTabContents={renderTabContents}
-                        isReadOnly={
-                          isPreview ||
-                          imageIsPlaceholder ||
-                          !isAuthorized("application", "", ["get", "update"])
-                        }
-                        onSubmit={onSubmit}
-                        includeMetadata
-                        rightTabOptions={rightTabOptions}
-                        leftTabOptions={leftTabOptions}
-                        color={isPreview ? "#f5cb42" : null}
-                        addendum={
-                          <TabButton
-                            onClick={toggleDevOpsMode}
-                            devOpsMode={devOpsMode}
-                          >
-                            <i className="material-icons">offline_bolt</i>{" "}
-                            DevOps Mode
-                          </TabButton>
-                        }
-                        saveValuesStatus={saveValuesStatus}
-                        injectedProps={{
-                          "key-value-array": {
-                            availableSyncEnvGroups:
-                              isStack && !isPreview
-                                ? stackEnvGroups
-                                : undefined,
-                          },
-                          "url-link": {
-                            chart: currentChart,
-                          },
-                        }}
-                      />
-                    </BodyWrapper>
+                  <RevisionSection
+                    showRevisions={showRevisions}
+                    toggleShowRevisions={() => {
+                      setShowRevisions(!showRevisions);
+                    }}
+                    chart={currentChart}
+                    refreshChart={() => getChartData(currentChart)}
+                    setRevision={setRevision}
+                    forceRefreshRevisions={forceRefreshRevisions}
+                    refreshRevisionsOff={() => setForceRefreshRevisions(false)}
+                    shouldUpdate={
+                      currentChart.latest_version &&
+                      currentChart.latest_version !==
+                        currentChart.chart.metadata.version
+                    }
+                    latestVersion={currentChart.latest_version}
+                    upgradeVersion={handleUpgradeVersion}
+                  />
+                  {isStack && isLoadingStackEnvGroups ? (
+                    <>
+                      <LineBreak />
+                      <Placeholder>
+                        <TextWrap>
+                          <Header>
+                            <Spinner src={loadingSrc} />
+                          </Header>
+                        </TextWrap>
+                      </Placeholder>
+                    </>
+                  ) : (
+                    <>
+                      {(isPreview || leftTabOptions.length > 0) && (
+                        <BodyWrapper>
+                          <PorterFormWrapper
+                            formData={cloneDeep(currentChart.form)}
+                            valuesToOverride={{
+                              namespace: props.namespace,
+                              clusterId: currentCluster.id,
+                            }}
+                            renderTabContents={renderTabContents}
+                            isReadOnly={
+                              isPreview ||
+                              imageIsPlaceholder ||
+                              !isAuthorized("application", "", ["get", "update"])
+                            }
+                            onSubmit={onSubmit}
+                            includeMetadata
+                            rightTabOptions={rightTabOptions}
+                            leftTabOptions={leftTabOptions}
+                            color={isPreview ? "#f5cb42" : null}
+                            addendum={
+                              <TabButton
+                                onClick={toggleDevOpsMode}
+                                devOpsMode={devOpsMode}
+                              >
+                                <i className="material-icons">offline_bolt</i>{" "}
+                                DevOps Mode
+                              </TabButton>
+                            }
+                            saveValuesStatus={saveValuesStatus}
+                            injectedProps={{
+                              "key-value-array": {
+                                availableSyncEnvGroups:
+                                  isStack && !isPreview
+                                    ? stackEnvGroups
+                                    : undefined,
+                              },
+                              "url-link": {
+                                chart: currentChart,
+                              },
+                            }}
+                          />
+                        </BodyWrapper>
+                      )}
+                    </>
                   )}
                   )}
                 </>
                 </>
               )}
               )}
-            </>
+            </StyledExpandedChart>
           )}
           )}
-        </StyledExpandedChart>
+        </>
       )}
       )}
     </>
     </>
   );
   );
@@ -936,7 +965,8 @@ const LineBreak = styled.div`
 
 
 const BodyWrapper = styled.div`
 const BodyWrapper = styled.div`
   position: relative;
   position: relative;
-  margin-bottom: 50px;
+  padding-bottom: 0;
+  margin-bottom: 0;
 `;
 `;
 
 
 const Header = styled.div`
 const Header = styled.div`
@@ -975,15 +1005,16 @@ const Bolded = styled.div`
   margin-right: 6px;
   margin-right: 6px;
 `;
 `;
 
 
-const Url = styled.a`
+const Url = styled.div`
   display: block;
   display: block;
-  margin-left: 2px;
+  margin-left: 5px;
   font-size: 13px;
   font-size: 13px;
   margin-top: 16px;
   margin-top: 16px;
   user-select: all;
   user-select: all;
   margin-bottom: -5px;
   margin-bottom: -5px;
   user-select: text;
   user-select: text;
   display: flex;
   display: flex;
+  color: #949eff;
   align-items: center;
   align-items: center;
 
 
   > i {
   > i {
@@ -1026,7 +1057,7 @@ const HeaderWrapper = styled.div`
 `;
 `;
 
 
 const Dot = styled.div`
 const Dot = styled.div`
-  margin-right: 9px;
+  margin-right: 16px;
 `;
 `;
 
 
 const InfoWrapper = styled.div`
 const InfoWrapper = styled.div`
@@ -1038,7 +1069,7 @@ const InfoWrapper = styled.div`
 
 
 const LastDeployed = styled.div`
 const LastDeployed = styled.div`
   font-size: 13px;
   font-size: 13px;
-  margin-left: 10px;
+  margin-left: 8px;
   margin-top: -1px;
   margin-top: -1px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -500,7 +500,7 @@ const StyledRevisionSection = styled.div`
   max-height: ${(props: { showRevisions: boolean }) =>
   max-height: ${(props: { showRevisions: boolean }) =>
     props.showRevisions ? "255px" : "40px"};
     props.showRevisions ? "255px" : "40px"};
   background: #ffffff11;
   background: #ffffff11;
-  margin: 25px 0px 18px;
+  margin: 20px 0px 18px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 8px;
   border-radius: 8px;
   animation: ${(props: { showRevisions: boolean }) =>
   animation: ${(props: { showRevisions: boolean }) =>

+ 378 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx

@@ -0,0 +1,378 @@
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import styled from "styled-components";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import ResourceTab from "./ResourceTab";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
+import PodRow from "./PodRow";
+import { timeFormat } from "d3-time-format";
+import { getAvailability, getPodStatus } from "./util";
+import _ from "lodash";
+
+type Props = {
+  controller: any;
+  selectedPod: any;
+  selectPod: (newPod: any) => unknown;
+  selectors: any;
+  isLast?: boolean;
+  isFirst?: boolean;
+  setPodError: (x: string) => void;
+  onUpdate: (update: any) => void;
+};
+
+// Controller tab in log section that displays list of pods on click.
+export type ControllerTabPodType = {
+  namespace: string;
+  name: string;
+  phase: string;
+  status: any;
+  replicaSetName: string;
+  restartCount: number | string;
+  podAge: string;
+  revisionNumber?: number;
+  containerStatus: any;
+};
+
+const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
+
+const ControllerTabFC: React.FunctionComponent<Props> = ({
+  controller,
+  selectPod,
+  isFirst,
+  isLast,
+  selectors,
+  setPodError,
+  selectedPod,
+  onUpdate,
+}) => {
+  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
+  const [rawPodList, setRawPodList] = useState<any[]>([]);
+  const [podPendingDelete, setPodPendingDelete] = useState<any>(null);
+  const [available, setAvailable] = useState<number>(null);
+  const [total, setTotal] = useState<number>(null);
+  const [userSelectedPod, setUserSelectedPod] = useState<boolean>(false);
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    closeWebsocket,
+  } = useWebsockets();
+
+  const currentSelectors = useMemo(() => {
+    if (controller.kind.toLowerCase() == "job" && selectors) {
+      return [...selectors];
+    }
+    let newSelectors = [] as string[];
+    let ml =
+      controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
+    let i = 1;
+    let selector = "";
+    for (var key in ml) {
+      selector += key + "=" + ml[key];
+      if (i != Object.keys(ml).length) {
+        selector += ",";
+      }
+      i += 1;
+    }
+    newSelectors.push(selector);
+    return [...newSelectors];
+  }, [controller, selectors]);
+
+  useEffect(() => {
+    updatePods();
+    [controller?.kind, "pod"].forEach((kind) => {
+      setupWebsocket(kind, controller?.metadata?.uid);
+    });
+    () => closeAllWebsockets();
+  }, [currentSelectors, controller, currentCluster, currentProject]);
+
+  const updatePods = async () => {
+    try {
+      const res = await api.getMatchingPods(
+        "<token>",
+        {
+          namespace: controller?.metadata?.namespace,
+          selectors: currentSelectors,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+      const data = res?.data as any[];
+      let newPods = data
+        // Parse only data that we need
+        .map<ControllerTabPodType>((pod: any) => {
+          const replicaSetName =
+            Array.isArray(pod?.metadata?.ownerReferences) &&
+            pod?.metadata?.ownerReferences[0]?.name;
+          const containerStatus =
+            Array.isArray(pod?.status?.containerStatuses) &&
+            pod?.status?.containerStatuses[0];
+
+          const restartCount = containerStatus
+            ? containerStatus.restartCount
+            : "N/A";
+
+          const podAge = formatCreationTimestamp(
+            new Date(pod?.metadata?.creationTimestamp)
+          );
+
+          return {
+            namespace: pod?.metadata?.namespace,
+            name: pod?.metadata?.name,
+            phase: pod?.status?.phase,
+            status: pod?.status,
+            replicaSetName,
+            restartCount,
+            containerStatus,
+            podAge: pod?.metadata?.creationTimestamp ? podAge : "N/A",
+            revisionNumber:
+              (pod?.metadata?.annotations &&
+                pod?.metadata?.annotations["helm.sh/revision"]) ||
+              "N/A",
+          };
+        });
+
+      setPods(newPods);
+      setRawPodList(data);
+      // If the user didn't click a pod, select the first returned from list.
+      if (!userSelectedPod) {
+        let status = getPodStatus(newPods[0].status);
+        status === "failed" &&
+          newPods[0].status?.message &&
+          setPodError(newPods[0].status?.message);
+        handleSelectPod(newPods[0], data);
+      }
+    } catch (error) {}
+  };
+
+  /**
+   * handleSelectPod is a wrapper for the selectPod function received from parent.
+   * Internally we use the ControllerPodType but we want to pass to the parent the
+   * raw pod returned from the API.
+   *
+   * @param pod A ControllerPodType pod that will be used to search the raw pod to pass
+   * @param rawList A rawList of pods in case we don't want to use the state one. Useful to
+   * avoid problems with reactivity
+   */
+  const handleSelectPod = (pod: ControllerTabPodType, rawList?: any[]) => {
+    const rawPod = [...rawPodList, ...(rawList || [])].find(
+      (rawPod) => rawPod?.metadata?.name === pod?.name
+    );
+    selectPod(rawPod);
+  };
+
+  const currentSelectedPod = useMemo(() => {
+    const pod = selectedPod;
+    const replicaSetName =
+      Array.isArray(pod?.metadata?.ownerReferences) &&
+      pod?.metadata?.ownerReferences[0]?.name;
+    return {
+      namespace: pod?.metadata?.namespace,
+      name: pod?.metadata?.name,
+      phase: pod?.status?.phase,
+      status: pod?.status,
+      replicaSetName,
+    } as ControllerTabPodType;
+  }, [selectedPod]);
+
+  const currentControllerStatus = useMemo(() => {
+    let status = available == total ? "running" : "waiting";
+
+    controller?.status?.conditions?.forEach((condition: any) => {
+      if (
+        condition.type == "Progressing" &&
+        condition.status == "False" &&
+        condition.reason == "ProgressDeadlineExceeded"
+      ) {
+        status = "failed";
+      }
+    });
+
+    if (controller.kind.toLowerCase() === "job" && pods.length == 0) {
+      status = "completed";
+    }
+    return status;
+  }, [controller, available, total, pods]);
+
+  const handleDeletePod = (pod: any) => {
+    api
+      .deletePod(
+        "<token>",
+        {},
+        {
+          cluster_id: currentCluster.id,
+          name: pod?.name,
+          namespace: pod?.namespace,
+          id: currentProject.id,
+        }
+      )
+      .then((res) => {
+        updatePods();
+        setPodPendingDelete(null);
+      })
+      .catch((err) => {
+        setCurrentError(JSON.stringify(err));
+        setPodPendingDelete(null);
+      });
+  };
+
+  const replicaSetArray = useMemo(() => {
+    const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"]).reverse().reduce<
+      Array<Array<ControllerTabPodType>>
+    >(function (prev, currentPod, i) {
+      if (
+        !i ||
+        prev[prev.length - 1][0].replicaSetName !== currentPod.replicaSetName
+      ) {
+        return prev.concat([[currentPod]]);
+      }
+      prev[prev.length - 1].push(currentPod);
+      return prev;
+    }, []);
+
+    return podsDividedByReplicaSet.length === 1 ? [] : podsDividedByReplicaSet;
+  }, [pods]);
+
+  const setupWebsocket = (kind: string, controllerUid: string) => {
+    let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
+    if (kind == "pod" && currentSelectors) {
+      apiEndpoint += `selectors=${currentSelectors[0]}`;
+    }
+
+    const options: NewWebsocketOptions = {};
+    options.onopen = () => {
+      console.log("connected to websocket");
+    };
+
+    options.onmessage = (evt: MessageEvent) => {
+      let event = JSON.parse(evt.data);
+      let object = event.Object;
+      object.metadata.kind = event.Kind;
+
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
+      // update pods no matter what if ws message is a pod event.
+      // If controller event, check if ws message corresponds to the designated controller in props.
+      if (event.Kind != "pod" && object.metadata.uid !== controllerUid) {
+        return;
+      }
+
+      if (event.Kind != "pod") {
+        let [available, total] = getAvailability(object.metadata.kind, object);
+        setAvailable(available);
+        setTotal(total);
+        return;
+      }
+      updatePods();
+    };
+
+    options.onclose = () => {
+      console.log("closing websocket");
+    };
+
+    options.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      closeWebsocket(kind);
+    };
+
+    newWebsocket(kind, apiEndpoint, options);
+    openWebsocket(kind);
+  };
+
+  const mapPods = (podList: ControllerTabPodType[]) => {
+    return podList.map((pod, i, arr) => {
+      let status = getPodStatus(pod.status);
+      return (
+        <PodRow
+          key={i}
+          pod={pod}
+          isSelected={currentSelectedPod?.name === pod?.name}
+          podStatus={status}
+          isLastItem={i === arr.length - 1}
+          onTabClick={() => {
+            setPodError("");
+            status === "failed" &&
+              pod.status?.message &&
+              setPodError(pod.status?.message);
+            handleSelectPod(pod);
+            setUserSelectedPod(true);
+          }}
+          onDeleteClick={() => setPodPendingDelete(pod)}
+        />
+      );
+    });
+  };
+
+  useEffect(() => {
+    onUpdate({ pods, available, total, replicaSetArray });
+  }, [pods, replicaSetArray, available, total]);
+
+  return (
+    <ResourceTab
+      label={controller.kind}
+      // handle CronJob case
+      name={controller.metadata?.name || controller.name}
+      status={{ label: currentControllerStatus, available, total }}
+      isLast={isLast}
+      expanded={isFirst}
+    >
+      {!!replicaSetArray.length &&
+        replicaSetArray.map((subArray, index) => {
+          const firstItem = subArray[0];
+          return (
+            <div key={firstItem.replicaSetName + index}>
+              <ReplicaSetContainer>
+                <ReplicaSetName>
+                  {firstItem?.revisionNumber &&
+                    firstItem?.revisionNumber.toString() != "N/A" && (
+                      <Bold>Revision {firstItem.revisionNumber}:</Bold>
+                    )}{" "}
+                  {firstItem.replicaSetName}
+                </ReplicaSetName>
+              </ReplicaSetContainer>
+              {mapPods(subArray)}
+            </div>
+          );
+        })}
+      {!replicaSetArray.length && mapPods(pods)}
+      <ConfirmOverlay
+        message="Are you sure you want to delete this pod?"
+        show={podPendingDelete}
+        onYes={() => handleDeletePod(podPendingDelete)}
+        onNo={() => setPodPendingDelete(null)}
+      />
+    </ResourceTab>
+  );
+};
+
+export default ControllerTabFC;
+
+const Bold = styled.span`
+  font-weight: 500;
+  display: inline;
+  color: #ffffff;
+`;
+
+const ReplicaSetContainer = styled.div`
+  padding: 10px 5px;
+  display: flex;
+  overflow-wrap: anywhere;
+  justify-content: space-between;
+`;
+
+const ReplicaSetName = styled.span`
+  padding-left: 10px;
+  overflow-wrap: anywhere;
+  max-width: calc(100% - 45px);
+  line-height: 1.5em;
+  color: #ffffff33;
+`;

+ 201 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx

@@ -0,0 +1,201 @@
+import React, { useState, useRef, useEffect } from "react";
+import PodDropdown from "./PodDropdown";
+
+import styled from "styled-components";
+import { getPodStatus } from "./util";
+
+type Props = {
+  chart?: any;
+};
+
+type DeployStatus = "Deploying" | "Deployed" | "Failed";
+
+const DeployStatusSection: React.FC<Props> = (props) => {
+  const [status, setStatus] = useState<DeployStatus>("Deployed");
+  const [percentage, setPercentage] = useState("0%");
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  const wrapperRef = useRef<HTMLInputElement>(null);
+  const parentRef = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    document.addEventListener("mousedown", handleClickOutside.bind(this));
+    return () =>
+      document.removeEventListener("mousedown", handleClickOutside.bind(this));
+  }, []);
+
+  const handleClickOutside = (event: any) => {
+    if (
+      wrapperRef &&
+      wrapperRef.current &&
+      !wrapperRef.current.contains(event.target) &&
+      parentRef &&
+      parentRef.current &&
+      !parentRef.current.contains(event.target)
+    ) {
+      setIsExpanded(false);
+    }
+  };
+
+  const onUpdate = (props: any) => {
+    const { available, total, replicaSetArray } = props;
+    let pods = props.pods;
+
+    if (total) {
+      const remaining = (total - available) / props.total;
+      setPercentage(Math.floor(remaining * 100) + "%");
+    }
+
+    if (replicaSetArray.length) {
+      pods = replicaSetArray[0];
+    }
+
+    const podStatuses = pods.map((pod: any) => getPodStatus(pod.status));
+
+    if (
+      podStatuses.every((status: string) =>
+        ["running", "Ready", "completed", "Completed"].includes(status)
+      )
+    ) {
+      setStatus("Deployed");
+      return;
+    }
+
+    if (
+      podStatuses.some((status: string) =>
+        ["failed", "failedValidation"].includes(status)
+      )
+    ) {
+      setStatus("Failed");
+      return;
+    }
+
+    setStatus("Deploying");
+  };
+
+  return (
+    <>
+      <StyledDeployStatusSection
+        onClick={() => setIsExpanded(!isExpanded)}
+        ref={parentRef}
+        isExpanded={isExpanded}
+      >
+        {status === "Deploying" ? (
+          <>
+            <StatusCircle percentage={percentage} />
+            {status}
+          </>
+        ) : (
+          <StatusWrapper>
+            <StatusColor status={status} />
+            {status}
+          </StatusWrapper>
+        )}
+      </StyledDeployStatusSection>
+      <DropdownWrapper expanded={isExpanded}>
+        <Dropdown ref={wrapperRef}>
+          <PodDropdown currentChart={props.chart} onUpdate={onUpdate} />
+        </Dropdown>
+      </DropdownWrapper>
+    </>
+  );
+};
+
+export default DeployStatusSection;
+
+const StatusCircle = styled.div<{ percentage?: any }>`
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  margin-right: 10px;
+  background: conic-gradient(
+    from 0deg,
+    #ffffff33 ${(props) => props.percentage},
+    #ffffffaa 0% ${(props) => props.percentage}
+  );
+`;
+
+const DropdownWrapper = styled.div<{
+  dropdownAlignRight?: boolean;
+  expanded?: boolean;
+}>`
+  display: ${(props) => (props.expanded ? "block" : "none")};
+  position: absolute;
+  left: ${(props) => (props.dropdownAlignRight ? "" : "0")};
+  right: ${(props) => (props.dropdownAlignRight ? "0" : "")};
+  z-index: 1;
+  top: calc(100% + 7px);
+  width: 35%;
+  min-width: 400px;
+  animation: floatIn 0.2s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const Dropdown = styled.div`
+  z-index: 999;
+  overflow-y: auto;
+  background: #2f3135;
+  padding: 0;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>`
+  font-size: 13px;
+  height: 30px;
+  border-radius: 5px;
+  padding: 0 9px;
+  padding-left: 7px;
+  display: flex;
+  margin-left: -1px;
+  align-items: center;
+  ${(props) =>
+    props.isExpanded &&
+    `
+  background: #26292e;
+  border: 1px solid #494b4f;
+  border: 1px solid #7a7b80;
+  margin-left: -2px;
+  margin-right: -1px;
+  `}
+  justify-content: center;
+  cursor: pointer;
+  :hover {
+    background: #26292e;
+    border: 1px solid #494b4f;
+    border: 1px solid #7a7b80;
+    margin-left: -2px;
+    margin-right: -1px;
+  }
+`;
+
+const StatusWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 5px;
+`;
+
+const StatusColor = styled.div`
+  width: 8px;
+  min-width: 8px;
+  height: 8px;
+  background: ${(props: { status: DeployStatus }) =>
+    props.status === "Deployed"
+      ? "#4797ff"
+      : props.status === "Failed"
+      ? "#ed5f85"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;

+ 147 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx

@@ -0,0 +1,147 @@
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { ChartType } from "shared/types";
+import Loading from "components/Loading";
+
+import ControllerTab from "./ControllerTab";
+
+type Props = {
+  selectors?: string[];
+  currentChart: ChartType;
+  onUpdate: (props: any) => void;
+};
+
+const PodDropdown: React.FunctionComponent<Props> = ({
+  currentChart,
+  selectors,
+  onUpdate,
+}) => {
+  const [selectedPod, setSelectedPod] = useState<any>({});
+  const [controllers, setControllers] = useState<any[]>([]);
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [podError, setPodError] = useState<string>("");
+
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+
+  useEffect(() => {
+    let isSubscribed = true;
+    api
+      .getChartControllers(
+        "<token>",
+        {},
+        {
+          namespace: currentChart.namespace,
+          cluster_id: currentCluster.id,
+          id: currentProject.id,
+          name: currentChart.name,
+          revision: currentChart.version,
+        }
+      )
+      .then((res: any) => {
+        if (!isSubscribed) {
+          return;
+        }
+        let controllers =
+          currentChart.chart.metadata.name == "job"
+            ? res.data[0]?.status.active
+            : res.data;
+        setControllers(controllers);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        if (!isSubscribed) {
+          return;
+        }
+        setCurrentError(JSON.stringify(err));
+        setControllers([]);
+        setIsLoading(false);
+      });
+    return () => {
+      isSubscribed = false;
+    };
+  }, [currentProject, currentCluster, setCurrentError, currentChart]);
+
+  const renderTabs = () => {
+    return controllers.map((c, i) => {
+      return (
+        <ControllerTab
+          // handle CronJob case
+          key={c.metadata?.uid || c.uid}
+          selectedPod={selectedPod}
+          selectPod={setSelectedPod}
+          selectors={selectors ? [selectors[i]] : null}
+          controller={c}
+          isLast={i === controllers?.length - 1}
+          isFirst={i === 0}
+          setPodError={(x: string) => setPodError(x)}
+          onUpdate={onUpdate}
+        />
+      );
+    });
+  };
+
+  const renderStatusSection = () => {
+    if (isLoading) {
+      return (
+        <NoControllers>
+          <Loading />
+        </NoControllers>
+      );
+    }
+    if (controllers?.length > 0) {
+      return (
+        <TabWrapper>{renderTabs()}</TabWrapper>
+      );
+    }
+
+    return (
+      <NoControllers>
+        <i className="material-icons">category</i>
+        No objects to display. This might happen while your app is still
+        deploying.
+      </NoControllers>
+    );
+  };
+
+  return (
+    <StyledStatusSection>
+      {renderStatusSection()}
+    </StyledStatusSection>
+  );
+};
+
+export default PodDropdown;
+
+const TabWrapper = styled.div`
+  width: 100%; 
+  min-height: 50px;
+`;
+
+const StyledStatusSection = styled.div`
+  padding: 0px;
+  user-select: text;
+  overflow: hidden;
+  width: 100%;
+  font-size: 13px;
+`;
+
+const NoControllers = styled.div`
+  position: relative;
+  width: 100%;
+  display: flex;
+  min-height: 50px;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;

+ 222 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx

@@ -0,0 +1,222 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import { ControllerTabPodType } from "./ControllerTab";
+
+type PodRowProps = {
+  pod: ControllerTabPodType;
+  isSelected: boolean;
+  isLastItem: boolean;
+  onTabClick: any;
+  onDeleteClick: any;
+  podStatus: string;
+};
+
+const PodRow: React.FunctionComponent<PodRowProps> = ({
+  pod,
+  isSelected,
+  onTabClick,
+  onDeleteClick,
+  isLastItem,
+  podStatus,
+}) => {
+  const [showTooltip, setShowTooltip] = useState(false);
+
+  return (
+    <Tab key={pod?.name}>
+      <Gutter>
+        <Rail />
+        <Circle />
+        <Rail lastTab={isLastItem} />
+      </Gutter>
+      <Name
+        onMouseOver={() => {
+          // setShowTooltip(true);
+        }}
+        onMouseOut={() => {
+          setShowTooltip(false);
+        }}
+      >
+        {pod?.name}
+      </Name>
+      {showTooltip && (
+        <Tooltip>
+          {pod?.name}
+          <Grey>Restart count: {pod.restartCount}</Grey>
+          <Grey>Created on: {pod.podAge}</Grey>
+          {podStatus === "failed" ? (
+            <FailedStatusContainer>
+              <Grey>
+                Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}
+              </Grey>
+              <Grey>{pod?.containerStatus?.state?.waiting?.message}</Grey>
+            </FailedStatusContainer>
+          ) : null}
+        </Tooltip>
+      )}
+
+      <Status>
+        {podStatus}
+        <StatusColor status={podStatus} />
+        {podStatus === "failed" && (
+          <CloseIcon
+            className="material-icons-outlined"
+            onClick={onDeleteClick}
+          >
+            close
+          </CloseIcon>
+        )}
+      </Status>
+    </Tab>
+  );
+};
+
+export default PodRow;
+
+const Grey = styled.div`
+  margin-top: 5px;
+  color: #aaaabb;
+`;
+
+const FailedStatusContainer = styled.div`
+  width: 100%;
+  border: 1px solid hsl(0deg, 100%, 30%);
+  padding: 5px;
+  margin-block: 5px;
+`;
+
+const Tooltip = styled.div`
+  position: absolute;
+  left: 35px;
+  word-wrap: break-word;
+  top: 38px;
+  min-height: 18px;
+  max-width: calc(100% - 75px);
+  padding: 5px 7px;
+  background: #272731;
+  z-index: 999;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  flex: 1;
+  color: white;
+  text-transform: none;
+  font-size: 12px;
+  font-family: "Work Sans", sans-serif;
+  outline: 1px solid #ffffff55;
+  opacity: 0;
+  animation: faded-in 0.2s 0.15s;
+  animation-fill-mode: forwards;
+  @keyframes faded-in {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const CloseIcon = styled.i`
+  font-size: 14px;
+  display: flex;
+  font-weight: bold;
+  align-items: center;
+  justify-content: center;
+  border-radius: 5px;
+  background: #ffffff22;
+  width: 18px;
+  height: 18px;
+  margin-right: -6px;
+  margin-left: 10px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff44;
+  }
+`;
+
+const Tab = styled.div`
+  width: 100%;
+  height: 50px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  padding: 20px 18px 20px 42px;
+  text-shadow: 0px 0px 8px none;
+  overflow: visible;
+`;
+
+const Rail = styled.div`
+  width: 2px;
+  background: ${(props: { lastTab?: boolean }) =>
+    props.lastTab ? "" : "#52545D"};
+  height: 50%;
+`;
+
+const Circle = styled.div`
+  min-width: 10px;
+  min-height: 2px;
+  margin-bottom: -2px;
+  margin-left: 8px;
+  background: #52545d;
+`;
+
+const Gutter = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 10px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  overflow: visible;
+`;
+
+const Status = styled.div`
+  display: flex;
+  font-size: 12px;
+  text-transform: capitalize;
+  margin-left: 10px;
+  justify-content: flex-end;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StatusColor = styled.div`
+  margin-left: 12px;
+  mnargin-right: -1px;
+  width: 8px;
+  min-width: 7px;
+  height: 8px;
+  background: ${(props: { status: string }) =>
+    props.status === "running"
+      ? "#4797ff"
+      : props.status === "failed"
+      ? "#ed5f85"
+      : props.status === "completed"
+      ? "#00d12a"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;
+
+const Name = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.5em;
+  display: -webkit-box;
+  overflow-wrap: anywhere;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+`;

+ 316 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx

@@ -0,0 +1,316 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { kindToIcon } from "shared/rosettaStone";
+
+type PropsType = {
+  label: string;
+  name: string;
+  handleClick?: () => void;
+  selected?: boolean;
+  isLast?: boolean;
+  roundAllCorners?: boolean;
+  status?: {
+    label: string;
+    available?: number;
+    total?: number;
+  } | null;
+  expanded?: boolean;
+};
+
+type StateType = {
+  expanded: boolean;
+  showTooltip: boolean;
+};
+
+export default class ResourceTab extends Component<PropsType, StateType> {
+  state = {
+    expanded: this.props.expanded || false,
+    showTooltip: false,
+  };
+
+  renderDropdownIcon = () => {
+    if (this.props.children) {
+      return (
+        <DropdownIcon expanded={this.state.expanded}>
+          <i className="material-icons">arrow_right</i>
+        </DropdownIcon>
+      );
+    }
+  };
+
+  renderIcon = (kind: string) => {
+    let icon = "tonality";
+    if (Object.keys(kindToIcon).includes(kind)) {
+      icon = kindToIcon[kind];
+    }
+
+    return (
+      <IconWrapper>
+        <i className="material-icons">{icon}</i>
+      </IconWrapper>
+    );
+  };
+
+  renderTooltip = (x: string): JSX.Element | undefined => {
+    if (this.state.showTooltip) {
+      return <Tooltip>{x}</Tooltip>;
+    }
+  };
+
+  getStatusText = () => {
+    let { status } = this.props;
+    if (status.available && status.total) {
+      return `${status.available}/${status.total}`;
+    } else if (status.label) {
+      return status.label;
+    }
+  };
+
+  renderStatus = () => {
+    let { status } = this.props;
+    if (status) {
+      return (
+        <Status>
+          {this.getStatusText()}
+
+          <StatusColor status={status.label} />
+        </Status>
+      );
+    }
+  };
+
+  renderExpanded = () => {
+    if (this.props.children && this.state.expanded) {
+      return <ExpandWrapper>{this.props.children}</ExpandWrapper>;
+    }
+  };
+
+  render() {
+    let {
+      label,
+      name,
+      children,
+      isLast,
+      handleClick,
+      selected,
+      status,
+      roundAllCorners,
+    } = this.props;
+    return (
+      <StyledResourceTab
+        isLast={isLast}
+        onClick={() => handleClick && handleClick()}
+        roundAllCorners={roundAllCorners}
+      >
+        <ResourceHeader
+          hasChildren={children && true}
+          expanded={this.state.expanded || selected}
+          onClick={() => {
+            if (children) {
+              this.setState({ expanded: !this.state.expanded });
+            }
+          }}
+        >
+          <Info>
+            {this.renderDropdownIcon()}
+            <Metadata hasStatus={status && true}>
+              {this.renderIcon(label)}
+              {label}
+              <ResourceName
+                showKindLabels={true}
+                onMouseOver={() => {
+                  this.setState({ showTooltip: true });
+                }}
+                onMouseOut={() => {
+                  this.setState({ showTooltip: false });
+                }}
+              >
+                {name}
+              </ResourceName>
+              {this.renderTooltip(name)}
+            </Metadata>
+          </Info>
+          {this.renderStatus()}
+        </ResourceHeader>
+        {this.renderExpanded()}
+      </StyledResourceTab>
+    );
+  }
+}
+
+const StyledResourceTab = styled.div`
+  width: 100%;
+  font-size: 13px;
+  border-bottom-left-radius: ${(props: {
+    isLast: boolean;
+    roundAllCorners: boolean;
+  }) => (props.isLast ? "10px" : "")};
+  animation: fadeIn 0.2s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const Tooltip = styled.div`
+  position: absolute;
+  right: 0px;
+  top: 25px;
+  white-space: nowrap;
+  height: 18px;
+  padding: 2px 5px;
+  background: #383842dd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  color: white;
+  text-transform: none;
+  font-size: 12px;
+  font-family: "Work Sans", sans-serif;
+  outline: 1px solid #ffffff55;
+  opacity: 0;
+  animation: faded-in 0.2s 0.15s;
+  animation-fill-mode: forwards;
+  @keyframes faded-in {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const ExpandWrapper = styled.div``;
+
+const ResourceHeader = styled.div`
+  width: 100%;
+  height: 50px;
+  display: flex;
+  font-size: 13px;
+  align-items: center;
+  justify-content: space-between;
+  color: #ffffff66;
+  user-select: none;
+  padding: 8px 18px;
+  padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) =>
+    props.hasChildren ? "10px" : "22px"};
+  cursor: pointer;
+  :hover {
+    background: #ffffff18;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+`;
+
+const Info = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  width: 80%;
+  height: 100%;
+`;
+
+const Metadata = styled.div`
+  display: flex;
+  align-items: center;
+  position: relative;
+  max-width: ${(props: { hasStatus: boolean }) =>
+    props.hasStatus ? "calc(100% - 20px)" : "100%"};
+`;
+
+const Status = styled.div`
+  display: flex;
+  width; 20%;
+  font-size: 12px;
+  text-transform: capitalize;
+  justify-content: flex-end;
+  align-items: center;
+  font-family: 'Work Sans', sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
+`;
+
+const StatusColor = styled.div`
+  margin-left: 12px;
+  width: 8px;
+  min-width: 8px;
+  height: 8px;
+  background: ${(props: { status: string }) =>
+    props.status === "running" ||
+    props.status === "Ready" ||
+    props.status === "Completed"
+      ? "#4797ff"
+      : props.status === "failed" || props.status === "FailedValidation"
+      ? "#ed5f85"
+      : props.status === "completed"
+      ? "#00d12a"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;
+
+const ResourceName = styled.div`
+  color: #ffffff;
+  margin-right: 15px;
+  margin-left: ${(props: { showKindLabels: boolean }) =>
+    props.showKindLabels ? "10px" : ""};
+  text-transform: none;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const IconWrapper = styled.div`
+  width: 25px;
+  height: 25px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 15px;
+    color: #ffffff;
+    margin-right: 14px;
+  }
+`;
+
+const DropdownIcon = styled.div`
+  > i {
+    margin-top: 2px;
+    margin-right: 11px;
+    font-size: 20px;
+    color: #ffffff66;
+    cursor: pointer;
+    border-radius: 20px;
+    background: ${(props: { expanded: boolean }) =>
+      props.expanded ? "#ffffff18" : ""};
+    transform: ${(props: { expanded: boolean }) =>
+      props.expanded ? "rotate(180deg)" : ""};
+    animation: ${(props: { expanded: boolean }) =>
+      props.expanded ? "quarterTurn 0.3s" : ""};
+    animation-fill-mode: forwards;
+
+    @keyframes quarterTurn {
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(90deg);
+      }
+    }
+  }
+`;

+ 53 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts

@@ -0,0 +1,53 @@
+export const getPodStatus = (status: any) => {
+  if (status?.phase === "Pending" && status?.containerStatuses !== undefined) {
+    return status.containerStatuses[0].state?.waiting?.reason || "Pending";
+  } else if (status?.phase === "Pending") {
+    return "Pending";
+  }
+
+  if (status?.phase === "Failed") {
+    return "failed";
+  }
+
+  if (status?.phase === "Running") {
+    let collatedStatus = "running";
+
+    status?.containerStatuses?.forEach((s: any) => {
+      if (s.state?.waiting) {
+        collatedStatus =
+          s.state?.waiting?.reason === "CrashLoopBackOff"
+            ? "failed"
+            : "waiting";
+      } else if (
+        s.state?.terminated &&
+        (s.state.terminated?.exitCode !== 0 ||
+          s.state.terminated?.reason !== "Completed")
+      ) {
+        collatedStatus = "failed";
+      }
+    });
+    return collatedStatus;
+  }
+};
+
+export const getAvailability = (kind: string, c: any) => {
+  switch (kind?.toLowerCase()) {
+    case "deployment":
+    case "replicaset":
+      return [
+        c.status?.availableReplicas ||
+          c.status?.replicas - c.status?.unavailableReplicas ||
+          0,
+        c.status?.replicas || 0,
+      ];
+    case "statefulset":
+      return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
+    case "daemonset":
+      return [
+        c.status?.numberAvailable || 0,
+        c.status?.desiredNumberScheduled || 0,
+      ];
+    case "job":
+      return [1, 1];
+  }
+};

+ 461 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -0,0 +1,461 @@
+import React, { useState, useEffect } from "react";
+import { CellProps } from "react-table";
+
+import styled from "styled-components";
+import EventTable from "./EventTable";
+import Loading from "components/Loading";
+import danger from "assets/danger.svg";
+import document from "assets/document.svg";
+import info from "assets/info-outlined.svg";
+import status from "assets/info-circle.svg";
+import { readableDate } from "shared/string_utils";
+import TitleSection from "components/TitleSection";
+import api from "shared/api";
+import Modal from "main/home/modals/Modal";
+import time from "assets/time.svg";
+
+const iconDict: any = {
+};
+
+type Props = {
+  filters: any;
+  setExpandedMonitor: any;
+};
+
+const EventList: React.FC<Props> = (props) => {
+  const [events, setEvents] = useState([]);
+  const [expandedEvent, setExpandedEvent] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+
+    // Dummy event list query
+    setTimeout(() => {
+      const events = [
+        {
+          "id": "e0311c9231eea5c47e8bb83cc10faf2b",
+          "release_name": "deployment-invalid-image",
+          "release_namespace": "default",
+          "chart_name": "worker",
+          "created_at": "2022-10-06T17:56:06.834088422Z",
+          "updated_at": "2022-10-07T19:32:33.842552438Z",
+          "last_seen": "2022-10-06T17:55:50Z",
+          "status": "active",
+          "summary": "The application has an invalid image",
+          "severity": "normal",
+          "involved_object_kind": "pod",
+          "involved_object_name": "deployment-invalid-image-worker-cf976b476-h782c",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "6818ee8df06db55c310c24a6b0c17cfd",
+          "release_name": "oom-killed",
+          "release_namespace": "default",
+          "chart_name": "web",
+          "created_at": "2022-10-05T17:13:45.563510898Z",
+          "updated_at": "2022-10-07T19:32:02.580935074Z",
+          "last_seen": "2022-10-05T17:59:10Z",
+          "status": "active",
+          "summary": "The application ran out of memory",
+          "severity": "critical",
+          "involved_object_kind": "Deployment",
+          "involved_object_name": "oom-killed-web",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "00f3b38dd0b2adc2ce0bab9586d487b5",
+          "release_name": "non-zero-exit-code",
+          "release_namespace": "default",
+          "chart_name": "web",
+          "created_at": "2022-10-05T18:31:07.80325966Z",
+          "updated_at": "2022-10-07T19:31:34.591091925Z",
+          "last_seen": "2022-10-05T18:31:07Z",
+          "status": "active",
+          "summary": "The application exited with a non-zero exit code",
+          "severity": "normal",
+          "involved_object_kind": "pod",
+          "involved_object_name": "non-zero-exit-code-web-797d5ddb64-g5d7x",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "c435fe96260e472af9808013687a876c",
+          "release_name": "multi-replica-failure-less",
+          "release_namespace": "default",
+          "chart_name": "worker",
+          "created_at": "2022-10-07T15:05:16.823354171Z",
+          "updated_at": "2022-10-07T19:31:29.840878022Z",
+          "last_seen": "2022-10-07T15:09:17Z",
+          "status": "active",
+          "summary": "The application exited with a non-zero exit code",
+          "severity": "critical",
+          "involved_object_kind": "Deployment",
+          "involved_object_name": "multi-replica-failure-less-worker",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "d9aec89e437617f28de47ab700c92cb4",
+          "release_name": "multi-replica-failure-more",
+          "release_namespace": "default",
+          "chart_name": "worker",
+          "created_at": "2022-10-07T15:08:21.405351792Z",
+          "updated_at": "2022-10-07T19:31:14.773187536Z",
+          "last_seen": "2022-10-07T19:25:06Z",
+          "status": "active",
+          "summary": "The application exited with a non-zero exit code",
+          "severity": "critical",
+          "involved_object_kind": "Deployment",
+          "involved_object_name": "multi-replica-failure-more-worker",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "f7f2021acb0e5fd104fb30b8c914a563",
+          "release_name": "oom-killed-2",
+          "release_namespace": "default",
+          "chart_name": "web",
+          "created_at": "2022-10-05T18:00:45.655719615Z",
+          "updated_at": "2022-10-07T19:28:38.571531252Z",
+          "last_seen": "2022-10-05T18:00:44Z",
+          "status": "active",
+          "summary": "The application ran out of memory",
+          "severity": "critical",
+          "involved_object_kind": "deployment",
+          "involved_object_name": "oom-killed-2-web",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "fa76cfb2daa58649e6aa7dc47262f632",
+          "release_name": "deployment-bad-image-tag",
+          "release_namespace": "default",
+          "chart_name": "worker",
+          "created_at": "2022-10-06T17:51:04.846528578Z",
+          "updated_at": "2022-10-07T19:28:11.835881268Z",
+          "last_seen": "2022-10-06T17:50:46Z",
+          "status": "active",
+          "summary": "The application has an invalid image",
+          "severity": "normal",
+          "involved_object_kind": "pod",
+          "involved_object_name": "deployment-bad-image-tag-worker-5f676bdb9-q4d7w",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "b58f6e28bc3884f50cb60c93b8427cb2",
+          "release_name": "deployment-stuck-longtime",
+          "release_namespace": "default",
+          "chart_name": "worker",
+          "created_at": "2022-10-07T15:04:23.807783654Z",
+          "updated_at": "2022-10-07T17:41:27.155369452Z",
+          "last_seen": "2022-10-07T15:04:23.695228777Z",
+          "status": "active",
+          "summary": "The application cannot be scheduled",
+          "severity": "critical",
+          "involved_object_kind": "deployment",
+          "involved_object_name": "deployment-stuck-longtime-worker",
+          "involved_object_namespace": "default"
+        },
+        {
+          "id": "6c6cc62a398d831bfddb6ef5faceafa9",
+          "release_name": "failing-job-run",
+          "release_namespace": "default",
+          "chart_name": "job",
+          "created_at": "2022-10-07T16:43:06.025081995Z",
+          "updated_at": "2022-10-07T17:29:02.722048271Z",
+          "last_seen": "2022-10-07T16:43:48Z",
+          "status": "active",
+          "summary": "The application has an invalid start command",
+          "severity": "normal",
+          "involved_object_kind": "Job",
+          "involved_object_name": "failing-job-run-dr1vbs563d",
+          "involved_object_namespace": "default"
+        }
+      ];
+      setEvents(events);
+      setIsLoading(false);
+    }, 1000);
+  }, []);
+
+  const columns = React.useMemo(
+    () => [
+      {
+        Header: "Monitors",
+        columns: [
+          {
+            Header: "Name",
+            accessor: "release_name",
+            width: 180,
+            Cell: ({ row }: CellProps<any>) => {
+              return (
+                <NameWrapper>
+                  <AlertIcon src={danger} />
+                  {row.original.release_name}
+                  {row?.original &&
+                  row.original.severity === "normal" ? (
+                    <></>
+                  ) : (
+                    <Status color="#cc3d42">Critical</Status>
+                  )}
+                </NameWrapper>
+              );
+            },
+          },
+          {
+            Header: "Summary",
+            accessor: "summary",
+            width: 270,
+          },
+          {
+            Header: "Last updated",
+            accessor: "updated_at",
+            width: 140,
+            Cell: ({ row }: CellProps<any>) => {
+              return (
+                <Flex>
+                  {readableDate(row.original.updated_at)}
+                </Flex>
+              );
+            },
+          },
+          {
+            id: "details",
+            accessor: "",
+            width: 20,
+            Cell: ({ row }: CellProps<any>) => {
+              return (
+                <TableButton onClick={() => {
+                  setExpandedEvent(row.original);
+                }}>
+                  <Icon src={info} />
+                  Details
+                </TableButton>
+              );
+            },
+          },
+          {
+            id: "logs",
+            accessor: "",
+            width: 30,
+            Cell: ({ row }: CellProps<any>) => {
+              return (
+                <TableButton width="102px">
+                  <Icon src={document} />
+                  View logs
+                </TableButton>
+              );
+            },
+          },
+        ],
+      },
+    ],
+    []
+  );
+
+  return (
+    <>
+      {
+        expandedEvent && (
+          <Modal onRequestClose={() => setExpandedEvent(null)} height="auto">
+            <TitleSection icon={danger}>
+              <Text>{expandedEvent.release_name}</Text>
+            </TitleSection>
+            <InfoRow>
+              <InfoTab>
+                <img src={time} /> <Bold>Last updated:</Bold>
+                {readableDate(expandedEvent.updated_at)}
+              </InfoTab>
+              <InfoTab>
+                <img src={info} /> <Bold>Status:</Bold>
+                <Capitalize>{expandedEvent.status}</Capitalize>
+              </InfoTab>
+              <InfoTab>
+                <img src={status} /> <Bold>Priority:</Bold> <Capitalize>{expandedEvent.severity}</Capitalize>
+              </InfoTab>
+            </InfoRow>
+            <Message>
+              <img src={document} /> This is a placeholder message where event details should be.
+            </Message>
+          </Modal>
+        )
+      }
+      {isLoading ? (
+        <LoadWrapper>
+          <Loading />
+        </LoadWrapper>
+      ) : (
+        <>
+        {events.length > 0 ? (
+          <TableWrapper>
+            <EventTable 
+              columns={columns} 
+              data={events} 
+            />
+          </TableWrapper>
+        ) : (
+          <Placeholder>
+            <div>
+            <Title>No results found</Title>
+            There were no results found for this filter.
+            </div>
+          </Placeholder>
+        )}
+        </>
+      )}
+    </>
+  );
+};
+
+export default EventList;
+
+const Message = styled.div`
+  padding: 20px;
+  background: #26292E;
+  border-radius: 5px;
+  line-height: 1.5em;
+  border: 1px solid #aaaabb33;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  > img {
+    width: 13px;
+    margin-right: 20px;
+  }
+`;
+
+const Capitalize = styled.div`
+  text-transform: capitalize;
+`;
+
+const Bold = styled.div`
+  font-weight: 500;
+  margin-right: 5px;
+`;
+
+const InfoTab = styled.div`
+  display: flex;
+  align-items: center;
+  opacity: 50%;
+  font-size: 13px;
+  margin-right: 15px;
+  justify-content: center;
+
+  > img {
+    width: 13px;
+    margin-right: 7px;
+  }
+`;
+
+const InfoRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  margin-bottom: 25px;
+`;
+
+const Text = styled.div`
+  font-weight: 500;
+  font-size: 18px;
+  z-index: 999;
+`;
+
+const Icon = styled.img`
+  width: 16px;
+  margin-right: 6px;
+`;
+
+const TableButton = styled.div<{ width?: string }>`
+  border-radius: 5px;
+  height: 30px;
+  color: white;
+  width: ${props => props.width || "85px"};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #ffffff11;
+  border: 1px solid #aaaabb33;
+  cursor: pointer;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const ClusterName = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  background: blue;
+  width: 100px;
+`;
+
+const Title = styled.div`
+  font-size: 18px;
+  margin-bottom: 10px;
+  color: #ffffff;
+`;
+
+const Placeholder = styled.div`
+  width: 100%;
+  height: 300px;
+  color: #aaaabb55;
+  display: flex;
+  font-size: 14px;
+  padding-right: 50px;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ClusterIcon = styled.img`
+  width: 14px;
+  margin-right: 9px;
+  opacity: 70%;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const AlertIcon = styled.img`
+  width: 20px;
+  margin-right: 15px;
+  margin-left: 0px;
+`;
+
+const NameWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  color: white;
+`;
+
+const LoadWrapper = styled.div`
+  width: 100%;
+  height: 300px;
+`;
+
+const Status = styled.div<{ color: string }>`
+  padding: 5px 7px;
+  background: ${(props) => props.color};
+  font-size: 12px;
+  border-radius: 3px;
+  word-break: keep-all;
+  display: flex;
+  color: white;
+  margin-right: 50px;
+  align-items: center;
+  margin-left: 15px;
+  justify-content: center;
+  height: 20px;
+`;
+
+const TableWrapper = styled.div`
+  overflow-x: auto;
+  animation: fadeIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+`;
+
+const StyledMonitorList = styled.div`
+  height: 200px;
+  width: 100%;
+  font-size: 13px;
+  background: #ffffff11;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;

+ 94 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventTable.tsx

@@ -0,0 +1,94 @@
+import React from "react";
+import {
+  Column,
+  Row,
+  useGlobalFilter,
+  usePagination,
+  useTable,
+} from "react-table";
+import {
+  StyledTd,
+  StyledTable,
+  StyledTHead,
+  StyledTh,
+  StyledTBody,
+} from "./styles";
+
+export type TableProps = {
+  columns: Column<any>[];
+  data: any[];
+  onRowClick?: (row: Row) => void;
+};
+
+const EventTable: React.FC<TableProps> = ({
+  columns: columnsData,
+  data,
+  onRowClick,
+}) => {
+  const {
+    rows,
+    getTableProps,
+    getTableBodyProps,
+    prepareRow,
+    headerGroups,
+  } = useTable(
+    {
+      columns: columnsData,
+      data,
+    },
+    useGlobalFilter,
+    usePagination
+  );
+
+  const renderRows = () => {
+    return (
+      <>
+        {rows.map((row: any) => {
+          prepareRow(row);
+
+          return (
+            <tr
+              {...row.getRowProps()}
+              onClick={() => onRowClick && onRowClick(row)}
+              selected={false}
+            >
+              {row.cells.map((cell: any) => {
+                return (
+                  <StyledTd
+                    {...cell.getCellProps()}
+                    style={{
+                      width: cell.column.totalWidth,
+                    }}
+                  >
+                    {cell.render("Cell")}
+                  </StyledTd>
+                );
+              })}
+            </tr>
+          );
+        })}
+      </>
+    );
+  };
+
+  return (
+    <>
+      <StyledTable {...getTableProps()}>
+        <StyledTHead>
+          {headerGroups.map((headerGroup) => (
+            <tr {...headerGroup.getHeaderGroupProps()}>
+              {headerGroup.headers.map((column) => (
+                <StyledTh {...column.getHeaderProps()}>
+                  {column.render("Header")}
+                </StyledTh>
+              ))}
+            </tr>
+          ))}
+        </StyledTHead>
+        <StyledTBody {...getTableBodyProps()}>{renderRows()}</StyledTBody>
+      </StyledTable>
+    </>
+  );
+};
+
+export default EventTable;

+ 32 - 154
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -1,97 +1,40 @@
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useEffect, useContext, useState } from "react";
+import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
-import EventCard from "components/events/EventCard";
+import EventList from "./EventList";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import InfiniteScroll from "react-infinite-scroll-component";
 import InfiniteScroll from "react-infinite-scroll-component";
+import { Context } from "shared/Context";
 import Dropdown from "components/Dropdown";
 import Dropdown from "components/Dropdown";
-import { useKubeEvents } from "components/events/useEvents";
-import { ChartType } from "shared/types";
-import _, { isEmpty, isObject } from "lodash";
-import SubEventsList from "components/events/SubEventsList";
 
 
-const availableResourceTypes = [
-  { label: "Pods", value: "pod" },
-  { label: "HPA", value: "hpa" },
-];
-
-const EventsTab: React.FC<{
-  controllers: Record<string, Record<string, any>>;
-}> = (props) => {
-  const { controllers } = props;
-  const [resourceType, setResourceType] = useState(availableResourceTypes[0]);
-  const [currentEvent, setCurrentEvent] = useState(null);
-
-  const [selectedControllerKey, setSelectedControllerKey] = useState(null);
-
-  const [hasControllers, setHasControllers] = useState(null);
-
-  const controllerOptions = useMemo(() => {
-    if (typeof controllers !== "object") {
-      return [];
-    }
-
-    return Object.entries(controllers).map(([key, value]) => ({
-      label: value?.metadata?.name,
-      value: key,
-    }));
-  }, [controllers]);
-
-  const currentControllerOption = useMemo(() => {
-    return (
-      controllerOptions?.find((c) => c.value === selectedControllerKey) ||
-      controllerOptions[0]
-    );
-  }, [selectedControllerKey, controllerOptions]);
-
-  const selectedController = controllers[currentControllerOption?.value];
-
-  const {
-    isLoading,
-    hasPorterAgent,
-    triggerInstall,
-    kubeEvents,
-    loadMoreEvents,
-    hasMore,
-  } = useKubeEvents({
-    resourceType: resourceType.value as any,
-    ownerName: selectedController?.metadata?.name,
-    ownerType: selectedController?.kind,
-    ownerNamespace: selectedController?.metadata?.namespace,
-    shouldWaitForOwner: true,
-  });
+const EventsTab: React.FC = () => {
+  const [hasPorterAgent, setHasPorterAgent] = useState(true);
+  const { currentProject, currentCluster } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
 
 
   useEffect(() => {
   useEffect(() => {
-    let timer: NodeJS.Timeout = null;
-
-    const checkControllers = (counter = 0) => {
-      if (timer !== null) {
-        clearTimeout(timer);
-      }
-
-      if (isEmpty(controllers) && counter === 5) {
-        clearTimeout(timer);
-        setHasControllers(false);
-      } else {
-        if (isEmpty(controllers)) {
-          timer = setTimeout(() => {
-            checkControllers(counter + 1);
-          }, 2000);
-        } else {
-          setHasControllers(true);
-        }
-      }
-    };
-
-    checkControllers();
-
-    return () => {
-      if (timer !== null) {
-        clearTimeout(timer);
-      }
-    };
-  }, [controllers]);
-
-  if (isLoading && hasControllers === null) {
+    setIsLoading(false);
+  }, []);
+
+  const installAgent = async () => {
+    const project_id = currentProject?.id;
+    const cluster_id = currentCluster?.id;
+
+    api
+      .installPorterAgent("<token>", {}, { project_id, cluster_id })
+      .then(() => {
+        setHasPorterAgent(true);
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  };
+
+  const triggerInstall = () => {
+    installAgent();
+  };
+
+  if (isLoading) {
     return (
     return (
       <Placeholder>
       <Placeholder>
         <Loading />
         <Loading />
@@ -99,22 +42,12 @@ const EventsTab: React.FC<{
     );
     );
   }
   }
 
 
-  if (!hasControllers) {
-    return (
-      <Placeholder>
-        <i className="material-icons">search</i>
-        We coulnd't find any controllers for this application.
-      </Placeholder>
-    );
-  }
-
   if (!hasPorterAgent) {
   if (!hasPorterAgent) {
     return (
     return (
       <Placeholder>
       <Placeholder>
         <div>
         <div>
           <Header>We couldn't detect the Porter agent on your cluster</Header>
           <Header>We couldn't detect the Porter agent on your cluster</Header>
-          In order to use the events tab, you need to install the Porter agent
-          on your cluster.
+          In order to use the events tab, you need to install the Porter agent.
           <InstallPorterAgentButton onClick={() => triggerInstall()}>
           <InstallPorterAgentButton onClick={() => triggerInstall()}>
             <i className="material-icons">add</i> Install Porter agent
             <i className="material-icons">add</i> Install Porter agent
           </InstallPorterAgentButton>
           </InstallPorterAgentButton>
@@ -123,64 +56,9 @@ const EventsTab: React.FC<{
     );
     );
   }
   }
 
 
-  if (currentEvent) {
-    return (
-      <SubEventsList
-        event={currentEvent}
-        clearSelectedEvent={() => setCurrentEvent(null)}
-      />
-    );
-  }
-
   return (
   return (
     <EventsPageWrapper>
     <EventsPageWrapper>
-      {kubeEvents.length > 0 ? (
-        <>
-          <ControlRow>
-            {/*
-              <Dropdown
-                selectedOption={resourceType}
-                options={availableResourceTypes}
-                onSelect={(o) => setResourceType({ ...o, value: o.value as string })}
-              />
-              */}
-            <Label>Controller -</Label>
-            <Dropdown
-              selectedOption={currentControllerOption}
-              options={controllerOptions}
-              onSelect={(o) => setSelectedControllerKey(o?.value)}
-            />
-          </ControlRow>
-
-          <InfiniteScroll
-            dataLength={kubeEvents.length}
-            next={loadMoreEvents}
-            hasMore={hasMore}
-            loader={<h4>Loading...</h4>}
-            scrollableTarget="HomeViewWrapper"
-          >
-            <EventsGrid>
-              {kubeEvents.map((event, i) => {
-                return (
-                  <React.Fragment key={i}>
-                    <EventCard
-                      event={event as any}
-                      selectEvent={() => {
-                        setCurrentEvent(event);
-                      }}
-                    />
-                  </React.Fragment>
-                );
-              })}
-            </EventsGrid>
-          </InfiniteScroll>
-        </>
-      ) : (
-        <Placeholder>
-          <i className="material-icons">search</i>
-          No matching events were found.
-        </Placeholder>
-      )}
+      <EventList />
     </EventsPageWrapper>
     </EventsPageWrapper>
   );
   );
 };
 };

+ 155 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts

@@ -0,0 +1,155 @@
+import styled, { css } from "styled-components";
+
+const textFontStack = css`
+  font-family: "Work Sans", Arial, sans-serif;
+`;
+
+export const theme = {
+  bg: {
+    default: "#FFFFFF",
+    reverse: "#16171A",
+    wash: "#FAFAFA",
+    divider: "#F6F7F8",
+    border: "#EBECED",
+    inactive: "#DFE7EF",
+    shadeone: "#26292E",
+    shadetwo: "#26292E",
+  },
+  line: {
+    default: "1px solid #aaaabb33",
+  },
+  brand: {
+    default: "#4400CC",
+    alt: "#7B16FF",
+    wash: "#E8E5FF",
+    border: "#DDD9FF",
+    dark: "#2A0080",
+  },
+  generic: {
+    default: "#E6ECF7",
+    alt: "#F6FBFC",
+  },
+  space: {
+    default: "#0062D6",
+    alt: "#1CD2F2",
+    wash: "#E5F0FF",
+    border: "#BDD8FF",
+    dark: "#0F015E",
+  },
+  success: {
+    default: "#00B88B",
+    alt: "#00D5BD",
+    dark: "#00663C",
+    wash: "#D9FFF2",
+    border: "#9FF5D9",
+  },
+  text: {
+    default: "#ffffffaa",
+    secondary: "#384047",
+    alt: "#67717A",
+    placeholder: "#7C8894",
+    reverse: "#FFFFFF",
+  },
+  warn: {
+    default: "#E22F2F",
+    alt: "#E2197A",
+    dark: "#85000C",
+    wash: "#FFEDF6",
+    border: "#FFCCE5",
+  },
+};
+
+export const StyledTable = styled.table`
+  width: 100%;
+  min-width: 500px;
+  border-radius: 5px;
+  overflow: hidden;
+  border: 1px solid #aaaabb33;
+  border-spacing: 0;
+`;
+
+export const StyledTHead = styled.thead`
+  width: 100%;
+  position: sticky;
+
+  > tr {
+    background: ${theme.bg.shadeone};
+    line-height: 2.2em;
+
+    > th {
+      border-bottom: ${theme.line.default};
+    }
+  }
+
+  > tr:first-child {
+    > th:first-child {
+      border-top-left-radius: 6px;
+      display: none;
+    }
+
+    > th:last-child {
+      border-top-right-radius: 6px;
+    }
+  }
+`;
+
+export const StyledTBody = styled.tbody`
+  > tr {
+    background: ${theme.bg.shadetwo};
+    height: 80px;
+    line-height: 1.2em;
+
+    > td {
+      border-bottom: ${theme.line.default};
+    }
+
+    > td:first-child {
+    }
+
+    > td:last-child {
+    }
+  }
+
+  > tr:last-child {
+    > td:first-child {
+      border-bottom-left-radius: 6px;
+    }
+
+    > td:last-child {
+      border-bottom-right-radius: 6px;
+    }
+
+    > td {
+      border-bottom: none;
+    }
+  }
+`;
+
+export const StyledTd = styled.td`
+  ${textFontStack}
+  font-size: 13px;
+  color: ${theme.text.default};
+  :first-child {
+    padding-left: 20px;
+  }
+
+  :last-child {
+  }
+
+  user-select: text;
+`;
+
+export const StyledTh = styled.th`
+  ${textFontStack}
+
+  text-align: left;
+  font-size: 13px;
+  font-weight: 400;
+  color: #ffffffaa;
+  :first-child {
+    padding-left: 20px;
+  }
+  :last-child {
+    padding-right: 10px;
+  }
+`;

+ 0 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/IncidentsTab.tsx → dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/EventsTab.tsx


+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -560,7 +560,7 @@ const Subtitle = styled.div`
 `;
 `;
 
 
 const JobLogsWrapper = styled.div`
 const JobLogsWrapper = styled.div`
-  height: 250px;
+  max-height: 500px;
   width: 100%;
   width: 100%;
   background-color: black;
   background-color: black;
   overflow-y: auto;
   overflow-y: auto;

+ 345 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -0,0 +1,345 @@
+import React, { useEffect, useState } from "react";
+
+import styled from "styled-components";
+import RadioFilter from "components/RadioFilter";
+
+import filterOutline from "assets/filter-outline.svg";
+import DateTimePicker from "components/date-time-picker/DateTimePicker";
+
+type Props = {
+  currentChart?: any;
+  isFullscreen: boolean;
+  setIsFullscreen: (x: boolean) => void;
+};
+
+const LogsSection: React.FC<Props> = ({
+  currentChart,
+  isFullscreen,
+  setIsFullscreen,
+}) => {
+  const [podFilter, setPodFilter] = useState("pod-a");
+  const [scrollToBottom, setScrollToBottom] = useState(true);
+  const [searchText, setSearchText] = useState("");
+
+  useEffect(() => {
+    console.log(currentChart);
+  }, []);
+
+  const [startDate, setStartDate] = useState(new Date());
+
+  const renderContents = () => {
+    return (
+      <>
+        <FlexRow isFullscreen={isFullscreen}>
+          <Flex>
+            <SearchRowWrapper>
+              <SearchBarWrapper>
+                <i className="material-icons">search</i>
+                <SearchInput
+                  value={searchText}
+                  onChange={(e: any) => {
+                    setSearchText(e.value);
+                  }}
+                  placeholder="Search logs..."
+                />
+              </SearchBarWrapper>
+            </SearchRowWrapper>
+            <DateTimePicker startDate={startDate} setStartDate={setStartDate} />
+            <RadioFilter
+              icon={filterOutline}
+              selected={podFilter}
+              setSelected={setPodFilter}
+              options={[
+                {
+                  value: "pod-a",
+                  label: "Pod A",
+                },
+                {
+                  value: "pod-b",
+                  label: "Pod B",
+                },
+                {
+                  value: "pod-c",
+                  label: "Pod C",
+                },
+                {
+                  value: "pod-d",
+                  label: "Pod D",
+                },
+              ]}
+              name="Filter logs"
+            />
+          </Flex>
+          <Flex>
+            <Button onClick={() => setScrollToBottom(!scrollToBottom)}>
+              <Checkbox checked={scrollToBottom}>
+                <i className="material-icons">done</i>
+              </Checkbox>
+              Scroll to bottom
+            </Button>
+            <Spacer />
+            <Button>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Button>
+            {!isFullscreen && (
+              <>
+                <Spacer />
+                <Icon onClick={() => setIsFullscreen(true)}>
+                  <i className="material-icons">open_in_full</i>
+                </Icon>
+              </>
+            )}
+          </Flex>
+        </FlexRow>
+        <StyledLogsSection isFullscreen={isFullscreen}>
+          <Message>
+            No matching logs found.
+            <Highlight onClick={() => {}}>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Highlight>
+          </Message>
+        </StyledLogsSection>
+      </>
+    );
+  };
+
+  return (
+    <>
+      {isFullscreen ? (
+        <Fullscreen>
+          <AbsoluteTitle>
+            <BackButton onClick={() => setIsFullscreen(false)}>
+              <i className="material-icons">navigate_before</i>
+            </BackButton>
+            Logs ({currentChart.name})
+          </AbsoluteTitle>
+          {renderContents()}
+        </Fullscreen>
+      ) : (
+        <>{renderContents()}</>
+      )}
+    </>
+  );
+};
+
+export default LogsSection;
+
+const BackButton = styled.div`
+  display: flex;
+  width: 30px;
+  z-index: 999;
+  cursor: pointer;
+  height: 30px;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  cursor: pointer;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  > i {
+    font-size: 18px;
+  }
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const AbsoluteTitle = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+  font-size: 18px;
+  font-weight: 500;
+  user-select: text;
+`;
+
+const Fullscreen = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  padding-top: 60px;
+`;
+
+const Icon = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  width: 30px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  > i {
+    font-size: 14px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Checkbox = styled.div<{ checked: boolean }>`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props) => (props.checked ? "" : "none")};
+  }
+`;
+
+const Spacer = styled.div<{ width?: string }>`
+  height: 100%;
+  width: ${(props) => props.width || "10px"};
+`;
+
+const Button = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  font-size: 13px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  padding: 10px;
+  padding-left: 8px;
+  > i {
+    font-size: 16px;
+    margin-right: 5px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 25px solid transparent;
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  color: #8590ff;
+  cursor: pointer;
+
+  > i {
+    font-size: 16px;
+    margin-right: 3px;
+  }
+`;
+
+const FlexRow = styled.div<{ isFullscreen?: boolean }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  margin-top: ${(props) => (props.isFullscreen ? "10px" : "")};
+  padding: ${(props) => (props.isFullscreen ? "0 20px" : "")};
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-radius: 5px;
+  width: 250px;
+`;
+
+const StyledLogsSection = styled.div<{ isFullscreen: boolean }>`
+  width: 100%;
+  min-height: 400px;
+  height: ${(props) =>
+    props.isFullscreen ? "calc(100vh - 125px)" : "calc(100vh - 460px)"};
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  border-radius: ${(props) => (props.isFullscreen ? "" : "8px")};
+  border: ${(props) => (props.isFullscreen ? "" : "1px solid #ffffff33")};
+  border-top: ${(props) => (props.isFullscreen ? "1px solid #ffffff33" : "")};
+  padding: 18px 22px;
+  background: #121318;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -650,7 +650,7 @@ const DropdownAlt = styled(Dropdown)`
 const RangeWrapper = styled.div`
 const RangeWrapper = styled.div`
   float: right;
   float: right;
   font-weight: bold;
   font-weight: bold;
-  width: 156px;
+  width: 158px;
   margin-top: -8px;
   margin-top: -8px;
 `;
 `;
 
 

+ 3 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import Anser from "anser";
 import Anser from "anser";
 import CommandLineIcon from "assets/command-line-icon";
 import CommandLineIcon from "assets/command-line-icon";
-import ConnectToLogsInstructionModal from "./ConnectToLogsInstructionModal";
 import { SelectedPodType } from "./types";
 import { SelectedPodType } from "./types";
 import { useLogs } from "./useLogs";
 import { useLogs } from "./useLogs";
 
 
@@ -180,7 +179,7 @@ const LogsFC: React.FC<{
             checked={isScrollToBottomEnabled}
             checked={isScrollToBottomEnabled}
             onChange={() => {}}
             onChange={() => {}}
           />
           />
-          Scroll to Bottom
+          Scroll to bottom
         </Scroll>
         </Scroll>
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
           <Scroll
           <Scroll
@@ -193,7 +192,7 @@ const LogsFC: React.FC<{
               checked={showPreviousLogs}
               checked={showPreviousLogs}
               onChange={() => {}}
               onChange={() => {}}
             />
             />
-            Show previous Logs
+            Show previous logs
           </Scroll>
           </Scroll>
         )}
         )}
         <Refresh onClick={() => refresh()}>
         <Refresh onClick={() => refresh()}>
@@ -288,7 +287,7 @@ const Refresh = styled.div`
 const LogTabs = styled.div`
 const LogTabs = styled.div`
   width: 100%;
   width: 100%;
   height: 25px;
   height: 25px;
-  background: #121318;
+  margin-top: -25px;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;

+ 1 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -3,9 +3,8 @@ import styled from "styled-components";
 
 
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ChartType, StorageType } from "shared/types";
+import { ChartType } from "shared/types";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
-import backArrow from "assets/back_arrow.png";
 
 
 import Logs from "./Logs";
 import Logs from "./Logs";
 import ControllerTab from "./ControllerTab";
 import ControllerTab from "./ControllerTab";
@@ -211,11 +210,6 @@ const BackButton = styled.div`
   }
   }
 `;
 `;
 
 
-const BackButtonImg = styled.img`
-  width: 12px;
-  opacity: 0.75;
-`;
-
 const AbsoluteTitle = styled.div`
 const AbsoluteTitle = styled.div`
   position: absolute;
   position: absolute;
   top: 0px;
   top: 0px;

+ 7 - 1
dashboard/webpack.config.js

@@ -88,7 +88,13 @@ module.exports = () => {
           test: /\.(png|svg|jpg|gif|mp3)$/,
           test: /\.(png|svg|jpg|gif|mp3)$/,
           use: ["file-loader"],
           use: ["file-loader"],
         },
         },
-        { test: /\.css$/, use: ["css-loader"] },
+        {
+          test: /\.css$/i,
+          loader: "css-loader",
+          options: {
+            import: true,
+          },
+        },
         {
         {
           test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
           test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
           use: [
           use: [

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor