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", {
"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",

View File

@ -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,

View File

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

View File

@ -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<Ingress> {
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
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<T = IMetrics> {
[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 {

View File

@ -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<T extends IMetrics> {
networkTransmit: T;
}
export const metricsApi = {
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;
let { start, end } = reqParams;
export async function getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
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<MetricProviderInfo[]> {
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<MetricProviderInfo[]> {
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 {
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<string, IMetrics>) {
return !Object.values(metrics).some(metric => metric?.data?.result?.length);
export function isMetricsEmpty(metrics: Record<string, IMetrics> | Falsey) {
return !metrics || !Object.values(metrics).some(metric => metric?.data?.result?.length);
}
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) {
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];
}

View File

@ -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<T = IMetrics> {
[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

View File

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

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 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 {
<ResourceMetrics
loader={this.loadMetrics}
tabs={[MetricType.CPU, MetricType.MEMORY]}
params={{ metrics }}
metrics={this.metrics}
>
<ClusterMetrics />
<ClusterPieCharts />

View File

@ -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 (
<BarChart

View File

@ -35,6 +35,7 @@ import { getBackendServiceNamePort, getMetricsForIngress, IIngressMetrics } from
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { boundMethod } from "../../utils";
import { ingressMetricTabs } from "../metrics-helpers";
interface Props extends KubeObjectDetailsProps<Ingress> {
}
@ -63,42 +64,41 @@ export class IngressDetails extends React.Component<Props> {
renderPaths(ingress: Ingress) {
const { spec: { rules } } = ingress;
if (!rules || !rules.length) return null;
if (!rules) {
return null;
}
return rules.map((rule, index) => {
return (
<div className="rules" key={index}>
{rule.host && (
<div className="host-title">
<>Host: {rule.host}</>
</div>
)}
{rule.http && (
<Table className="paths">
<TableHead>
<TableCell className="path">Path</TableCell>
<TableCell className="backends">Backends</TableCell>
</TableHead>
{
rule.http.paths.map((path, index) => {
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
const backend = `${serviceName}:${servicePort}`;
return rules.map((rule, index) => (
<div className="rules" key={index}>
{rule.host && (
<div className="host-title">
Host: {rule.host}
</div>
)}
{rule.http && (
<Table className="paths">
<TableHead>
<TableCell className="path">Path</TableCell>
<TableCell className="backends">Backends</TableCell>
</TableHead>
{
rule.http.paths.map((path, index) => {
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
return (
<TableRow key={index}>
<TableCell className="path">{path.path || ""}</TableCell>
<TableCell className="backends">
<p key={backend}>{backend}</p>
</TableCell>
</TableRow>
);
})
}
</Table>
)}
</div>
);
});
return (
<TableRow key={index}>
<TableCell className="path">{path.path || ""}</TableCell>
<TableCell className="backends">
<p>{serviceName}:{servicePort}</p>
</TableCell>
</TableRow>
);
})
}
</Table>
)}
</div>
));
}
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
@ -134,11 +134,6 @@ export class IngressDetails extends React.Component<Props> {
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<Props> {
{!isMetricHidden && (
<ResourceMetrics
loader={this.loadMetrics}
tabs={metricTabs} object={ingress} params={{ metrics }}
tabs={ingressMetricTabs}
object={ingress}
metrics={this.metrics}
>
<IngressCharts/>
</ResourceMetrics>

View File

@ -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 (
<BarChart

View File

@ -40,6 +40,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { NodeDetailsResources } from "./node-details-resources";
import { DrawerTitle } from "../drawer/drawer-title";
import { boundMethod } from "../../utils";
import { nodeDetailsMetricTabs } from "../metrics-helpers";
interface Props extends KubeObjectDetailsProps<Node> {
}
@ -78,13 +79,6 @@ export class NodeDetails extends React.Component<Props> {
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<Props> {
{!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics
loader={this.loadMetrics}
tabs={metricTabs} object={node} params={{ metrics }}
tabs={nodeDetailsMetricTabs}
object={node}
metrics={this.metrics}
>
<NodeCharts/>
</ResourceMetrics>

View File

@ -69,7 +69,6 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
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<Props> {
{!isMetricHidden && (
<ResourceMetrics
loader={this.loadMetrics}
tabs={metricTabs} object={volumeClaim} params={{ metrics }}
tabs={metricTabs} object={volumeClaim} metrics={this.metrics}
>
<VolumeClaimDiskChart/>
<VolumeClaimDiskChart />
</ResourceMetrics>
)}
<KubeObjectMeta object={volumeClaim}/>
<KubeObjectMeta object={volumeClaim} />
<DrawerItem name="Access Modes">
{accessModes.join(", ")}
</DrawerItem>
@ -107,10 +106,10 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
{volumeClaim.getStatus()}
</DrawerItem>
<DrawerTitle title="Selector"/>
<DrawerTitle title="Selector" />
<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 name="Match Expressions">

View File

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

View File

@ -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<DaemonSet> {
}
@ -85,7 +86,9 @@ export class DaemonSetDetails extends React.Component<Props> {
{!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics
loader={this.loadMetrics}
tabs={podMetricTabs} object={daemonSet} params={{ metrics: this.metrics }}
tabs={podMetricTabs}
object={daemonSet}
metrics={this.metrics}
>
<PodCharts/>
</ResourceMetrics>

View File

@ -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<any, { metrics: IPodMetrics }>;
export const ContainerCharts = observer(() => {
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
function getDatasets(tab: string, metrics: FlattenedMetrics<IPodMetrics>): ChartDataSets[] | null {
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
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 <NoMetrics />;
}
const datasets = getDatasets(tab, flattenMatricResults(metrics as IPodMetrics));
if (!datasets) {
return <NoMetrics />;
}
return (
<BarChart
name={`metrics-${tabId}`}
options={options}
data={{ datasets: datasets[tabId] }}
name={`metrics-${tab}`}
options={getBarChartOptions(tab)}
data={{ datasets }}
/>
);
});

View File

@ -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<WorkloadKubeObject, { metrics: IPodMetrics }>;
export const PodCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
function getDatasets(object: KubeObject, tab: string, metrics: FlattenedMetrics<IPodMetrics>): ChartDataSets[] | null {
const id = object.getId();
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
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 <NoMetrics />;
}
const datasets = getDatasets(object, tab, flattenMatricResults(metrics as IPodMetrics));
if (!datasets) {
return <NoMetrics />;
}
return (
<BarChart
name={`${object.getName()}-metric-${tabId}`}
options={options}
data={{ datasets: datasets[tabId] }}
name={`${object.getName()}-metric-${tab}`}
options={getBarChartOptions(tab)}
data={{ datasets }}
/>
);
});

View File

@ -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<Props> {
return (
<span className={cssNames("status", state)}>
{state}{ready ? `, ready` : ""}
{state}{ready ? ", ready" : ""}
{state === "terminated" ? ` - ${status.state.terminated.reason} (exit code: ${status.state.terminated.exitCode})` : ""}
</span>
);
@ -68,10 +69,10 @@ export class PodDetailsContainer extends React.Component<Props> {
if (lastState === "terminated") {
return (
<span>
{lastState}<br/>
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/>
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br/>
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br/>
{lastState}<br />
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br />
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br />
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br />
</span>
);
}
@ -88,113 +89,111 @@ export class PodDetailsContainer extends React.Component<Props> {
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 (
<div className="PodDetailsContainer">
<div className="pod-container-title">
<StatusBrick className={cssNames(state, { ready })}/>{name}
<StatusBrick className={cssNames(state, { ready })} />{name}
</div>
{!isMetricHidden && !isInitContainer &&
<ResourceMetrics tabs={metricTabs} params={{ metrics }}>
<ContainerCharts/>
</ResourceMetrics>
<ResourceMetrics
tabs={containerMetricTabs}
metrics={metrics}
>
<ContainerCharts />
</ResourceMetrics>
}
{status &&
<DrawerItem name="Status">
{this.renderStatus(state, status)}
</DrawerItem>
<DrawerItem name="Status">
{this.renderStatus(state, status)}
</DrawerItem>
}
{lastState &&
<DrawerItem name="Last Status">
{this.renderLastState(lastState, status)}
</DrawerItem>
<DrawerItem name="Last Status">
{this.renderLastState(lastState, status)}
</DrawerItem>
}
<DrawerItem name="Image">
<Badge label={image} tooltip={imageId}/>
<Badge label={image} tooltip={imageId} />
</DrawerItem>
{imagePullPolicy && imagePullPolicy !== "IfNotPresent" &&
<DrawerItem name="ImagePullPolicy">
{imagePullPolicy}
</DrawerItem>
<DrawerItem name="ImagePullPolicy">
{imagePullPolicy}
</DrawerItem>
}
{ports && ports.length > 0 &&
<DrawerItem name="Ports">
{
ports.map((port) => {
const key = `${container.name}-port-${port.containerPort}-${port.protocol}`;
<DrawerItem name="Ports">
{
ports.map((port) => {
const key = `${container.name}-port-${port.containerPort}-${port.protocol}`;
return (
<PodContainerPort pod={pod} port={port} key={key}/>
);
})
}
</DrawerItem>
return (
<PodContainerPort pod={pod} port={port} key={key} />
);
})
}
</DrawerItem>
}
{<ContainerEnvironment container={container} namespace={pod.getNs()}/>}
{<ContainerEnvironment container={container} namespace={pod.getNs()} />}
{volumeMounts && volumeMounts.length > 0 &&
<DrawerItem name="Mounts">
{
volumeMounts.map(mount => {
const { name, mountPath, readOnly } = mount;
<DrawerItem name="Mounts">
{
volumeMounts.map(mount => {
const { name, mountPath, readOnly } = mount;
return (
<React.Fragment key={name + mountPath}>
<span className="mount-path">{mountPath}</span>
<span className="mount-from">from {name} ({readOnly ? "ro" : "rw"})</span>
</React.Fragment>
);
})
}
</DrawerItem>
return (
<React.Fragment key={name + mountPath}>
<span className="mount-path">{mountPath}</span>
<span className="mount-from">from {name} ({readOnly ? "ro" : "rw"})</span>
</React.Fragment>
);
})
}
</DrawerItem>
}
{liveness.length > 0 &&
<DrawerItem name="Liveness" labelsOnly>
{
liveness.map((value, index) => (
<Badge key={index} label={value}/>
))
}
</DrawerItem>
<DrawerItem name="Liveness" labelsOnly>
{
liveness.map((value, index) => (
<Badge key={index} label={value} />
))
}
</DrawerItem>
}
{readiness.length > 0 &&
<DrawerItem name="Readiness" labelsOnly>
{
readiness.map((value, index) => (
<Badge key={index} label={value}/>
))
}
</DrawerItem>
<DrawerItem name="Readiness" labelsOnly>
{
readiness.map((value, index) => (
<Badge key={index} label={value} />
))
}
</DrawerItem>
}
{startup.length > 0 &&
<DrawerItem name="Startup" labelsOnly>
{
startup.map((value, index) => (
<Badge key={index} label={value}/>
))
}
</DrawerItem>
<DrawerItem name="Startup" labelsOnly>
{
startup.map((value, index) => (
<Badge key={index} label={value} />
))
}
</DrawerItem>
}
{command &&
<DrawerItem name="Command">
{command.join(" ")}
</DrawerItem>
<DrawerItem name="Command">
{command.join(" ")}
</DrawerItem>
}
{args &&
<DrawerItem name="Arguments">
{args.join(" ")}
</DrawerItem>
<DrawerItem name="Arguments">
{args.join(" ")}
</DrawerItem>
}
</div>
);

View File

@ -82,7 +82,6 @@ export class PodDetails extends React.Component<Props> {
}
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<Props> {
loader={this.loadMetrics}
tabs={podMetricTabs}
object={pod}
params={{ metrics }}
metrics={this.metrics}
>
<PodCharts/>
</ResourceMetrics>

View File

@ -72,7 +72,6 @@ export class ReplicaSetDetails extends React.Component<Props> {
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<Props> {
{!isMetricHidden && podsStore.isLoaded && (
<ResourceMetrics
loader={this.loadMetrics}
tabs={podMetricTabs} object={replicaSet} params={{ metrics }}
tabs={podMetricTabs} object={replicaSet} metrics={this.metrics}
>
<PodCharts/>
</ResourceMetrics>
@ -121,7 +120,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
<DrawerItem name="Pod Status" className="pod-status">
<PodDetailsStatuses pods={childPods}/>
</DrawerItem>
<ResourceMetricsText metrics={metrics}/>
<ResourceMetricsText metrics={this.metrics}/>
<PodDetailsList pods={childPods} owner={replicaSet}/>
</div>
);

View File

@ -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<Props> {
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<Props> {
}
// 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,
});

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