Lens app source code (#119)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-03-15 09:52:02 +02:00
1 parent 936cbd53d4
commit 1d0815abd2
797 files changed
+73714 -215

No files matched your search

@@ -0,0 +1,4 @@
export * from "./nodes"
export * from "./nodes.route"
export * from "./node-menu"
export * from "./node-details"
@@ -0,0 +1,154 @@
import React, { useContext } from "react";
import { t } from "@lingui/macro";
import { IClusterMetrics, Node } from "../../api/endpoints";
import { BarChart, cpuOptions, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { observer } from "mobx-react";
import { ChartOptions, ChartPoint } from "chart.js";
import { themeStore } from "../../theme.store";
import { _i18n } from "../../i18n";
type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
export const NodeCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const id = object.getId();
const { chartCapacityColor } = themeStore.activeTheme.colors;
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const values = Object.values(metrics).map(metric =>
normalizeMetrics(metric).data.result[0].values
);
const [
memoryUsage,
memoryRequests,
memoryLimits,
memoryCapacity,
cpuUsage,
cpuRequests,
cpuLimits,
cpuCapacity,
podUsage,
podCapacity,
fsSize,
fsUsage
] = values;
const datasets = [
// CPU
[
{
id: `${id}-cpuUsage`,
label: _i18n._(t`Usage`),
tooltip: _i18n._(t`CPU cores usage`),
borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-cpuRequests`,
label: _i18n._(t`Requests`),
tooltip: _i18n._(t`CPU requests`),
borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-cpuCapacity`,
label: _i18n._(t`Capacity`),
tooltip: _i18n._(t`CPU capacity`),
borderColor: chartCapacityColor,
data: cpuCapacity.map(([x, y]) => ({ x, y }))
}
],
// Memory
[
{
id: `${id}-memoryUsage`,
label: _i18n._(t`Usage`),
tooltip: _i18n._(t`Memory usage`),
borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y }))
},
{
id: "memoryRequests",
label: _i18n._(t`Requests`),
tooltip: _i18n._(t`Memory requests`),
borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-memoryCapacity`,
label: _i18n._(t`Capacity`),
tooltip: _i18n._(t`Memory capacity`),
borderColor: chartCapacityColor,
data: memoryCapacity.map(([x, y]) => ({ x, y }))
}
],
// Disk
[
{
id: `${id}-fsUsage`,
label: _i18n._(t`Usage`),
tooltip: _i18n._(t`Node filesystem usage in bytes`),
borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-fsSize`,
label: _i18n._(t`Size`),
tooltip: _i18n._(t`Node filesystem size in bytes`),
borderColor: chartCapacityColor,
data: fsSize.map(([x, y]) => ({ x, y }))
}
],
// Pods
[
{
id: `${id}-podUsage`,
label: _i18n._(t`Usage`),
tooltip: _i18n._(t`Number of running Pods`),
borderColor: "#30b24d",
data: podUsage.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-podCapacity`,
label: _i18n._(t`Capacity`),
tooltip: _i18n._(t`Node Pods capacity`),
borderColor: chartCapacityColor,
data: podCapacity.map(([x, y]) => ({ x, y }))
}
]
];
const podOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: value => value
}
}]
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
return `${label}: ${value.y}`;
}
}
}
}
const options = [cpuOptions, memoryOptions, memoryOptions, podOptions];
return (
<BarChart
name={`${object.getName()}-metric-${tabId}`}
options={options[tabId]}
data={{ datasets: datasets[tabId] }}
/>
);
})
@@ -0,0 +1,11 @@
.NodeDetails {
--status-ready-bg: #{$colorOk};
--status-default-bg: #{$colorError};
.conditions {
.Badge {
cursor: default;
@include node-status-bgs;
}
}
}
@@ -0,0 +1,158 @@
import "./node-details.scss";
import React from "react";
import upperFirst from "lodash/upperFirst";
import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerItemLabels } from "../drawer";
import { Badge } from "../badge";
import { TooltipContent } from "../tooltip";
import { nodesStore } from "./nodes.store";
import { ResourceMetrics } from "../resource-metrics";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectDetailsProps } from "../kube-object";
import { Node, nodesApi } from "../../api/endpoints";
import { NodeCharts } from "./node-charts";
import { reaction } from "mobx";
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { apiManager } from "../../api/api-manager";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps<Node> {
}
@observer
export class NodeDetails extends React.Component<Props> {
@disposeOnUnmount
clean = reaction(() => this.props.object.getName(), () => {
nodesStore.nodeMetrics = null;
});
async componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
}
componentWillUnmount() {
nodesStore.nodeMetrics = null;
}
render() {
const { object: node } = this.props;
if (!node) return;
const { status } = node
const { nodeInfo, addresses, capacity, allocatable } = status
const conditions = node.getActiveConditions();
const taints = node.getTaints()
const childPods = podsStore.getPodsByNode(node.getName())
const metrics = nodesStore.nodeMetrics
const metricTabs = [
<Trans>CPU</Trans>,
<Trans>Memory</Trans>,
<Trans>Disk</Trans>,
<Trans>Pods</Trans>,
];
return (
<div className="NodeDetails">
{podsStore.isLoaded && (
<ResourceMetrics
loader={() => nodesStore.loadMetrics(node.getName())}
tabs={metricTabs} object={node} params={{ metrics }}
>
<NodeCharts/>
</ResourceMetrics>
)}
<KubeObjectMeta object={node} hideFields={["labels", "annotations", "uid", "resourceVersion", "selfLink"]}/>
<DrawerItem name={<Trans>Capacity</Trans>}>
<Trans>CPU</Trans>: {capacity.cpu},{" "}
<Trans>Memory</Trans>: {Math.floor(parseInt(capacity.memory) / 1024)}Mi,{" "}
<Trans>Pods</Trans>: {capacity.pods}
</DrawerItem>
<DrawerItem name={<Trans>Allocatable</Trans>}>
<Trans>CPU</Trans>: {allocatable.cpu},{" "}
<Trans>Memory</Trans>: {Math.floor(parseInt(allocatable.memory) / 1024)}Mi,{" "}
<Trans>Pods</Trans>: {allocatable.pods}
</DrawerItem>
{addresses &&
<DrawerItem name={<Trans>Addresses</Trans>}>
{
addresses.map(({ type, address }) => (
<p key={type}>{type}: {address}</p>
))
}
</DrawerItem>
}
<DrawerItem name={<Trans>OS</Trans>}>
{nodeInfo.operatingSystem} ({nodeInfo.architecture})
</DrawerItem>
<DrawerItem name={<Trans>OS Image</Trans>}>
{nodeInfo.osImage}
</DrawerItem>
<DrawerItem name={<Trans>Kernel version</Trans>}>
{nodeInfo.kernelVersion}
</DrawerItem>
<DrawerItem name={<Trans>Container runtime</Trans>}>
{nodeInfo.containerRuntimeVersion}
</DrawerItem>
<DrawerItem name={<Trans>Kubelet version</Trans>}>
{nodeInfo.kubeletVersion}
</DrawerItem>
<DrawerItemLabels
name={<Trans>Labels</Trans>}
labels={node.getLabels()}
/>
<DrawerItemLabels
name={<Trans>Annotations</Trans>}
labels={node.getAnnotations()}
/>
{taints.length > 0 && (
<DrawerItem name={<Trans>Taints</Trans>} labelsOnly>
{
taints.map(({ key, effect, value }) => (
<Badge key={key} label={key + ": " + effect} tooltip={value}/>
))
}
</DrawerItem>
)}
{conditions &&
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
{
conditions.map(condition => {
const { type } = condition
return (
<Badge
key={type}
label={type}
className={kebabCase(type)}
tooltip={
<TooltipContent tableView>
{Object.entries(condition).map(([key, value]) =>
<div key={key} className="flex gaps align-center">
<div className="name">{upperFirst(key)}</div>
<div className="value">{value}</div>
</div>
)}
</TooltipContent>
}
/>
)
})
}
</DrawerItem>
}
<PodDetailsList
pods={childPods}
owner={node}
maxCpu={node.getCpuCapacity()}
maxMemory={node.getMemoryCapacity()}
/>
</div>
)
}
}
apiManager.registerViews(nodesApi, {
Details: NodeDetails,
});
@@ -0,0 +1,78 @@
import * as React from "react";
import { t, Trans } from "@lingui/macro";
import { Node } from "../../api/endpoints/nodes.api";
import { createTerminalTab, terminalStore } from "../dock/terminal.store";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { ConfirmDialog } from "../confirm-dialog";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { _i18n } from "../../i18n";
import { hideDetails } from "../../navigation";
export function NodeMenu(props: KubeObjectMenuProps<Node>) {
const { object: node, toolbar } = props;
if (!node) return null;
const nodeName = node.getName();
const sendToTerminal = (command: string) => {
terminalStore.sendCommand(command, {
enter: true,
newTab: true,
});
hideDetails();
}
const shell = () => {
createTerminalTab({
title: _i18n._(t`Shell: ${nodeName}`),
node: nodeName,
});
hideDetails();
}
const cordon = () => {
sendToTerminal(`kubectl cordon ${nodeName}`);
}
const unCordon = () => {
sendToTerminal(`kubectl uncordon ${nodeName}`)
}
const drain = () => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
ConfirmDialog.open({
ok: () => sendToTerminal(command),
labelOk: _i18n._(t`Drain Node`),
message: (
<p>
<Trans>Are you sure you want to drain <b>{nodeName}</b>?</Trans>
</p>
),
})
}
return (
<KubeObjectMenu {...props}>
<MenuItem onClick={shell}>
<Icon svg="ssh" interactive={toolbar} title={_i18n._(t`Node shell`)}/>
<span className="title"><Trans>Shell</Trans></span>
</MenuItem>
{!node.isUnschedulable() && (
<MenuItem onClick={cordon}>
<Icon material="pause_circle_filled" title={_i18n._(t`Cordon`)} interactive={toolbar}/>
<span className="title"><Trans>Cordon</Trans></span>
</MenuItem>
)}
{node.isUnschedulable() && (
<MenuItem onClick={unCordon}>
<Icon material="play_circle_filled" title={_i18n._(t`Uncordon`)} interactive={toolbar}/>
<span className="title"><Trans>Uncordon</Trans></span>
</MenuItem>
)}
<MenuItem onClick={drain}>
<Icon material="delete_sweep" title={_i18n._(t`Drain`)} interactive={toolbar}/>
<span className="title"><Trans>Drain</Trans></span>
</MenuItem>
</KubeObjectMenu>
);
}
@@ -0,0 +1,28 @@
$node-status-color-list: (
out-of-disk: #ce3933,
network-unavailable: #812727,
memory-pressure: #901995,
disk-pressure: #CC8D00,
pid-pressure: #068DBF,
ready: #4caf50,
scheduling-disabled: #ff9800,
invalid-license: #ce3933,
cordoned: $colorWarning
);
@mixin node-status-bgs {
@each $status, $color in $node-status-color-list {
&.#{$status} {
background: $color;
color: white;
}
}
}
@mixin node-status-colors {
@each $status, $color in $node-status-color-list {
&.#{$status} {
color: $color;
}
}
}
@@ -0,0 +1,11 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
export const nodesRoute: RouteProps = {
path: "/nodes"
}
export interface INodesRouteParams {
}
export const nodesURL = buildURL<INodesRouteParams>(nodesRoute.path)
@@ -0,0 +1,53 @@
@import "./nodes-mixins";
.Nodes {
.TableCell {
&.name {
flex: 2;
text-overflow: ellipsis;
}
&.cpu {
flex: 1.0;
align-self: center;
.LineProgress {
color: $kontenaBlue;
}
}
&.memory {
flex: 1.0;
align-self: center;
.LineProgress {
color: $kontenaMagenta;
}
}
&.disk {
flex: 1.0;
align-self: center;
.LineProgress {
color: $golden;
}
}
&.taints {
flex: 0.6;
}
&.conditions {
flex: 1.7;
text-align: right;
.condition {
display: inline-block;
margin-right: $margin;
@include node-status-colors;
}
}
}
}
@@ -0,0 +1,72 @@
import { action, computed, observable } from "mobx"
import { clusterApi, IClusterMetrics, INodeMetrics, Node, nodesApi } from "../../api/endpoints";
import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
@autobind()
export class NodesStore extends KubeObjectStore<Node> {
api = nodesApi
@observable metrics: Partial<INodeMetrics> = {};
@observable nodeMetrics: Partial<IClusterMetrics> = null;
@observable metricsLoading = false;
@observable metricsLoaded = false;
@action
async loadUsageMetrics() {
this.metricsLoading = true;
try {
this.metrics = await nodesApi.getMetrics();
this.metricsLoaded = true;
} finally {
this.metricsLoading = false;
}
}
@action
async loadMetrics(nodeName: string) {
this.nodeMetrics = await clusterApi.getMetrics([nodeName]);
}
@computed get masterNodes() {
return this.items.filter(node => node.getRoleLabels().includes("master"))
}
@computed get workerNodes() {
return this.items.filter(node => !node.getRoleLabels().includes("master"))
}
getLastMetricValues(node: Node, metricNames: string[]): number[] {
if (!this.metricsLoaded) {
return;
}
const nodeName = node.getName();
return metricNames.map(metricName => {
try {
const metric = this.metrics[metricName];
const result = metric.data.result.find(result => {
return [
result.metric.node,
result.metric.instance,
result.metric.kubernetes_node,
].includes(nodeName)
});
return result ? parseFloat(result.values.slice(-1)[0][1]) : 0;
} catch (e) {
return 0;
}
});
}
reset() {
super.reset();
this.metrics = {};
this.nodeMetrics = null;
this.metricsLoading = false;
this.metricsLoaded = false;
}
}
export const nodesStore = new NodesStore()
apiManager.registerStore(nodesApi, nodesStore);
@@ -0,0 +1,186 @@
import "./nodes.scss";
import * as React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { cssNames, interval } from "../../utils";
import { MainLayout } from "../layout/main-layout";
import { nodesStore } from "./nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object";
import { INodesRouteParams } from "./nodes.route";
import { Node, nodesApi } from "../../api/endpoints/nodes.api";
import { NodeMenu } from "./node-menu";
import { LineProgress } from "../line-progress";
import { _i18n } from "../../i18n";
import { bytesToUnits } from "../../utils/convertMemory";
import { Tooltip, TooltipContent } from "../tooltip";
import kebabCase from "lodash/kebabCase";
import upperFirst from "lodash/upperFirst";
import { apiManager } from "../../api/api-manager";
enum sortBy {
name = "name",
cpu = "cpu",
memory = "memory",
disk = "disk",
conditions = "condition",
taints = "taints",
roles = "roles",
age = "age",
version = "version",
status = "status",
}
interface Props extends RouteComponentProps<INodesRouteParams> {
}
@observer
export class Nodes extends React.Component<Props> {
private metricsWatcher = interval(30, () => nodesStore.loadUsageMetrics());
componentDidMount() {
this.metricsWatcher.start(true);
}
componentWillUnmount() {
this.metricsWatcher.stop();
}
renderCpuUsage(node: Node) {
const metrics = nodesStore.getLastMetricValues(node, ["cpuUsage", "cpuCapacity"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
const usage = metrics[0];
const cores = metrics[1];
return (
<LineProgress
max={cores}
value={usage}
tooltip={_i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`}
/>
)
}
renderMemoryUsage(node: Node) {
const metrics = nodesStore.getLastMetricValues(node, ["memoryUsage", "memoryCapacity"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
const usage = metrics[0];
const capacity = metrics[1];
return (
<LineProgress
max={capacity}
value={usage}
tooltip={_i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
/>
)
}
renderDiskUsage(node: Node): any {
const metrics = nodesStore.getLastMetricValues(node, ["fsUsage", "fsSize"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
const usage = metrics[0];
const capacity = metrics[1];
return (
<LineProgress
max={capacity}
value={usage}
tooltip={_i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
/>
)
}
renderConditions(node: Node) {
if (!node.status.conditions) {
return null
}
const conditions = node.getActiveConditions();
return conditions.map(condition => {
const { type } = condition
const tooltipId = `node-${node.getName()}-condition-${type}`
return (
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
{type}
<Tooltip htmlFor={tooltipId} following>
<TooltipContent tableView>
{Object.entries(condition).map(([key, value]) =>
<div key={key} className="flex gaps align-center">
<div className="name">{upperFirst(key)}</div>
<div className="value">{value}</div>
</div>
)}
</TooltipContent>
</Tooltip>
</div>)
})
}
render() {
return (
<MainLayout>
<KubeObjectListLayout
className="Nodes"
store={nodesStore} isClusterScoped
isReady={nodesStore.isLoaded && nodesStore.metricsLoaded}
dependentStores={[podsStore]}
isSelectable={false}
sortingCallbacks={{
[sortBy.name]: (node: Node) => node.getName(),
[sortBy.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]),
[sortBy.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]),
[sortBy.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]),
[sortBy.conditions]: (node: Node) => node.getNodeConditionText(),
[sortBy.taints]: (node: Node) => node.getTaints().length,
[sortBy.roles]: (node: Node) => node.getRoleLabels(),
[sortBy.age]: (node: Node) => node.getAge(false),
[sortBy.version]: (node: Node) => node.getKubeletVersion(),
}}
searchFilters={[
(node: Node) => node.getSearchFields(),
(node: Node) => node.getRoleLabels(),
(node: Node) => node.getKubeletVersion(),
(node: Node) => node.getNodeConditionText(),
]}
renderHeaderTitle={<Trans>Nodes</Trans>}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>CPU</Trans>, className: "cpu", sortBy: sortBy.cpu },
{ title: <Trans>Memory</Trans>, className: "memory", sortBy: sortBy.memory },
{ title: <Trans>Disk</Trans>, className: "disk", sortBy: sortBy.disk },
{ title: <Trans>Taints</Trans>, className: "taints", sortBy: sortBy.taints },
{ title: <Trans>Roles</Trans>, className: "roles", sortBy: sortBy.roles },
{ title: <Trans>Version</Trans>, className: "version", sortBy: sortBy.version },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Conditions</Trans>, className: "conditions", sortBy: sortBy.conditions },
]}
renderTableContents={(node: Node) => {
const tooltipId = `node-taints-${node.getId()}`;
return [
node.getName(),
this.renderCpuUsage(node),
this.renderMemoryUsage(node),
this.renderDiskUsage(node),
<>
<span id={tooltipId}>{node.getTaints().length}</span>
<Tooltip htmlFor={tooltipId} style={{ whiteSpace: "pre-line" }}>
{node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")}
</Tooltip>
</>,
node.getRoleLabels(),
node.status.nodeInfo.kubeletVersion,
node.getAge(),
this.renderConditions(node),
]
}}
renderItemMenu={(item: Node) => {
return <NodeMenu object={item}/>
}}
/>
</MainLayout>
)
}
}
apiManager.registerViews(nodesApi, {
Menu: NodeMenu,
});