Переглянути джерело

allow event list to set logs tab

Alexander Belanger 3 роки тому
батько
коміт
141fa56bc1

+ 1 - 0
api/server/handlers/namespace/stream_pod_logs_loki.go

@@ -64,6 +64,7 @@ func (c *StreamPodLogsLokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	err = agent.StreamPorterAgentLokiLog([]string{
 		fmt.Sprintf("pod=%s", request.PodSelector),
 		fmt.Sprintf("namespace=%s", request.Namespace),
+		fmt.Sprintf("helm_sh_revision=%s", request.Revision),
 	}, string(startTime), request.SearchParam, 0, safeRW)
 
 	if err != nil {

+ 1 - 0
api/types/incident.go

@@ -100,6 +100,7 @@ type GetLogRequest struct {
 	StartRange  *time.Time `schema:"start_range"`
 	EndRange    *time.Time `schema:"end_range"`
 	SearchParam string     `schema:"search_param"`
+	Revision    string     `schema:"revision"`
 	PodSelector string     `schema:"pod_selector" form:"required"`
 	Namespace   string     `schema:"namespace" form:"required"`
 	Direction   string     `schema:"direction"`

+ 68 - 97
dashboard/package-lock.json

@@ -38,6 +38,7 @@
         "cronstrue": "^2.2.0",
         "d3-array": "^2.11.0",
         "d3-time-format": "^3.0.0",
+        "dayjs": "^1.11.5",
         "dotenv": "^8.2.0",
         "highlight.run": "^1.4.5",
         "ini": ">=1.3.6",
@@ -365,11 +366,11 @@
       }
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz",
-      "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+      "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
       "dependencies": {
-        "@babel/types": "^7.16.0"
+        "@babel/types": "^7.18.6"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -407,9 +408,9 @@
       }
     },
     "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz",
-      "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==",
+      "version": "7.19.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz",
+      "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -479,10 +480,18 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+      "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.15.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
-      "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
+      "version": "7.19.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+      "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
       "engines": {
         "node": ">=6.9.0"
       }
@@ -905,12 +914,12 @@
       }
     },
     "node_modules/@babel/plugin-syntax-jsx": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz",
-      "integrity": "sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz",
+      "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.14.5"
+        "@babel/helper-plugin-utils": "^7.18.6"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1773,9 +1782,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.16.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
-      "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz",
+      "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==",
       "dependencies": {
         "regenerator-runtime": "^0.13.4"
       },
@@ -1829,11 +1838,12 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
-      "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz",
+      "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.15.7",
+        "@babel/helper-string-parser": "^7.19.4",
+        "@babel/helper-validator-identifier": "^7.19.1",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
@@ -4049,37 +4059,6 @@
         "styled-components": ">= 2"
       }
     },
-    "node_modules/babel-plugin-styled-components/node_modules/@babel/helper-module-imports": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz",
-      "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==",
-      "dependencies": {
-        "@babel/types": "^7.16.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/babel-plugin-styled-components/node_modules/@babel/helper-validator-identifier": {
-      "version": "7.15.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
-      "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/babel-plugin-styled-components/node_modules/@babel/types": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
-      "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.15.7",
-        "to-fast-properties": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/babel-plugin-syntax-jsx": {
       "version": "6.18.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
@@ -5527,6 +5506,11 @@
         "url": "https://opencollective.com/date-fns"
       }
     },
+    "node_modules/dayjs": {
+      "version": "1.11.5",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
+      "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA=="
+    },
     "node_modules/debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -14751,11 +14735,11 @@
       }
     },
     "@babel/helper-module-imports": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz",
-      "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+      "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
       "requires": {
-        "@babel/types": "^7.16.0"
+        "@babel/types": "^7.18.6"
       }
     },
     "@babel/helper-module-transforms": {
@@ -14784,9 +14768,9 @@
       }
     },
     "@babel/helper-plugin-utils": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz",
-      "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==",
+      "version": "7.19.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz",
+      "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==",
       "dev": true
     },
     "@babel/helper-remap-async-to-generator": {
@@ -14838,10 +14822,15 @@
         "@babel/types": "^7.16.0"
       }
     },
+    "@babel/helper-string-parser": {
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
+      "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="
+    },
     "@babel/helper-validator-identifier": {
-      "version": "7.15.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
-      "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w=="
+      "version": "7.19.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
+      "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
     },
     "@babel/helper-validator-option": {
       "version": "7.14.5",
@@ -15120,12 +15109,12 @@
       }
     },
     "@babel/plugin-syntax-jsx": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz",
-      "integrity": "sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==",
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz",
+      "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.14.5"
+        "@babel/helper-plugin-utils": "^7.18.6"
       }
     },
     "@babel/plugin-syntax-logical-assignment-operators": {
@@ -15702,9 +15691,9 @@
       }
     },
     "@babel/runtime": {
-      "version": "7.16.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
-      "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz",
+      "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==",
       "requires": {
         "regenerator-runtime": "^0.13.4"
       }
@@ -15746,11 +15735,12 @@
       }
     },
     "@babel/types": {
-      "version": "7.16.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
-      "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
+      "version": "7.19.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz",
+      "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==",
       "requires": {
-        "@babel/helper-validator-identifier": "^7.15.7",
+        "@babel/helper-string-parser": "^7.19.4",
+        "@babel/helper-validator-identifier": "^7.19.1",
         "to-fast-properties": "^2.0.0"
       }
     },
@@ -17602,30 +17592,6 @@
         "@babel/helper-module-imports": "^7.15.4",
         "babel-plugin-syntax-jsx": "^6.18.0",
         "lodash": "^4.17.11"
-      },
-      "dependencies": {
-        "@babel/helper-module-imports": {
-          "version": "7.16.0",
-          "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz",
-          "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==",
-          "requires": {
-            "@babel/types": "^7.16.0"
-          }
-        },
-        "@babel/helper-validator-identifier": {
-          "version": "7.15.7",
-          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
-          "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w=="
-        },
-        "@babel/types": {
-          "version": "7.16.0",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
-          "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
-          "requires": {
-            "@babel/helper-validator-identifier": "^7.15.7",
-            "to-fast-properties": "^2.0.0"
-          }
-        }
       }
     },
     "babel-plugin-syntax-jsx": {
@@ -18843,6 +18809,11 @@
       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
       "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
     },
+    "dayjs": {
+      "version": "1.11.5",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
+      "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA=="
+    },
     "debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",

+ 2 - 1
dashboard/package.json

@@ -33,6 +33,7 @@
     "cronstrue": "^2.2.0",
     "d3-array": "^2.11.0",
     "d3-time-format": "^3.0.0",
+    "dayjs": "^1.11.5",
     "dotenv": "^8.2.0",
     "highlight.run": "^1.4.5",
     "ini": ">=1.3.6",
@@ -61,7 +62,7 @@
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
-    "start": "webpack-dev-server",
+    "start": "npx webpack-dev-server",
     "build": "NODE_ENV=\"production\" webpack",
     "build-and-analyze": "ENABLE_ANALYZER=true NODE_ENV=\"production\" webpack"
   },

+ 10 - 0
dashboard/src/components/Loading.tsx

@@ -6,6 +6,7 @@ type PropsType = {
   offset?: string;
   width?: string;
   height?: string;
+  message?: string;
 };
 
 type StateType = {};
@@ -21,6 +22,9 @@ export default class Loading extends Component<PropsType, StateType> {
         height={this.props.height || "100%"}
       >
         <Spinner src={loading} />
+        {this.props.message ? (
+          <StyledMessage>{this.props.message}</StyledMessage>
+        ) : null}
       </StyledLoading>
     );
   }
@@ -38,5 +42,11 @@ const StyledLoading = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
+  flex-direction: column;
   margin-top: ${(props: StyleLoadingProps) => props.offset};
 `;
+
+const StyledMessage = styled.div`
+  margin-block: 15px;
+  color: #aaaabb;
+`;

+ 9 - 1
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 
 import PorterForm from "./PorterForm";
 import { InjectedProps, PorterFormData } from "./types";
@@ -24,6 +24,7 @@ type PropsType = {
   redirectTabAfterSave?: string;
   includeMetadata?: boolean;
   injectedProps?: InjectedProps;
+  overrideCurrentTab?: string;
 };
 
 const PorterFormWrapper: React.FC<PropsType> = ({
@@ -46,6 +47,7 @@ const PorterFormWrapper: React.FC<PropsType> = ({
   redirectTabAfterSave,
   includeMetadata,
   injectedProps,
+  overrideCurrentTab,
 }) => {
   const hashCode = (s: string) => {
     return s?.split("").reduce(function (a, b) {
@@ -75,6 +77,12 @@ const PorterFormWrapper: React.FC<PropsType> = ({
   // Lifted into PorterFormWrapper to allow tab to be remembered on re-render (e.g., on revision select)
   const [currentTab, setCurrentTab] = useState(getInitialTab());
 
+  useEffect(() => {
+    if (overrideCurrentTab) {
+      setCurrentTab(overrideCurrentTab);
+    }
+  }, [overrideCurrentTab]);
+
   return (
     <React.Fragment key={hashCode(JSON.stringify(formData))}>
       <PorterFormContextProvider

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

@@ -77,6 +77,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
   const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
   const [logData, setLogData] = useState<InitLogData>();
+  const [overrideCurrentTab, setOverrideCurrentTab] = useState("");
 
   const {
     isStack,
@@ -98,6 +99,11 @@ const ExpandedChart: React.FC<Props> = (props) => {
     setCurrentOverlay,
   } = useContext(Context);
 
+  const renderLogsAtTimestamp = (initLogData: InitLogData) => {
+    setLogData(initLogData);
+    setOverrideCurrentTab("logs");
+  };
+
   // Retrieve full chart data (includes form and values)
   const getChartData = async (chart: ChartType) => {
     setIsLoadingChartData(true);
@@ -433,7 +439,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
         if (DisabledNamespacesForIncidents.includes(currentChart.namespace)) {
           return null;
         }
-        return <EventsTab currentChart={chart} setLogData={setLogData} />;
+        return (
+          <EventsTab currentChart={chart} setLogData={renderLogsAtTimestamp} />
+        );
       case "status":
         if (isLoadingChartData) {
           return (
@@ -902,6 +910,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
                                 chart: currentChart,
                               },
                             }}
+                            overrideCurrentTab={overrideCurrentTab}
                           />
                         </BodyWrapper>
                       )}

+ 4 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -138,8 +138,10 @@ const EventList: React.FC<Props> = ({ filters, setLogData }) => {
                 <TableButton
                   width="102px"
                   onClick={() => {
-                    // setLogData()
-                    // TODO: POD NAME AND TIMESTAMP
+                    setLogData({
+                      podName: "hello",
+                      timestamp: row.original.last_seen,
+                    });
                   }}
                 >
                   <Icon src={document} />

+ 200 - 26
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -1,17 +1,24 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, {
+  useCallback,
+  useContext,
+  useEffect,
+  useRef,
+  useState,
+} from "react";
 
 import styled from "styled-components";
 import RadioFilter from "components/RadioFilter";
 
 import filterOutline from "assets/filter-outline.svg";
-import downArrow from "assets/down-arrow.svg";
+import time from "assets/time.svg";
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { useLogs } from "./useAgentLogs";
+import { Direction, useLogs } from "./useAgentLogs";
 import Anser from "anser";
-import { flatMap } from "lodash";
-import time from "assets/time.svg";
 import DateTimePicker from "components/date-time-picker/DateTimePicker";
+import dayjs from "dayjs";
+import Loading from "components/Loading";
+import _ from "lodash";
 
 export type InitLogData = {
   podName: string;
@@ -29,25 +36,80 @@ const escapeRegExp = (str: string) => {
   return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 };
 
+interface QueryModeSelectionToggleProps {
+  selectedDate?: Date;
+  setSelectedDate: React.Dispatch<React.SetStateAction<Date>>;
+}
+
+const QueryModeSelectionToggle = (props: QueryModeSelectionToggleProps) => {
+  return (
+    <div
+      style={{
+        marginRight: "10px",
+        display: "flex",
+        gap: "10px",
+      }}
+    >
+      <ToggleButton>
+        <ToggleOption
+          onClick={() => props.setSelectedDate(undefined)}
+          selected={!props.selectedDate}
+        >
+          <span
+            style={{
+              display: "inline-black",
+              width: "0.5rem",
+              height: "0.5rem",
+              marginRight: "5px",
+              borderRadius: "20px",
+              backgroundColor: "#ed5f85",
+              border: "0px",
+              outline: "none",
+              boxShadow: props.selectedDate
+                ? "none"
+                : "0px 0px 5px 1px #ed5f85",
+            }}
+          ></span>
+          Live
+        </ToggleOption>
+        <ToggleOption
+          nudgeLeft
+          onClick={() => props.setSelectedDate(dayjs().toDate())}
+          selected={!!props.selectedDate}
+        >
+          <TimeIcon src={time} />
+        </ToggleOption>
+      </ToggleButton>
+      {props.selectedDate && (
+        <DateTimePicker
+          startDate={props.selectedDate}
+          setStartDate={props.setSelectedDate}
+        />
+      )}
+    </div>
+  );
+};
+
 const LogsSection: React.FC<Props> = ({
   currentChart,
   isFullscreen,
   setIsFullscreen,
   initData,
 }) => {
+  const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
   const { currentProject, currentCluster } = useContext(Context);
   const [podFilter, setPodFilter] = useState(initData?.podName);
   const [podFilterOpts, setPodFilterOpts] = useState<string[]>(
-    initData?.podName ? [initData?.podName] : []
+    initData ? [initData?.podName] : null
   );
-  const [scrollToBottom, setScrollToBottom] = useState(true);
+  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
   const [searchText, setSearchText] = useState("");
   const [enteredSearchText, setEnteredSearchText] = useState("");
-  const [selectedDate, setSelectedDate] = useState<Date>(
-    initData ? new Date(initData?.timestamp) : null
+  const [selectedDate, setSelectedDate] = useState<Date | undefined>(
+    initData ? dayjs(initData?.timestamp).toDate() : undefined
   );
 
-  const { logs, refresh, moveCursor } = useLogs(
+  const { loading, logs, refresh, moveCursor, paginationInfo } = useLogs(
     podFilter,
     currentChart.namespace,
     enteredSearchText,
@@ -67,25 +129,35 @@ const LogsSection: React.FC<Props> = ({
         }
       )
       .then((res) => {
-        console.log(res.data);
-        setPodFilterOpts(res.data);
+        setPodFilterOpts(_.uniq(res.data ?? []));
         setPodFilter(res.data[0]);
       });
-
-    console.log(currentChart);
   }, []);
 
+  useEffect(() => {
+    if (!loading && scrollToBottomRef.current && scrollToBottomEnabled) {
+      scrollToBottomRef.current.scrollIntoView({
+        behavior: "smooth",
+        block: "end",
+      });
+    }
+  }, [loading, logs, scrollToBottomRef, scrollToBottomEnabled]);
+
   const renderLogs = () => {
     return logs?.map((log, i) => {
       return (
-        <Log key={i}>
-          {log.map((ansi, j) => {
+        <Log key={[log.lineNumber, i].join(".")}>
+          <span className="line-number">{log.lineNumber}.</span>
+          <span className="line-timestamp">
+            {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
+          </span>
+          {log.line?.map((ansi, j) => {
             if (ansi.clearLine) {
               return null;
             }
 
             return (
-              <LogSpan key={i + "." + j} ansi={ansi}>
+              <LogSpan key={[log.lineNumber, i, j].join(".")} ansi={ansi}>
                 {ansi.content.replace(/ /g, "\u00a0")}
               </LogSpan>
             );
@@ -95,6 +167,15 @@ const LogsSection: React.FC<Props> = ({
     });
   };
 
+  const onLoadPrevious = useCallback(() => {
+    if (!selectedDate) {
+      setSelectedDate(dayjs(logs[0].timestamp).toDate());
+      return;
+    }
+
+    moveCursor(Direction.backward);
+  }, [logs, selectedDate]);
+
   const renderContents = () => {
     return (
       <>
@@ -117,9 +198,9 @@ const LogsSection: React.FC<Props> = ({
                 />
               </SearchBarWrapper>
             </SearchRowWrapper>
-            <DateTimePicker
-              startDate={selectedDate}
-              setStartDate={setSelectedDate}
+            <QueryModeSelectionToggle
+              selectedDate={selectedDate}
+              setSelectedDate={setSelectedDate}
             />
             <RadioFilter
               icon={filterOutline}
@@ -135,8 +216,8 @@ const LogsSection: React.FC<Props> = ({
             />
           </Flex>
           <Flex>
-            <Button onClick={() => setScrollToBottom(!scrollToBottom)}>
-              <Checkbox checked={scrollToBottom}>
+            <Button onClick={() => setScrollToBottomEnabled((s) => !s)}>
+              <Checkbox checked={scrollToBottomEnabled}>
                 <i className="material-icons">done</i>
               </Checkbox>
               Scroll to bottom
@@ -157,8 +238,21 @@ const LogsSection: React.FC<Props> = ({
           </Flex>
         </FlexRow>
         <StyledLogsSection isFullscreen={isFullscreen}>
-          {renderLogs()}
-          {/* <Message>
+          {loading || !logs.length ? (
+            <Loading message="Waiting for logs..." />
+          ) : (
+            <>
+              <LoadMoreButton
+                active={
+                  logs.length !== 0 && paginationInfo.previousCursor !== null
+                }
+                role="button"
+                onClick={onLoadPrevious}
+              >
+                Load Previous
+              </LoadMoreButton>
+              {renderLogs()}
+              {/* <Message>
             
             No matching logs found.
             <Highlight onClick={() => {}}>
@@ -166,6 +260,16 @@ const LogsSection: React.FC<Props> = ({
               Refresh
             </Highlight>
           </Message> */}
+              <LoadMoreButton
+                active={selectedDate && logs.length !== 0}
+                role="button"
+                onClick={() => moveCursor(Direction.forward)}
+              >
+                Load more
+              </LoadMoreButton>
+            </>
+          )}
+          <div ref={scrollToBottomRef} />
         </StyledLogsSection>
       </>
     );
@@ -393,8 +497,7 @@ const StyledLogsSection = styled.div<{ isFullscreen: boolean }>`
   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;
+  background: #101420;
   animation: floatIn 0.3s;
   animation-timing-function: ease-out;
   animation-fill-mode: forwards;
@@ -415,9 +518,37 @@ const StyledLogsSection = styled.div<{ isFullscreen: boolean }>`
 const Log = styled.div`
   font-family: monospace;
   user-select: text;
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+  width: 100%;
+  & > * {
+    padding-block: 5px;
+  }
+  & > .line-timestamp {
+    height: 100%;
+    color: #949effff;
+    opacity: 0.5;
+    font-family: monospace;
+    min-width: fit-content;
+    padding-inline-end: 5px;
+  }
+  & > .line-number {
+    height: 100%;
+    background: #202538;
+    display: inline-block;
+    text-align: right;
+    min-width: 45px;
+    padding-inline-end: 5px;
+    opacity: 0.3;
+    font-family: monospace;
+  }
 `;
 
 const LogSpan = styled.span`
+  display: inline-block;
+  word-wrap: anywhere;
+  flex-grow: 1;
   font-family: monospace, sans-serif;
   font-size: 12px;
   font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
@@ -427,3 +558,46 @@ const LogSpan = styled.span`
   background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
     props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
 `;
+
+const LoadMoreButton = styled.div<{ active: boolean }>`
+  width: 100%;
+  display: ${(props) => (props.active ? "flex" : "none")};
+  justify-content: center;
+  align-items: center;
+  padding-block: 10px;
+  background: #1f2023;
+  cursor: pointer;
+  font-family: monospace;
+`;
+
+const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
+  padding: 0 10px;
+  color: ${(props) => (props.selected ? "" : "#494b4f")};
+  border: 1px solid #494b4f;
+  height: 100%;
+  display: flex;
+  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
+  align-items: center;
+  border-radius: ${(props) =>
+    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
+  :hover {
+    border: 1px solid #7a7b80;
+    z-index: 999;
+  }
+`;
+
+const ToggleButton = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  font-size: 13px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
+const TimeIcon = styled.img`
+  width: 16px;
+  height: 16px;
+  z-index: 999;
+`;

+ 262 - 84
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts

@@ -1,27 +1,74 @@
-import Anser from "anser";
-import { flatMap } from "lodash";
-import { useContext, useEffect, useMemo, useRef, useState } from "react";
+import Anser, { AnserJsonEntry } from "anser";
+import dayjs from "dayjs";
+import _ from "lodash";
+import { useContext, useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
+import { isJSON } from "shared/util";
 
-const MAX_LOGS = 250;
+const MAX_LOGS = 5000;
+const MAX_BUFFER_LOGS = 1000;
+const QUERY_LIMIT = 1000;
+
+export enum Direction {
+  forward = "forward",
+  backward = "backward",
+}
+
+interface Log {
+  line: AnserJsonEntry[];
+  lineNumber: number;
+  timestamp: string;
+}
+
+interface LogLine {
+  log: string;
+  stream: string;
+  time: string;
+}
+
+const parseLogs = (logs: string[] = []): Log[] => {
+  return logs
+    .filter(Boolean)
+    .filter(isJSON)
+    .map((logLine: string, idx) => {
+      try {
+        const parsedLine: LogLine = JSON.parse(logLine);
+        // TODO Move log parsing to the render method
+        const ansiLog = Anser.ansiToJson(parsedLine.log);
+        return {
+          line: ansiLog,
+          lineNumber: idx + 1,
+          timestamp: parsedLine.time,
+        };
+      } catch (err) {
+        console.error(err, logLine);
+      }
+    });
+};
+
+interface PaginationInfo {
+  previousCursor: string | null;
+  nextCursor: string | null;
+}
 
 export const useLogs = (
   currentPod: string,
   namespace: string,
   searchParam: string,
   // if setDate is set, results are not live
-  setDate: Date
+  setDate?: Date
 ) => {
-  var d = new Date();
-  d.setDate(d.getDate() - 14);
-
   const isLive = !setDate;
+  const logsBufferRef = useRef<Log[]>([]);
   const { currentCluster, currentProject } = useContext(Context);
-  const [logs, setLogs] = useState<Anser.AnserJsonEntry[][]>([]);
-  const [startDate, setStartDate] = useState<Date>(d);
-  const [endDate, setEndDate] = useState<Date>(setDate || new Date());
+  const [logs, setLogs] = useState<Log[]>([]);
+  const [paginationInfo, setPaginationInfo] = useState<PaginationInfo>({
+    previousCursor: null,
+    nextCursor: null,
+  });
+  const [loading, setLoading] = useState(true);
 
   // if we are live:
   // - start date is initially set to 2 weeks ago
@@ -41,16 +88,80 @@ export const useLogs = (
     closeAllWebsockets,
   } = useWebsockets();
 
-  useEffect(() => {
-    return refresh();
-  }, [currentPod, namespace, searchParam, setDate]);
+  const updateLogs = (
+    newLogs: Log[],
+    direction: Direction = Direction.forward
+  ) => {
+    // Nothing to update here
+    if (!newLogs.length) {
+      return;
+    }
 
-  useEffect(() => {
-    // if the streaming is no longer live, close all websockets
-    if (!isLive) {
-      closeAllWebsockets();
+    setLogs((logs) => {
+      let updatedLogs = _.cloneDeep(logs);
+
+      /**
+       * If direction = Direction.forward, we want to append the new logs
+       * at the end of the current logs, else we want to append before the current logs
+       *
+       */
+      if (direction === Direction.forward) {
+        const lastLineNumber = updatedLogs.at(-1)?.lineNumber ?? 0;
+
+        updatedLogs.push(
+          ...newLogs.map((log, idx) => ({
+            ...log,
+            lineNumber: lastLineNumber + idx + 1,
+          }))
+        );
+
+        // For direction = Direction.forward, remove logs from the front
+        if (updatedLogs.length > MAX_LOGS) {
+          const logsToBeRemoved =
+            newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS;
+          updatedLogs = updatedLogs.slice(logsToBeRemoved);
+        }
+      } else {
+        updatedLogs = newLogs.concat(
+          updatedLogs.map((log) => ({
+            ...log,
+            lineNumber: log.lineNumber + newLogs.length,
+          }))
+        );
+
+        // For direction = Direction.backward, remove logs from the back
+        if (updatedLogs.length > MAX_LOGS) {
+          const logsToBeRemoved =
+            newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS;
+
+          updatedLogs = updatedLogs.slice(0, logsToBeRemoved);
+        }
+      }
+
+      return updatedLogs;
+    });
+  };
+
+  /**
+   * Flushes the logs buffer. If `discard` is true,
+   * it will update `current logs` before executing
+   * the flush operation
+   */
+  const flushLogsBuffer = (discard: boolean = false) => {
+    if (!discard) {
+      updateLogs(logsBufferRef.current ?? []);
     }
-  }, [isLive]);
+
+    logsBufferRef.current = [];
+  };
+
+  const pushLogs = (newLogs: Log[]) => {
+    logsBufferRef.current.push(...newLogs);
+
+    if (logsBufferRef.current.length >= MAX_BUFFER_LOGS) {
+      flushLogsBuffer();
+    }
+  };
 
   const setupWebsocket = (websocketKey: string) => {
     const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki?pod_selector=${currentPod}&namespace=${namespace}&search_param=${searchParam}`;
@@ -60,20 +171,16 @@ export const useLogs = (
         console.log("Opened websocket:", websocketKey);
       },
       onmessage: (evt: MessageEvent) => {
-        let newLogs: Anser.AnserJsonEntry[][] = [];
-
-        evt?.data?.split("\n").forEach((logLine: string) => {
-          if (logLine) {
-            var parsedLine = JSON.parse(logLine);
+        // Nothing to do here
+        if (!evt?.data || typeof evt.data !== "string") {
+          return;
+        }
 
-            let ansiLog = Anser.ansiToJson(parsedLine.log);
-            newLogs.push(ansiLog);
-          }
-        });
+        const newLogs = parseLogs(
+          evt?.data?.split("}\n").map((line: string) => line + "}")
+        );
 
-        setLogs((prevLogs) => {
-          return prevLogs.concat(newLogs);
-        });
+        pushLogs(newLogs);
       },
       onclose: () => {
         console.log("Closed websocket:", websocketKey);
@@ -85,23 +192,22 @@ export const useLogs = (
   };
 
   const queryLogs = (
-    initLogs: Anser.AnserJsonEntry[][],
-    startDate: Date,
-    endDate: Date,
-    direction: string,
-    cb?: () => void
+    startDate: string,
+    endDate: string,
+    direction: Direction,
+    limit: number = QUERY_LIMIT
   ) => {
-    api
+    return api
       .getLogs(
         "<token>",
         {
           pod_selector: currentPod,
-          namespace: namespace,
+          namespace,
           search_param: searchParam,
-          start_range: startDate.toISOString(),
-          end_range: endDate.toISOString(),
-          limit: 1000,
-          direction: direction,
+          start_range: startDate,
+          end_range: endDate,
+          limit,
+          direction,
         },
         {
           cluster_id: currentCluster.id,
@@ -109,61 +215,84 @@ export const useLogs = (
         }
       )
       .then((res) => {
-        var newLogs: Anser.AnserJsonEntry[][] = [];
-        res.data.logs?.forEach((logLine: any) => {
-          if (logLine) {
-            var parsedLine = JSON.parse(logLine.line);
-
-            let ansiLog = Anser.ansiToJson(parsedLine.log);
-            newLogs.push(ansiLog);
-          }
-        });
-
-        var modifiedLogs: Anser.AnserJsonEntry[][] = initLogs;
-
-        if (direction == "forward") {
-          modifiedLogs.push(...newLogs);
-        } else if (direction == "backward") {
-          modifiedLogs.push(...newLogs.reverse());
+        const newLogs = parseLogs(
+          res.data.logs?.filter(Boolean).map((logLine: any) => logLine.line)
+        );
+
+        if (direction === Direction.backward) {
+          newLogs.reverse();
         }
 
-        setLogs([...modifiedLogs]);
-        cb && cb();
+        return {
+          logs: newLogs,
+          previousCursor:
+            // There are no more historical logs so don't set the previous cursor
+            newLogs.length < QUERY_LIMIT && direction == Direction.backward
+              ? null
+              : res.data.backward_continue_time,
+          nextCursor: res.data.forward_continue_time,
+        };
       });
   };
 
-  const refresh = () => {
+  const refresh = async () => {
     if (!currentPod) {
       return;
     }
 
+    setLoading(true);
+    setLogs([]);
+    flushLogsBuffer(true);
     const websocketKey = `${currentPod}-${namespace}-websocket`;
-    var newEndDate = setDate || new Date();
+    const endDate = dayjs(setDate);
+    const twoWeeksAgo = endDate.subtract(14, "days");
 
-    queryLogs([], startDate, newEndDate, "backward", () => {
-      setEndDate(newEndDate);
-      closeWebsocket(websocketKey);
+    const { logs: initialLogs, previousCursor, nextCursor } = await queryLogs(
+      twoWeeksAgo.toISOString(),
+      endDate.toISOString(),
+      Direction.backward
+    );
 
-      if (isLive) {
-        setupWebsocket(websocketKey);
-        return () => {
-          closeWebsocket(websocketKey);
-        };
-      }
+    setPaginationInfo({
+      previousCursor,
+      nextCursor,
     });
+
+    updateLogs(initialLogs);
+
+    closeWebsocket(websocketKey);
+
+    setLoading(false);
+
+    if (isLive) {
+      setupWebsocket(websocketKey);
+    }
+
+    return () => isLive && closeWebsocket(websocketKey);
   };
 
-  const moveCursor = (direction: number) => {
-    if (direction < 0) {
+  const moveCursor = async (direction: Direction) => {
+    if (direction === Direction.backward) {
       // we query by setting the endDate equal to the previous startDate, and setting the direction
       // to "backward"
-      var twoWeeksAgo = new Date();
-      twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
+      const refDate = paginationInfo.previousCursor ?? dayjs().toISOString();
+      const twoWeeksAgo = dayjs(refDate).subtract(14, "days");
 
-      queryLogs(logs, twoWeeksAgo, startDate, "backward", () => {
-        setEndDate(startDate);
-        setStartDate(twoWeeksAgo);
-      });
+      const { logs: newLogs, previousCursor } = await queryLogs(
+        twoWeeksAgo.toISOString(),
+        refDate,
+        Direction.backward
+      );
+
+      updateLogs(
+        paginationInfo.previousCursor ? newLogs.slice(0, -1) : newLogs,
+        direction
+      );
+
+      setPaginationInfo((paginationInfo) => ({
+        ...paginationInfo,
+        previousCursor,
+      }));
     } else {
       if (isLive) {
         return;
@@ -171,17 +300,66 @@ export const useLogs = (
 
       // we query by setting the startDate equal to the previous endDate, setting the endDate equal to the
       // current time, and setting the direction to "forward"
-      var currDate = new Date();
-      queryLogs(logs, endDate, currDate, "forward", () => {
-        setStartDate(endDate);
-        setEndDate(currDate);
-      });
+      const refDate = paginationInfo.nextCursor ?? dayjs(setDate).toISOString();
+      const currDate = dayjs();
+
+      const { logs: newLogs, nextCursor } = await queryLogs(
+        refDate,
+        currDate.toISOString(),
+        Direction.forward
+      );
+
+      // If previously we had next cursor set, it is likely that the log might have a duplicate entry so we ignore the first line
+      updateLogs(paginationInfo.nextCursor ? newLogs.slice(1) : newLogs);
+
+      setPaginationInfo((paginationInfo) => ({
+        ...paginationInfo,
+        nextCursor,
+      }));
     }
   };
 
+  useEffect(() => {
+    setLogs([]);
+    flushLogsBuffer(true);
+  }, []);
+
+  /**
+   * In some situations, we might never hit the limit for the max buffer size.
+   * An example is if the total logs for the pod < MAX_BUFFER_LOGS.
+   *
+   * For handling situations like this, we would want to force a flush operation
+   * on the buffer so that we dont have any stale logs
+   */
+  useEffect(() => {
+    /**
+     * We don't want users to wait for too long for the initial
+     * logs to appear. So we use a setTimeout for 1s to force-flush
+     * logs after 1s of load
+     */
+    setTimeout(flushLogsBuffer, 500);
+
+    const flushLogsBufferInterval = setInterval(flushLogsBuffer, 3000);
+
+    return () => clearInterval(flushLogsBufferInterval);
+  }, []);
+
+  useEffect(() => {
+    refresh();
+  }, [currentPod, namespace, searchParam, setDate]);
+
+  useEffect(() => {
+    // if the streaming is no longer live, close all websockets
+    if (!isLive) {
+      closeAllWebsockets();
+    }
+  }, [isLive]);
+
   return {
     logs,
     refresh,
     moveCursor,
+    paginationInfo,
+    loading,
   };
 };

+ 16 - 16
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -75,47 +75,47 @@ class Templates extends Component<PropsType, StateType> {
           <path
             d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
           <path
             d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
           <path
-            fill-rule="evenodd"
-            clip-rule="evenodd"
+            fillRule="evenodd"
+            clipRule="evenodd"
             d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
           <path
             d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
           <path
             d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
           <path
-            fill-rule="evenodd"
-            clip-rule="evenodd"
+            fillRule="evenodd"
+            clipRule="evenodd"
             d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
             stroke="white"
-            stroke-width="1.5"
-            stroke-linecap="round"
+            strokeWidth="1.5"
+            strokeLinecap="round"
             stroke-linejoin="round"
           />
         </svg>

+ 22 - 22
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -170,48 +170,48 @@ export const ClusterSection: React.FC<Props> = ({
               <path
                 d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
               <path
                 d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
               <path
-                fill-rule="evenodd"
-                clip-rule="evenodd"
+                fillRule="evenodd"
+                clipRule="evenodd"
                 d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
               <path
                 d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
               <path
                 d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
               <path
-                fill-rule="evenodd"
-                clip-rule="evenodd"
+                fillRule="evenodd"
+                clipRule="evenodd"
                 d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
                 stroke="white"
-                stroke-width="1.5"
-                stroke-linecap="round"
-                stroke-linejoin="round"
+                strokeWidth="1.5"
+                strokeLinecap="round"
+                strokeLinejoin="round"
               />
             </svg>
           </ClusterIcon>

+ 8 - 0
dashboard/src/shared/util.ts

@@ -0,0 +1,8 @@
+export const isJSON = (value: string): boolean => {
+  try {
+    JSON.parse(value);
+    return true;
+  } catch (err) {
+    return false;
+  }
+};