Răsfoiți Sursa

Merge pull request #929 from porter-dev/0.7.0-hpa-metrics

[0.7.0] Add auto scaling threshold line to application metrics
abelanger5 4 ani în urmă
părinte
comite
34910165aa

+ 733 - 0
dashboard/package-lock.json

@@ -274,6 +274,99 @@
         "react-is": "^16.8.0 || ^17.0.0"
       }
     },
+    "@react-spring/animated": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz",
+      "integrity": "sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==",
+      "requires": {
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/core": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.2.4.tgz",
+      "integrity": "sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/konva": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.2.4.tgz",
+      "integrity": "sha512-19anDOIkfjcydDTfGgVIuZ3lruZxKubYGs9oHCswaP8SRLj7c1kkopJHUr/S4LXGxiIdqdF0XucWm0iTEPEq4w==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/native": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.2.4.tgz",
+      "integrity": "sha512-xKJWKh5qOhSclpL3iuGwJRLoZzTNvlBEnIrMs8yh8xvX6z9Lmnu4uGu5DpfrnM1GzBvRoktoCoLEx/VcEYFSng==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/rafz": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.2.4.tgz",
+      "integrity": "sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ=="
+    },
+    "@react-spring/shared": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.2.4.tgz",
+      "integrity": "sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==",
+      "requires": {
+        "@react-spring/rafz": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/three": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.2.4.tgz",
+      "integrity": "sha512-ljFig7XW099VWwRPKPUf+4yYLivp/sSWXN3oO5SJOF/9BSoV1quS/9chZ5Myl5J14od3CsHf89Tv4FdlX5kHlA==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/types": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.2.4.tgz",
+      "integrity": "sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA=="
+    },
+    "@react-spring/web": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.2.4.tgz",
+      "integrity": "sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
+    "@react-spring/zdog": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.2.4.tgz",
+      "integrity": "sha512-rv7ptedS37SHr6yuCbRkUErAzAhebdgt8f4KUtZWzseC+7qLNkaZWf+uujgsb881qAuX9b9yz8rre9UKeYepgw==",
+      "requires": {
+        "@react-spring/animated": "~9.2.0",
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/shared": "~9.2.0",
+        "@react-spring/types": "~9.2.0"
+      }
+    },
     "@sheerun/mutationobserver-shim": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
@@ -484,6 +577,11 @@
       "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.0.tgz",
       "integrity": "sha512-UpLg1mn/8PLyjr+J/JwdQJM/GzysMvv2CS8y+WYAL5K0+wbvXv/pPSLEfdNaprCZsGcXTxPsFMy8QtkYv9ueew=="
     },
+    "@types/d3-voronoi": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz",
+      "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ=="
+    },
     "@types/glob": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@@ -906,6 +1004,139 @@
       "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==",
       "dev": true
     },
+    "@visx/annotation": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/@visx/annotation/-/annotation-1.18.1.tgz",
+      "integrity": "sha512-z6zCk6PKmmeFziKKMBhJqJG4utg3dDWOCBZdR70HWSt9rl5cTMwIfATCuhJNbE2KuW6+QvTLiMCONySFwuVR+g==",
+      "requires": {
+        "@types/react": "*",
+        "@visx/drag": "1.18.1",
+        "@visx/group": "1.17.1",
+        "@visx/point": "1.7.0",
+        "@visx/shape": "1.17.1",
+        "@visx/text": "1.17.1",
+        "classnames": "^2.3.1",
+        "prop-types": "^15.5.10",
+        "react-use-measure": "^2.0.4"
+      },
+      "dependencies": {
+        "@types/d3-scale": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
+          "integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==",
+          "requires": {
+            "@types/d3-time": "^2"
+          }
+        },
+        "@types/d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg=="
+        },
+        "@visx/curve": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-1.7.0.tgz",
+          "integrity": "sha512-n0/SHM4YXjke+aEinhHFZPLMxWu3jbqtvqzfGJyibX8OmbDjavk9P+MHfGokUcw0xHy6Ch3YTuwbYuvVw5ny9A==",
+          "requires": {
+            "@types/d3-shape": "^1.3.1",
+            "d3-shape": "^1.0.6"
+          }
+        },
+        "@visx/group": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.17.1.tgz",
+          "integrity": "sha512-g8pSqy8TXAisiOzypnVycDynEGlBhfxtVlwDmsbYB+XSFGEjnOheQSDohDI+ia7ek54Mw9uYe05tx5kP1hRMYw==",
+          "requires": {
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "@visx/point": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.7.0.tgz",
+          "integrity": "sha512-oaoY/HXYHhmpkkeKI4rBPmFtjHWtxSrIhZCVm1ipPoyQp3voJ8L6JD5eUIVmmaUCdUGUGwL1lFLnJiQ2p1Vlwg=="
+        },
+        "@visx/scale": {
+          "version": "1.14.0",
+          "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-1.14.0.tgz",
+          "integrity": "sha512-ovbtEOF/d76uGMJ5UZlxdS3t2T8I6md+aIwOXBaq0HdjaCLbe7HLlMyHJKjak/sqBxLAiCGVnechTUpSkfgSQw==",
+          "requires": {
+            "@types/d3-interpolate": "^1.3.1",
+            "@types/d3-scale": "^3.3.0",
+            "@types/d3-time": "^2.0.0",
+            "d3-interpolate": "^1.4.0",
+            "d3-scale": "^3.3.0",
+            "d3-time": "^2.1.1"
+          }
+        },
+        "@visx/shape": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.17.1.tgz",
+          "integrity": "sha512-rVYFpytPCnV4s5U0za+jQ2jqFzKnmB3c8RP6fuOfF6kKosFPJcOYg9ikvewojARyMBTr1u3XvWV960Da+xyUdQ==",
+          "requires": {
+            "@types/d3-path": "^1.0.8",
+            "@types/d3-shape": "^1.3.1",
+            "@types/lodash": "^4.14.146",
+            "@types/react": "*",
+            "@visx/curve": "1.7.0",
+            "@visx/group": "1.17.1",
+            "@visx/scale": "1.14.0",
+            "classnames": "^2.3.1",
+            "d3-path": "^1.0.5",
+            "d3-shape": "^1.2.0",
+            "lodash": "^4.17.15",
+            "prop-types": "^15.5.10"
+          }
+        },
+        "@visx/text": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.17.1.tgz",
+          "integrity": "sha512-Cx6iH0kVq3YqCfFj7U6bMiKwa/bz4Z3q0vPdxmnVGcPjGZM1ac/y61KFH263e164LJ5jFaTYpPrrFmbZoy8+Vg==",
+          "requires": {
+            "@types/lodash": "^4.14.160",
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "lodash": "^4.17.20",
+            "prop-types": "^15.7.2",
+            "reduce-css-calc": "^1.3.0"
+          }
+        },
+        "classnames": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+          "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+        },
+        "d3-scale": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
+          "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
+          "requires": {
+            "d3-array": "^2.3.0",
+            "d3-format": "1 - 2",
+            "d3-interpolate": "1.2.0 - 2",
+            "d3-time": "^2.1.1",
+            "d3-time-format": "2 - 3"
+          }
+        },
+        "d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
+          "requires": {
+            "d3-array": "2"
+          }
+        },
+        "react-use-measure": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.4.tgz",
+          "integrity": "sha512-7K2HIGaPMl3Q9ZQiEVjen3tRXl4UDda8LiTPy/QxP8dP2rl5gPBhf7mMH6MVjjRNv3loU7sNzey/ycPNnHVTxQ==",
+          "requires": {
+            "debounce": "^1.2.0"
+          }
+        }
+      }
+    },
     "@visx/axis": {
       "version": "1.6.1",
       "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.6.1.tgz",
@@ -1001,6 +1232,32 @@
         "d3-shape": "^1.0.6"
       }
     },
+    "@visx/drag": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/@visx/drag/-/drag-1.18.1.tgz",
+      "integrity": "sha512-5xsgUUthG/0Nq51HFWYIe3NaHT5csxuVqx/+VfNsjkGgCHntWkcS2soPlEMh4wUT2iPKRs9z9VtRAyrpN4TtKw==",
+      "requires": {
+        "@types/react": "*",
+        "@visx/event": "1.7.0",
+        "prop-types": "^15.5.10"
+      },
+      "dependencies": {
+        "@visx/event": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/event/-/event-1.7.0.tgz",
+          "integrity": "sha512-RbAoKxvy+ildX2dVXC9/ZX94lQXPwjKgtO9jy7COc15knG4zmzsMCDYDC3uLd0+jE2o/+gSaZ/9r52p6zG5+IQ==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/point": "1.7.0"
+          }
+        },
+        "@visx/point": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.7.0.tgz",
+          "integrity": "sha512-oaoY/HXYHhmpkkeKI4rBPmFtjHWtxSrIhZCVm1ipPoyQp3voJ8L6JD5eUIVmmaUCdUGUGwL1lFLnJiQ2p1Vlwg=="
+        }
+      }
+    },
     "@visx/event": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@visx/event/-/event-1.3.0.tgz",
@@ -1010,6 +1267,36 @@
         "@visx/point": "1.0.0"
       }
     },
+    "@visx/glyph": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/@visx/glyph/-/glyph-1.17.1.tgz",
+      "integrity": "sha512-9KAPmO7DsH1Iq+2kZs8oTgirgYWRq7EacNyEtsq78uuHqw0gFqmOuyYV6+iHelLbulNCAzHKVAZ7Aebfy7G8ZA==",
+      "requires": {
+        "@types/d3-shape": "^1.3.1",
+        "@types/react": "*",
+        "@visx/group": "1.17.1",
+        "classnames": "^2.3.1",
+        "d3-shape": "^1.2.0",
+        "prop-types": "^15.6.2"
+      },
+      "dependencies": {
+        "@visx/group": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.17.1.tgz",
+          "integrity": "sha512-g8pSqy8TXAisiOzypnVycDynEGlBhfxtVlwDmsbYB+XSFGEjnOheQSDohDI+ia7ek54Mw9uYe05tx5kP1hRMYw==",
+          "requires": {
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "classnames": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+          "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+        }
+      }
+    },
     "@visx/gradient": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@visx/gradient/-/gradient-1.0.0.tgz",
@@ -1059,6 +1346,159 @@
       "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.0.0.tgz",
       "integrity": "sha512-0L3ILwv6ro0DsQVbA1lo8fo6q3wvIeSTt9C8NarUUkoTNSFZaJtlmvwg2238r8fwwmSv0v9QFBj1hBz4o0bHrg=="
     },
+    "@visx/react-spring": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/@visx/react-spring/-/react-spring-1.17.1.tgz",
+      "integrity": "sha512-U2+cXYpmuwN8/TNNJAiqAcXHPewKbb2vT+YmDmkIk9G20INO95EKJbHE5+homf+Jg7mRr5En31Orxjt6eIKgzA==",
+      "requires": {
+        "@types/react": "*",
+        "@visx/axis": "1.17.1",
+        "@visx/grid": "1.17.1",
+        "@visx/scale": "1.14.0",
+        "@visx/text": "1.17.1",
+        "classnames": "^2.3.1",
+        "prop-types": "^15.6.2"
+      },
+      "dependencies": {
+        "@types/d3-scale": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
+          "integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==",
+          "requires": {
+            "@types/d3-time": "^2"
+          }
+        },
+        "@types/d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg=="
+        },
+        "@visx/axis": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.17.1.tgz",
+          "integrity": "sha512-3JdAY8xwA4xVnzkbXdIzCOWYCknCgw3L185lOJTXWNGO7kIgzbQ2YrLXnet37BFgD83MfxmlP6LhiHLkKVI6OQ==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/group": "1.17.1",
+            "@visx/point": "1.7.0",
+            "@visx/scale": "1.14.0",
+            "@visx/shape": "1.17.1",
+            "@visx/text": "1.17.1",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.0"
+          }
+        },
+        "@visx/curve": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-1.7.0.tgz",
+          "integrity": "sha512-n0/SHM4YXjke+aEinhHFZPLMxWu3jbqtvqzfGJyibX8OmbDjavk9P+MHfGokUcw0xHy6Ch3YTuwbYuvVw5ny9A==",
+          "requires": {
+            "@types/d3-shape": "^1.3.1",
+            "d3-shape": "^1.0.6"
+          }
+        },
+        "@visx/grid": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-1.17.1.tgz",
+          "integrity": "sha512-dse9q3weDqPNmeXK0lGKKPRgGiDuUjJ7Mt7NNonPUyXPctNmv6lJEWZu9HJrXEGiCAVNa8PHJ7Qkns/z+mH88Q==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/curve": "1.7.0",
+            "@visx/group": "1.17.1",
+            "@visx/point": "1.7.0",
+            "@visx/scale": "1.14.0",
+            "@visx/shape": "1.17.1",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "@visx/group": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.17.1.tgz",
+          "integrity": "sha512-g8pSqy8TXAisiOzypnVycDynEGlBhfxtVlwDmsbYB+XSFGEjnOheQSDohDI+ia7ek54Mw9uYe05tx5kP1hRMYw==",
+          "requires": {
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "@visx/point": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.7.0.tgz",
+          "integrity": "sha512-oaoY/HXYHhmpkkeKI4rBPmFtjHWtxSrIhZCVm1ipPoyQp3voJ8L6JD5eUIVmmaUCdUGUGwL1lFLnJiQ2p1Vlwg=="
+        },
+        "@visx/scale": {
+          "version": "1.14.0",
+          "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-1.14.0.tgz",
+          "integrity": "sha512-ovbtEOF/d76uGMJ5UZlxdS3t2T8I6md+aIwOXBaq0HdjaCLbe7HLlMyHJKjak/sqBxLAiCGVnechTUpSkfgSQw==",
+          "requires": {
+            "@types/d3-interpolate": "^1.3.1",
+            "@types/d3-scale": "^3.3.0",
+            "@types/d3-time": "^2.0.0",
+            "d3-interpolate": "^1.4.0",
+            "d3-scale": "^3.3.0",
+            "d3-time": "^2.1.1"
+          }
+        },
+        "@visx/shape": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.17.1.tgz",
+          "integrity": "sha512-rVYFpytPCnV4s5U0za+jQ2jqFzKnmB3c8RP6fuOfF6kKosFPJcOYg9ikvewojARyMBTr1u3XvWV960Da+xyUdQ==",
+          "requires": {
+            "@types/d3-path": "^1.0.8",
+            "@types/d3-shape": "^1.3.1",
+            "@types/lodash": "^4.14.146",
+            "@types/react": "*",
+            "@visx/curve": "1.7.0",
+            "@visx/group": "1.17.1",
+            "@visx/scale": "1.14.0",
+            "classnames": "^2.3.1",
+            "d3-path": "^1.0.5",
+            "d3-shape": "^1.2.0",
+            "lodash": "^4.17.15",
+            "prop-types": "^15.5.10"
+          }
+        },
+        "@visx/text": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.17.1.tgz",
+          "integrity": "sha512-Cx6iH0kVq3YqCfFj7U6bMiKwa/bz4Z3q0vPdxmnVGcPjGZM1ac/y61KFH263e164LJ5jFaTYpPrrFmbZoy8+Vg==",
+          "requires": {
+            "@types/lodash": "^4.14.160",
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "lodash": "^4.17.20",
+            "prop-types": "^15.7.2",
+            "reduce-css-calc": "^1.3.0"
+          }
+        },
+        "classnames": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+          "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+        },
+        "d3-scale": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
+          "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
+          "requires": {
+            "d3-array": "^2.3.0",
+            "d3-format": "1 - 2",
+            "d3-interpolate": "1.2.0 - 2",
+            "d3-time": "^2.1.1",
+            "d3-time-format": "2 - 3"
+          }
+        },
+        "d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
+          "requires": {
+            "d3-array": "2"
+          }
+        }
+      }
+    },
     "@visx/responsive": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-1.3.0.tgz",
@@ -1131,6 +1571,271 @@
         "react-use-measure": "2.0.1"
       }
     },
+    "@visx/voronoi": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/@visx/voronoi/-/voronoi-1.17.1.tgz",
+      "integrity": "sha512-XpgQ5siRYI9Vvw+Q82avIntDzfkSrIT3EmN2J/L/6ZnT3nTCjWksTEgQQ3G9GqoX510srbX8wL+mRqkYP+3O4Q==",
+      "requires": {
+        "@types/d3-voronoi": "^1.1.9",
+        "@types/react": "*",
+        "classnames": "^2.3.1",
+        "d3-voronoi": "^1.1.2",
+        "prop-types": "^15.6.1"
+      },
+      "dependencies": {
+        "classnames": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+          "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+        }
+      }
+    },
+    "@visx/xychart": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/@visx/xychart/-/xychart-1.18.1.tgz",
+      "integrity": "sha512-VyC7yBpLUSgzLZIWWRtf1pCP0xuYLjHEwAXl2LdCSrnyUs2pMTms7xMUNbq8uwR4e8bbBY5dbsRg/7X95tB8Yg==",
+      "requires": {
+        "@types/lodash": "^4.14.146",
+        "@types/react": "*",
+        "@visx/annotation": "1.18.1",
+        "@visx/axis": "1.17.1",
+        "@visx/event": "1.7.0",
+        "@visx/glyph": "1.17.1",
+        "@visx/grid": "1.17.1",
+        "@visx/react-spring": "1.17.1",
+        "@visx/responsive": "1.10.1",
+        "@visx/scale": "1.14.0",
+        "@visx/shape": "1.17.1",
+        "@visx/text": "1.17.1",
+        "@visx/tooltip": "1.17.1",
+        "@visx/voronoi": "1.17.1",
+        "classnames": "^2.3.1",
+        "d3-array": "^2.6.0",
+        "d3-interpolate-path": "2.2.1",
+        "d3-shape": "^2.0.0",
+        "lodash": "^4.17.10",
+        "mitt": "^2.1.0",
+        "prop-types": "^15.6.2"
+      },
+      "dependencies": {
+        "@types/d3-scale": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
+          "integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==",
+          "requires": {
+            "@types/d3-time": "^2"
+          }
+        },
+        "@types/d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg=="
+        },
+        "@visx/axis": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.17.1.tgz",
+          "integrity": "sha512-3JdAY8xwA4xVnzkbXdIzCOWYCknCgw3L185lOJTXWNGO7kIgzbQ2YrLXnet37BFgD83MfxmlP6LhiHLkKVI6OQ==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/group": "1.17.1",
+            "@visx/point": "1.7.0",
+            "@visx/scale": "1.14.0",
+            "@visx/shape": "1.17.1",
+            "@visx/text": "1.17.1",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.0"
+          }
+        },
+        "@visx/bounds": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.7.0.tgz",
+          "integrity": "sha512-ajF6PTgDoZTfwv5J0ZTx1miXY8lk3sGhMVqE3UsMubdTZBlOgeZMT4OmtTPtbCJTBTgw0FD0gd7X3gZ+3X9HgQ==",
+          "requires": {
+            "@types/react": "*",
+            "@types/react-dom": "*",
+            "prop-types": "^15.5.10"
+          }
+        },
+        "@visx/curve": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-1.7.0.tgz",
+          "integrity": "sha512-n0/SHM4YXjke+aEinhHFZPLMxWu3jbqtvqzfGJyibX8OmbDjavk9P+MHfGokUcw0xHy6Ch3YTuwbYuvVw5ny9A==",
+          "requires": {
+            "@types/d3-shape": "^1.3.1",
+            "d3-shape": "^1.0.6"
+          },
+          "dependencies": {
+            "d3-shape": {
+              "version": "1.3.7",
+              "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+              "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+              "requires": {
+                "d3-path": "1"
+              }
+            }
+          }
+        },
+        "@visx/event": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/event/-/event-1.7.0.tgz",
+          "integrity": "sha512-RbAoKxvy+ildX2dVXC9/ZX94lQXPwjKgtO9jy7COc15knG4zmzsMCDYDC3uLd0+jE2o/+gSaZ/9r52p6zG5+IQ==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/point": "1.7.0"
+          }
+        },
+        "@visx/grid": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-1.17.1.tgz",
+          "integrity": "sha512-dse9q3weDqPNmeXK0lGKKPRgGiDuUjJ7Mt7NNonPUyXPctNmv6lJEWZu9HJrXEGiCAVNa8PHJ7Qkns/z+mH88Q==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/curve": "1.7.0",
+            "@visx/group": "1.17.1",
+            "@visx/point": "1.7.0",
+            "@visx/scale": "1.14.0",
+            "@visx/shape": "1.17.1",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "@visx/group": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.17.1.tgz",
+          "integrity": "sha512-g8pSqy8TXAisiOzypnVycDynEGlBhfxtVlwDmsbYB+XSFGEjnOheQSDohDI+ia7ek54Mw9uYe05tx5kP1hRMYw==",
+          "requires": {
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.6.2"
+          }
+        },
+        "@visx/point": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.7.0.tgz",
+          "integrity": "sha512-oaoY/HXYHhmpkkeKI4rBPmFtjHWtxSrIhZCVm1ipPoyQp3voJ8L6JD5eUIVmmaUCdUGUGwL1lFLnJiQ2p1Vlwg=="
+        },
+        "@visx/responsive": {
+          "version": "1.10.1",
+          "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-1.10.1.tgz",
+          "integrity": "sha512-7FT2BBmWFkFFqynI9C1NYfVOKT1FsNOm6MwWMqXKA7TMomdBW0wdtQNB1bHvwJvWurM/sNqxcQ/CBED6t9xujQ==",
+          "requires": {
+            "@types/lodash": "^4.14.146",
+            "@types/react": "*",
+            "lodash": "^4.17.10",
+            "prop-types": "^15.6.1",
+            "resize-observer-polyfill": "1.5.1"
+          }
+        },
+        "@visx/scale": {
+          "version": "1.14.0",
+          "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-1.14.0.tgz",
+          "integrity": "sha512-ovbtEOF/d76uGMJ5UZlxdS3t2T8I6md+aIwOXBaq0HdjaCLbe7HLlMyHJKjak/sqBxLAiCGVnechTUpSkfgSQw==",
+          "requires": {
+            "@types/d3-interpolate": "^1.3.1",
+            "@types/d3-scale": "^3.3.0",
+            "@types/d3-time": "^2.0.0",
+            "d3-interpolate": "^1.4.0",
+            "d3-scale": "^3.3.0",
+            "d3-time": "^2.1.1"
+          }
+        },
+        "@visx/shape": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.17.1.tgz",
+          "integrity": "sha512-rVYFpytPCnV4s5U0za+jQ2jqFzKnmB3c8RP6fuOfF6kKosFPJcOYg9ikvewojARyMBTr1u3XvWV960Da+xyUdQ==",
+          "requires": {
+            "@types/d3-path": "^1.0.8",
+            "@types/d3-shape": "^1.3.1",
+            "@types/lodash": "^4.14.146",
+            "@types/react": "*",
+            "@visx/curve": "1.7.0",
+            "@visx/group": "1.17.1",
+            "@visx/scale": "1.14.0",
+            "classnames": "^2.3.1",
+            "d3-path": "^1.0.5",
+            "d3-shape": "^1.2.0",
+            "lodash": "^4.17.15",
+            "prop-types": "^15.5.10"
+          },
+          "dependencies": {
+            "d3-shape": {
+              "version": "1.3.7",
+              "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+              "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+              "requires": {
+                "d3-path": "1"
+              }
+            }
+          }
+        },
+        "@visx/text": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.17.1.tgz",
+          "integrity": "sha512-Cx6iH0kVq3YqCfFj7U6bMiKwa/bz4Z3q0vPdxmnVGcPjGZM1ac/y61KFH263e164LJ5jFaTYpPrrFmbZoy8+Vg==",
+          "requires": {
+            "@types/lodash": "^4.14.160",
+            "@types/react": "*",
+            "classnames": "^2.3.1",
+            "lodash": "^4.17.20",
+            "prop-types": "^15.7.2",
+            "reduce-css-calc": "^1.3.0"
+          }
+        },
+        "@visx/tooltip": {
+          "version": "1.17.1",
+          "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.17.1.tgz",
+          "integrity": "sha512-YfRgVtKSLTn3iW8CT5+CfTWhSXGeAp01SaPDThtdaUTx89rKv5wb4oyVgeQ5g2ScRYVC8mYj5RzY/pj3RrezFQ==",
+          "requires": {
+            "@types/react": "*",
+            "@visx/bounds": "1.7.0",
+            "classnames": "^2.3.1",
+            "prop-types": "^15.5.10",
+            "react-use-measure": "^2.0.4"
+          }
+        },
+        "classnames": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+          "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+        },
+        "d3-scale": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
+          "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
+          "requires": {
+            "d3-array": "^2.3.0",
+            "d3-format": "1 - 2",
+            "d3-interpolate": "1.2.0 - 2",
+            "d3-time": "^2.1.1",
+            "d3-time-format": "2 - 3"
+          }
+        },
+        "d3-shape": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
+          "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
+          "requires": {
+            "d3-path": "1 - 2"
+          }
+        },
+        "d3-time": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
+          "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
+          "requires": {
+            "d3-array": "2"
+          }
+        },
+        "react-use-measure": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.4.tgz",
+          "integrity": "sha512-7K2HIGaPMl3Q9ZQiEVjen3tRXl4UDda8LiTPy/QxP8dP2rl5gPBhf7mMH6MVjjRNv3loU7sNzey/ycPNnHVTxQ==",
+          "requires": {
+            "debounce": "^1.2.0"
+          }
+        }
+      }
+    },
     "@webassemblyjs/ast": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@@ -2582,6 +3287,11 @@
         "d3-color": "1"
       }
     },
+    "d3-interpolate-path": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate-path/-/d3-interpolate-path-2.2.1.tgz",
+      "integrity": "sha512-6qLLh/KJVzls0XtMsMpcxhqMhgVEN7VIbR/6YGZe2qlS8KDgyyVB20XcmGnDyB051HcefQXM/Tppa9vcANEA4Q=="
+    },
     "d3-path": {
       "version": "1.0.9",
       "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
@@ -2625,6 +3335,11 @@
         "d3-time": "1 - 2"
       }
     },
+    "d3-voronoi": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
+      "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
+    },
     "debounce": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
@@ -5260,6 +5975,11 @@
         "through2": "^2.0.0"
       }
     },
+    "mitt": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
+      "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg=="
+    },
     "mixin-deep": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@@ -6291,6 +7011,19 @@
         "tiny-warning": "^1.0.0"
       }
     },
+    "react-spring": {
+      "version": "9.2.4",
+      "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-9.2.4.tgz",
+      "integrity": "sha512-bMjbyTW0ZGd+/h9cjtohLqCwOGqX2OuaTvalOVfLCGmhzEg/u3GgopI3LAm4UD2Br3MNdVdGgNVoESg4MGqKFQ==",
+      "requires": {
+        "@react-spring/core": "~9.2.0",
+        "@react-spring/konva": "~9.2.0",
+        "@react-spring/native": "~9.2.0",
+        "@react-spring/three": "~9.2.0",
+        "@react-spring/web": "~9.2.0",
+        "@react-spring/zdog": "~9.2.0"
+      }
+    },
     "react-table": {
       "version": "7.7.0",
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz",

+ 325 - 235
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx

@@ -1,48 +1,30 @@
-import React, { useMemo, useCallback } from "react";
-import { AreaClosed, Line, Bar } from "@visx/shape";
+import React, { useMemo, useCallback, useRef } from "react";
+import { AreaClosed, Line, Bar, LinePath } from "@visx/shape";
 import { curveMonotoneX } from "@visx/curve";
 import { scaleTime, scaleLinear } from "@visx/scale";
 import { AxisLeft, AxisBottom } from "@visx/axis";
 
 import {
-  withTooltip,
   Tooltip,
   TooltipWithBounds,
   defaultStyles,
+  useTooltip,
 } from "@visx/tooltip";
 
 import { GridRows, GridColumns } from "@visx/grid";
 
-import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
 import { localPoint } from "@visx/event";
 import { LinearGradient } from "@visx/gradient";
 import { max, extent, bisector } from "d3-array";
 import { timeFormat } from "d3-time-format";
+import { NormalizedMetricsData } from "./types";
 
-/*
-export const accentColor = '#f5cb42';
-export const accentColorDark = '#949eff';
-*/
-
-export type MetricsData = {
-  date: number; // unix timestamp
-  value: number; // value
-};
-
-type TooltipData = MetricsData;
-
-var globalData: MetricsData[];
+var globalData: NormalizedMetricsData[];
 
 export const background = "#3b697800";
 export const background2 = "#20405100";
 export const accentColor = "#949eff";
 export const accentColorDark = "#949eff";
-const tooltipStyles = {
-  ...defaultStyles,
-  background,
-  border: "1px solid white",
-  color: "white",
-};
 
 // util
 const formatDate = timeFormat("%H:%M:%S %b %d, '%y");
@@ -59,243 +41,351 @@ const formats: { [range: string]: (date: Date) => string } = {
 };
 
 // accessors
-const getDate = (d: MetricsData) => new Date(d.date * 1000);
-const getValue = (d: MetricsData) => d.value;
+const getDate = (d: NormalizedMetricsData) => new Date(d.date * 1000);
+const getValue = (d: NormalizedMetricsData) =>
+  d?.value && Number(d.value?.toFixed(4));
 
-const bisectDate = bisector<MetricsData, Date>((d) => new Date(d.date * 1000))
-  .left;
+const bisectDate = bisector<NormalizedMetricsData, Date>(
+  (d) => new Date(d.date * 1000)
+).left;
 
 export type AreaProps = {
-  data: MetricsData[];
+  data: NormalizedMetricsData[];
+  dataKey: string;
+  hpaEnabled?: boolean;
+  hpaData?: NormalizedMetricsData[];
   resolution: string;
   width: number;
   height: number;
   margin?: { top: number; right: number; bottom: number; left: number };
 };
 
-export default withTooltip<AreaProps, TooltipData>(
-  ({
-    data,
-    resolution,
-    width,
-    height,
-    margin = { top: 0, right: 0, bottom: 0, left: 0 },
+const AreaChart: React.FunctionComponent<AreaProps> = ({
+  data,
+  dataKey,
+  hpaEnabled = false,
+  hpaData = [],
+  resolution,
+  width,
+  height,
+  margin = { top: 0, right: 0, bottom: 0, left: 0 },
+}) => {
+  globalData = data;
+
+  const {
     showTooltip,
     hideTooltip,
     tooltipData,
-    tooltipTop = 0,
-    tooltipLeft = 0,
-  }: AreaProps & WithTooltipProvidedProps<TooltipData>) => {
-    globalData = data;
+    tooltipTop,
+    tooltipLeft,
+  } = useTooltip<{
+    data: NormalizedMetricsData;
+    tooltipHpaData: NormalizedMetricsData;
+  }>();
 
-    if (width == 0 || height == 0 || width < 10) {
-      return null;
-    }
+  const svgContainer = useRef();
+  // bounds
+  const innerWidth = width - margin.left - margin.right - 40;
+  const innerHeight = height - margin.top - margin.bottom - 20;
+  const isHpaEnabled = hpaEnabled && !!hpaData.length;
 
-    // bounds
-    const innerWidth = width - margin.left - margin.right - 40;
-    const innerHeight = height - margin.top - margin.bottom - 20;
+  // scales
+  const dateScale = useMemo(
+    () =>
+      scaleTime({
+        range: [margin.left, innerWidth + margin.left],
+        domain: extent(
+          [...globalData, ...(isHpaEnabled ? hpaData : [])],
+          getDate
+        ) as [Date, Date],
+      }),
+    [margin.left, width, height, data, hpaData, isHpaEnabled]
+  );
+  const valueScale = useMemo(
+    () =>
+      scaleLinear({
+        range: [innerHeight + margin.top, margin.top],
+        domain: [
+          0,
+          1.25 *
+            max([...globalData, ...(isHpaEnabled ? hpaData : [])], getValue),
+        ],
+        nice: true,
+      }),
+    [margin.top, width, height, data, hpaData, isHpaEnabled]
+  );
 
-    // scales
-    const dateScale = useMemo(
-      () =>
-        scaleTime({
-          range: [margin.left, innerWidth + margin.left],
-          domain: extent(globalData, getDate) as [Date, Date],
-        }),
-      [innerWidth, margin.left, width, height, data]
-    );
-    const valueScale = useMemo(
-      () =>
-        scaleLinear({
-          range: [innerHeight + margin.top, margin.top],
-          domain: [0, 1.25 * max(globalData, getValue)],
-          nice: true,
-        }),
-      [margin.top, innerHeight, width, height, data]
-    );
+  // tooltip handler
+  const handleTooltip = useCallback(
+    (
+      event: React.TouchEvent<SVGRectElement> | React.MouseEvent<SVGRectElement>
+    ) => {
+      const isHpaEnabled = hpaEnabled && !!hpaData.length;
 
-    // tooltip handler
-    const handleTooltip = useCallback(
-      (
-        event:
-          | React.TouchEvent<SVGRectElement>
-          | React.MouseEvent<SVGRectElement>
-      ) => {
-        const { x } = localPoint(event) || { x: 0 };
-        const x0 = dateScale.invert(x);
-        const index = bisectDate(globalData, x0, 1);
-        const d0 = globalData[index - 1];
-        const d1 = globalData[index];
-        let d = d0;
+      const { x } = localPoint(event) || { x: 0 };
+      const x0 = dateScale.invert(x);
 
-        if (d1 && getDate(d1)) {
-          d =
-            x0.valueOf() - getDate(d0).valueOf() >
-            getDate(d1).valueOf() - x0.valueOf()
-              ? d1
-              : d0;
-        }
+      const index = bisectDate(globalData, x0, 1);
+      const d0 = globalData[index - 1];
+      const d1 = globalData[index];
+      let d = d0;
 
+      if (d1 && getDate(d1)) {
+        d =
+          x0.valueOf() - getDate(d0).valueOf() >
+          getDate(d1).valueOf() - x0.valueOf()
+            ? d1
+            : d0;
+      }
+
+      if (!isHpaEnabled) {
         showTooltip({
-          tooltipData: d,
+          tooltipData: { data: d, tooltipHpaData: undefined },
           tooltipLeft: x || 0,
           tooltipTop: valueScale(getValue(d)) || 0,
         });
-      },
-      [showTooltip, valueScale, dateScale, width, height, data]
-    );
+        return;
+      }
 
-    return (
-      <div>
-        <svg width={width} height={height}>
-          <rect
-            x={0}
-            y={0}
-            width={width}
-            height={height}
-            fill="url(#area-background-gradient)"
-            rx={14}
-          />
-          <LinearGradient
-            id="area-background-gradient"
-            from={background}
-            to={background2}
-          />
-          <LinearGradient
-            id="area-gradient"
-            from={accentColor}
-            to={accentColor}
-            toOpacity={0}
-          />
-          <GridRows
-            left={margin.left}
-            scale={valueScale}
-            width={innerWidth}
-            strokeDasharray="1,3"
-            stroke="white"
-            strokeOpacity={0.2}
-            pointerEvents="none"
-          />
-          <GridColumns
-            top={margin.top}
-            scale={dateScale}
-            height={innerHeight}
-            strokeDasharray="1,3"
-            stroke="white"
-            strokeOpacity={0.2}
-            pointerEvents="none"
-          />
-          <AreaClosed<MetricsData>
-            data={data}
+      const tooltipHpaData0 = hpaData[index - 1];
+      const tooltipHpaData1 = hpaData[index];
+      let tooltipHpaData = tooltipHpaData0;
+
+      if (tooltipHpaData1 && getDate(tooltipHpaData1)) {
+        tooltipHpaData =
+          x0.valueOf() - getDate(tooltipHpaData0).valueOf() >
+          getDate(tooltipHpaData1).valueOf() - x0.valueOf()
+            ? tooltipHpaData1
+            : tooltipHpaData0;
+      }
+
+      const container: SVGSVGElement = svgContainer.current;
+
+      let point = container.createSVGPoint();
+      // @ts-ignore
+      point.x = (event as any)?.clientX || 0;
+      // @ts-ignore
+      point.y = (event as any)?.clientY || 0;
+      point = point?.matrixTransform(container.getScreenCTM().inverse());
+
+      showTooltip({
+        tooltipData: { data: d, tooltipHpaData },
+        tooltipLeft: x || 0,
+        tooltipTop: point.y || 0,
+      });
+    },
+    [
+      showTooltip,
+      valueScale,
+      dateScale,
+      width,
+      height,
+      data,
+      hpaData,
+      svgContainer,
+      hpaEnabled,
+    ]
+  );
+
+  if (width == 0 || height == 0 || width < 10) {
+    return null;
+  }
+  const hpaGraphTooltipGlyphPosition =
+    (hpaEnabled &&
+      tooltipData?.tooltipHpaData &&
+      valueScale(getValue(tooltipData?.tooltipHpaData))) ||
+    0;
+
+  const dataGraphTooltipGlyphPosition =
+    (tooltipData?.data && valueScale(getValue(tooltipData.data))) || 0;
+
+  return (
+    <div>
+      <svg width={width} height={height} ref={svgContainer}>
+        <rect
+          x={0}
+          y={0}
+          width={width}
+          height={height}
+          fill="url(#area-background-gradient)"
+          rx={14}
+        />
+
+        <LinearGradient
+          id="area-background-gradient"
+          from={background}
+          to={background2}
+        />
+        <LinearGradient
+          id="area-gradient"
+          from={accentColor}
+          to={accentColor}
+          toOpacity={0}
+        />
+        <GridRows
+          left={margin.left}
+          scale={valueScale}
+          width={innerWidth}
+          strokeDasharray="1,3"
+          stroke="white"
+          strokeOpacity={0.2}
+          pointerEvents="none"
+        />
+        <GridColumns
+          top={margin.top}
+          scale={dateScale}
+          height={innerHeight}
+          strokeDasharray="1,3"
+          stroke="white"
+          strokeOpacity={0.2}
+          pointerEvents="none"
+        />
+        <AreaClosed<NormalizedMetricsData>
+          data={data}
+          x={(d) => dateScale(getDate(d)) ?? 0}
+          y={(d) => valueScale(getValue(d)) ?? 0}
+          height={innerHeight}
+          yScale={valueScale}
+          strokeWidth={1}
+          stroke="url(#area-gradient)"
+          fill="url(#area-gradient)"
+          curve={curveMonotoneX}
+        />
+        {isHpaEnabled && (
+          <LinePath<NormalizedMetricsData>
+            stroke="#ffffff"
+            strokeWidth={2}
+            data={hpaData}
             x={(d) => dateScale(getDate(d)) ?? 0}
             y={(d) => valueScale(getValue(d)) ?? 0}
-            height={innerHeight}
-            yScale={valueScale}
-            strokeWidth={1}
-            stroke="url(#area-gradient)"
-            fill="url(#area-gradient)"
-            curve={curveMonotoneX}
-          />
-          <AxisLeft
-            left={10}
-            scale={valueScale}
-            hideAxisLine={true}
-            hideTicks={true}
-            tickLabelProps={() => ({
-              fill: "white",
-              fontSize: 11,
-              textAnchor: "start",
-              fillOpacity: 0.4,
-              dy: 0,
-            })}
-          />
-          <AxisBottom
-            top={height - 20}
-            scale={dateScale}
-            tickFormat={formats[resolution]}
-            hideAxisLine={true}
-            hideTicks={true}
-            tickLabelProps={() => ({
-              fill: "white",
-              fontSize: 11,
-              textAnchor: "middle",
-              fillOpacity: 0.4,
-            })}
-          />
-          <Bar
-            x={margin.left}
-            y={margin.top}
-            width={innerWidth}
-            height={innerHeight}
-            fill="transparent"
-            rx={14}
-            onTouchStart={handleTooltip}
-            onTouchMove={handleTooltip}
-            onMouseMove={handleTooltip}
-            onMouseLeave={() => hideTooltip()}
+            strokeDasharray="6,4"
+            strokeOpacity={1}
+            pointerEvents="none"
           />
-          {tooltipData && (
-            <g>
-              <Line
-                from={{ x: tooltipLeft, y: margin.top }}
-                to={{ x: tooltipLeft, y: innerHeight + margin.top }}
-                stroke={accentColorDark}
-                strokeWidth={2}
-                pointerEvents="none"
-                strokeDasharray="5,2"
-              />
-              <circle
-                cx={tooltipLeft}
-                cy={tooltipTop + 1}
-                r={4}
-                fill="black"
-                fillOpacity={0.1}
-                stroke="black"
-                strokeOpacity={0.1}
-                strokeWidth={2}
-                pointerEvents="none"
-              />
-              <circle
-                cx={tooltipLeft}
-                cy={tooltipTop}
-                r={4}
-                fill={accentColorDark}
-                stroke="white"
-                strokeWidth={2}
-                pointerEvents="none"
-              />
-            </g>
-          )}
-        </svg>
+        )}
+
+        <AxisLeft
+          left={10}
+          scale={valueScale}
+          hideAxisLine={true}
+          hideTicks={true}
+          tickLabelProps={() => ({
+            fill: "white",
+            fontSize: 11,
+            textAnchor: "start",
+            fillOpacity: 0.4,
+            dy: 0,
+          })}
+        />
+        <AxisBottom
+          top={height - 20}
+          scale={dateScale}
+          tickFormat={formats[resolution]}
+          hideAxisLine={true}
+          hideTicks={true}
+          tickLabelProps={() => ({
+            fill: "white",
+            fontSize: 11,
+            textAnchor: "middle",
+            fillOpacity: 0.4,
+          })}
+        />
+        <Bar
+          x={margin.left}
+          y={margin.top}
+          width={innerWidth}
+          height={innerHeight}
+          fill="transparent"
+          rx={14}
+          onTouchStart={handleTooltip}
+          onTouchMove={handleTooltip}
+          onMouseMove={handleTooltip}
+          onMouseLeave={() => hideTooltip()}
+        />
         {tooltipData && (
-          <div>
-            <TooltipWithBounds
-              key={Math.random()}
-              top={tooltipTop - 12}
-              left={tooltipLeft + 12}
-              style={tooltipStyles}
-            >
-              {getValue(tooltipData)}
-            </TooltipWithBounds>
-            <Tooltip
-              top={-10}
-              left={tooltipLeft}
-              style={{
-                ...defaultStyles,
-                background: "#26272f",
-                color: "#aaaabb",
-                width: 100,
-                paddingTop: 35,
-                textAlign: "center",
-                transform: "translateX(-60px)",
-              }}
-            >
-              {formatDate(getDate(tooltipData))}
-            </Tooltip>
-          </div>
+          <g>
+            <Line
+              from={{ x: tooltipLeft, y: margin.top }}
+              to={{ x: tooltipLeft, y: innerHeight + margin.top }}
+              stroke={accentColorDark}
+              strokeWidth={2}
+              pointerEvents="none"
+              strokeDasharray="5,2"
+            />
+            <circle
+              cx={tooltipLeft}
+              cy={dataGraphTooltipGlyphPosition + 1}
+              r={4}
+              fill="black"
+              fillOpacity={0.1}
+              stroke="black"
+              strokeOpacity={0.1}
+              strokeWidth={2}
+              pointerEvents="none"
+            />
+            <circle
+              cx={tooltipLeft}
+              cy={dataGraphTooltipGlyphPosition}
+              r={4}
+              fill={accentColorDark}
+              stroke="white"
+              strokeWidth={2}
+              pointerEvents="none"
+            />
+            {isHpaEnabled && (
+              <>
+                <circle
+                  cx={tooltipLeft}
+                  cy={hpaGraphTooltipGlyphPosition + 1}
+                  r={4}
+                  fill="black"
+                  fillOpacity={0.1}
+                  stroke="black"
+                  strokeOpacity={0.1}
+                  strokeWidth={2}
+                  pointerEvents="none"
+                />
+                <circle
+                  cx={tooltipLeft}
+                  cy={hpaGraphTooltipGlyphPosition}
+                  r={4}
+                  fill={accentColorDark}
+                  stroke="white"
+                  strokeWidth={2}
+                  pointerEvents="none"
+                />
+              </>
+            )}
+          </g>
         )}
-      </div>
-    );
-  }
-);
+      </svg>
+      {tooltipData && (
+        <div>
+          <TooltipWithBounds
+            key={Math.random()}
+            top={tooltipTop - 12}
+            left={tooltipLeft + 12}
+            style={{
+              ...defaultStyles,
+              background: "#26272f",
+              color: "#aaaabb",
+              textAlign: "center",
+            }}
+          >
+            {formatDate(getDate(tooltipData.data))}
+            <div style={{ color: accentColor }}>
+              {dataKey}: {getValue(tooltipData.data)}
+            </div>
+            {isHpaEnabled && (
+              <div style={{ color: "#FFF" }}>
+                HPA Threshold: {getValue(tooltipData.tooltipHpaData)}
+              </div>
+            )}
+          </TooltipWithBounds>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default AreaChart;

+ 96 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -0,0 +1,96 @@
+import {
+  GenericMetricResponse,
+  NormalizedMetricsData,
+  MetricsMemoryDataResponse,
+  MetricsCPUDataResponse,
+  MetricsNetworkDataResponse,
+  MetricsNGINXErrorsDataResponse,
+  AvailableMetrics,
+  MetricsHpaReplicasDataResponse,
+} from "./types";
+
+/**
+ * Normalize values from the API to be readable by the AreaChart component.
+ * This class was created to reduce the amount of parsing inside the MetricsSection component
+ * and improve readability
+ */
+export class MetricNormalizer {
+  metric_results: GenericMetricResponse["results"];
+  kind: AvailableMetrics;
+
+  constructor(data: GenericMetricResponse[], kind: AvailableMetrics) {
+    if (!Array.isArray(data) || !data[0]?.results) {
+      throw new Error("Failed parsing response" + JSON.stringify(data));
+    }
+    this.metric_results = data[0].results;
+    this.kind = kind;
+  }
+
+  getParsedData(): NormalizedMetricsData[] {
+    if (this.kind.includes("cpu")) {
+      return this.parseCPUMetrics(this.metric_results);
+    }
+    if (this.kind.includes("memory")) {
+      return this.parseMemoryMetrics(this.metric_results);
+    }
+    if (this.kind.includes("network")) {
+      return this.parseNetworkMetrics(this.metric_results);
+    }
+    if (this.kind.includes("nginx:errors")) {
+      return this.parseNGINXErrorsMetrics(this.metric_results);
+    }
+    if (this.kind.includes("hpa_replicas")) {
+      return this.parseHpaReplicaMetrics(this.metric_results);
+    }
+    return [];
+  }
+
+  private parseCPUMetrics(arr: MetricsCPUDataResponse["results"]) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: parseFloat(d.cpu),
+      };
+    });
+  }
+
+  private parseMemoryMetrics(arr: MetricsMemoryDataResponse["results"]) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: parseFloat(d.memory) / (1024 * 1024), // put units in Mi
+      };
+    });
+  }
+
+  private parseNetworkMetrics(arr: MetricsNetworkDataResponse["results"]) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: parseFloat(d.bytes) / 1024, // put units in Ki
+      };
+    });
+  }
+
+  private parseNGINXErrorsMetrics(
+    arr: MetricsNGINXErrorsDataResponse["results"]
+  ) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: parseFloat(d.error_pct),
+      };
+    });
+  }
+
+  private parseHpaReplicaMetrics(
+    arr: MetricsHpaReplicasDataResponse["results"]
+  ) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: parseInt(d.replicas),
+      };
+    });
+  }
+}

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

@@ -1,75 +1,22 @@
-import React, { Component } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import ParentSize from "@visx/responsive/lib/components/ParentSize";
 
 import settings from "assets/settings.svg";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { ChartType, StorageType } from "shared/types";
+import { ChartTypeWithExtendedConfig, StorageType } from "shared/types";
 
 import TabSelector from "components/TabSelector";
 import Loading from "components/Loading";
 import SelectRow from "components/values-form/SelectRow";
-import AreaChart, { MetricsData } from "./AreaChart";
+import AreaChart from "./AreaChart";
+import { MetricNormalizer } from "./MetricNormalizer";
+import { AvailableMetrics, NormalizedMetricsData } from "./types";
+import CheckboxRow from "components/values-form/CheckboxRow";
 
 type PropsType = {
-  currentChart: ChartType;
-};
-
-type StateType = {
-  controllerOptions: any[];
-  ingressOptions: any[];
-  selectedController: any;
-  selectedIngress: any;
-  pods: any[];
-  selectedPod: string;
-  selectedRange: string;
-  selectedMetric: string;
-  selectedMetricLabel: string;
-  controllerDropdownExpanded: boolean;
-  podDropdownExpanded: boolean;
-  dropdownExpanded: boolean;
-  data: MetricsData[];
-  showMetricsSettings: boolean;
-  metricsOptions: MetricsOption[];
-  isLoading: number;
-};
-
-type MetricsCPUDataResponse = {
-  pod?: string;
-  results: {
-    date: number;
-    cpu: string;
-  }[];
-}[];
-
-type MetricsMemoryDataResponse = {
-  pod?: string;
-  results: {
-    date: number;
-    memory: string;
-  }[];
-}[];
-
-type MetricsNetworkDataResponse = {
-  pod?: string;
-  results: {
-    date: number;
-    bytes: string;
-  }[];
-}[];
-
-type MetricsNGINXErrorsDataResponse = {
-  pod?: string;
-  results: {
-    date: number;
-    error_pct: string;
-  }[];
-}[];
-
-type MetricsOption = {
-  value: string;
-  label: string;
+  currentChart: ChartTypeWithExtendedConfig;
 };
 
 const resolutions: { [range: string]: string } = {
@@ -86,39 +33,63 @@ const secondsBeforeNow: { [range: string]: number } = {
   "1M": 60 * 60 * 24 * 30,
 };
 
-export default class MetricsSection extends Component<PropsType, StateType> {
-  state = {
-    pods: [] as any[],
-    selectedPod: "",
-    controllerOptions: [] as any[],
-    selectedController: null as any,
-    ingressOptions: [] as any[],
-    selectedIngress: null as any,
-    selectedRange: "1H",
-    selectedMetric: "cpu",
-    selectedMetricLabel: "CPU Utilization (vCPUs)",
-    dropdownExpanded: false,
-    podDropdownExpanded: false,
-    controllerDropdownExpanded: false,
-    data: [] as MetricsData[],
-    showMetricsSettings: false,
-    metricsOptions: [
-      { value: "cpu", label: "CPU Utilization (vCPUs)" },
-      { value: "memory", label: "RAM Utilization (Mi)" },
-      { value: "network", label: "Network Received Bytes (Ki)" },
-    ],
-    isLoading: 0,
-  };
+const MetricsSection: React.FunctionComponent<PropsType> = ({
+  currentChart,
+}) => {
+  const [pods, setPods] = useState([]);
+  const [selectedPod, setSelectedPod] = useState("");
+  const [controllerOptions, setControllerOptions] = useState([]);
+  const [selectedController, setSelectedController] = useState(null);
+  const [ingressOptions, setIngressOptions] = useState([]);
+  const [selectedIngress, setSelectedIngress] = useState(null);
+  const [selectedRange, setSelectedRange] = useState("1H");
+  const [selectedMetric, setSelectedMetric] = useState("cpu");
+  const [selectedMetricLabel, setSelectedMetricLabel] = useState(
+    "CPU Utilization (vCPUs)"
+  );
+  const [dropdownExpanded, setDropdownExpanded] = useState(false);
+  const [data, setData] = useState<NormalizedMetricsData[]>([]);
+  const [showMetricsSettings, setShowMetricsSettings] = useState(false);
+  const [metricsOptions, setMetricsOptions] = useState([
+    { value: "cpu", label: "CPU Utilization (vCPUs)" },
+    { value: "memory", label: "RAM Utilization (Mi)" },
+    { value: "network", label: "Network Received Bytes (Ki)" },
+  ]);
+  const [isLoading, setIsLoading] = useState(0);
+  const [hpaData, setHpaData] = useState([]);
+  const [hpaEnabled, setHpaEnabled] = useState(
+    currentChart?.config?.autoscaling?.enabled
+  );
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+
+  // Add or remove hpa replicas chart option when current chart is updated
+  useEffect(() => {
+    if (currentChart?.config?.autoscaling?.enabled) {
+      setMetricsOptions((prev) => {
+        if (prev.find((option) => option.value === "hpa_replicas")) {
+          return [...prev];
+        }
+        return [...prev, { value: "hpa_replicas", label: "HPA Replicas" }];
+      });
+    } else {
+      setMetricsOptions((prev) => {
+        const hpaReplicasOptionIndex = prev.findIndex(
+          (option) => option.value === "hpa_replicas"
+        );
+        const options = [...prev];
+        options.splice(hpaReplicasOptionIndex, 1);
+        return [...options];
+      });
+    }
+  }, [currentChart]);
 
-  componentDidMount() {
-    // get all controllers and read in a list of pods
-    let { currentChart } = this.props;
-    let { currentCluster, currentProject, setCurrentError } = this.context;
+  useEffect(() => {
+    if (currentChart?.chart?.metadata?.name == "ingress-nginx") {
+      setIsLoading((prev) => prev + 1);
 
-    if (currentChart.chart?.metadata?.name == "ingress-nginx") {
-      this.setState(({ isLoading }) => {
-        return { isLoading: isLoading + 1 };
-      });
       api
         .getNGINXIngresses(
           "<token>",
@@ -130,37 +101,35 @@ export default class MetricsSection extends Component<PropsType, StateType> {
           }
         )
         .then((res) => {
-          let metricsOptions = this.state.metricsOptions;
-          metricsOptions.push({
-            value: "nginx:errors",
-            label: "5XX Error Percentage",
-          });
-
-          let ingressOptions = [] as any[];
-          res.data.map((ingress: any) => {
-            ingressOptions.push({ value: ingress, label: ingress.name });
+          setMetricsOptions((prev) => {
+            return [
+              ...prev,
+              {
+                value: "nginx:errors",
+                label: "5XX Error Percentage",
+              },
+            ];
           });
 
+          const ingressOptions = res.data.map((ingress: any) => ({
+            value: ingress,
+            label: ingress.name,
+          }));
+          setIngressOptions(ingressOptions);
+          setSelectedIngress(ingressOptions[0]?.value);
           // iterate through the controllers to get the list of pods
-          this.setState({
-            metricsOptions,
-            ingressOptions,
-            selectedIngress: ingressOptions[0].value,
-          });
         })
         .catch((err) => {
           setCurrentError(JSON.stringify(err));
-          this.setState({ controllerOptions: [] as any[] });
+          setControllerOptions([]);
         })
         .finally(() => {
-          this.setState(({ isLoading }) => {
-            return { isLoading: isLoading - 1 };
-          });
+          setIsLoading((prev) => prev - 1);
         });
     }
-    this.setState(({ isLoading }) => {
-      return { isLoading: isLoading + 1 };
-    });
+
+    setIsLoading((prev) => prev + 1);
+
     api
       .getChartControllers(
         "<token>",
@@ -176,208 +145,28 @@ export default class MetricsSection extends Component<PropsType, StateType> {
         }
       )
       .then((res) => {
-        // TODO -- check at least one controller returned
-        let controllerOptions = [] as any[];
-        res.data.map((controller: any) => {
+        const controllerOptions = res.data.map((controller: any) => {
           let name = controller?.metadata?.name;
-          controllerOptions.push({ value: controller, label: name });
-        });
-
-        // iterate through the controllers to get the list of pods
-        this.setState({
-          controllerOptions,
-          selectedController: controllerOptions[0].value,
-        });
-
-        this.getPods();
-      })
-      .catch((err) => {
-        setCurrentError(JSON.stringify(err));
-        this.setState({ controllerOptions: [] as any[] });
-      })
-      .finally(() => {
-        this.setState(({ isLoading }) => {
-          return { isLoading: isLoading - 1 };
+          return { value: controller, label: name };
         });
-      });
-  }
-
-  componentDidUpdate(prevProps: PropsType, prevState: StateType) {
-    // if resolution, data kind, controllers, or pods have changed, update data
-    if (this.state.selectedMetric != prevState.selectedMetric) {
-      this.getMetrics();
-    }
-
-    if (this.state.selectedRange != prevState.selectedRange) {
-      this.getMetrics();
-    }
-
-    if (this.state.selectedPod != prevState.selectedPod) {
-      this.getMetrics();
-    }
-
-    if (
-      this.state.selectedController?.metadata?.name !=
-      prevState.selectedController?.metadata?.name
-    ) {
-      this.getMetrics();
-    }
-
-    if (this.state.selectedIngress?.name != prevState.selectedIngress?.name) {
-      this.getMetrics();
-    }
-  }
-
-  getMetrics = () => {
-    if (this.state.pods.length == 0) {
-      return;
-    }
-
-    let { currentChart } = this.props;
-    let { currentCluster, currentProject, setCurrentError } = this.context;
-    let kind = this.state.selectedMetric;
-    let shouldsum = true;
-    let namespace = currentChart.namespace;
-
-    // calculate start and end range
-    var d = new Date();
-    var end = Math.round(d.getTime() / 1000);
-    var start = end - secondsBeforeNow[this.state.selectedRange];
-
-    let pods = this.state.pods.map((pod: any) => {
-      return pod.value;
-    });
-
-    if (this.state.selectedPod != "All") {
-      pods = [this.state.selectedPod];
-    }
-
-    if (this.state.selectedMetric == "nginx:errors") {
-      pods = [this.state.selectedIngress?.name];
-      namespace = this.state.selectedIngress?.namespace || "default";
-      shouldsum = false;
-    }
 
-    this.setState(({ isLoading }) => {
-      return { isLoading: isLoading + 1 };
-    });
-
-    api
-      .getMetrics(
-        "<token>",
-        {
-          cluster_id: currentCluster.id,
-          metric: kind,
-          shouldsum: shouldsum,
-          pods,
-          namespace: namespace,
-          startrange: start,
-          endrange: end,
-          resolution: resolutions[this.state.selectedRange],
-        },
-        {
-          id: currentProject.id,
-        }
-      )
-      .then((res) => {
-        if (!Array.isArray(res.data) || !res.data[0]?.results) {
-          return;
-        }
-        // transform the metrics to expected form
-        if (kind == "cpu") {
-          let data = res.data as MetricsCPUDataResponse;
-
-          // if summed, just look at the first data
-          let tData = data[0].results.map(
-            (
-              d: {
-                date: number;
-                cpu: string;
-              },
-              i: number
-            ) => {
-              return {
-                date: d.date,
-                value: parseFloat(d.cpu),
-              };
-            }
-          );
-
-          this.setState({ data: tData });
-        } else if (kind == "memory") {
-          let data = res.data as MetricsMemoryDataResponse;
-
-          let tData = data[0].results.map(
-            (
-              d: {
-                date: number;
-                memory: string;
-              },
-              i: number
-            ) => {
-              return {
-                date: d.date,
-                value: parseFloat(d.memory) / (1024 * 1024), // put units in Mi
-              };
-            }
-          );
-
-          this.setState({ data: tData });
-        } else if (kind == "network") {
-          let data = res.data as MetricsNetworkDataResponse;
-
-          let tData = data[0].results.map(
-            (
-              d: {
-                date: number;
-                bytes: string;
-              },
-              i: number
-            ) => {
-              return {
-                date: d.date,
-                value: parseFloat(d.bytes) / 1024, // put units in Ki
-              };
-            }
-          );
-
-          this.setState({ data: tData });
-        } else if (kind == "nginx:errors") {
-          let data = res.data as MetricsNGINXErrorsDataResponse;
-
-          let tData = data[0].results.map(
-            (
-              d: {
-                date: number;
-                error_pct: string;
-              },
-              i: number
-            ) => {
-              return {
-                date: d.date,
-                value: parseFloat(d.error_pct), // put units in Ki
-              };
-            }
-          );
-
-          this.setState({ data: tData });
-        }
+        setControllerOptions(controllerOptions);
+        setSelectedController(controllerOptions[0]?.value);
       })
       .catch((err) => {
         setCurrentError(JSON.stringify(err));
-        // this.setState({ controllers: [], loading: false });
+        setControllerOptions([]);
       })
       .finally(() => {
-        this.setState(({ isLoading }) => {
-          return { isLoading: isLoading - 1 };
-        });
+        setIsLoading((prev) => prev - 1);
       });
-  };
+  }, [currentChart, currentCluster, currentProject]);
 
-  getPods = () => {
-    let { selectedController } = this.state;
-    let { currentCluster, currentProject, setCurrentError } = this.context;
+  useEffect(() => {
+    getPods();
+  }, [selectedController]);
 
+  const getPods = () => {
     let selectors = [] as string[];
     let ml =
       selectedController?.spec?.selector?.matchLabels ||
@@ -393,9 +182,7 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     }
     selectors.push(selector);
 
-    this.setState(({ isLoading }) => {
-      return { isLoading: isLoading + 1 };
-    });
+    setIsLoading((prev) => prev + 1);
 
     api
       .getMatchingPods(
@@ -415,80 +202,170 @@ export default class MetricsSection extends Component<PropsType, StateType> {
           let name = pod?.metadata?.name;
           pods.push({ value: name, label: name });
         });
+        setPods(pods);
+        setSelectedPod("All");
 
-        this.setState({ pods, selectedPod: "All" });
-
-        this.getMetrics();
+        getMetrics();
       })
       .catch((err) => {
         setCurrentError(JSON.stringify(err));
         return;
       })
       .finally(() => {
-        this.setState(({ isLoading }) => {
-          return { isLoading: isLoading - 1 };
-        });
+        setIsLoading((prev) => prev - 1);
       });
   };
 
-  renderDropdown = () => {
-    if (this.state.dropdownExpanded) {
-      return (
-        <>
-          <DropdownOverlay
-            onClick={() => this.setState({ dropdownExpanded: false })}
-          />
-          <Dropdown
-            dropdownWidth="230px"
-            dropdownMaxHeight="200px"
-            onClick={() => this.setState({ dropdownExpanded: false })}
-          >
-            {this.renderOptionList()}
-          </Dropdown>
-        </>
+  const getAutoscalingThreshold = async (
+    metricType: "cpu_hpa_threshold" | "memory_hpa_threshold",
+    shouldsum: boolean,
+    namespace: string,
+    start: number,
+    end: number
+  ) => {
+    setIsLoading((prev) => prev + 1);
+    setHpaData([]);
+    try {
+      const res = await api.getMetrics(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+          metric: metricType,
+          shouldsum: shouldsum,
+          kind: selectedController?.kind,
+          name: selectedController?.metadata.name,
+          namespace: namespace,
+          startrange: start,
+          endrange: end,
+          resolution: resolutions[selectedRange],
+          pods: [],
+        },
+        {
+          id: currentProject.id,
+        }
       );
+
+      if (!Array.isArray(res.data) || !res.data[0]?.results) {
+        return;
+      }
+      const autoscalingMetrics = new MetricNormalizer(res.data, metricType);
+      setHpaData(autoscalingMetrics.getParsedData());
+      return;
+    } catch (error) {
+      console.error(error);
+    } finally {
+      setIsLoading((prev) => prev - 1);
     }
   };
 
-  renderOptionList = () => {
-    return this.state.metricsOptions.map(
-      (option: { value: string; label: string }, i: number) => {
-        return (
-          <Option
-            key={i}
-            selected={option.value === this.state.selectedMetric}
-            onClick={() =>
-              this.setState({
-                selectedMetric: option.value,
-                selectedMetricLabel: option.label,
-              })
-            }
-            lastItem={i === this.state.metricsOptions.length - 1}
-          >
-            {option.label}
-          </Option>
-        );
+  const getMetrics = async () => {
+    if (pods?.length == 0) {
+      return;
+    }
+    try {
+      let shouldsum = selectedPod === "All";
+      let namespace = currentChart.namespace;
+
+      // calculate start and end range
+      const d = new Date();
+      const end = Math.round(d.getTime() / 1000);
+      const start = end - secondsBeforeNow[selectedRange];
+
+      let podNames = [] as string[];
+
+      if (!shouldsum) {
+        podNames = [selectedPod];
       }
-    );
+
+      if (selectedMetric == "nginx:errors") {
+        podNames = [selectedIngress?.name];
+        namespace = selectedIngress?.namespace || "default";
+        shouldsum = false;
+      }
+
+      setIsLoading((prev) => prev + 1);
+      setData([]);
+
+      const res = await api.getMetrics(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+          metric: selectedMetric,
+          shouldsum: shouldsum,
+          kind: selectedController?.kind,
+          name: selectedController?.metadata.name,
+          namespace: namespace,
+          startrange: start,
+          endrange: end,
+          resolution: resolutions[selectedRange],
+          pods: podNames,
+        },
+        {
+          id: currentProject.id,
+        }
+      );
+
+      setHpaData([]);
+      const isHpaEnabled = currentChart.config.autoscaling.enabled;
+      if (shouldsum && isHpaEnabled) {
+        if (selectedMetric === "cpu") {
+          await getAutoscalingThreshold(
+            "cpu_hpa_threshold",
+            shouldsum,
+            namespace,
+            start,
+            end
+          );
+        } else if (selectedMetric === "memory") {
+          await getAutoscalingThreshold(
+            "memory_hpa_threshold",
+            shouldsum,
+            namespace,
+            start,
+            end
+          );
+        }
+      }
+
+      const metrics = new MetricNormalizer(
+        res.data,
+        selectedMetric as AvailableMetrics
+      );
+
+      // transform the metrics to expected form
+      setData(metrics.getParsedData());
+    } catch (error) {
+      setCurrentError(JSON.stringify(error));
+    } finally {
+      setIsLoading((prev) => prev - 1);
+    }
   };
 
-  renderMetricsSettings = () => {
-    if (this.state.showMetricsSettings && true) {
-      if (this.state.selectedMetric == "nginx:errors") {
+  useEffect(() => {
+    if (selectedMetric && selectedRange && selectedPod && selectedController) {
+      getMetrics();
+    }
+  }, [
+    selectedMetric,
+    selectedRange,
+    selectedPod,
+    selectedController,
+    selectedIngress,
+  ]);
+
+  const renderMetricsSettings = () => {
+    if (showMetricsSettings && true) {
+      if (selectedMetric == "nginx:errors") {
         return (
           <>
-            <DropdownOverlay
-              onClick={() => this.setState({ showMetricsSettings: false })}
-            />
+            <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
             <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
               <Label>Additional Settings</Label>
               <SelectRow
                 label="Target Ingress"
-                value={this.state.selectedIngress}
-                setActiveValue={(x: any) =>
-                  this.setState({ selectedIngress: x })
-                }
-                options={this.state.ingressOptions}
+                value={selectedIngress}
+                setActiveValue={(x: any) => setSelectedIngress(x)}
+                options={ingressOptions}
                 width="100%"
               />
             </DropdownAlt>
@@ -498,25 +375,21 @@ export default class MetricsSection extends Component<PropsType, StateType> {
 
       return (
         <>
-          <DropdownOverlay
-            onClick={() => this.setState({ showMetricsSettings: false })}
-          />
+          <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
           <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
             <Label>Additional Settings</Label>
             <SelectRow
               label="Target Controller"
-              value={this.state.selectedController}
-              setActiveValue={(x: any) =>
-                this.setState({ selectedController: x })
-              }
-              options={this.state.controllerOptions}
+              value={selectedController}
+              setActiveValue={(x: any) => setSelectedController(x)}
+              options={controllerOptions}
               width="100%"
             />
             <SelectRow
               label="Target Pod"
-              value={this.state.selectedPod}
-              setActiveValue={(x: any) => this.setState({ selectedPod: x })}
-              options={this.state.pods}
+              value={selectedPod}
+              setActiveValue={(x: any) => setSelectedPod(x)}
+              options={pods}
               width="100%"
             />
           </DropdownAlt>
@@ -525,85 +398,122 @@ export default class MetricsSection extends Component<PropsType, StateType> {
     }
   };
 
-  render() {
-    return (
-      <StyledMetricsSection>
-        <MetricsHeader>
-          <Flex>
-            <MetricSelector
-              onClick={() =>
-                this.setState({
-                  dropdownExpanded: !this.state.dropdownExpanded,
-                })
-              }
-            >
-              <MetricsLabel>{this.state.selectedMetricLabel}</MetricsLabel>
-              <i className="material-icons">arrow_drop_down</i>
-              {this.renderDropdown()}
-            </MetricSelector>
-            <Relative>
-              <IconWrapper
-                onClick={() => this.setState({ showMetricsSettings: true })}
-              >
-                <SettingsIcon src={settings} />
-              </IconWrapper>
-              {this.renderMetricsSettings()}
-            </Relative>
-            {/* <RefreshMetrics
-              className="material-icons-outlined"
-              onClick={() => this.getMetrics()}
-            >
-              refresh
-            </RefreshMetrics> */}
-
-            <Highlight color={"#7d7d81"} onClick={this.getMetrics}>
-              <i className="material-icons">autorenew</i>
-            </Highlight>
-          </Flex>
-          <RangeWrapper>
-            <TabSelector
-              noBuffer={true}
-              options={[
-                { value: "1H", label: "1H" },
-                { value: "6H", label: "6H" },
-                { value: "1D", label: "1D" },
-                { value: "1M", label: "1M" },
-              ]}
-              currentTab={this.state.selectedRange}
-              setCurrentTab={(x: string) => this.setState({ selectedRange: x })}
-            />
-          </RangeWrapper>
-        </MetricsHeader>
-        {this.state.isLoading > 0 && <Loading />}
-        {this.state.data.length === 0 && this.state.isLoading === 0 && (
-          <Message>
-            No data available yet.
-            <Highlight color={"#8590ff"} onClick={this.getMetrics}>
-              <i className="material-icons">autorenew</i>
-              Refresh
-            </Highlight>
-          </Message>
-        )}
-
-        {this.state.data.length > 0 && this.state.isLoading === 0 && (
+  const renderDropdown = () => {
+    if (dropdownExpanded) {
+      return (
+        <>
+          <DropdownOverlay onClick={() => setDropdownExpanded(false)} />
+          <Dropdown
+            dropdownWidth="230px"
+            dropdownMaxHeight="200px"
+            onClick={() => setDropdownExpanded(false)}
+          >
+            {renderOptionList()}
+          </Dropdown>
+        </>
+      );
+    }
+  };
+
+  const renderOptionList = () => {
+    return metricsOptions.map(
+      (option: { value: string; label: string }, i: number) => {
+        return (
+          <Option
+            key={i}
+            selected={option.value === selectedMetric}
+            onClick={() => {
+              setSelectedMetric(option.value);
+              setSelectedMetricLabel(option.label);
+            }}
+            lastItem={i === metricsOptions.length - 1}
+          >
+            {option.label}
+          </Option>
+        );
+      }
+    );
+  };
+
+  return (
+    <StyledMetricsSection>
+      <MetricsHeader>
+        <Flex>
+          <MetricSelector
+            onClick={() => setDropdownExpanded(!dropdownExpanded)}
+          >
+            <MetricsLabel>{selectedMetricLabel}</MetricsLabel>
+            <i className="material-icons">arrow_drop_down</i>
+            {renderDropdown()}
+          </MetricSelector>
+          <Relative>
+            <IconWrapper onClick={() => setShowMetricsSettings(true)}>
+              <SettingsIcon src={settings} />
+            </IconWrapper>
+            {renderMetricsSettings()}
+          </Relative>
+
+          <Highlight color={"#7d7d81"} onClick={getMetrics}>
+            <i className="material-icons">autorenew</i>
+          </Highlight>
+        </Flex>
+        <RangeWrapper>
+          <TabSelector
+            noBuffer={true}
+            options={[
+              { value: "1H", label: "1H" },
+              { value: "6H", label: "6H" },
+              { value: "1D", label: "1D" },
+              { value: "1M", label: "1M" },
+            ]}
+            currentTab={selectedRange}
+            setCurrentTab={(x: string) => setSelectedRange(x)}
+          />
+        </RangeWrapper>
+      </MetricsHeader>
+      {isLoading > 0 && <Loading />}
+      {data.length === 0 && isLoading === 0 && (
+        <Message>
+          No data available yet.
+          <Highlight color={"#8590ff"} onClick={getMetrics}>
+            <i className="material-icons">autorenew</i>
+            Refresh
+          </Highlight>
+        </Message>
+      )}
+      {data.length > 0 && isLoading === 0 && (
+        <>
+          {currentChart?.config?.autoscaling?.enabled &&
+            ["cpu", "memory"].includes(selectedMetric) && (
+              <CheckboxRow
+                toggle={() => setHpaEnabled((prev) => !prev)}
+                checked={hpaEnabled}
+                label="Enable HPA Metrics"
+              />
+            )}
           <ParentSize>
             {({ width, height }) => (
               <AreaChart
-                data={this.state.data}
+                dataKey={selectedMetricLabel}
+                data={data}
+                hpaData={hpaData}
+                hpaEnabled={
+                  hpaEnabled && ["cpu", "memory"].includes(selectedMetric)
+                }
                 width={width}
                 height={height - 10}
-                resolution={this.state.selectedRange}
+                resolution={selectedRange}
                 margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
               />
             )}
           </ParentSize>
-        )}
-      </StyledMetricsSection>
-    );
-  }
-}
+        </>
+      )}
+    </StyledMetricsSection>
+  );
+};
 
-MetricsSection.contextType = Context;
+export default MetricsSection;
 
 const Highlight = styled.div`
   display: flex;
@@ -619,20 +529,6 @@ const Highlight = styled.div`
   }
 `;
 
-const RefreshMetrics = styled.span`
-  :hover {
-    cursor: pointer;
-  }
-`;
-
-const NoDataPlaceholder = styled.div`
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-`;
-
 const Label = styled.div`
   font-weight: bold;
 `;

+ 65 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/types.ts

@@ -0,0 +1,65 @@
+export type MetricsCPUDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    cpu: string;
+  }[];
+};
+
+export type MetricsMemoryDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    memory: string;
+  }[];
+};
+
+export type MetricsNetworkDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    bytes: string;
+  }[];
+};
+
+export type MetricsNGINXErrorsDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    error_pct: string;
+  }[];
+};
+
+export type MetricsHpaReplicasDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    replicas: string;
+  }[];
+};
+
+export type GenericMetricResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    cpu: string;
+    memory: string;
+    bytes: string;
+    error_pct: string;
+    replicas: string;
+  }[];
+};
+
+export type NormalizedMetricsData = {
+  date: number; // unix timestamp
+  value: number; // value
+};
+
+export type AvailableMetrics =
+  | "cpu"
+  | "memory"
+  | "network"
+  | "nginx:errors"
+  | "cpu_hpa_threshold"
+  | "memory_hpa_threshold"
+  | "hpa_replicas";

+ 3 - 1
dashboard/src/shared/api.tsx

@@ -558,7 +558,9 @@ const getMetrics = baseApi<
     cluster_id: number;
     metric: string;
     shouldsum: boolean;
-    pods: string[];
+    pods?: string[];
+    kind?: string; // the controller kind
+    name: string;
     namespace: string;
     startrange: number;
     endrange: number;

+ 57 - 0
dashboard/src/shared/types.tsx

@@ -48,6 +48,63 @@ export interface ChartType {
   latest_version: string;
 }
 
+export interface ChartTypeWithExtendedConfig extends ChartType {
+  config: {
+    auto_deploy: boolean;
+    autoscaling: {
+      enabled: boolean;
+      maxReplicas: number;
+      minReplicas: number;
+      targetCPUUtilizationPercentage: number;
+      targetMemoryUtilizationPercentage: number;
+    };
+    cloudsql: {
+      connectionName: string;
+      dbPort: number;
+      enabled: boolean;
+      serviceAccountJSON: string;
+    };
+    container: {
+      command: string;
+      env: any;
+      lifecycle: { postStart: string; preStop: string };
+      port: number;
+    };
+    currentCluster: {
+      service: { is_aws: boolean; is_do: boolean; is_gcp: boolean };
+    };
+    health: {
+      enabled: boolean;
+      failureThreshold: number;
+      path: string;
+      periodSeconds: number;
+    };
+    image: {
+      pullPolicy: string;
+      repository: string;
+      tag: string;
+    };
+    ingress: {
+      annotations: any;
+      custom_domain: boolean;
+      custom_paths: any[];
+      enabled: boolean;
+      hosts: any[];
+      porter_hosts: string[];
+      provider: string;
+      wildcard: boolean;
+    };
+    pvc: { enabled: boolean; mountPath: string; storage: string };
+    replicaCount: number;
+    resources: { requests: { cpu: string; memory: string } };
+    service: { port: number };
+    serviceAccount: { annotations: any; create: boolean; name: string };
+    showStartCommand: boolean;
+    statefulset: { enabled: boolean };
+    terminationGracePeriodSeconds: number;
+  };
+}
+
 export interface ResourceType {
   ID: number;
   Kind: string;

+ 194 - 4
internal/kubernetes/prometheus/metrics.go

@@ -62,7 +62,9 @@ func GetIngressesWithNGINXAnnotation(clientset kubernetes.Interface) ([]SimpleIn
 type QueryOpts struct {
 	Metric     string   `schema:"metric"`
 	ShouldSum  bool     `schema:"shouldsum"`
+	Kind       string   `schema:"kind"`
 	PodList    []string `schema:"pods"`
+	Name       string   `schema:"name"`
 	Namespace  string   `schema:"namespace"`
 	StartRange uint     `schema:"startrange"`
 	EndRange   uint     `schema:"endrange"`
@@ -78,7 +80,20 @@ func QueryPrometheus(
 		return nil, fmt.Errorf("prometheus service has no exposed ports to query")
 	}
 
-	podSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
+	podSelectionRegex, err := getPodSelectionRegex(opts.Kind, opts.Name)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var podSelector string
+
+	if len(opts.PodList) > 0 {
+		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
+	} else {
+		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, podSelectionRegex)
+	}
+
 	query := ""
 
 	if opts.Metric == "cpu" {
@@ -86,12 +101,25 @@ func QueryPrometheus(
 	} else if opts.Metric == "memory" {
 		query = fmt.Sprintf("container_memory_usage_bytes{%s}", podSelector)
 	} else if opts.Metric == "network" {
-		netPodSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container="POD"`, opts.Namespace, strings.Join(opts.PodList, "|"))
+		netPodSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container="POD"`, opts.Namespace, podSelectionRegex)
 		query = fmt.Sprintf("rate(container_network_receive_bytes_total{%s}[5m])", netPodSelector)
 	} else if opts.Metric == "nginx:errors" {
-		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, strings.Join(opts.PodList, "|"))
-		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{namespace="%s",ingress=~"%s"}[5m]) > 0)`, opts.Namespace, strings.Join(opts.PodList, "|"))
+		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, podSelectionRegex)
+		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{namespace="%s",ingress=~"%s"}[5m]) > 0)`, opts.Namespace, podSelectionRegex)
 		query = fmt.Sprintf(`%s / %s * 100 OR on() vector(0)`, num, denom)
+	} else if opts.Metric == "cpu_hpa_threshold" {
+		// get the name of the kube hpa metric
+		metricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
+
+		query = createHPAAbsoluteCPUThresholdQuery(metricName, podSelectionRegex, opts.Name, opts.Namespace, service.ObjectMeta.Labels["app"])
+	} else if opts.Metric == "memory_hpa_threshold" {
+		metricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
+
+		query = createHPAAbsoluteMemoryThresholdQuery(metricName, podSelectionRegex, opts.Name, opts.Namespace, service.ObjectMeta.Labels["app"])
+	} else if opts.Metric == "hpa_replicas" {
+		metricName := getKubeHPAMetricName(clientset, service, opts, "status_current_replicas")
+
+		query = createHPACurrentReplicasQuery(metricName, opts.Name, opts.Namespace, service.ObjectMeta.Labels["app"])
 	}
 
 	if opts.ShouldSum {
@@ -137,6 +165,7 @@ type promRawQuery struct {
 type promParsedSingletonQueryResult struct {
 	Date     interface{} `json:"date,omitempty"`
 	CPU      interface{} `json:"cpu,omitempty"`
+	Replicas interface{} `json:"replicas,omitempty"`
 	Memory   interface{} `json:"memory,omitempty"`
 	Bytes    interface{} `json:"bytes,omitempty"`
 	ErrorPct interface{} `json:"error_pct,omitempty"`
@@ -174,6 +203,12 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 				singletonResult.Bytes = values[1]
 			} else if metric == "nginx:errors" {
 				singletonResult.ErrorPct = values[1]
+			} else if metric == "cpu_hpa_threshold" {
+				singletonResult.CPU = values[1]
+			} else if metric == "memory_hpa_threshold" {
+				singletonResult.Memory = values[1]
+			} else if metric == "hpa_replicas" {
+				singletonResult.Replicas = values[1]
 			}
 
 			singletonResults = append(singletonResults, *singletonResult)
@@ -186,3 +221,158 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 
 	return json.Marshal(res)
 }
+
+func getPodSelectionRegex(kind, name string) (string, error) {
+	var suffix string
+
+	switch strings.ToLower(kind) {
+	case "deployment":
+		suffix = "[a-z0-9]+-[a-z0-9]+"
+	case "statefulset":
+		suffix = "[0-9]+"
+	case "job":
+		suffix = "[a-z0-9]+"
+	case "cronjob":
+		suffix = "[a-z0-9]+-[a-z0-9]+"
+	default:
+		return "", fmt.Errorf("not a supported controller to query for metrics")
+	}
+
+	return fmt.Sprintf("%s-%s", name, suffix), nil
+}
+
+func createHPAAbsoluteCPUThresholdQuery(metricName, podSelectionRegex, hpaName, namespace, appLabel string) string {
+	kubeMetricsPodSelector := getKubeMetricsPodSelector(podSelectionRegex, namespace)
+
+	kubeMetricsHPASelector := fmt.Sprintf(
+		`hpa="%s",namespace="%s",metric_name="cpu",metric_target_type="utilization"`,
+		hpaName,
+		namespace,
+	)
+
+	// the kube-state-metrics queries are less prone to error if the field app_kubernetes_io_instance is matched
+	// as well
+	if appLabel != "" {
+		kubeMetricsPodSelector += fmt.Sprintf(`,app_kubernetes_io_instance="%s"`, appLabel)
+		kubeMetricsHPASelector += fmt.Sprintf(`,app_kubernetes_io_instance="%s"`, appLabel)
+	}
+
+	requestCPU := fmt.Sprintf(
+		`sum by (hpa) (label_replace(kube_pod_container_resource_requests_cpu_cores{%s},"hpa", "%s", "", ""))`,
+		kubeMetricsPodSelector,
+		hpaName,
+	)
+
+	targetCPUUtilThreshold := fmt.Sprintf(
+		`%s{%s} / 100`,
+		metricName,
+		kubeMetricsHPASelector,
+	)
+
+	return fmt.Sprintf(`%s * on(hpa) %s`, requestCPU, targetCPUUtilThreshold)
+}
+
+func createHPAAbsoluteMemoryThresholdQuery(metricName, podSelectionRegex, hpaName, namespace, appLabel string) string {
+	kubeMetricsPodSelector := getKubeMetricsPodSelector(podSelectionRegex, namespace)
+
+	kubeMetricsHPASelector := fmt.Sprintf(
+		`hpa="%s",namespace="%s",metric_name="memory",metric_target_type="utilization"`,
+		hpaName,
+		namespace,
+	)
+
+	// the kube-state-metrics queries are less prone to error if the field app_kubernetes_io_instance is matched
+	// as well
+	if appLabel != "" {
+		kubeMetricsPodSelector += fmt.Sprintf(`,app_kubernetes_io_instance="%s"`, appLabel)
+		kubeMetricsHPASelector += fmt.Sprintf(`,app_kubernetes_io_instance="%s"`, appLabel)
+	}
+
+	requestMem := fmt.Sprintf(
+		`sum by (hpa) (label_replace(kube_pod_container_resource_requests_memory_bytes{%s},"hpa", "%s", "", ""))`,
+		kubeMetricsPodSelector,
+		hpaName,
+	)
+
+	targetMemUtilThreshold := fmt.Sprintf(
+		`%s{%s} / 100`,
+		metricName,
+		kubeMetricsHPASelector,
+	)
+
+	return fmt.Sprintf(`%s * on(hpa) %s`, requestMem, targetMemUtilThreshold)
+}
+
+func getKubeMetricsPodSelector(podSelectionRegex, namespace string) string {
+	return fmt.Sprintf(
+		`pod=~"%s",namespace="%s",container!="POD",container!=""`,
+		podSelectionRegex,
+		namespace,
+	)
+}
+
+func createHPACurrentReplicasQuery(metricName, hpaName, namespace, appLabel string) string {
+	kubeMetricsHPASelector := fmt.Sprintf(
+		`hpa="%s",namespace="%s"`,
+		hpaName,
+		namespace,
+	)
+
+	// the kube-state-metrics queries are less prone to error if the field app_kubernetes_io_instance is matched
+	// as well
+	if appLabel != "" {
+		kubeMetricsHPASelector += fmt.Sprintf(`,app_kubernetes_io_instance="%s"`, appLabel)
+	}
+
+	return fmt.Sprintf(
+		`%s{%s}`,
+		metricName,
+		kubeMetricsHPASelector,
+	)
+}
+
+type promRawValuesQuery struct {
+	Status string   `json:"status"`
+	Data   []string `json:"data"`
+}
+
+// getKubeHPAMetricName performs a "best guess" for the name of the kube HPA metric,
+// which was renamed to kube_horizontal_pod_autoscaler... in later versions of kube-state-metrics.
+// we query Prometheus for a list of metric names to see if any match the new query
+// value, otherwise we return the deprecated name.
+func getKubeHPAMetricName(
+	clientset kubernetes.Interface,
+	service *v1.Service,
+	opts *QueryOpts,
+	suffix string,
+) string {
+	queryParams := map[string]string{
+		"match[]": fmt.Sprintf("kube_horizontal_pod_autoscaler_%s", suffix),
+		"start":   fmt.Sprintf("%d", opts.StartRange),
+		"end":     fmt.Sprintf("%d", opts.EndRange),
+	}
+
+	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
+		"http",
+		service.Name,
+		fmt.Sprintf("%d", service.Spec.Ports[0].Port),
+		"/api/v1/label/__name__/values",
+		queryParams,
+	)
+
+	rawQuery, err := resp.DoRaw(context.TODO())
+
+	if err != nil {
+		return fmt.Sprintf("kube_hpa_%s", suffix)
+	}
+
+	rawQueryObj := &promRawValuesQuery{}
+
+	json.Unmarshal(rawQuery, rawQueryObj)
+
+	if rawQueryObj.Status == "success" && len(rawQueryObj.Data) == 1 {
+		return fmt.Sprintf("kube_horizontal_pod_autoscaler_%s", suffix)
+	}
+
+	return fmt.Sprintf("kube_hpa_%s", suffix)
+}