Преглед на файлове

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

Soham Parekh преди 3 години
родител
ревизия
f89c395f46
променени са 34 файла, в които са добавени 3589 реда и са изтрити 14624 реда
  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

Файловите разлики са ограничени, защото са твърде много
+ 1 - 14275
dashboard/package-lock.json


+ 3 - 0
dashboard/package.json

@@ -45,6 +45,7 @@
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-color": "^2.19.3",
+    "react-datepicker": "^4.8.0",
     "react-dom": "^16.13.1",
     "react-error-boundary": "^3.1.3",
     "react-infinite-scroll-component": "^6.1.0",
@@ -89,6 +90,7 @@
     "@types/random-words": "^1.1.0",
     "@types/react": "^16.14.14",
     "@types/react-color": "^3.0.6",
+    "@types/react-datepicker": "^4.4.2",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
@@ -101,6 +103,7 @@
     "babel-loader": "^8.2.2",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-styled-components": "^1.13.3",
+    "css-loader": "^5.2.7",
     "file-loader": "^6.1.0",
     "html-webpack-plugin": "^4.5.0",
     "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 = {};
 
-export const Boilerplate: React.FC<Props> = (props) => {
+const Boilerplate: React.FC<Props> = (props) => {
   const [someState, setSomeState] = useState("");
 
   return <StyledBoilerplate></StyledBoilerplate>;
 };
 
+export default Boilerplate;
+
 const StyledBoilerplate = styled.div``;

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

@@ -90,7 +90,7 @@ const RadioFilter: React.FC<Props> = (props) => {
         noMargin={props.noMargin}
       >
         {props.icon && <FilterIcon src={props.icon} />}
-        {props.name}
+        <TextAlt>{props.name}</TextAlt>
         <Bar />
         <Selected>
           {props.selected
@@ -113,6 +113,7 @@ const Bar = styled.div`
   height: calc(18px);
   background: #494b4f;
   margin: 0 8px;
+  margin-left: 0;
 `;
 
 const Selected = styled.div`
@@ -131,6 +132,13 @@ const Text = styled.div`
   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 }>`
   width: 100%;
   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`
   color: #aaaabb88;
   font-size: 12px;
@@ -176,35 +169,6 @@ const ScrollableWrapper = styled.div`
   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`
   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%")};
   background: #ffffff11;
   color: #ffffff;
-  padding: 0px 35px 25px;
+  padding: 0px 35px 20px;
   position: relative;
   border-radius: 8px;
   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 Loading from "components/Loading";
 import InfiniteScroll from "react-infinite-scroll-component";
-import Dropdown from "components/Dropdown";
 import { useKubeEvents } from "components/events/useEvents";
 import SubEventsList from "components/events/SubEventsList";
 
@@ -39,8 +38,7 @@ const EventsTab = () => {
       <Placeholder>
         <div>
           <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()}>
             <i className="material-icons">add</i> Install Porter agent
           </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 GraphSection from "./GraphSection";
 import MetricsSection from "./metrics/MetricsSection";
+import LogsSection from "./logs-section/LogsSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
@@ -22,10 +23,11 @@ import { useWebsockets } from "shared/hooks/useWebsockets";
 import useAuth from "shared/auth/useAuth";
 import TitleSection from "components/TitleSection";
 import DeploymentType from "./DeploymentType";
-import IncidentsTab from "./incidents/IncidentsTab";
+import EventsTab from "./events/EventsTab";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces";
 import { useStackEnvGroups } from "./useStackEnvGroups";
+import DeployStatusSection from "./deploy-status-section/DeployStatusSection";
 
 type Props = {
   namespace: string;
@@ -74,6 +76,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [isAuthorized] = useAuth();
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
+  const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
 
   const {
     isStack,
@@ -131,6 +134,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   };
 
   const getControllers = async (chart: ChartType) => {
+    
     // don't retrieve controllers for chart that failed to even deploy.
     if (chart.info.status == "failed") return;
 
@@ -412,16 +416,23 @@ const ExpandedChart: React.FC<Props> = (props) => {
     let { setSidebar } = props;
     let chart = currentChart;
     switch (currentTab) {
+      case "logs":
+        return (
+          <LogsSection 
+            currentChart={chart}
+            isFullscreen={isFullscreen}
+            setIsFullscreen={setIsFullscreen}
+          />
+        );
       case "metrics":
         return <MetricsSection currentChart={chart} />;
-      case "incidents":
+      case "events":
         if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
           return null;
         }
         return (
-          <IncidentsTab
-            releaseName={chart?.name}
-            namespace={chart?.namespace}
+          <EventsTab
+            controllers={controllers}
           />
         );
       case "status":
@@ -525,13 +536,18 @@ const ExpandedChart: React.FC<Props> = (props) => {
     // Collate non-form tabs
     let rightTabOptions = [] 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) {
       leftTabOptions.push({ label: "Metrics", value: "metrics" });
@@ -586,9 +602,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const renderUrl = () => {
     if (url) {
       return (
-        <Url href={url} target="_blank">
+        <Url>
           <i className="material-icons">link</i>
-          {url}
+          <a href={url} target="_blank">{url}</a>
         </Url>
       );
     }
@@ -756,134 +772,147 @@ const ExpandedChart: React.FC<Props> = (props) => {
           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 />
                   <Placeholder>
                     <TextWrap>
                       <Header>
-                        <Spinner src={loadingSrc} />
+                        <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
                       </Header>
+                      You will be automatically redirected after deletion is
+                      complete.
                     </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>
+                  <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`
   position: relative;
-  margin-bottom: 50px;
+  padding-bottom: 0;
+  margin-bottom: 0;
 `;
 
 const Header = styled.div`
@@ -975,15 +1005,16 @@ const Bolded = styled.div`
   margin-right: 6px;
 `;
 
-const Url = styled.a`
+const Url = styled.div`
   display: block;
-  margin-left: 2px;
+  margin-left: 5px;
   font-size: 13px;
   margin-top: 16px;
   user-select: all;
   margin-bottom: -5px;
   user-select: text;
   display: flex;
+  color: #949eff;
   align-items: center;
 
   > i {
@@ -1026,7 +1057,7 @@ const HeaderWrapper = styled.div`
 `;
 
 const Dot = styled.div`
-  margin-right: 9px;
+  margin-right: 16px;
 `;
 
 const InfoWrapper = styled.div`
@@ -1038,7 +1069,7 @@ const InfoWrapper = styled.div`
 
 const LastDeployed = styled.div`
   font-size: 13px;
-  margin-left: 10px;
+  margin-left: 8px;
   margin-top: -1px;
   display: flex;
   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 }) =>
     props.showRevisions ? "255px" : "40px"};
   background: #ffffff11;
-  margin: 25px 0px 18px;
+  margin: 20px 0px 18px;
   overflow: hidden;
   border-radius: 8px;
   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 EventCard from "components/events/EventCard";
+import EventList from "./EventList";
 import Loading from "components/Loading";
 import InfiniteScroll from "react-infinite-scroll-component";
+import { Context } from "shared/Context";
 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(() => {
-    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 (
       <Placeholder>
         <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) {
     return (
       <Placeholder>
         <div>
           <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()}>
             <i className="material-icons">add</i> Install Porter agent
           </InstallPorterAgentButton>
@@ -123,64 +56,9 @@ const EventsTab: React.FC<{
     );
   }
 
-  if (currentEvent) {
-    return (
-      <SubEventsList
-        event={currentEvent}
-        clearSelectedEvent={() => setCurrentEvent(null)}
-      />
-    );
-  }
-
   return (
     <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>
   );
 };

+ 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`
-  height: 250px;
+  max-height: 500px;
   width: 100%;
   background-color: black;
   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`
   float: right;
   font-weight: bold;
-  width: 156px;
+  width: 158px;
   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 Anser from "anser";
 import CommandLineIcon from "assets/command-line-icon";
-import ConnectToLogsInstructionModal from "./ConnectToLogsInstructionModal";
 import { SelectedPodType } from "./types";
 import { useLogs } from "./useLogs";
 
@@ -180,7 +179,7 @@ const LogsFC: React.FC<{
             checked={isScrollToBottomEnabled}
             onChange={() => {}}
           />
-          Scroll to Bottom
+          Scroll to bottom
         </Scroll>
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
           <Scroll
@@ -193,7 +192,7 @@ const LogsFC: React.FC<{
               checked={showPreviousLogs}
               onChange={() => {}}
             />
-            Show previous Logs
+            Show previous logs
           </Scroll>
         )}
         <Refresh onClick={() => refresh()}>
@@ -288,7 +287,7 @@ const Refresh = styled.div`
 const LogTabs = styled.div`
   width: 100%;
   height: 25px;
-  background: #121318;
+  margin-top: -25px;
   display: flex;
   flex-direction: row;
   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 { Context } from "shared/Context";
-import { ChartType, StorageType } from "shared/types";
+import { ChartType } from "shared/types";
 import Loading from "components/Loading";
-import backArrow from "assets/back_arrow.png";
 
 import Logs from "./Logs";
 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`
   position: absolute;
   top: 0px;

+ 7 - 1
dashboard/webpack.config.js

@@ -88,7 +88,13 @@ module.exports = () => {
           test: /\.(png|svg|jpg|gif|mp3)$/,
           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+)?$/,
           use: [

Някои файлове не бяха показани, защото твърде много файлове са промени