1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

more work

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-10-15 16:45:54 -04:00
parent 1508aa9999
commit 70d8eeb3c8
24 changed files with 588 additions and 436 deletions

View File

@ -69,7 +69,6 @@ module.exports = {
], ],
"quotes": ["error", "double", { "quotes": ["error", "double", {
"avoidEscape": true, "avoidEscape": true,
"allowTemplateLiterals": true,
}], }],
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
@ -128,7 +127,6 @@ module.exports = {
}], }],
"quotes": ["error", "double", { "quotes": ["error", "double", {
"avoidEscape": true, "avoidEscape": true,
"allowTemplateLiterals": true,
}], }],
"react/prop-types": "off", "react/prop-types": "off",
"semi": "off", "semi": "off",
@ -196,7 +194,6 @@ module.exports = {
}], }],
"quotes": ["error", "double", { "quotes": ["error", "double", {
"avoidEscape": true, "avoidEscape": true,
"allowTemplateLiterals": true,
}], }],
"react/prop-types": "off", "react/prop-types": "off",
"semi": "off", "semi": "off",

View File

@ -23,10 +23,10 @@ import get from "lodash/get";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import type { IPodContainer, IPodMetrics } from "./pods.api"; import type { IPodContainer, IPodMetrics } from "./pods.api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import { getMetrics } from "./metrics.api";
export class DaemonSet extends WorkloadKubeObject { export class DaemonSet extends WorkloadKubeObject {
static kind = "DaemonSet"; 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 podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({ return getMetrics({
cpuUsage: opts, cpuUsage: opts,
memoryUsage: opts, memoryUsage: opts,
fsUsage: opts, fsUsage: opts,

View File

@ -22,9 +22,9 @@
// Kubernetes apis // Kubernetes apis
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/ // 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-binding.api";
export * from "./cluster-role.api";
export * from "./cluster.api";
export * from "./configmap.api"; export * from "./configmap.api";
export * from "./crd.api"; export * from "./crd.api";
export * from "./cron-job.api"; export * from "./cron-job.api";
@ -36,22 +36,23 @@ export * from "./hpa.api";
export * from "./ingress.api"; export * from "./ingress.api";
export * from "./job.api"; export * from "./job.api";
export * from "./limit-range.api"; export * from "./limit-range.api";
export * from "./metrics.api";
export * from "./namespaces.api"; export * from "./namespaces.api";
export * from "./network-policy.api"; export * from "./network-policy.api";
export * from "./nodes.api"; export * from "./nodes.api";
export * from "./persistent-volume.api";
export * from "./persistent-volume-claims.api"; export * from "./persistent-volume-claims.api";
export * from "./pods.api"; export * from "./persistent-volume.api";
export * from "./poddisruptionbudget.api";
export * from "./pod-metrics.api"; export * from "./pod-metrics.api";
export * from "./poddisruptionbudget.api";
export * from "./pods.api";
export * from "./podsecuritypolicy.api"; export * from "./podsecuritypolicy.api";
export * from "./replica-set.api"; export * from "./replica-set.api";
export * from "./resource-quota.api"; export * from "./resource-quota.api";
export * from "./role.api";
export * from "./role-binding.api"; export * from "./role-binding.api";
export * from "./role.api";
export * from "./secret.api"; export * from "./secret.api";
export * from "./selfsubjectrulesreviews.api"; export * from "./selfsubjectrulesreviews.api";
export * from "./service.api";
export * from "./service-accounts.api"; export * from "./service-accounts.api";
export * from "./service.api";
export * from "./stateful-set.api"; export * from "./stateful-set.api";
export * from "./storage-class.api"; export * from "./storage-class.api";

View File

@ -21,7 +21,7 @@
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, getMetrics } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
@ -32,7 +32,7 @@ export class IngressApi extends KubeApi<Ingress> {
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> { export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
const opts = { category: "ingress", ingress }; const opts = { category: "ingress", ingress };
return metricsApi.getMetrics({ return getMetrics({
bytesSentSuccess: opts, bytesSentSuccess: opts,
bytesSentFailure: opts, bytesSentFailure: opts,
requestDurationSeconds: opts, requestDurationSeconds: opts,
@ -42,12 +42,12 @@ export function getMetricsForIngress(ingress: string, namespace: string): Promis
}); });
} }
export interface IIngressMetrics<T = IMetrics> { export interface IIngressMetrics {
[metric: string]: T; [metric: string]: IMetrics;
bytesSentSuccess: T; bytesSentSuccess: IMetrics;
bytesSentFailure: T; bytesSentFailure: IMetrics;
requestDurationSeconds: T; requestDurationSeconds: IMetrics;
responseDurationSeconds: T; responseDurationSeconds: IMetrics;
} }
export interface ILoadBalancerIngress { export interface ILoadBalancerIngress {

View File

@ -24,7 +24,9 @@
import moment from "moment"; import moment from "moment";
import { apiBase } from "../index"; import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics-route"; 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 { export interface IMetrics {
status: string; status: string;
@ -70,8 +72,7 @@ export interface IResourceMetrics<T extends IMetrics> {
networkTransmit: T; networkTransmit: T;
} }
export const metricsApi = { export async function getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
const { range = 3600, step = 60, namespace } = reqParams; const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = reqParams; let { start, end } = reqParams;
@ -90,13 +91,20 @@ export const metricsApi = {
"kubernetes_namespace": namespace, "kubernetes_namespace": namespace,
} }
}); });
}, }
async getMetricProviders(): Promise<MetricProviderInfo[]> { export async function getMetricProviders(): Promise<MetricProviderInfo[]> {
return apiBase.get("/metrics/providers"); return apiBase.get("/metrics/providers");
} }
export type FlattenedMetrics<M extends Record<string, IMetrics>> = {
[key in keyof M]: [number, string][];
}; };
export function flattenMatricResults<M extends Record<string, IMetrics>>(metrics: M): FlattenedMetrics<M> {
return mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
}
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
if (!metrics?.data?.result) { if (!metrics?.data?.result) {
return { return {
@ -113,8 +121,6 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
const { result } = metrics.data; const { result } = metrics.data;
console.log(toJS(result));
if (result.length) { if (result.length) {
if (frames > 0) { if (frames > 0) {
// fill the gaps // fill the gaps
@ -155,8 +161,8 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
return metrics; return metrics;
} }
export function isMetricsEmpty(metrics: Record<string, IMetrics>) { export function isMetricsEmpty(metrics: Record<string, IMetrics> | Falsey) {
return !Object.values(metrics).some(metric => metric?.data?.result?.length); return !metrics || !Object.values(metrics).some(metric => metric?.data?.result?.length);
} }
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> { export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> {
@ -171,7 +177,7 @@ export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: stri
if (value?.data?.result) { if (value?.data?.result) {
const result = value.data.result.find(res => res.metric.container === itemName); 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]; return [key, value];
} }

View File

@ -21,7 +21,7 @@
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api"; import { IMetrics, getMetrics } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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 podSelector = pods.map(pod => pod.getName()).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({ return getMetrics({
cpuUsage: opts, cpuUsage: opts,
cpuRequests: opts, cpuRequests: opts,
cpuLimits: opts, cpuLimits: opts,
@ -53,17 +53,17 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po
}); });
} }
export interface IPodMetrics<T = IMetrics> { export interface IPodMetrics {
[metric: string]: T; [metric: string]: IMetrics;
cpuUsage: T; cpuUsage: IMetrics;
memoryUsage: T; memoryUsage: IMetrics;
fsUsage: T; fsUsage: IMetrics;
networkReceive: T; networkReceive: IMetrics;
networkTransmit: T; networkTransmit: IMetrics;
cpuRequests?: T; cpuRequests?: IMetrics;
cpuLimits?: T; cpuLimits?: IMetrics;
memoryRequests?: T; memoryRequests?: IMetrics;
memoryLimits?: T; memoryLimits?: IMetrics;
} }
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core // Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core

View File

@ -59,6 +59,7 @@ export * from "./types";
export * from "./convertMemory"; export * from "./convertMemory";
export * from "./convertCpu"; export * from "./convertCpu";
import * as numbers from "./numbers";
import * as iter from "./iter"; import * as iter from "./iter";
export { iter }; export { iter, numbers };

View File

@ -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));
}

View File

@ -22,16 +22,13 @@
import "./cluster-overview.scss"; import "./cluster-overview.scss";
import React from "react"; import React from "react";
import { observable, reaction } from "mobx"; import { observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store"; import { boundMethod, createStorage } from "../../utils";
import { boundMethod, createStorage, interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner";
import { ClusterIssues } from "./cluster-issues"; import { ClusterIssues } from "./cluster-issues";
import { ClusterMetrics } from "./cluster-metrics"; import { ClusterMetrics } from "./cluster-metrics";
import { kubeClusterStore } from "./cluster-overview.store";
import { ClusterPieCharts } from "./cluster-pie-charts"; import { ClusterPieCharts } from "./cluster-pie-charts";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
@ -88,7 +85,6 @@ export class ClusterOverview extends React.Component {
} }
render() { render() {
const { metrics } = this;
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Cluster); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Cluster);
return ( return (
@ -98,7 +94,7 @@ export class ClusterOverview extends React.Component {
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={[MetricType.CPU, MetricType.MEMORY]} tabs={[MetricType.CPU, MetricType.MEMORY]}
params={{ metrics }} metrics={this.metrics}
> >
<ClusterMetrics /> <ClusterMetrics />
<ClusterPieCharts /> <ClusterPieCharts />

View File

@ -23,7 +23,7 @@ import React, { useContext } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js"; import type { ChartOptions, ChartPoint } from "chart.js";
import type { IIngressMetrics, Ingress } from "../../../common/k8s-api/endpoints"; 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 { normalizeMetrics, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext, IResourceMetricsValue } from "../resource-metrics"; import { ResourceMetricsContext, IResourceMetricsValue } from "../resource-metrics";
@ -52,15 +52,15 @@ export const IngressCharts = observer(() => {
[ [
{ {
id: `${id}-bytesSentSuccess`, id: `${id}-bytesSentSuccess`,
label: `Bytes sent, status 2xx`, label: "Bytes sent, status 2xx",
tooltip: `Bytes sent by Ingress controller with successful status`, tooltip: "Bytes sent by Ingress controller with successful status",
borderColor: "#46cd9e", borderColor: "#46cd9e",
data: bytesSentSuccess.map(([x, y]) => ({ x, y })) data: bytesSentSuccess.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-bytesSentFailure`, id: `${id}-bytesSentFailure`,
label: `Bytes sent, status 5xx`, label: "Bytes sent, status 5xx",
tooltip: `Bytes sent by Ingress controller with error status`, tooltip: "Bytes sent by Ingress controller with error status",
borderColor: "#cd465a", borderColor: "#cd465a",
data: bytesSentFailure.map(([x, y]) => ({ x, y })) data: bytesSentFailure.map(([x, y]) => ({ x, y }))
}, },
@ -69,15 +69,15 @@ export const IngressCharts = observer(() => {
[ [
{ {
id: `${id}-requestDurationSeconds`, id: `${id}-requestDurationSeconds`,
label: `Request`, label: "Request",
tooltip: `Request duration in seconds`, tooltip: "Request duration in seconds",
borderColor: "#48b18d", borderColor: "#48b18d",
data: requestDurationSeconds.map(([x, y]) => ({ x, y })) data: requestDurationSeconds.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-responseDurationSeconds`, id: `${id}-responseDurationSeconds`,
label: `Response`, label: "Response",
tooltip: `Response duration in seconds`, tooltip: "Response duration in seconds",
borderColor: "#73ba3c", borderColor: "#73ba3c",
data: responseDurationSeconds.map(([x, y]) => ({ x, y })) data: responseDurationSeconds.map(([x, y]) => ({ x, y }))
}, },
@ -97,7 +97,7 @@ export const IngressCharts = observer(() => {
label: ({ datasetIndex, index }, { datasets }) => { label: ({ datasetIndex, index }, { datasets }) => {
const { label, data } = datasets[datasetIndex]; const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint; const value = data[index] as ChartPoint;
const chartTooltipSec = `sec`; const chartTooltipSec = "sec";
return `${label}: ${parseFloat(value.y as string).toFixed(3)} ${chartTooltipSec}`; 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 ( return (
<BarChart <BarChart

View File

@ -35,6 +35,7 @@ import { getBackendServiceNamePort, getMetricsForIngress, IIngressMetrics } from
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import { ingressMetricTabs } from "../metrics-helpers";
interface Props extends KubeObjectDetailsProps<Ingress> { interface Props extends KubeObjectDetailsProps<Ingress> {
} }
@ -63,14 +64,15 @@ export class IngressDetails extends React.Component<Props> {
renderPaths(ingress: Ingress) { renderPaths(ingress: Ingress) {
const { spec: { rules } } = ingress; const { spec: { rules } } = ingress;
if (!rules || !rules.length) return null; if (!rules) {
return null;
}
return rules.map((rule, index) => { return rules.map((rule, index) => (
return (
<div className="rules" key={index}> <div className="rules" key={index}>
{rule.host && ( {rule.host && (
<div className="host-title"> <div className="host-title">
<>Host: {rule.host}</> Host: {rule.host}
</div> </div>
)} )}
{rule.http && ( {rule.http && (
@ -82,13 +84,12 @@ export class IngressDetails extends React.Component<Props> {
{ {
rule.http.paths.map((path, index) => { rule.http.paths.map((path, index) => {
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend); const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
const backend = `${serviceName}:${servicePort}`;
return ( return (
<TableRow key={index}> <TableRow key={index}>
<TableCell className="path">{path.path || ""}</TableCell> <TableCell className="path">{path.path || ""}</TableCell>
<TableCell className="backends"> <TableCell className="backends">
<p key={backend}>{backend}</p> <p>{serviceName}:{servicePort}</p>
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
@ -97,8 +98,7 @@ export class IngressDetails extends React.Component<Props> {
</Table> </Table>
)} )}
</div> </div>
); ));
});
} }
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) { renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
@ -134,11 +134,6 @@ export class IngressDetails extends React.Component<Props> {
const { spec, status } = ingress; const { spec, status } = ingress;
const ingressPoints = status?.loadBalancer?.ingress; const ingressPoints = status?.loadBalancer?.ingress;
const { metrics } = this;
const metricTabs = [
"Network",
"Duration",
];
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Ingress); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Ingress);
const { serviceName, servicePort } = ingress.getServiceNamePort(); const { serviceName, servicePort } = ingress.getServiceNamePort();
@ -147,7 +142,9 @@ export class IngressDetails extends React.Component<Props> {
{!isMetricHidden && ( {!isMetricHidden && (
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={metricTabs} object={ingress} params={{ metrics }} tabs={ingressMetricTabs}
object={ingress}
metrics={this.metrics}
> >
<IngressCharts/> <IngressCharts/>
</ResourceMetrics> </ResourceMetrics>

View File

@ -21,7 +21,7 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import type { IClusterMetrics, Node } from "../../../common/k8s-api/endpoints"; 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 { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
@ -66,29 +66,29 @@ export const NodeCharts = observer(() => {
[ [
{ {
id: `${id}-cpuUsage`, id: `${id}-cpuUsage`,
label: `Usage`, label: "Usage",
tooltip: `CPU cores usage`, tooltip: "CPU cores usage",
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })) data: cpuUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-cpuRequests`, id: `${id}-cpuRequests`,
label: `Requests`, label: "Requests",
tooltip: `CPU requests`, tooltip: "CPU requests",
borderColor: "#30b24d", borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })) data: cpuRequests.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-cpuAllocatableCapacity`, id: `${id}-cpuAllocatableCapacity`,
label: `Allocatable Capacity`, label: "Allocatable Capacity",
tooltip: `CPU allocatable capacity`, tooltip: "CPU allocatable capacity",
borderColor: "#032b4d", borderColor: "#032b4d",
data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y })) data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-cpuCapacity`, id: `${id}-cpuCapacity`,
label: `Capacity`, label: "Capacity",
tooltip: `CPU capacity`, tooltip: "CPU capacity",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: cpuCapacity.map(([x, y]) => ({ x, y })) data: cpuCapacity.map(([x, y]) => ({ x, y }))
} }
@ -97,36 +97,36 @@ export const NodeCharts = observer(() => {
[ [
{ {
id: `${id}-memoryUsage`, id: `${id}-memoryUsage`,
label: `Usage`, label: "Usage",
tooltip: `Memory usage`, tooltip: "Memory usage",
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })) data: memoryUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-workloadMemoryUsage`, id: `${id}-workloadMemoryUsage`,
label: `Workload Memory Usage`, label: "Workload Memory Usage",
tooltip: `Workload memory usage`, tooltip: "Workload memory usage",
borderColor: "#9cd3ce", borderColor: "#9cd3ce",
data: workloadMemoryUsage.map(([x, y]) => ({ x, y })) data: workloadMemoryUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: "memoryRequests", id: "memoryRequests",
label: `Requests`, label: "Requests",
tooltip: `Memory requests`, tooltip: "Memory requests",
borderColor: "#30b24d", borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })) data: memoryRequests.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-memoryAllocatableCapacity`, id: `${id}-memoryAllocatableCapacity`,
label: `Allocatable Capacity`, label: "Allocatable Capacity",
tooltip: `Memory allocatable capacity`, tooltip: "Memory allocatable capacity",
borderColor: "#032b4d", borderColor: "#032b4d",
data: memoryAllocatableCapacity.map(([x, y]) => ({ x, y })) data: memoryAllocatableCapacity.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-memoryCapacity`, id: `${id}-memoryCapacity`,
label: `Capacity`, label: "Capacity",
tooltip: `Memory capacity`, tooltip: "Memory capacity",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: memoryCapacity.map(([x, y]) => ({ x, y })) data: memoryCapacity.map(([x, y]) => ({ x, y }))
} }
@ -135,15 +135,15 @@ export const NodeCharts = observer(() => {
[ [
{ {
id: `${id}-fsUsage`, id: `${id}-fsUsage`,
label: `Usage`, label: "Usage",
tooltip: `Node filesystem usage in bytes`, tooltip: "Node filesystem usage in bytes",
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })) data: fsUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-fsSize`, id: `${id}-fsSize`,
label: `Size`, label: "Size",
tooltip: `Node filesystem size in bytes`, tooltip: "Node filesystem size in bytes",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: fsSize.map(([x, y]) => ({ x, y })) data: fsSize.map(([x, y]) => ({ x, y }))
} }
@ -152,15 +152,15 @@ export const NodeCharts = observer(() => {
[ [
{ {
id: `${id}-podUsage`, id: `${id}-podUsage`,
label: `Usage`, label: "Usage",
tooltip: `Number of running Pods`, tooltip: "Number of running Pods",
borderColor: "#30b24d", borderColor: "#30b24d",
data: podUsage.map(([x, y]) => ({ x, y })) data: podUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-podCapacity`, id: `${id}-podCapacity`,
label: `Capacity`, label: "Capacity",
tooltip: `Node Pods capacity`, tooltip: "Node Pods capacity",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: podCapacity.map(([x, y]) => ({ x, y })) 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 ( return (
<BarChart <BarChart

View File

@ -40,6 +40,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { NodeDetailsResources } from "./node-details-resources"; import { NodeDetailsResources } from "./node-details-resources";
import { DrawerTitle } from "../drawer/drawer-title"; import { DrawerTitle } from "../drawer/drawer-title";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import { nodeDetailsMetricTabs } from "../metrics-helpers";
interface Props extends KubeObjectDetailsProps<Node> { interface Props extends KubeObjectDetailsProps<Node> {
} }
@ -78,13 +79,6 @@ export class NodeDetails extends React.Component<Props> {
const conditions = node.getActiveConditions(); const conditions = node.getActiveConditions();
const taints = node.getTaints(); const taints = node.getTaints();
const childPods = podsStore.getPodsByNode(node.getName()); const childPods = podsStore.getPodsByNode(node.getName());
const { metrics } = this;
const metricTabs = [
"CPU",
"Memory",
"Disk",
"Pods",
];
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Node); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Node);
return ( return (
@ -92,7 +86,9 @@ export class NodeDetails extends React.Component<Props> {
{!isMetricHidden && podsStore.isLoaded && ( {!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={metricTabs} object={node} params={{ metrics }} tabs={nodeDetailsMetricTabs}
object={node}
metrics={this.metrics}
> >
<NodeCharts/> <NodeCharts/>
</ResourceMetrics> </ResourceMetrics>

View File

@ -69,7 +69,6 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
return null; return null;
} }
const { storageClassName, accessModes } = volumeClaim.spec; const { storageClassName, accessModes } = volumeClaim.spec;
const { metrics } = this;
const pods = volumeClaim.getPods(podsStore.items); const pods = volumeClaim.getPods(podsStore.items);
const metricTabs = [ const metricTabs = [
"Disk" "Disk"
@ -81,12 +80,12 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
{!isMetricHidden && ( {!isMetricHidden && (
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={metricTabs} object={volumeClaim} params={{ metrics }} tabs={metricTabs} object={volumeClaim} metrics={this.metrics}
> >
<VolumeClaimDiskChart/> <VolumeClaimDiskChart />
</ResourceMetrics> </ResourceMetrics>
)} )}
<KubeObjectMeta object={volumeClaim}/> <KubeObjectMeta object={volumeClaim} />
<DrawerItem name="Access Modes"> <DrawerItem name="Access Modes">
{accessModes.join(", ")} {accessModes.join(", ")}
</DrawerItem> </DrawerItem>
@ -107,10 +106,10 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
{volumeClaim.getStatus()} {volumeClaim.getStatus()}
</DrawerItem> </DrawerItem>
<DrawerTitle title="Selector"/> <DrawerTitle title="Selector" />
<DrawerItem name="Match Labels" labelsOnly> <DrawerItem name="Match Labels" labelsOnly>
{volumeClaim.getMatchLabels().map(label => <Badge key={label} label={label}/>)} {volumeClaim.getMatchLabels().map(label => <Badge key={label} label={label} />)}
</DrawerItem> </DrawerItem>
<DrawerItem name="Match Expressions"> <DrawerItem name="Match Expressions">

View File

@ -22,7 +22,7 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { IPvcMetrics, PersistentVolumeClaim } from "../../../common/k8s-api/endpoints"; 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 { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
@ -45,15 +45,15 @@ export const VolumeClaimDiskChart = observer(() => {
const datasets: ChartDataSets[] = [ const datasets: ChartDataSets[] = [
{ {
id: `${id}-diskUsage`, id: `${id}-diskUsage`,
label: `Usage`, label: "Usage",
tooltip: `Volume disk usage`, tooltip: "Volume disk usage",
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: usage.map(([x, y]) => ({ x, y })) data: usage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-diskCapacity`, id: `${id}-diskCapacity`,
label: `Capacity`, label: "Capacity",
tooltip: `Volume disk capacity`, tooltip: "Volume disk capacity",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: capacity.map(([x, y]) => ({ x, y })) data: capacity.map(([x, y]) => ({ x, y }))
} }
@ -64,7 +64,7 @@ export const VolumeClaimDiskChart = observer(() => {
className="VolumeClaimDiskChart flex box grow column" className="VolumeClaimDiskChart flex box grow column"
name={`pvc-${object.getName()}-disk`} name={`pvc-${object.getName()}-disk`}
timeLabelStep={10} timeLabelStep={10}
options={memoryOptions} options={defaultBarChartOptions}
data={{ datasets }} data={{ datasets }}
/> />
); );

View File

@ -33,13 +33,14 @@ import { podsStore } from "../+workloads-pods/pods.store";
import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { KubeObjectDetailsProps } from "../kube-object-details";
import { DaemonSet, getMetricsForDaemonSets, IPodMetrics } from "../../../common/k8s-api/endpoints"; import { DaemonSet, getMetricsForDaemonSets, IPodMetrics } from "../../../common/k8s-api/endpoints";
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics"; 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 { makeObservable, observable, reaction } from "mobx";
import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import { podMetricTabs } from "../metrics-helpers";
interface Props extends KubeObjectDetailsProps<DaemonSet> { interface Props extends KubeObjectDetailsProps<DaemonSet> {
} }
@ -85,7 +86,9 @@ export class DaemonSetDetails extends React.Component<Props> {
{!isMetricHidden && podsStore.isLoaded && ( {!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={podMetricTabs} object={daemonSet} params={{ metrics: this.metrics }} tabs={podMetricTabs}
object={daemonSet}
metrics={this.metrics}
> >
<PodCharts/> <PodCharts/>
</ResourceMetrics> </ResourceMetrics>

View File

@ -22,22 +22,15 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { IPodMetrics } from "../../../common/k8s-api/endpoints"; import type { IPodMetrics } from "../../../common/k8s-api/endpoints";
import { BarChart, cpuOptions, memoryOptions } from "../chart"; import { BarChart, ChartDataSets } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { FlattenedMetrics, flattenMatricResults, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { ResourceMetricsContext } from "../resource-metrics";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { mapValues } from "lodash"; import { ContainerMetricsTab, getBarChartOptions } from "../metrics-helpers";
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>; function getDatasets(tab: string, metrics: FlattenedMetrics<IPodMetrics>): ChartDataSets[] | null {
export const ContainerCharts = observer(() => {
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const { const {
cpuUsage, cpuUsage,
cpuRequests, cpuRequests,
@ -46,76 +39,90 @@ export const ContainerCharts = observer(() => {
memoryRequests, memoryRequests,
memoryLimits, memoryLimits,
fsUsage fsUsage
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values); } = metrics;
const datasets = [ switch (tab) {
// CPU case ContainerMetricsTab.CPU:
[ return [
{ {
id: "cpuUsage", id: "cpuUsage",
label: `Usage`, label: "Usage",
tooltip: `CPU cores usage`, tooltip: "CPU cores usage",
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })) data: cpuUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: "cpuRequests", id: "cpuRequests",
label: `Requests`, label: "Requests",
tooltip: `CPU requests`, tooltip: "CPU requests",
borderColor: "#30b24d", borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })) data: cpuRequests.map(([x, y]) => ({ x, y }))
}, },
{ {
id: "cpuLimits", id: "cpuLimits",
label: `Limits`, label: "Limits",
tooltip: `CPU limits`, tooltip: "CPU limits",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: cpuLimits.map(([x, y]) => ({ x, y })) data: cpuLimits.map(([x, y]) => ({ x, y }))
} }
], ];
// Memory case ContainerMetricsTab.MEMORY:
[ return [
{ {
id: "memoryUsage", id: "memoryUsage",
label: `Usage`, label: "Usage",
tooltip: `Memory usage`, tooltip: "Memory usage",
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })) data: memoryUsage.map(([x, y]) => ({ x, y }))
}, },
{ {
id: "memoryRequests", id: "memoryRequests",
label: `Requests`, label: "Requests",
tooltip: `Memory requests`, tooltip: "Memory requests",
borderColor: "#30b24d", borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })) data: memoryRequests.map(([x, y]) => ({ x, y }))
}, },
{ {
id: "memoryLimits", id: "memoryLimits",
label: `Limits`, label: "Limits",
tooltip: `Memory limits`, tooltip: "Memory limits",
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: memoryLimits.map(([x, y]) => ({ x, y })) data: memoryLimits.map(([x, y]) => ({ x, y }))
} }
], ];
// Filesystem case ContainerMetricsTab.FILESYSTEM:
[ return [
{ {
id: "fsUsage", id: "fsUsage",
label: `Usage`, label: "Usage",
tooltip: `Bytes consumed on this filesystem`, tooltip: "Bytes consumed on this filesystem",
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })) 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 <NoMetrics />;
}
const datasets = getDatasets(tab, flattenMatricResults(metrics as IPodMetrics));
if (!datasets) {
return <NoMetrics />;
}
return ( return (
<BarChart <BarChart
name={`metrics-${tabId}`} name={`metrics-${tab}`}
options={options} options={getBarChartOptions(tab)}
data={{ datasets: datasets[tabId] }} data={{ datasets }}
/> />
); );
}); });

View File

@ -19,97 +19,97 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { mapValues } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { FlattenedMetrics, flattenMatricResults, isMetricsEmpty } from "../../../common/k8s-api/endpoints/metrics.api";
import { BarChart, cpuOptions, memoryOptions } from "../chart"; import { BarChart, ChartDataSets } from "../chart";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { ResourceMetricsContext } from "../resource-metrics";
import { NoMetrics } from "../resource-metrics/no-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 { IPodMetrics } from "../../../common/k8s-api/endpoints";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { getBarChartOptions, PodMetricsTab } from "../metrics-helpers";
export const podMetricTabs = [ function getDatasets(object: KubeObject, tab: string, metrics: FlattenedMetrics<IPodMetrics>): ChartDataSets[] | null {
"CPU",
"Memory",
"Network",
"Filesystem",
];
type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics }>;
export const PodCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const id = object.getId(); const id = object.getId();
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const options = tabId == 0 ? cpuOptions : memoryOptions;
const { const {
cpuUsage, cpuUsage,
memoryUsage, memoryUsage,
fsUsage,
networkReceive, networkReceive,
networkTransmit networkTransmit,
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values); fsUsage,
} = metrics;
const datasets = [ switch (tab) {
// CPU case PodMetricsTab.CPU:
[ return [
{ {
id: `${id}-cpuUsage`, id: `${id}-cpuUsage`,
label: `Usage`, label: "Usage",
tooltip: `Container CPU cores usage`, tooltip: "Container CPU cores usage",
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })) data: cpuUsage.map(([x, y]) => ({ x, y }))
} }
], ];
// Memory case PodMetricsTab.MEMORY:
[ return [
{ {
id: `${id}-memoryUsage`, id: `${id}-memoryUsage`,
label: `Usage`, label: "Usage",
tooltip: `Container memory usage`, tooltip: "Container memory usage",
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })) data: memoryUsage.map(([x, y]) => ({ x, y }))
}, },
], ];
// Network case PodMetricsTab.NETWORK:
[ return [
{ {
id: `${id}-networkReceive`, id: `${id}-networkReceive`,
label: `Receive`, label: "Receive",
tooltip: `Bytes received by all containers`, tooltip: "Bytes received by all containers",
borderColor: "#64c5d6", borderColor: "#64c5d6",
data: networkReceive.map(([x, y]) => ({ x, y })) data: networkReceive.map(([x, y]) => ({ x, y }))
}, },
{ {
id: `${id}-networkTransmit`, id: `${id}-networkTransmit`,
label: `Transmit`, label: "Transmit",
tooltip: `Bytes transmitted from all containers`, tooltip: "Bytes transmitted from all containers",
borderColor: "#46cd9e", borderColor: "#46cd9e",
data: networkTransmit.map(([x, y]) => ({ x, y })) data: networkTransmit.map(([x, y]) => ({ x, y }))
} }
], ];
// Filesystem case PodMetricsTab.FILESYSTEM:
[ return [
{ {
id: `${id}-fsUsage`, id: `${id}-fsUsage`,
label: `Usage`, label: "Usage",
tooltip: `Bytes consumed on this filesystem`, tooltip: "Bytes consumed on this filesystem",
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })) data: fsUsage.map(([x, y]) => ({ x, y }))
} }
]
]; ];
default:
return null;
}
}
export const PodCharts = observer(() => {
const { metrics, tab, object } = useContext(ResourceMetricsContext);
if (isMetricsEmpty(metrics)) {
return <NoMetrics />;
}
const datasets = getDatasets(object, tab, flattenMatricResults(metrics as IPodMetrics));
if (!datasets) {
return <NoMetrics />;
}
return ( return (
<BarChart <BarChart
name={`${object.getName()}-metric-${tabId}`} name={`${object.getName()}-metric-${tab}`}
options={options} options={getBarChartOptions(tab)}
data={{ datasets: datasets[tabId] }} data={{ datasets }}
/> />
); );
}); });

View File

@ -37,6 +37,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { portForwardStore } from "../../port-forward/port-forward.store"; import { portForwardStore } from "../../port-forward/port-forward.store";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { containerMetricTabs } from "../metrics-helpers";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -58,7 +59,7 @@ export class PodDetailsContainer extends React.Component<Props> {
return ( return (
<span className={cssNames("status", state)}> <span className={cssNames("status", state)}>
{state}{ready ? `, ready` : ""} {state}{ready ? ", ready" : ""}
{state === "terminated" ? ` - ${status.state.terminated.reason} (exit code: ${status.state.terminated.exitCode})` : ""} {state === "terminated" ? ` - ${status.state.terminated.reason} (exit code: ${status.state.terminated.exitCode})` : ""}
</span> </span>
); );
@ -68,10 +69,10 @@ export class PodDetailsContainer extends React.Component<Props> {
if (lastState === "terminated") { if (lastState === "terminated") {
return ( return (
<span> <span>
{lastState}<br/> {lastState}<br />
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/> Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br />
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br/> Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br />
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br/> Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br />
</span> </span>
); );
} }
@ -88,26 +89,24 @@ export class PodDetailsContainer extends React.Component<Props> {
const state = status ? Object.keys(status.state)[0] : ""; const state = status ? Object.keys(status.state)[0] : "";
const lastState = status ? Object.keys(status.lastState)[0] : ""; const lastState = status ? Object.keys(status.lastState)[0] : "";
const ready = status ? status.ready : ""; const ready = status ? status.ready : "";
const imageId = status? status.imageID : ""; const imageId = status ? status.imageID : "";
const liveness = pod.getLivenessProbe(container); const liveness = pod.getLivenessProbe(container);
const readiness = pod.getReadinessProbe(container); const readiness = pod.getReadinessProbe(container);
const startup = pod.getStartupProbe(container); const startup = pod.getStartupProbe(container);
const isInitContainer = !!pod.getInitContainers().find(c => c.name == name); const isInitContainer = !!pod.getInitContainers().find(c => c.name == name);
const metricTabs = [
"CPU",
"Memory",
"Filesystem",
];
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Container); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Container);
return ( return (
<div className="PodDetailsContainer"> <div className="PodDetailsContainer">
<div className="pod-container-title"> <div className="pod-container-title">
<StatusBrick className={cssNames(state, { ready })}/>{name} <StatusBrick className={cssNames(state, { ready })} />{name}
</div> </div>
{!isMetricHidden && !isInitContainer && {!isMetricHidden && !isInitContainer &&
<ResourceMetrics tabs={metricTabs} params={{ metrics }}> <ResourceMetrics
<ContainerCharts/> tabs={containerMetricTabs}
metrics={metrics}
>
<ContainerCharts />
</ResourceMetrics> </ResourceMetrics>
} }
{status && {status &&
@ -121,7 +120,7 @@ export class PodDetailsContainer extends React.Component<Props> {
</DrawerItem> </DrawerItem>
} }
<DrawerItem name="Image"> <DrawerItem name="Image">
<Badge label={image} tooltip={imageId}/> <Badge label={image} tooltip={imageId} />
</DrawerItem> </DrawerItem>
{imagePullPolicy && imagePullPolicy !== "IfNotPresent" && {imagePullPolicy && imagePullPolicy !== "IfNotPresent" &&
<DrawerItem name="ImagePullPolicy"> <DrawerItem name="ImagePullPolicy">
@ -135,13 +134,13 @@ export class PodDetailsContainer extends React.Component<Props> {
const key = `${container.name}-port-${port.containerPort}-${port.protocol}`; const key = `${container.name}-port-${port.containerPort}-${port.protocol}`;
return ( return (
<PodContainerPort pod={pod} port={port} key={key}/> <PodContainerPort pod={pod} port={port} key={key} />
); );
}) })
} }
</DrawerItem> </DrawerItem>
} }
{<ContainerEnvironment container={container} namespace={pod.getNs()}/>} {<ContainerEnvironment container={container} namespace={pod.getNs()} />}
{volumeMounts && volumeMounts.length > 0 && {volumeMounts && volumeMounts.length > 0 &&
<DrawerItem name="Mounts"> <DrawerItem name="Mounts">
{ {
@ -162,7 +161,7 @@ export class PodDetailsContainer extends React.Component<Props> {
<DrawerItem name="Liveness" labelsOnly> <DrawerItem name="Liveness" labelsOnly>
{ {
liveness.map((value, index) => ( liveness.map((value, index) => (
<Badge key={index} label={value}/> <Badge key={index} label={value} />
)) ))
} }
</DrawerItem> </DrawerItem>
@ -171,7 +170,7 @@ export class PodDetailsContainer extends React.Component<Props> {
<DrawerItem name="Readiness" labelsOnly> <DrawerItem name="Readiness" labelsOnly>
{ {
readiness.map((value, index) => ( readiness.map((value, index) => (
<Badge key={index} label={value}/> <Badge key={index} label={value} />
)) ))
} }
</DrawerItem> </DrawerItem>
@ -180,7 +179,7 @@ export class PodDetailsContainer extends React.Component<Props> {
<DrawerItem name="Startup" labelsOnly> <DrawerItem name="Startup" labelsOnly>
{ {
startup.map((value, index) => ( startup.map((value, index) => (
<Badge key={index} label={value}/> <Badge key={index} label={value} />
)) ))
} }
</DrawerItem> </DrawerItem>

View File

@ -82,7 +82,6 @@ export class PodDetails extends React.Component<Props> {
} }
const { status: { conditions, podIP }, spec: { nodeName } } = pod; const { status: { conditions, podIP }, spec: { nodeName } } = pod;
const { metrics } = this;
const podIPs = pod.getIPs(); const podIPs = pod.getIPs();
const nodeSelector = pod.getNodeSelectors(); const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes(); const volumes = pod.getVolumes();
@ -96,7 +95,7 @@ export class PodDetails extends React.Component<Props> {
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={podMetricTabs} tabs={podMetricTabs}
object={pod} object={pod}
params={{ metrics }} metrics={this.metrics}
> >
<PodCharts/> <PodCharts/>
</ResourceMetrics> </ResourceMetrics>

View File

@ -72,7 +72,6 @@ export class ReplicaSetDetails extends React.Component<Props> {
const { object: replicaSet } = this.props; const { object: replicaSet } = this.props;
if (!replicaSet) return null; if (!replicaSet) return null;
const { metrics } = this;
const { status } = replicaSet; const { status } = replicaSet;
const { availableReplicas, replicas } = status; const { availableReplicas, replicas } = status;
const selectors = replicaSet.getSelectors(); const selectors = replicaSet.getSelectors();
@ -86,7 +85,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
{!isMetricHidden && podsStore.isLoaded && ( {!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics <ResourceMetrics
loader={this.loadMetrics} loader={this.loadMetrics}
tabs={podMetricTabs} object={replicaSet} params={{ metrics }} tabs={podMetricTabs} object={replicaSet} metrics={this.metrics}
> >
<PodCharts/> <PodCharts/>
</ResourceMetrics> </ResourceMetrics>
@ -121,7 +120,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
<DrawerItem name="Pod Status" className="pod-status"> <DrawerItem name="Pod Status" className="pod-status">
<PodDetailsStatuses pods={childPods}/> <PodDetailsStatuses pods={childPods}/>
</DrawerItem> </DrawerItem>
<ResourceMetricsText metrics={metrics}/> <ResourceMetricsText metrics={this.metrics}/>
<PodDetailsList pods={childPods} owner={replicaSet}/> <PodDetailsList pods={childPods} owner={replicaSet}/>
</div> </div>
); );

View File

@ -26,7 +26,7 @@ import Color from "color";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js"; import type { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js";
import { Chart, ChartKind, ChartProps } from "./chart"; import { Chart, ChartKind, ChartProps } from "./chart";
import { bytesToUnits, cssNames } from "../../utils"; import { bytesToUnits, cssNames, numbers } from "../../utils";
import { ZebraStripes } from "./zebra-stripes.plugin"; import { ZebraStripes } from "./zebra-stripes.plugin";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
@ -144,12 +144,10 @@ export class BarChart extends React.Component<Props> {
return String(xLabel); return String(xLabel);
}, },
labelColor: ({ datasetIndex }) => { labelColor: ({ datasetIndex }) => ({
return {
borderColor: "darkgray", borderColor: "darkgray",
backgroundColor: chartData.datasets[datasetIndex].borderColor as string backgroundColor: chartData.datasets[datasetIndex].borderColor as string
}; })
}
} }
}, },
animation: { animation: {
@ -183,7 +181,7 @@ export class BarChart extends React.Component<Props> {
} }
// Default options for all charts containing memory units (network, disk, memory, etc) // Default options for all charts containing memory units (network, disk, memory, etc)
export const memoryOptions: ChartOptions = { const memoryUnits: ChartOptions = {
scales: { scales: {
yAxes: [{ yAxes: [{
ticks: { ticks: {
@ -217,18 +215,18 @@ export const memoryOptions: ChartOptions = {
}; };
// Default options for all charts with cpu units or other decimal numbers // Default options for all charts with cpu units or other decimal numbers
export const cpuOptions: ChartOptions = { const decimalUnits: ChartOptions = {
scales: { scales: {
yAxes: [{ yAxes: [{
ticks: { ticks: {
callback: (value: number | string): string => { 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"; return parsed.toFixed(precision);
if (float < 10) return float.toFixed(3);
if (float < 100) return float.toFixed(2);
return float.toFixed(1);
} }
} }
}] }]
@ -244,3 +242,8 @@ export const cpuOptions: ChartOptions = {
} }
} }
}; };
export const barChartOptions = Object.freeze({
decimalUnits,
memoryUnits,
});

View File

@ -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;
}
}

View File

@ -27,33 +27,34 @@ import { useInterval } from "../../hooks";
import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import type { IMetrics } from "../../../common/k8s-api/endpoints";
interface Props<T> { interface Props {
tabs: string[] | string[][]; tabs: string[];
object?: KubeObject; object?: KubeObject;
loader?: () => void; loader?: () => void;
interval?: number; interval?: number;
className?: IClassName; className?: IClassName;
params?: T; metrics?: Record<string, IMetrics>;
children?: React.ReactNode; children?: React.ReactNode;
} }
export type IResourceMetricsValue<T extends KubeObject = any, P = any> = { export type IResourceMetricsValue<K extends KubeObject = KubeObject, Metrics extends Record<string, IMetrics> = Record<string, IMetrics>> = {
object: T; object?: K;
tabId: number; tab: string;
params?: P; metrics?: Metrics;
}; };
export const ResourceMetricsContext = createContext<IResourceMetricsValue>(null); export const ResourceMetricsContext = createContext<IResourceMetricsValue>(null);
const defaultProps: Partial<Props<any>> = { const defaultProps: Partial<Props> = {
interval: 60 // 1 min interval: 60 // 1 min
}; };
ResourceMetrics.defaultProps = defaultProps; ResourceMetrics.defaultProps = defaultProps;
export function ResourceMetrics<T>({ object, loader, interval, tabs, children, className, params }: Props<T>) { export function ResourceMetrics({ object, loader, interval, tabs, children, className, metrics }: Props) {
const [tabId, setTabId] = useState<number>(0); const [tab, setTab] = useState<string>(tabs[0]);
useEffect(() => { useEffect(() => {
if (loader) loader(); if (loader) loader();
@ -69,15 +70,15 @@ export function ResourceMetrics<T>({ object, loader, interval, tabs, children, c
<RadioGroup <RadioGroup
asButtons asButtons
className="flex box grow gaps" className="flex box grow gaps"
value={tabs[tabId]} value={tab}
onChange={value => setTabId(tabs.findIndex(tab => tab == value))} onChange={value => setTab(value)}
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<Radio key={index} className="box grow" label={tab} value={tab} /> <Radio key={index} className="box grow" label={tab} value={tab} />
))} ))}
</RadioGroup> </RadioGroup>
</div> </div>
<ResourceMetricsContext.Provider value={{ object, tabId, params }}> <ResourceMetricsContext.Provider value={{ object, tab, metrics }}>
<div className="graph"> <div className="graph">
{children} {children}
</div> </div>