From 70d8eeb3c8d53133391d8c8317c676cfe236fa93 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 15 Oct 2021 16:45:54 -0400 Subject: [PATCH] more work Signed-off-by: Sebastian Malton --- .eslintrc.js | 3 - .../k8s-api/endpoints/daemon-set.api.ts | 4 +- src/common/k8s-api/endpoints/index.ts | 15 +- src/common/k8s-api/endpoints/ingress.api.ts | 16 +- src/common/k8s-api/endpoints/metrics.api.ts | 66 +++---- src/common/k8s-api/endpoints/pods.api.ts | 26 +-- src/common/utils/index.ts | 3 +- src/common/utils/numbers.ts | 30 ++++ .../components/+cluster/cluster-overview.tsx | 12 +- .../+network-ingresses/ingress-charts.tsx | 22 +-- .../+network-ingresses/ingress-details.tsx | 77 ++++----- .../components/+nodes/node-charts.tsx | 56 +++--- .../components/+nodes/node-details.tsx | 12 +- .../volume-claim-details.tsx | 11 +- .../volume-claim-disk-chart.tsx | 12 +- .../daemonset-details.tsx | 7 +- .../+workloads-pods/container-charts.tsx | 161 +++++++++--------- .../components/+workloads-pods/pod-charts.tsx | 154 ++++++++--------- .../+workloads-pods/pod-details-container.tsx | 151 ++++++++-------- .../+workloads-pods/pod-details.tsx | 3 +- .../replicaset-details.tsx | 5 +- src/renderer/components/chart/bar-chart.tsx | 33 ++-- .../components/metrics-helpers/index.ts | 118 +++++++++++++ .../resource-metrics/resource-metrics.tsx | 27 +-- 24 files changed, 588 insertions(+), 436 deletions(-) create mode 100644 src/common/utils/numbers.ts create mode 100644 src/renderer/components/metrics-helpers/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index 1426f74e7f..9b99ff5df7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -69,7 +69,6 @@ module.exports = { ], "quotes": ["error", "double", { "avoidEscape": true, - "allowTemplateLiterals": true, }], "linebreak-style": ["error", "unix"], "eol-last": ["error", "always"], @@ -128,7 +127,6 @@ module.exports = { }], "quotes": ["error", "double", { "avoidEscape": true, - "allowTemplateLiterals": true, }], "react/prop-types": "off", "semi": "off", @@ -196,7 +194,6 @@ module.exports = { }], "quotes": ["error", "double", { "avoidEscape": true, - "allowTemplateLiterals": true, }], "react/prop-types": "off", "semi": "off", diff --git a/src/common/k8s-api/endpoints/daemon-set.api.ts b/src/common/k8s-api/endpoints/daemon-set.api.ts index 4cd3eff4f4..3e6f9023ad 100644 --- a/src/common/k8s-api/endpoints/daemon-set.api.ts +++ b/src/common/k8s-api/endpoints/daemon-set.api.ts @@ -23,10 +23,10 @@ import get from "lodash/get"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; import type { KubeJsonApiData } from "../kube-json-api"; import type { IPodContainer, IPodMetrics } from "./pods.api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import { getMetrics } from "./metrics.api"; export class DaemonSet extends WorkloadKubeObject { static kind = "DaemonSet"; @@ -106,7 +106,7 @@ export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: stri const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|"); const opts = { category: "pods", pods: podSelector, namespace, selector }; - return metricsApi.getMetrics({ + return getMetrics({ cpuUsage: opts, memoryUsage: opts, fsUsage: opts, diff --git a/src/common/k8s-api/endpoints/index.ts b/src/common/k8s-api/endpoints/index.ts index c3827cd5e5..bd52559071 100644 --- a/src/common/k8s-api/endpoints/index.ts +++ b/src/common/k8s-api/endpoints/index.ts @@ -22,9 +22,9 @@ // Kubernetes apis // Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/ -export * from "./cluster.api"; -export * from "./cluster-role.api"; export * from "./cluster-role-binding.api"; +export * from "./cluster-role.api"; +export * from "./cluster.api"; export * from "./configmap.api"; export * from "./crd.api"; export * from "./cron-job.api"; @@ -36,22 +36,23 @@ export * from "./hpa.api"; export * from "./ingress.api"; export * from "./job.api"; export * from "./limit-range.api"; +export * from "./metrics.api"; export * from "./namespaces.api"; export * from "./network-policy.api"; export * from "./nodes.api"; -export * from "./persistent-volume.api"; export * from "./persistent-volume-claims.api"; -export * from "./pods.api"; -export * from "./poddisruptionbudget.api"; +export * from "./persistent-volume.api"; export * from "./pod-metrics.api"; +export * from "./poddisruptionbudget.api"; +export * from "./pods.api"; export * from "./podsecuritypolicy.api"; export * from "./replica-set.api"; export * from "./resource-quota.api"; -export * from "./role.api"; export * from "./role-binding.api"; +export * from "./role.api"; export * from "./secret.api"; export * from "./selfsubjectrulesreviews.api"; -export * from "./service.api"; export * from "./service-accounts.api"; +export * from "./service.api"; export * from "./stateful-set.api"; export * from "./storage-class.api"; diff --git a/src/common/k8s-api/endpoints/ingress.api.ts b/src/common/k8s-api/endpoints/ingress.api.ts index 5a1a404f20..88eb75cad4 100644 --- a/src/common/k8s-api/endpoints/ingress.api.ts +++ b/src/common/k8s-api/endpoints/ingress.api.ts @@ -21,7 +21,7 @@ import { KubeObject } from "../kube-object"; import { autoBind } from "../../utils"; -import { IMetrics, metricsApi } from "./metrics.api"; +import { IMetrics, getMetrics } from "./metrics.api"; import { KubeApi } from "../kube-api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; @@ -32,7 +32,7 @@ export class IngressApi extends KubeApi { export function getMetricsForIngress(ingress: string, namespace: string): Promise { const opts = { category: "ingress", ingress }; - return metricsApi.getMetrics({ + return getMetrics({ bytesSentSuccess: opts, bytesSentFailure: opts, requestDurationSeconds: opts, @@ -42,12 +42,12 @@ export function getMetricsForIngress(ingress: string, namespace: string): Promis }); } -export interface IIngressMetrics { - [metric: string]: T; - bytesSentSuccess: T; - bytesSentFailure: T; - requestDurationSeconds: T; - responseDurationSeconds: T; +export interface IIngressMetrics { + [metric: string]: IMetrics; + bytesSentSuccess: IMetrics; + bytesSentFailure: IMetrics; + requestDurationSeconds: IMetrics; + responseDurationSeconds: IMetrics; } export interface ILoadBalancerIngress { diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index cb86383c9c..75ce671785 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -24,7 +24,9 @@ import moment from "moment"; import { apiBase } from "../index"; import type { IMetricsQuery } from "../../../main/routes/metrics-route"; -import { iter, toJS } from "../../utils"; +import { iter } from "../../utils"; +import { mapValues } from "lodash"; +import type { Falsey } from "../../utils/iter"; export interface IMetrics { status: string; @@ -70,33 +72,39 @@ export interface IResourceMetrics { networkTransmit: T; } -export const metricsApi = { - async getMetrics(query: T, reqParams: IMetricsReqParams = {}): Promise { - const { range = 3600, step = 60, namespace } = reqParams; - let { start, end } = reqParams; +export async function getMetrics(query: T, reqParams: IMetricsReqParams = {}): Promise { + const { range = 3600, step = 60, namespace } = reqParams; + let { start, end } = reqParams; - if (!start && !end) { - const timeNow = Date.now() / 1000; - const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes + if (!start && !end) { + const timeNow = Date.now() / 1000; + const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes - start = now - range; - end = now; - } - - return apiBase.post("/metrics", { - data: query, - query: { - start, end, step, - "kubernetes_namespace": namespace, - } - }); - }, - - async getMetricProviders(): Promise { - return apiBase.get("/metrics/providers"); + start = now - range; + end = now; } + + return apiBase.post("/metrics", { + data: query, + query: { + start, end, step, + "kubernetes_namespace": namespace, + } + }); +} + +export async function getMetricProviders(): Promise { + return apiBase.get("/metrics/providers"); +} + +export type FlattenedMetrics> = { + [key in keyof M]: [number, string][]; }; +export function flattenMatricResults>(metrics: M): FlattenedMetrics { + return mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values); +} + export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { if (!metrics?.data?.result) { return { @@ -113,8 +121,6 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { const { result } = metrics.data; - console.log(toJS(result)); - if (result.length) { if (frames > 0) { // fill the gaps @@ -126,8 +132,8 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { const now = moment().startOf("minute").subtract(1, "minute").unix(); for ( - let timestamp = res.values[0][0]; - timestamp <= now; + let timestamp = res.values[0][0]; + timestamp <= now; timestamp = moment.unix(timestamp).add(1, "minute").unix() ) { if (!res.values.find((value) => value[0] === timestamp)) { @@ -155,8 +161,8 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { return metrics; } -export function isMetricsEmpty(metrics: Record) { - return !Object.values(metrics).some(metric => metric?.data?.result?.length); +export function isMetricsEmpty(metrics: Record | Falsey) { + return !metrics || !Object.values(metrics).some(metric => metric?.data?.result?.length); } export function getItemMetrics(metrics: Record, itemName: string): Record { @@ -171,7 +177,7 @@ export function getItemMetrics(metrics: Record, itemName: stri if (value?.data?.result) { const result = value.data.result.find(res => res.metric.container === itemName); - value.data.result = [result].filter(Boolean); + value.data.result = result ? [result] : []; return [key, value]; } diff --git a/src/common/k8s-api/endpoints/pods.api.ts b/src/common/k8s-api/endpoints/pods.api.ts index f002260de4..8d9eeda94c 100644 --- a/src/common/k8s-api/endpoints/pods.api.ts +++ b/src/common/k8s-api/endpoints/pods.api.ts @@ -21,7 +21,7 @@ import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autoBind } from "../../utils"; -import { IMetrics, metricsApi } from "./metrics.api"; +import { IMetrics, getMetrics } from "./metrics.api"; import { KubeApi } from "../kube-api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; @@ -38,7 +38,7 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po const podSelector = pods.map(pod => pod.getName()).join("|"); const opts = { category: "pods", pods: podSelector, namespace, selector }; - return metricsApi.getMetrics({ + return getMetrics({ cpuUsage: opts, cpuRequests: opts, cpuLimits: opts, @@ -53,17 +53,17 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po }); } -export interface IPodMetrics { - [metric: string]: T; - cpuUsage: T; - memoryUsage: T; - fsUsage: T; - networkReceive: T; - networkTransmit: T; - cpuRequests?: T; - cpuLimits?: T; - memoryRequests?: T; - memoryLimits?: T; +export interface IPodMetrics { + [metric: string]: IMetrics; + cpuUsage: IMetrics; + memoryUsage: IMetrics; + fsUsage: IMetrics; + networkReceive: IMetrics; + networkTransmit: IMetrics; + cpuRequests?: IMetrics; + cpuLimits?: IMetrics; + memoryRequests?: IMetrics; + memoryLimits?: IMetrics; } // Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 6035b9f408..398348cc7d 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -59,6 +59,7 @@ export * from "./types"; export * from "./convertMemory"; export * from "./convertCpu"; +import * as numbers from "./numbers"; import * as iter from "./iter"; -export { iter }; +export { iter, numbers }; diff --git a/src/common/utils/numbers.ts b/src/common/utils/numbers.ts new file mode 100644 index 0000000000..602ed75e71 --- /dev/null +++ b/src/common/utils/numbers.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Return the clamp of `x` by the bounds `min` and `max`. + * @param x The number to clamp + * @param min The lower bound + * @param max The upper bound + */ +export function clamp(x: number, { min, max }: { min: number, max: number}): number { + return Math.min(max, Math.max(x, min)); +} diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index ba27f1fb6d..99dc4ef063 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -22,16 +22,13 @@ import "./cluster-overview.scss"; import React from "react"; -import { observable, reaction } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; -import { podsStore } from "../+workloads-pods/pods.store"; -import { boundMethod, createStorage, interval } from "../../utils"; +import { boundMethod, createStorage } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; -import { Spinner } from "../spinner"; import { ClusterIssues } from "./cluster-issues"; import { ClusterMetrics } from "./cluster-metrics"; -import { kubeClusterStore } from "./cluster-overview.store"; import { ClusterPieCharts } from "./cluster-pie-charts"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; @@ -88,7 +85,6 @@ export class ClusterOverview extends React.Component { } render() { - const { metrics } = this; const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Cluster); return ( @@ -98,7 +94,7 @@ export class ClusterOverview extends React.Component { diff --git a/src/renderer/components/+network-ingresses/ingress-charts.tsx b/src/renderer/components/+network-ingresses/ingress-charts.tsx index 0811209450..1556470f08 100644 --- a/src/renderer/components/+network-ingresses/ingress-charts.tsx +++ b/src/renderer/components/+network-ingresses/ingress-charts.tsx @@ -23,7 +23,7 @@ import React, { useContext } from "react"; import { observer } from "mobx-react"; import type { ChartOptions, ChartPoint } from "chart.js"; import type { IIngressMetrics, Ingress } from "../../../common/k8s-api/endpoints"; -import { BarChart, memoryOptions } from "../chart"; +import { BarChart, defaultBarChartOptions } from "../chart"; import { normalizeMetrics, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { ResourceMetricsContext, IResourceMetricsValue } from "../resource-metrics"; @@ -52,15 +52,15 @@ export const IngressCharts = observer(() => { [ { id: `${id}-bytesSentSuccess`, - label: `Bytes sent, status 2xx`, - tooltip: `Bytes sent by Ingress controller with successful status`, + label: "Bytes sent, status 2xx", + tooltip: "Bytes sent by Ingress controller with successful status", borderColor: "#46cd9e", data: bytesSentSuccess.map(([x, y]) => ({ x, y })) }, { id: `${id}-bytesSentFailure`, - label: `Bytes sent, status 5xx`, - tooltip: `Bytes sent by Ingress controller with error status`, + label: "Bytes sent, status 5xx", + tooltip: "Bytes sent by Ingress controller with error status", borderColor: "#cd465a", data: bytesSentFailure.map(([x, y]) => ({ x, y })) }, @@ -69,15 +69,15 @@ export const IngressCharts = observer(() => { [ { id: `${id}-requestDurationSeconds`, - label: `Request`, - tooltip: `Request duration in seconds`, + label: "Request", + tooltip: "Request duration in seconds", borderColor: "#48b18d", data: requestDurationSeconds.map(([x, y]) => ({ x, y })) }, { id: `${id}-responseDurationSeconds`, - label: `Response`, - tooltip: `Response duration in seconds`, + label: "Response", + tooltip: "Response duration in seconds", borderColor: "#73ba3c", data: responseDurationSeconds.map(([x, y]) => ({ x, y })) }, @@ -97,7 +97,7 @@ export const IngressCharts = observer(() => { label: ({ datasetIndex, index }, { datasets }) => { const { label, data } = datasets[datasetIndex]; const value = data[index] as ChartPoint; - const chartTooltipSec = `sec`; + const chartTooltipSec = "sec"; return `${label}: ${parseFloat(value.y as string).toFixed(3)} ${chartTooltipSec}`; } @@ -105,7 +105,7 @@ export const IngressCharts = observer(() => { } }; - const options = [memoryOptions, durationOptions]; + const options = [defaultBarChartOptions, durationOptions]; return ( { } @@ -63,42 +64,41 @@ export class IngressDetails extends React.Component { renderPaths(ingress: Ingress) { const { spec: { rules } } = ingress; - if (!rules || !rules.length) return null; + if (!rules) { + return null; + } - return rules.map((rule, index) => { - return ( -
- {rule.host && ( -
- <>Host: {rule.host} -
- )} - {rule.http && ( - - - Path - Backends - - { - rule.http.paths.map((path, index) => { - const { serviceName, servicePort } = getBackendServiceNamePort(path.backend); - const backend = `${serviceName}:${servicePort}`; + return rules.map((rule, index) => ( +
+ {rule.host && ( +
+ Host: {rule.host} +
+ )} + {rule.http && ( +
+ + Path + Backends + + { + rule.http.paths.map((path, index) => { + const { serviceName, servicePort } = getBackendServiceNamePort(path.backend); - return ( - - {path.path || ""} - -

{backend}

-
-
- ); - }) - } -
- )} -
- ); - }); + return ( + + {path.path || ""} + +

{serviceName}:{servicePort}

+
+
+ ); + }) + } + + )} + + )); } renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) { @@ -134,11 +134,6 @@ export class IngressDetails extends React.Component { const { spec, status } = ingress; const ingressPoints = status?.loadBalancer?.ingress; - const { metrics } = this; - const metricTabs = [ - "Network", - "Duration", - ]; const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Ingress); const { serviceName, servicePort } = ingress.getServiceNamePort(); @@ -147,7 +142,9 @@ export class IngressDetails extends React.Component { {!isMetricHidden && ( diff --git a/src/renderer/components/+nodes/node-charts.tsx b/src/renderer/components/+nodes/node-charts.tsx index 6f9495fce5..84b7b20582 100644 --- a/src/renderer/components/+nodes/node-charts.tsx +++ b/src/renderer/components/+nodes/node-charts.tsx @@ -21,7 +21,7 @@ import React, { useContext } from "react"; import type { IClusterMetrics, Node } from "../../../common/k8s-api/endpoints"; -import { BarChart, cpuOptions, memoryOptions } from "../chart"; +import { BarChart, cpuOptions, defaultBarChartOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; @@ -66,29 +66,29 @@ export const NodeCharts = observer(() => { [ { id: `${id}-cpuUsage`, - label: `Usage`, - tooltip: `CPU cores usage`, + label: "Usage", + tooltip: "CPU cores usage", borderColor: "#3D90CE", data: cpuUsage.map(([x, y]) => ({ x, y })) }, { id: `${id}-cpuRequests`, - label: `Requests`, - tooltip: `CPU requests`, + label: "Requests", + tooltip: "CPU requests", borderColor: "#30b24d", data: cpuRequests.map(([x, y]) => ({ x, y })) }, { id: `${id}-cpuAllocatableCapacity`, - label: `Allocatable Capacity`, - tooltip: `CPU allocatable capacity`, + label: "Allocatable Capacity", + tooltip: "CPU allocatable capacity", borderColor: "#032b4d", data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y })) }, { id: `${id}-cpuCapacity`, - label: `Capacity`, - tooltip: `CPU capacity`, + label: "Capacity", + tooltip: "CPU capacity", borderColor: chartCapacityColor, data: cpuCapacity.map(([x, y]) => ({ x, y })) } @@ -97,36 +97,36 @@ export const NodeCharts = observer(() => { [ { id: `${id}-memoryUsage`, - label: `Usage`, - tooltip: `Memory usage`, + label: "Usage", + tooltip: "Memory usage", borderColor: "#c93dce", data: memoryUsage.map(([x, y]) => ({ x, y })) }, { id: `${id}-workloadMemoryUsage`, - label: `Workload Memory Usage`, - tooltip: `Workload memory usage`, + label: "Workload Memory Usage", + tooltip: "Workload memory usage", borderColor: "#9cd3ce", data: workloadMemoryUsage.map(([x, y]) => ({ x, y })) }, { id: "memoryRequests", - label: `Requests`, - tooltip: `Memory requests`, + label: "Requests", + tooltip: "Memory requests", borderColor: "#30b24d", data: memoryRequests.map(([x, y]) => ({ x, y })) }, { id: `${id}-memoryAllocatableCapacity`, - label: `Allocatable Capacity`, - tooltip: `Memory allocatable capacity`, + label: "Allocatable Capacity", + tooltip: "Memory allocatable capacity", borderColor: "#032b4d", data: memoryAllocatableCapacity.map(([x, y]) => ({ x, y })) }, { id: `${id}-memoryCapacity`, - label: `Capacity`, - tooltip: `Memory capacity`, + label: "Capacity", + tooltip: "Memory capacity", borderColor: chartCapacityColor, data: memoryCapacity.map(([x, y]) => ({ x, y })) } @@ -135,15 +135,15 @@ export const NodeCharts = observer(() => { [ { id: `${id}-fsUsage`, - label: `Usage`, - tooltip: `Node filesystem usage in bytes`, + label: "Usage", + tooltip: "Node filesystem usage in bytes", borderColor: "#ffc63d", data: fsUsage.map(([x, y]) => ({ x, y })) }, { id: `${id}-fsSize`, - label: `Size`, - tooltip: `Node filesystem size in bytes`, + label: "Size", + tooltip: "Node filesystem size in bytes", borderColor: chartCapacityColor, data: fsSize.map(([x, y]) => ({ x, y })) } @@ -152,15 +152,15 @@ export const NodeCharts = observer(() => { [ { id: `${id}-podUsage`, - label: `Usage`, - tooltip: `Number of running Pods`, + label: "Usage", + tooltip: "Number of running Pods", borderColor: "#30b24d", data: podUsage.map(([x, y]) => ({ x, y })) }, { id: `${id}-podCapacity`, - label: `Capacity`, - tooltip: `Node Pods capacity`, + label: "Capacity", + tooltip: "Node Pods capacity", borderColor: chartCapacityColor, data: podCapacity.map(([x, y]) => ({ x, y })) } @@ -187,7 +187,7 @@ export const NodeCharts = observer(() => { } }; - const options = [cpuOptions, memoryOptions, memoryOptions, podOptions]; + const options = [cpuOptions, defaultBarChartOptions, defaultBarChartOptions, podOptions]; return ( { } @@ -78,13 +79,6 @@ export class NodeDetails extends React.Component { const conditions = node.getActiveConditions(); const taints = node.getTaints(); const childPods = podsStore.getPodsByNode(node.getName()); - const { metrics } = this; - const metricTabs = [ - "CPU", - "Memory", - "Disk", - "Pods", - ]; const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Node); return ( @@ -92,7 +86,9 @@ export class NodeDetails extends React.Component { {!isMetricHidden && podsStore.isLoaded && ( diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index 4d515a0056..f61d8739d1 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -69,7 +69,6 @@ export class PersistentVolumeClaimDetails extends React.Component { return null; } const { storageClassName, accessModes } = volumeClaim.spec; - const { metrics } = this; const pods = volumeClaim.getPods(podsStore.items); const metricTabs = [ "Disk" @@ -81,12 +80,12 @@ export class PersistentVolumeClaimDetails extends React.Component { {!isMetricHidden && ( - + )} - + {accessModes.join(", ")} @@ -107,10 +106,10 @@ export class PersistentVolumeClaimDetails extends React.Component { {volumeClaim.getStatus()} - + - {volumeClaim.getMatchLabels().map(label => )} + {volumeClaim.getMatchLabels().map(label => )} diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx index 04e888e4f1..cac6369e25 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx @@ -22,7 +22,7 @@ import React, { useContext } from "react"; import { observer } from "mobx-react"; import type { IPvcMetrics, PersistentVolumeClaim } from "../../../common/k8s-api/endpoints"; -import { BarChart, ChartDataSets, memoryOptions } from "../chart"; +import { BarChart, ChartDataSets, defaultBarChartOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; @@ -45,15 +45,15 @@ export const VolumeClaimDiskChart = observer(() => { const datasets: ChartDataSets[] = [ { id: `${id}-diskUsage`, - label: `Usage`, - tooltip: `Volume disk usage`, + label: "Usage", + tooltip: "Volume disk usage", borderColor: "#ffc63d", data: usage.map(([x, y]) => ({ x, y })) }, { id: `${id}-diskCapacity`, - label: `Capacity`, - tooltip: `Volume disk capacity`, + label: "Capacity", + tooltip: "Volume disk capacity", borderColor: chartCapacityColor, data: capacity.map(([x, y]) => ({ x, y })) } @@ -64,7 +64,7 @@ export const VolumeClaimDiskChart = observer(() => { className="VolumeClaimDiskChart flex box grow column" name={`pvc-${object.getName()}-disk`} timeLabelStep={10} - options={memoryOptions} + options={defaultBarChartOptions} data={{ datasets }} /> ); diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 92615e54ef..97a7624247 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -33,13 +33,14 @@ import { podsStore } from "../+workloads-pods/pods.store"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { DaemonSet, getMetricsForDaemonSets, IPodMetrics } from "../../../common/k8s-api/endpoints"; import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics"; -import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; +import { PodCharts } from "../+workloads-pods/pod-charts"; import { makeObservable, observable, reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; +import { podMetricTabs } from "../metrics-helpers"; interface Props extends KubeObjectDetailsProps { } @@ -85,7 +86,9 @@ export class DaemonSetDetails extends React.Component { {!isMetricHidden && podsStore.isLoaded && ( diff --git a/src/renderer/components/+workloads-pods/container-charts.tsx b/src/renderer/components/+workloads-pods/container-charts.tsx index 9586d7bf7c..54af6eb63c 100644 --- a/src/renderer/components/+workloads-pods/container-charts.tsx +++ b/src/renderer/components/+workloads-pods/container-charts.tsx @@ -22,22 +22,15 @@ import React, { useContext } from "react"; import { observer } from "mobx-react"; import type { IPodMetrics } from "../../../common/k8s-api/endpoints"; -import { BarChart, cpuOptions, memoryOptions } from "../chart"; -import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; +import { BarChart, ChartDataSets } from "../chart"; +import { FlattenedMetrics, flattenMatricResults, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; -import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; +import { ResourceMetricsContext } from "../resource-metrics"; import { ThemeStore } from "../../theme.store"; -import { mapValues } from "lodash"; +import { ContainerMetricsTab, getBarChartOptions } from "../metrics-helpers"; -type IContext = IResourceMetricsValue; - -export const ContainerCharts = observer(() => { - const { params: { metrics }, tabId } = useContext(ResourceMetricsContext); +function getDatasets(tab: string, metrics: FlattenedMetrics): ChartDataSets[] | null { const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; - - if (!metrics) return null; - if (isMetricsEmpty(metrics)) return ; - const { cpuUsage, cpuRequests, @@ -46,76 +39,90 @@ export const ContainerCharts = observer(() => { memoryRequests, memoryLimits, fsUsage - } = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values); + } = metrics; - const datasets = [ - // CPU - [ - { - id: "cpuUsage", - label: `Usage`, - tooltip: `CPU cores usage`, - borderColor: "#3D90CE", - data: cpuUsage.map(([x, y]) => ({ x, y })) - }, - { - id: "cpuRequests", - label: `Requests`, - tooltip: `CPU requests`, - borderColor: "#30b24d", - data: cpuRequests.map(([x, y]) => ({ x, y })) - }, - { - id: "cpuLimits", - label: `Limits`, - tooltip: `CPU limits`, - borderColor: chartCapacityColor, - data: cpuLimits.map(([x, y]) => ({ x, y })) - } - ], - // Memory - [ - { - id: "memoryUsage", - label: `Usage`, - tooltip: `Memory usage`, - borderColor: "#c93dce", - data: memoryUsage.map(([x, y]) => ({ x, y })) - }, - { - id: "memoryRequests", - label: `Requests`, - tooltip: `Memory requests`, - borderColor: "#30b24d", - data: memoryRequests.map(([x, y]) => ({ x, y })) - }, - { - id: "memoryLimits", - label: `Limits`, - tooltip: `Memory limits`, - borderColor: chartCapacityColor, - data: memoryLimits.map(([x, y]) => ({ x, y })) - } - ], - // Filesystem - [ - { - id: "fsUsage", - label: `Usage`, - tooltip: `Bytes consumed on this filesystem`, - borderColor: "#ffc63d", - data: fsUsage.map(([x, y]) => ({ x, y })) - } - ] - ]; + switch (tab) { + case ContainerMetricsTab.CPU: + return [ + { + id: "cpuUsage", + label: "Usage", + tooltip: "CPU cores usage", + borderColor: "#3D90CE", + data: cpuUsage.map(([x, y]) => ({ x, y })) + }, + { + id: "cpuRequests", + label: "Requests", + tooltip: "CPU requests", + borderColor: "#30b24d", + data: cpuRequests.map(([x, y]) => ({ x, y })) + }, + { + id: "cpuLimits", + label: "Limits", + tooltip: "CPU limits", + borderColor: chartCapacityColor, + data: cpuLimits.map(([x, y]) => ({ x, y })) + } + ]; + case ContainerMetricsTab.MEMORY: + return [ + { + id: "memoryUsage", + label: "Usage", + tooltip: "Memory usage", + borderColor: "#c93dce", + data: memoryUsage.map(([x, y]) => ({ x, y })) + }, + { + id: "memoryRequests", + label: "Requests", + tooltip: "Memory requests", + borderColor: "#30b24d", + data: memoryRequests.map(([x, y]) => ({ x, y })) + }, + { + id: "memoryLimits", + label: "Limits", + tooltip: "Memory limits", + borderColor: chartCapacityColor, + data: memoryLimits.map(([x, y]) => ({ x, y })) + } + ]; + case ContainerMetricsTab.FILESYSTEM: + return [ + { + id: "fsUsage", + label: "Usage", + tooltip: "Bytes consumed on this filesystem", + borderColor: "#ffc63d", + data: fsUsage.map(([x, y]) => ({ x, y })) + } + ]; + default: + return null; + } +} - const options = tabId == 0 ? cpuOptions : memoryOptions; +export const ContainerCharts = observer(() => { + const { metrics, tab } = useContext(ResourceMetricsContext); + + if (isMetricsEmpty(metrics)) { + return ; + } + + const datasets = getDatasets(tab, flattenMatricResults(metrics as IPodMetrics)); + + if (!datasets) { + return ; + } return ( ); }); diff --git a/src/renderer/components/+workloads-pods/pod-charts.tsx b/src/renderer/components/+workloads-pods/pod-charts.tsx index 4caf12fbbd..f4497b4da4 100644 --- a/src/renderer/components/+workloads-pods/pod-charts.tsx +++ b/src/renderer/components/+workloads-pods/pod-charts.tsx @@ -19,97 +19,97 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { mapValues } from "lodash"; import { observer } from "mobx-react"; import React, { useContext } from "react"; -import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; -import { BarChart, cpuOptions, memoryOptions } from "../chart"; -import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; +import { FlattenedMetrics, flattenMatricResults, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api"; +import { BarChart, ChartDataSets } from "../chart"; +import { ResourceMetricsContext } from "../resource-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics"; - -import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object"; import type { IPodMetrics } from "../../../common/k8s-api/endpoints"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { getBarChartOptions, PodMetricsTab } from "../metrics-helpers"; -export const podMetricTabs = [ - "CPU", - "Memory", - "Network", - "Filesystem", -]; - -type IContext = IResourceMetricsValue; - -export const PodCharts = observer(() => { - const { params: { metrics }, tabId, object } = useContext(ResourceMetricsContext); +function getDatasets(object: KubeObject, tab: string, metrics: FlattenedMetrics): ChartDataSets[] | null { const id = object.getId(); - - if (!metrics) return null; - if (isMetricsEmpty(metrics)) return ; - - const options = tabId == 0 ? cpuOptions : memoryOptions; const { cpuUsage, memoryUsage, - fsUsage, networkReceive, - networkTransmit - } = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values); + networkTransmit, + fsUsage, + } = metrics; - const datasets = [ - // CPU - [ - { - id: `${id}-cpuUsage`, - label: `Usage`, - tooltip: `Container CPU cores usage`, - borderColor: "#3D90CE", - data: cpuUsage.map(([x, y]) => ({ x, y })) - } - ], - // Memory - [ - { - id: `${id}-memoryUsage`, - label: `Usage`, - tooltip: `Container memory usage`, - borderColor: "#c93dce", - data: memoryUsage.map(([x, y]) => ({ x, y })) - }, - ], - // Network - [ - { - id: `${id}-networkReceive`, - label: `Receive`, - tooltip: `Bytes received by all containers`, - borderColor: "#64c5d6", - data: networkReceive.map(([x, y]) => ({ x, y })) - }, - { - id: `${id}-networkTransmit`, - label: `Transmit`, - tooltip: `Bytes transmitted from all containers`, - borderColor: "#46cd9e", - data: networkTransmit.map(([x, y]) => ({ x, y })) - } - ], - // Filesystem - [ - { - id: `${id}-fsUsage`, - label: `Usage`, - tooltip: `Bytes consumed on this filesystem`, - borderColor: "#ffc63d", - data: fsUsage.map(([x, y]) => ({ x, y })) - } - ] - ]; + switch (tab) { + case PodMetricsTab.CPU: + return [ + { + id: `${id}-cpuUsage`, + label: "Usage", + tooltip: "Container CPU cores usage", + borderColor: "#3D90CE", + data: cpuUsage.map(([x, y]) => ({ x, y })) + } + ]; + case PodMetricsTab.MEMORY: + return [ + { + id: `${id}-memoryUsage`, + label: "Usage", + tooltip: "Container memory usage", + borderColor: "#c93dce", + data: memoryUsage.map(([x, y]) => ({ x, y })) + }, + ]; + case PodMetricsTab.NETWORK: + return [ + { + id: `${id}-networkReceive`, + label: "Receive", + tooltip: "Bytes received by all containers", + borderColor: "#64c5d6", + data: networkReceive.map(([x, y]) => ({ x, y })) + }, + { + id: `${id}-networkTransmit`, + label: "Transmit", + tooltip: "Bytes transmitted from all containers", + borderColor: "#46cd9e", + data: networkTransmit.map(([x, y]) => ({ x, y })) + } + ]; + case PodMetricsTab.FILESYSTEM: + return [ + { + id: `${id}-fsUsage`, + label: "Usage", + tooltip: "Bytes consumed on this filesystem", + borderColor: "#ffc63d", + data: fsUsage.map(([x, y]) => ({ x, y })) + } + ]; + default: + return null; + } +} + +export const PodCharts = observer(() => { + const { metrics, tab, object } = useContext(ResourceMetricsContext); + + if (isMetricsEmpty(metrics)) { + return ; + } + + const datasets = getDatasets(object, tab, flattenMatricResults(metrics as IPodMetrics)); + + if (!datasets) { + return ; + } return ( ); }); diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index b4a4c5eaf2..b63de4b136 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -37,6 +37,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { portForwardStore } from "../../port-forward/port-forward.store"; import { disposeOnUnmount, observer } from "mobx-react"; +import { containerMetricTabs } from "../metrics-helpers"; interface Props { pod: Pod; @@ -58,7 +59,7 @@ export class PodDetailsContainer extends React.Component { return ( - {state}{ready ? `, ready` : ""} + {state}{ready ? ", ready" : ""} {state === "terminated" ? ` - ${status.state.terminated.reason} (exit code: ${status.state.terminated.exitCode})` : ""} ); @@ -68,10 +69,10 @@ export class PodDetailsContainer extends React.Component { if (lastState === "terminated") { return ( - {lastState}
- Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}
- Started at: {}
- Finished at: {}
+ {lastState}
+ Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}
+ Started at: {}
+ Finished at: {}
); } @@ -88,113 +89,111 @@ export class PodDetailsContainer extends React.Component { const state = status ? Object.keys(status.state)[0] : ""; const lastState = status ? Object.keys(status.lastState)[0] : ""; const ready = status ? status.ready : ""; - const imageId = status? status.imageID : ""; + const imageId = status ? status.imageID : ""; const liveness = pod.getLivenessProbe(container); const readiness = pod.getReadinessProbe(container); const startup = pod.getStartupProbe(container); const isInitContainer = !!pod.getInitContainers().find(c => c.name == name); - const metricTabs = [ - "CPU", - "Memory", - "Filesystem", - ]; const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Container); return (
- {name} + {name}
{!isMetricHidden && !isInitContainer && - - - + + + } {status && - - {this.renderStatus(state, status)} - + + {this.renderStatus(state, status)} + } {lastState && - - {this.renderLastState(lastState, status)} - + + {this.renderLastState(lastState, status)} + } - + {imagePullPolicy && imagePullPolicy !== "IfNotPresent" && - - {imagePullPolicy} - + + {imagePullPolicy} + } {ports && ports.length > 0 && - - { - ports.map((port) => { - const key = `${container.name}-port-${port.containerPort}-${port.protocol}`; + + { + ports.map((port) => { + const key = `${container.name}-port-${port.containerPort}-${port.protocol}`; - return ( - - ); - }) - } - + return ( + + ); + }) + } + } - {} + {} {volumeMounts && volumeMounts.length > 0 && - - { - volumeMounts.map(mount => { - const { name, mountPath, readOnly } = mount; + + { + volumeMounts.map(mount => { + const { name, mountPath, readOnly } = mount; - return ( - - {mountPath} - from {name} ({readOnly ? "ro" : "rw"}) - - ); - }) - } - + return ( + + {mountPath} + from {name} ({readOnly ? "ro" : "rw"}) + + ); + }) + } + } {liveness.length > 0 && - - { - liveness.map((value, index) => ( - - )) - } - + + { + liveness.map((value, index) => ( + + )) + } + } {readiness.length > 0 && - - { - readiness.map((value, index) => ( - - )) - } - + + { + readiness.map((value, index) => ( + + )) + } + } {startup.length > 0 && - - { - startup.map((value, index) => ( - - )) - } - + + { + startup.map((value, index) => ( + + )) + } + } {command && - - {command.join(" ")} - + + {command.join(" ")} + } {args && - - {args.join(" ")} - + + {args.join(" ")} + }
); diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 08f7dab3ce..1b6f4e9ba2 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -82,7 +82,6 @@ export class PodDetails extends React.Component { } const { status: { conditions, podIP }, spec: { nodeName } } = pod; - const { metrics } = this; const podIPs = pod.getIPs(); const nodeSelector = pod.getNodeSelectors(); const volumes = pod.getVolumes(); @@ -96,7 +95,7 @@ export class PodDetails extends React.Component { loader={this.loadMetrics} tabs={podMetricTabs} object={pod} - params={{ metrics }} + metrics={this.metrics} >
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index 7da1e22361..261a7b6ac2 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -72,7 +72,6 @@ export class ReplicaSetDetails extends React.Component { const { object: replicaSet } = this.props; if (!replicaSet) return null; - const { metrics } = this; const { status } = replicaSet; const { availableReplicas, replicas } = status; const selectors = replicaSet.getSelectors(); @@ -86,7 +85,7 @@ export class ReplicaSetDetails extends React.Component { {!isMetricHidden && podsStore.isLoaded && ( @@ -121,7 +120,7 @@ export class ReplicaSetDetails extends React.Component { - + ); diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index b5946b6330..12aecff9ac 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -26,7 +26,7 @@ import Color from "color"; import { observer } from "mobx-react"; import type { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js"; import { Chart, ChartKind, ChartProps } from "./chart"; -import { bytesToUnits, cssNames } from "../../utils"; +import { bytesToUnits, cssNames, numbers } from "../../utils"; import { ZebraStripes } from "./zebra-stripes.plugin"; import { ThemeStore } from "../../theme.store"; import { NoMetrics } from "../resource-metrics/no-metrics"; @@ -144,12 +144,10 @@ export class BarChart extends React.Component { return String(xLabel); }, - labelColor: ({ datasetIndex }) => { - return { - borderColor: "darkgray", - backgroundColor: chartData.datasets[datasetIndex].borderColor as string - }; - } + labelColor: ({ datasetIndex }) => ({ + borderColor: "darkgray", + backgroundColor: chartData.datasets[datasetIndex].borderColor as string + }) } }, animation: { @@ -183,7 +181,7 @@ export class BarChart extends React.Component { } // Default options for all charts containing memory units (network, disk, memory, etc) -export const memoryOptions: ChartOptions = { +const memoryUnits: ChartOptions = { scales: { yAxes: [{ ticks: { @@ -217,18 +215,18 @@ export const memoryOptions: ChartOptions = { }; // Default options for all charts with cpu units or other decimal numbers -export const cpuOptions: ChartOptions = { +const decimalUnits: ChartOptions = { scales: { yAxes: [{ ticks: { callback: (value: number | string): string => { - const float = parseFloat(`${value}`); + const parsed = typeof value === "string" + ? parseFloat(value) + : value; + const magnitude = numbers.clamp(Math.floor(Math.log10(parsed)), { min: 0, max: 2 }); + const precision = 3 - magnitude; - if (float == 0) return "0"; - if (float < 10) return float.toFixed(3); - if (float < 100) return float.toFixed(2); - - return float.toFixed(1); + return parsed.toFixed(precision); } } }] @@ -244,3 +242,8 @@ export const cpuOptions: ChartOptions = { } } }; + +export const barChartOptions = Object.freeze({ + decimalUnits, + memoryUnits, +}); diff --git a/src/renderer/components/metrics-helpers/index.ts b/src/renderer/components/metrics-helpers/index.ts new file mode 100644 index 0000000000..34453554b5 --- /dev/null +++ b/src/renderer/components/metrics-helpers/index.ts @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { barChartOptions } from "../chart"; + +/** + * The CPU metrics tab + */ +enum CPUTab { + CPU = "CPU" +} + +/** + * The Memory metrics tab + */ +enum MemoryTab { + MEMORY = "Memory" +} + +/** + * The Filesystem metrics tab + */ +enum FilesystemTab { + FILESYSTEM = "Filesystem" +} + +/** + * The Disk metrics tab + */ +enum DiskTab { + DISK = "Disk" +} + +/** + * The Network metrics tab + */ +enum NetworkTab { + NETWORK = "Network" +} + +/** + * The Duration metrics tab + */ +enum DurationTab { + DURATION = "Duration" +} + +/** + * The Pods metrics tab + */ +enum PodsTab { + PODS = "Pods" +} + +export type PodMetricsTab = typeof PodMetricsTab; +export const PodMetricsTab = { + ...CPUTab, + ...MemoryTab, + ...NetworkTab, + ...FilesystemTab, +}; +export const podMetricTabs = Object.values(PodMetricsTab); + +export type ContainerMetricsTab = typeof ContainerMetricsTab; +export const ContainerMetricsTab = { + ...CPUTab, + ...MemoryTab, + ...FilesystemTab, +}; +export const containerMetricTabs = Object.values(ContainerMetricsTab); + +export type NodeDetailsMetricsTab = typeof NodeDetailsMetricsTab; +export const NodeDetailsMetricsTab = { + ...CPUTab, + ...MemoryTab, + ...DiskTab, + ...PodsTab, +}; +export const nodeDetailsMetricTabs = Object.values(NodeDetailsMetricsTab); + +export type IngressMetricsTab = typeof IngressMetricsTab; +export const IngressMetricsTab = { + ...NetworkTab, + ...DurationTab, +}; +export const ingressMetricTabs = Object.values(IngressMetricsTab); + +/** + * Get the bar chart options for a specific chart tab + * @param tab The tab ID + * @returns Bar chart options for that metrics tab + */ +export function getBarChartOptions(tab: string) { + switch (tab) { + case CPUTab.CPU: + return barChartOptions.decimalUnits; + default: + return barChartOptions.memoryUnits; + } +} diff --git a/src/renderer/components/resource-metrics/resource-metrics.tsx b/src/renderer/components/resource-metrics/resource-metrics.tsx index dbb7f7a37d..3cae37dd03 100644 --- a/src/renderer/components/resource-metrics/resource-metrics.tsx +++ b/src/renderer/components/resource-metrics/resource-metrics.tsx @@ -27,33 +27,34 @@ import { useInterval } from "../../hooks"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { cssNames, IClassName } from "../../utils"; import { Spinner } from "../spinner"; +import type { IMetrics } from "../../../common/k8s-api/endpoints"; -interface Props { - tabs: string[] | string[][]; +interface Props { + tabs: string[]; object?: KubeObject; loader?: () => void; interval?: number; className?: IClassName; - params?: T; + metrics?: Record; children?: React.ReactNode; } -export type IResourceMetricsValue = { - object: T; - tabId: number; - params?: P; +export type IResourceMetricsValue = Record> = { + object?: K; + tab: string; + metrics?: Metrics; }; export const ResourceMetricsContext = createContext(null); -const defaultProps: Partial> = { +const defaultProps: Partial = { interval: 60 // 1 min }; ResourceMetrics.defaultProps = defaultProps; -export function ResourceMetrics({ object, loader, interval, tabs, children, className, params }: Props) { - const [tabId, setTabId] = useState(0); +export function ResourceMetrics({ object, loader, interval, tabs, children, className, metrics }: Props) { + const [tab, setTab] = useState(tabs[0]); useEffect(() => { if (loader) loader(); @@ -69,15 +70,15 @@ export function ResourceMetrics({ object, loader, interval, tabs, children, c setTabId(tabs.findIndex(tab => tab == value))} + value={tab} + onChange={value => setTab(value)} > {tabs.map((tab, index) => ( ))} - +
{children}