mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import React, { useContext } from "react";
|
||||
import { t } from "@lingui/macro";
|
||||
import { IPodMetrics } 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 { _i18n } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
|
||||
|
||||
export const ContainerCharts = () => {
|
||||
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
|
||||
if (!metrics) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const values = Object.values(metrics)
|
||||
.map(normalizeMetrics)
|
||||
.map(({ data }) => data.result[0].values);
|
||||
const [
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
cpuLimits,
|
||||
memoryUsage,
|
||||
memoryRequests,
|
||||
memoryLimits,
|
||||
fsUsage
|
||||
] = values;
|
||||
|
||||
const datasets = [
|
||||
// CPU
|
||||
[
|
||||
{
|
||||
id: "cpuUsage",
|
||||
label: _i18n._(t`Usage`),
|
||||
tooltip: _i18n._(t`CPU cores usage`),
|
||||
borderColor: "#3D90CE",
|
||||
data: cpuUsage.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: "cpuRequests",
|
||||
label: _i18n._(t`Requests`),
|
||||
tooltip: _i18n._(t`CPU requests`),
|
||||
borderColor: "#30b24d",
|
||||
data: cpuRequests.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: "cpuLimits",
|
||||
label: _i18n._(t`Limits`),
|
||||
tooltip: _i18n._(t`CPU limits`),
|
||||
borderColor: chartCapacityColor,
|
||||
data: cpuLimits.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
],
|
||||
// Memory
|
||||
[
|
||||
{
|
||||
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: "memoryLimits",
|
||||
label: _i18n._(t`Limits`),
|
||||
tooltip: _i18n._(t`Memory limits`),
|
||||
borderColor: chartCapacityColor,
|
||||
data: memoryLimits.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
],
|
||||
// Filesystem
|
||||
[
|
||||
{
|
||||
id: "fsUsage",
|
||||
label: _i18n._(t`Usage`),
|
||||
tooltip: _i18n._(t`Bytes consumed on this filesystem`),
|
||||
borderColor: "#ffc63d",
|
||||
data: fsUsage.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const options = tabId == 0 ? cpuOptions : memoryOptions;
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
name={`metrics-${tabId}`}
|
||||
options={options}
|
||||
data={{ datasets: datasets[tabId] }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./pods"
|
||||
export * from "./pod-details"
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useContext } from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { observer } from "mobx-react";
|
||||
import { IPodMetrics } 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 { _i18n } from "../../i18n";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export const podMetricTabs = [
|
||||
<Trans>CPU</Trans>,
|
||||
<Trans>Memory</Trans>,
|
||||
<Trans>Network</Trans>,
|
||||
<Trans>Filesystem</Trans>,
|
||||
];
|
||||
|
||||
type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics }>;
|
||||
|
||||
export const PodCharts = observer(() => {
|
||||
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const id = object.getId();
|
||||
|
||||
if (!metrics) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const options = tabId == 0 ? cpuOptions : memoryOptions;
|
||||
const values = Object.values(metrics)
|
||||
.map(normalizeMetrics)
|
||||
.map(({ data }) => data.result[0].values);
|
||||
const [
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
cpuLimits,
|
||||
memoryUsage,
|
||||
memoryRequests,
|
||||
memoryLimits,
|
||||
fsUsage,
|
||||
networkReceive,
|
||||
networkTransmit
|
||||
] = values;
|
||||
|
||||
const datasets = [
|
||||
// CPU
|
||||
[
|
||||
{
|
||||
id: `${id}-cpuUsage`,
|
||||
label: _i18n._(t`Usage`),
|
||||
tooltip: _i18n._(t`Container CPU cores usage`),
|
||||
borderColor: "#3D90CE",
|
||||
data: cpuUsage.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: `${id}-cpuRequests`,
|
||||
label: _i18n._(t`Requests`),
|
||||
tooltip: _i18n._(t`Container CPU requests`),
|
||||
borderColor: "#30b24d",
|
||||
data: cpuRequests.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: `${id}-cpuLimits`,
|
||||
label: _i18n._(t`Limits`),
|
||||
tooltip: _i18n._(t`CPU limits`),
|
||||
borderColor: chartCapacityColor,
|
||||
data: cpuLimits.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
],
|
||||
// Memory
|
||||
[
|
||||
{
|
||||
id: `${id}-memoryUsage`,
|
||||
label: _i18n._(t`Usage`),
|
||||
tooltip: _i18n._(t`Container memory usage`),
|
||||
borderColor: "#c93dce",
|
||||
data: memoryUsage.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: `${id}-memoryRequests`,
|
||||
label: _i18n._(t`Requests`),
|
||||
tooltip: _i18n._(t`Container memory requests`),
|
||||
borderColor: "#30b24d",
|
||||
data: memoryRequests.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: `${id}-memoryLimits`,
|
||||
label: _i18n._(t`Limits`),
|
||||
tooltip: _i18n._(t`Container memory limits`),
|
||||
borderColor: chartCapacityColor,
|
||||
data: memoryLimits.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
],
|
||||
// Network
|
||||
[
|
||||
{
|
||||
id: `${id}-networkReceive`,
|
||||
label: _i18n._(t`Receive`),
|
||||
tooltip: _i18n._(t`Bytes received by all containers`),
|
||||
borderColor: "#64c5d6",
|
||||
data: networkReceive.map(([x, y]) => ({ x, y }))
|
||||
},
|
||||
{
|
||||
id: `${id}-networkTransmit`,
|
||||
label: _i18n._(t`Transmit`),
|
||||
tooltip: _i18n._(t`Bytes transmitted from all containers`),
|
||||
borderColor: "#46cd9e",
|
||||
data: networkTransmit.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
],
|
||||
// Filesystem
|
||||
[
|
||||
{
|
||||
id: `${id}-fsUsage`,
|
||||
label: _i18n._(t`Usage`),
|
||||
tooltip: _i18n._(t`Bytes consumed on this filesystem`),
|
||||
borderColor: "#ffc63d",
|
||||
data: fsUsage.map(([x, y]) => ({ x, y }))
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
name={`${object.getName()}-metric-${tabId}`}
|
||||
options={options}
|
||||
data={{ datasets: datasets[tabId] }}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
.ContainerEnvironment {
|
||||
.secret-button {
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.variable {
|
||||
padding-bottom: $padding;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.var-name {
|
||||
color: $textColorPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import "./pod-container-env.scss";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import flatten from "lodash/flatten";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { IPodContainer, Secret } from "../../api/endpoints";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { autorun } from "mobx";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { configMapsStore } from "../+config-maps/config-maps.store";
|
||||
import { Icon } from "../icon";
|
||||
import { base64, cssNames } from "../../utils";
|
||||
|
||||
interface Props {
|
||||
container: IPodContainer;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export const ContainerEnvironment = observer((props: Props) => {
|
||||
const { container: { env, envFrom }, namespace } = props
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
autorun(() => {
|
||||
env && env.forEach(variable => {
|
||||
const { valueFrom } = variable
|
||||
if (valueFrom && valueFrom.configMapKeyRef) {
|
||||
configMapsStore.load({ name: valueFrom.configMapKeyRef.name, namespace })
|
||||
}
|
||||
})
|
||||
envFrom && envFrom.forEach(item => {
|
||||
const { configMapRef } = item
|
||||
if (configMapRef && configMapRef.name) {
|
||||
configMapsStore.load({ name: configMapRef.name, namespace })
|
||||
}
|
||||
})
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const renderEnv = () => {
|
||||
return env.map(variable => {
|
||||
const { name, value, valueFrom } = variable
|
||||
let secretValue = null
|
||||
|
||||
if (value) {
|
||||
secretValue = value
|
||||
}
|
||||
if (valueFrom) {
|
||||
const { fieldRef, secretKeyRef, configMapKeyRef } = valueFrom
|
||||
if (fieldRef) {
|
||||
const { apiVersion, fieldPath } = fieldRef
|
||||
secretValue = `fieldRef(${apiVersion}:${fieldPath})`
|
||||
}
|
||||
if (secretKeyRef) {
|
||||
secretValue = (
|
||||
<SecretKey
|
||||
reference={secretKeyRef}
|
||||
namespace={namespace}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (configMapKeyRef) {
|
||||
const { name, key } = configMapKeyRef
|
||||
const configMap = configMapsStore.getByName(name, namespace)
|
||||
secretValue = configMap ?
|
||||
configMap.data[key] :
|
||||
`configMapKeyRef(${name}${key})`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="variable" key={name}>
|
||||
<span className="var-name">{name}</span>: {secretValue}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const renderEnvFrom = () => {
|
||||
const envVars = envFrom.map(vars => {
|
||||
if (!vars.configMapRef || !vars.configMapRef.name) return
|
||||
const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace)
|
||||
if (!configMap) return
|
||||
return Object.entries(configMap.data).map(([name, value]) => (
|
||||
<div className="variable" key={name}>
|
||||
<span className="var-name">{name}</span>: {value}
|
||||
</div>
|
||||
))
|
||||
})
|
||||
return flatten(envVars)
|
||||
}
|
||||
|
||||
return (
|
||||
<DrawerItem name={<Trans>Environment</Trans>} className="ContainerEnvironment">
|
||||
{env && renderEnv()}
|
||||
{envFrom && renderEnvFrom()}
|
||||
</DrawerItem>
|
||||
)
|
||||
})
|
||||
|
||||
interface SecretKeyProps {
|
||||
reference: {
|
||||
name: string;
|
||||
key: string;
|
||||
};
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
const SecretKey = (props: SecretKeyProps) => {
|
||||
const { reference: { name, key }, namespace } = props
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [secret, setSecret] = useState<Secret>()
|
||||
|
||||
const showKey = async () => {
|
||||
setLoading(true)
|
||||
const secret = await secretsStore.load({ name, namespace });
|
||||
setLoading(false)
|
||||
setSecret(secret)
|
||||
}
|
||||
|
||||
if (secret?.data?.[key]) {
|
||||
return <>{base64.decode(secret.data[key])}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
secretKeyRef({name}.{key})
|
||||
<Icon
|
||||
className={cssNames("secret-button", { loading })}
|
||||
material="visibility"
|
||||
tooltip={<Trans>Show</Trans>}
|
||||
onClick={showKey}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.PodDetailsAffinities {
|
||||
.ace-container {
|
||||
height: 200px
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import "./pod-details-affinities.scss";
|
||||
import React from "react";
|
||||
import jsYaml from "js-yaml";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { AceEditor } from "../ace-editor";
|
||||
import { DrawerParamToggler, DrawerItem } from "../drawer";
|
||||
import { Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job } from "../../api/endpoints";
|
||||
|
||||
interface Props {
|
||||
workload: Pod | Deployment | DaemonSet | StatefulSet | ReplicaSet | Job;
|
||||
}
|
||||
|
||||
export class PodDetailsAffinities extends React.Component<Props> {
|
||||
render() {
|
||||
const { workload } = this.props
|
||||
const affinitiesNum = workload.getAffinityNumber()
|
||||
const affinities = workload.getAffinity()
|
||||
if (!affinitiesNum) return null
|
||||
return (
|
||||
<DrawerItem name={<Trans>Affinities</Trans>} className="PodDetailsAffinities">
|
||||
<DrawerParamToggler label={affinitiesNum}>
|
||||
<div className="ace-container">
|
||||
<AceEditor
|
||||
mode="yaml"
|
||||
value={jsYaml.dump(affinities)}
|
||||
showGutter={false}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</DrawerParamToggler>
|
||||
</DrawerItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.PodDetailsContainer {
|
||||
margin: $margin * 2 0;
|
||||
|
||||
.mount-path {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
display: block;
|
||||
font-family: $font-monospace;
|
||||
font-size: 90%;
|
||||
background: $colorVague;
|
||||
color: $textColorSecondary;
|
||||
border-radius: $radius;
|
||||
padding: .2em .4em;
|
||||
margin-top: $margin;
|
||||
}
|
||||
|
||||
.pod-container-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: $margin;
|
||||
|
||||
.StatusBrick {
|
||||
background: $colorTerminated;
|
||||
margin-right: $margin;
|
||||
|
||||
@include pod-status-bgs;
|
||||
|
||||
&.running:not(.ready) {
|
||||
background-color: $pod-status-pending-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
color: $colorTerminated;
|
||||
|
||||
@include pod-status-colors;
|
||||
}
|
||||
|
||||
.Badge {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import "./pod-details-container.scss"
|
||||
|
||||
import React from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { IPodContainer, Pod } from "../../api/endpoints";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { cssNames } from "../../utils";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
import { Badge } from "../badge";
|
||||
import { ContainerEnvironment } from "./pod-container-env";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { IMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { ContainerCharts } from "./container-charts";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
container: IPodContainer;
|
||||
metrics?: { [key: string]: IMetrics };
|
||||
}
|
||||
|
||||
export class PodDetailsContainer extends React.Component<Props> {
|
||||
render() {
|
||||
const { pod, container, metrics } = this.props
|
||||
if (!pod || !container) return null
|
||||
const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container
|
||||
const status = pod.getContainerStatuses().find(status => status.name === container.name)
|
||||
const state = status ? Object.keys(status.state)[0] : ""
|
||||
const ready = status ? status.ready : ""
|
||||
const liveness = pod.getLivenessProbe(container)
|
||||
const readiness = pod.getReadinessProbe(container)
|
||||
const isInitContainer = !!pod.getInitContainers().find(c => c.name == name);
|
||||
const metricTabs = [
|
||||
<Trans>CPU</Trans>,
|
||||
<Trans>Memory</Trans>,
|
||||
<Trans>Filesystem</Trans>,
|
||||
];
|
||||
return (
|
||||
<div className="PodDetailsContainer">
|
||||
<div className="pod-container-title">
|
||||
<StatusBrick className={cssNames(state, { ready })}/>{name}
|
||||
</div>
|
||||
{!isInitContainer &&
|
||||
<ResourceMetrics tabs={metricTabs} params={{ metrics }}>
|
||||
<ContainerCharts/>
|
||||
</ResourceMetrics>
|
||||
}
|
||||
{status &&
|
||||
<DrawerItem name={<Trans>Status</Trans>}>
|
||||
<span className={cssNames("status", state)}>
|
||||
{state}{ready ? `, ${_i18n._(t`ready`)}` : ""}
|
||||
{state === 'terminated' ? ` - ${status.state.terminated.reason} (${_i18n._(t`exit code`)}: ${status.state.terminated.exitCode})` : ''}
|
||||
</span>
|
||||
</DrawerItem>
|
||||
}
|
||||
<DrawerItem name={<Trans>Image</Trans>}>
|
||||
{image}
|
||||
</DrawerItem>
|
||||
{imagePullPolicy && imagePullPolicy !== "IfNotPresent" &&
|
||||
<DrawerItem name={<Trans>ImagePullPolicy</Trans>}>
|
||||
{imagePullPolicy}
|
||||
</DrawerItem>
|
||||
}
|
||||
{ports && ports.length > 0 &&
|
||||
<DrawerItem name={<Trans>Ports</Trans>}>
|
||||
{
|
||||
ports.map(port => {
|
||||
const { name, containerPort, protocol } = port;
|
||||
const key = `${container.name}-port-${containerPort}-${protocol}`
|
||||
return (
|
||||
<div key={key}>
|
||||
{name ? name + ': ' : ''}{containerPort}/{protocol}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{<ContainerEnvironment container={container} namespace={pod.getNs()}/>}
|
||||
{volumeMounts && volumeMounts.length > 0 &&
|
||||
<DrawerItem name={<Trans>Mounts</Trans>}>
|
||||
{
|
||||
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>
|
||||
}
|
||||
{liveness.length > 0 &&
|
||||
<DrawerItem name={<Trans>Liveness</Trans>} labelsOnly>
|
||||
{
|
||||
liveness.map((value, index) => (
|
||||
<Badge key={index} label={value}/>
|
||||
))
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{readiness.length > 0 &&
|
||||
<DrawerItem name={<Trans>Readiness</Trans>} labelsOnly>
|
||||
{
|
||||
readiness.map((value, index) => (
|
||||
<Badge key={index} label={value}/>
|
||||
))
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{command &&
|
||||
<DrawerItem name={<Trans>Command</Trans>}>
|
||||
{command.join(' ')}
|
||||
</DrawerItem>
|
||||
}
|
||||
|
||||
{args &&
|
||||
<DrawerItem name={<Trans>Arguments</Trans>}>
|
||||
{args.join(' ')}
|
||||
</DrawerItem>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
.PodDetailsList {
|
||||
position: relative;
|
||||
|
||||
.Table {
|
||||
margin: 0 (-$margin * 3);
|
||||
|
||||
&.virtual {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
&:first-child {
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: $margin;
|
||||
}
|
||||
|
||||
&.name {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
flex-grow: 1.2;
|
||||
}
|
||||
|
||||
&.cpu {
|
||||
align-self: center;
|
||||
|
||||
.LineProgress {
|
||||
color: $lensBlue;
|
||||
}
|
||||
}
|
||||
|
||||
&.memory {
|
||||
align-self: center;
|
||||
|
||||
.LineProgress {
|
||||
color: $lensMagenta;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.status {
|
||||
@include pod-status-colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import "./pod-details-list.scss";
|
||||
|
||||
import React from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { podsStore } from "./pods.store";
|
||||
import { Pod } from "../../api/endpoints";
|
||||
import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../utils";
|
||||
import { KubeEventIcon } from "../+events/kube-event-icon";
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { showDetails } from "../../navigation";
|
||||
import { reaction } from "mobx";
|
||||
import { Spinner } from "../spinner";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
cpu = "cpu",
|
||||
memory = "memory",
|
||||
}
|
||||
|
||||
interface Props extends OptionalProps {
|
||||
pods: Pod[];
|
||||
owner: KubeObject;
|
||||
}
|
||||
|
||||
interface OptionalProps {
|
||||
maxCpu?: number;
|
||||
maxMemory?: number;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodDetailsList extends React.Component<Props> {
|
||||
static defaultProps: OptionalProps = {
|
||||
showTitle: true
|
||||
}
|
||||
|
||||
private metricsWatcher = interval(120, () => {
|
||||
podsStore.loadKubeMetrics(this.props.owner.getNs());
|
||||
});
|
||||
|
||||
private sortingCallbacks = {
|
||||
[sortBy.name]: (pod: Pod) => pod.getName(),
|
||||
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
|
||||
[sortBy.cpu]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).cpu,
|
||||
[sortBy.memory]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).memory,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.metricsWatcher.start(true);
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.owner, () => this.metricsWatcher.restart(true))
|
||||
])
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.metricsWatcher.stop();
|
||||
}
|
||||
|
||||
renderCpuUsage(id: string, usage: number) {
|
||||
const { maxCpu } = this.props;
|
||||
const value = usage.toFixed(3);
|
||||
const tooltip = (
|
||||
<p><Trans>CPU</Trans>: {Math.ceil(usage * 100) / maxCpu}%<br/>{usage.toFixed(3)}</p>
|
||||
);
|
||||
if (!maxCpu) {
|
||||
if (parseFloat(value) === 0) return 0;
|
||||
return value;
|
||||
}
|
||||
return (
|
||||
<LineProgress
|
||||
max={maxCpu} value={usage}
|
||||
tooltip={parseFloat(value) !== 0 ? tooltip : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMemoryUsage(id: string, usage: number) {
|
||||
const { maxMemory } = this.props;
|
||||
const tooltip = (
|
||||
<p><Trans>Memory</Trans>: {Math.ceil(usage * 100 / maxMemory)}%<br/>{bytesToUnits(usage, 3)}</p>
|
||||
);
|
||||
if (!maxMemory) return usage ? bytesToUnits(usage) : 0;
|
||||
return (
|
||||
<LineProgress
|
||||
max={maxMemory} value={usage}
|
||||
tooltip={usage != 0 ? tooltip : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
getTableRow(uid: string) {
|
||||
const { pods } = this.props;
|
||||
const pod = pods.find(pod => pod.getId() == uid);
|
||||
const metrics = podsStore.getPodKubeMetrics(pod);
|
||||
return (
|
||||
<TableRow
|
||||
key={pod.getId()}
|
||||
sortItem={pod}
|
||||
nowrap
|
||||
onClick={prevDefault(() => showDetails(pod.selfLink, false))}
|
||||
>
|
||||
<TableCell className="name">{pod.getName()}</TableCell>
|
||||
<TableCell className="warning">{pod.hasIssues() && <KubeEventIcon object={pod}/>}</TableCell>
|
||||
<TableCell className="namespace">{pod.getNs()}</TableCell>
|
||||
<TableCell className="cpu">{this.renderCpuUsage(`cpu-${pod.getId()}`, metrics.cpu)}</TableCell>
|
||||
<TableCell className="memory">{this.renderMemoryUsage(`memory-${pod.getId()}`, metrics.memory)}</TableCell>
|
||||
<TableCell className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pods, showTitle } = this.props;
|
||||
const virtual = pods.length > 100;
|
||||
if (!pods.length && !podsStore.isLoaded) return (
|
||||
<div className="PodDetailsList flex justify-center"><Spinner/></div>
|
||||
);
|
||||
if (!pods.length) return null;
|
||||
return (
|
||||
<div className="PodDetailsList flex column">
|
||||
{showTitle && <DrawerTitle title={<Trans>Pods</Trans>}/>}
|
||||
<Table
|
||||
items={pods}
|
||||
selectable
|
||||
virtual={virtual}
|
||||
scrollable={false}
|
||||
sortable={this.sortingCallbacks}
|
||||
sortByDefault={{ sortBy: sortBy.cpu, orderBy: "desc" }}
|
||||
sortSyncWithUrl={false}
|
||||
getTableRow={this.getTableRow}
|
||||
className="box grow"
|
||||
>
|
||||
<TableHead>
|
||||
<TableCell className="name" sortBy={sortBy.name}><Trans>Name</Trans></TableCell>
|
||||
<TableCell className="warning"/>
|
||||
<TableCell className="namespace" sortBy={sortBy.namespace}>Namespace</TableCell>
|
||||
<TableCell className="cpu" sortBy={sortBy.cpu}><Trans>CPU</Trans></TableCell>
|
||||
<TableCell className="memory" sortBy={sortBy.memory}><Trans>Memory</Trans></TableCell>
|
||||
<TableCell className="status"><Trans>Status</Trans></TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
!virtual && pods.map(pod => this.getTableRow(pod.getId()))
|
||||
}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.PodDetailsSecrets {
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: $margin;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import "./pod-details-secrets.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Pod, Secret, secretsApi } from "../../api/endpoints";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodDetailsSecrets extends Component<Props> {
|
||||
@observable secrets: Secret[] = [];
|
||||
|
||||
@disposeOnUnmount
|
||||
secretsLoader = autorun(async () => {
|
||||
const { pod } = this.props;
|
||||
this.secrets = await Promise.all(
|
||||
pod.getSecrets().map(secretName => secretsApi.get({
|
||||
name: secretName,
|
||||
namespace: pod.getNs(),
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="PodDetailsSecrets">
|
||||
{
|
||||
this.secrets.map(secret => {
|
||||
return (
|
||||
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
||||
{secret.getName()}
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.PodDetailsStatuses {
|
||||
span {
|
||||
padding-right: $margin;
|
||||
|
||||
&.running {
|
||||
color: $pod-status-running-color;
|
||||
}
|
||||
&.pending {
|
||||
color: $pod-status-pending-color;
|
||||
}
|
||||
&.succeeded {
|
||||
color: $pod-status-succeeded-color;
|
||||
}
|
||||
&.failed {
|
||||
color: $pod-status-failed-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import "./pod-details-statuses.scss";
|
||||
import React from "react";
|
||||
import countBy from "lodash/countBy";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { Pod } from "../../api/endpoints";
|
||||
|
||||
interface Props {
|
||||
pods: Pod[];
|
||||
}
|
||||
|
||||
export class PodDetailsStatuses extends React.Component<Props> {
|
||||
render() {
|
||||
const { pods } = this.props
|
||||
if (!pods.length) return null
|
||||
const statuses = countBy(pods.map(pod => pod.getStatus()))
|
||||
return (
|
||||
<div className="PodDetailsStatuses">
|
||||
{
|
||||
Object.keys(statuses).map(key => (
|
||||
<span key={key} className={kebabCase(key)}>
|
||||
{key}: {statuses[key]}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.PodDetailsTolerations {
|
||||
.toleration {
|
||||
margin-bottom: $margin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import "./pod-details-tolerations.scss";
|
||||
import React from "react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job } from "../../api/endpoints";
|
||||
import { DrawerParamToggler, DrawerItem } from "../drawer";
|
||||
|
||||
interface Props {
|
||||
workload: Pod | Deployment | DaemonSet | StatefulSet | ReplicaSet | Job;
|
||||
}
|
||||
|
||||
export class PodDetailsTolerations extends React.Component<Props> {
|
||||
render() {
|
||||
const { workload } = this.props
|
||||
const tolerations = workload.getTolerations()
|
||||
if (!tolerations.length) return null
|
||||
return (
|
||||
<DrawerItem name={<Trans>Tolerations</Trans>} className="PodDetailsTolerations">
|
||||
<DrawerParamToggler label={tolerations.length}>
|
||||
{
|
||||
tolerations.map((toleration, index) => {
|
||||
const { key, operator, effect, tolerationSeconds } = toleration
|
||||
return (
|
||||
<div className="toleration" key={index}>
|
||||
<DrawerItem name={<Trans>Key</Trans>}>{key}</DrawerItem>
|
||||
{operator && <DrawerItem name={<Trans>Operator</Trans>}>{operator}</DrawerItem>}
|
||||
{effect && <DrawerItem name={<Trans>Effect</Trans>}>{effect}</DrawerItem>}
|
||||
{!!tolerationSeconds && <DrawerItem name={<Trans>Effect</Trans>}>{tolerationSeconds}</DrawerItem>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DrawerParamToggler>
|
||||
</DrawerItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.PodDetails {
|
||||
.status {
|
||||
@include pod-status-colors;
|
||||
}
|
||||
|
||||
.volume {
|
||||
.title {
|
||||
margin-top: $margin * 2;
|
||||
margin-bottom: $margin;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import "./pod-details.scss"
|
||||
|
||||
import React from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { autorun, observable, reaction, toJS } from "mobx";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { IPodMetrics, nodesApi, Pod, podsApi, pvcApi, configMapApi } from "../../api/endpoints";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { autobind, cssNames, interval } from "../../utils";
|
||||
import { PodDetailsContainer } from "./pod-details-container";
|
||||
import { PodDetailsAffinities } from "./pod-details-affinities";
|
||||
import { PodDetailsTolerations } from "./pod-details-tolerations";
|
||||
import { Icon } from "../icon";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { PodDetailsSecrets } from "./pod-details-secrets";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { podsStore } from "./pods.store";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { getItemMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { PodCharts, podMetricTabs } from "./pod-charts";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Pod> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodDetails extends React.Component<Props> {
|
||||
@observable containerMetrics: IPodMetrics;
|
||||
|
||||
private watcher = interval(60, () => this.loadMetrics());
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
autorun(() => {
|
||||
this.containerMetrics = null;
|
||||
this.loadMetrics();
|
||||
}),
|
||||
reaction(() => this.props.object, () => {
|
||||
podsStore.reset();
|
||||
})
|
||||
]);
|
||||
this.watcher.start();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
podsStore.reset();
|
||||
}
|
||||
|
||||
@autobind()
|
||||
async loadMetrics() {
|
||||
const { object: pod } = this.props;
|
||||
this.containerMetrics = await podsStore.loadContainerMetrics(pod);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { object: pod } = this.props;
|
||||
if (!pod) return null;
|
||||
const { status, spec } = pod;
|
||||
const { conditions, podIP } = status;
|
||||
const { nodeName } = spec;
|
||||
const nodeSelector = pod.getNodeSelectors();
|
||||
const volumes = pod.getVolumes();
|
||||
const labels = pod.getLabels();
|
||||
const metrics = podsStore.metrics;
|
||||
return (
|
||||
<div className="PodDetails">
|
||||
<ResourceMetrics
|
||||
loader={() => podsStore.loadMetrics(pod)}
|
||||
tabs={podMetricTabs} object={pod} params={{ metrics }}
|
||||
>
|
||||
<PodCharts/>
|
||||
</ResourceMetrics>
|
||||
<KubeObjectMeta object={pod}/>
|
||||
<DrawerItem name={<Trans>Status</Trans>}>
|
||||
<span className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</span>
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Node</Trans>}>
|
||||
{nodeName && (
|
||||
<Link to={getDetailsUrl(nodesApi.getUrl({ name: nodeName }))}>
|
||||
{nodeName}
|
||||
</Link>
|
||||
)}
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Pod IP</Trans>}>
|
||||
{podIP}
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Priority Class</Trans>}>
|
||||
{pod.getPriorityClassName()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>QoS Class</Trans>}>
|
||||
{pod.getQosClass()}
|
||||
</DrawerItem>
|
||||
{conditions &&
|
||||
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
|
||||
{
|
||||
conditions.map(condition => {
|
||||
const { type, status, lastTransitionTime } = condition;
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
label={type}
|
||||
className={cssNames({ disabled: status === "False" })}
|
||||
tooltip={<Trans>Last transition time: {lastTransitionTime}</Trans>}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{nodeSelector.length > 0 &&
|
||||
<DrawerItem name={<Trans>Node Selector</Trans>}>
|
||||
{
|
||||
nodeSelector.map(label => (
|
||||
<Badge key={label} label={label}/>
|
||||
))
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
<PodDetailsTolerations workload={pod}/>
|
||||
<PodDetailsAffinities workload={pod}/>
|
||||
|
||||
{pod.getSecrets().length > 0 && (
|
||||
<DrawerItem name={<Trans>Secrets</Trans>}>
|
||||
<PodDetailsSecrets pod={pod}/>
|
||||
</DrawerItem>
|
||||
)}
|
||||
|
||||
{pod.getInitContainers() && pod.getInitContainers().length > 0 &&
|
||||
<DrawerTitle title={<Trans>Init Containers</Trans>}/>
|
||||
}
|
||||
{
|
||||
pod.getInitContainers() && pod.getInitContainers().map(container => {
|
||||
return <PodDetailsContainer key={container.name} pod={pod} container={container}/>
|
||||
})
|
||||
}
|
||||
<DrawerTitle title={<Trans>Containers</Trans>}/>
|
||||
{
|
||||
pod.getContainers().map(container => {
|
||||
const { name } = container;
|
||||
const metrics = getItemMetrics(toJS(this.containerMetrics), name);
|
||||
return (
|
||||
<PodDetailsContainer
|
||||
key={name}
|
||||
pod={pod}
|
||||
container={container}
|
||||
metrics={metrics}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
{volumes.length > 0 && (
|
||||
<>
|
||||
<DrawerTitle title={<Trans>Volumes</Trans>}/>
|
||||
{volumes.map(volume => {
|
||||
const claimName = volume.persistentVolumeClaim ? volume.persistentVolumeClaim.claimName : null;
|
||||
const configMap = volume.configMap ? volume.configMap.name : null;
|
||||
const type = Object.keys(volume)[1]
|
||||
|
||||
return (
|
||||
<div key={volume.name} className="volume">
|
||||
<div className="title flex gaps">
|
||||
<Icon small material="storage"/>
|
||||
<span>{volume.name}</span>
|
||||
</div>
|
||||
<DrawerItem name={<Trans>Type</Trans>}>
|
||||
{type}
|
||||
</DrawerItem>
|
||||
{ type == "configMap" && (
|
||||
<div>
|
||||
{configMap && (
|
||||
<DrawerItem name={<Trans>Name</Trans>}>
|
||||
<Link
|
||||
to={getDetailsUrl(configMapApi.getUrl({
|
||||
name: configMap,
|
||||
namespace: pod.getNs(),
|
||||
}))}>{configMap}
|
||||
</Link>
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{ type === "emptyDir" && (
|
||||
<div>
|
||||
{ volume.emptyDir.medium && (
|
||||
<DrawerItem name={<Trans>Medium</Trans>}>
|
||||
{volume.emptyDir.medium}
|
||||
</DrawerItem>
|
||||
)}
|
||||
{ volume.emptyDir.sizeLimit && (
|
||||
<DrawerItem name={<Trans>Size Limit</Trans>}>
|
||||
{volume.emptyDir.sizeLimit}
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{claimName && (
|
||||
<DrawerItem name={<Trans>Claim Name</Trans>}>
|
||||
<Link
|
||||
to={getDetailsUrl(pvcApi.getUrl({
|
||||
name: claimName,
|
||||
namespace: pod.getNs(),
|
||||
}))}
|
||||
>{claimName}
|
||||
</Link>
|
||||
</DrawerItem>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<KubeEventDetails object={pod}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
apiManager.registerViews(podsApi, {
|
||||
Details: PodDetails
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
.PodLogsDialog {
|
||||
--log-line-height: 16px;
|
||||
|
||||
.Wizard {
|
||||
width: 90vw;
|
||||
max-height: none;
|
||||
|
||||
.WizardStep {
|
||||
& > .step-content.scrollable {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
padding: $padding * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-controls {
|
||||
padding-bottom: $padding * 2;
|
||||
|
||||
.time-range {
|
||||
flex-grow: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
margin-right: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.Icon {
|
||||
border-radius: $radius;
|
||||
padding: 3px;
|
||||
|
||||
&:hover {
|
||||
color: $textColorPrimary;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $primary;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media("<=desktop") {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: $margin * 2;
|
||||
|
||||
.time-range {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-area {
|
||||
position: relative;
|
||||
@include custom-scrollbar;
|
||||
|
||||
// fix for `this.logsArea.scrollTop = this.logsArea.scrollHeight`
|
||||
// `overflow: overlay` don't allow scroll to the last line
|
||||
overflow: auto;
|
||||
|
||||
color: #C5C8C6;
|
||||
background: #1D1F21;
|
||||
line-height: var(--log-line-height);
|
||||
border-radius: 2px;
|
||||
height: 45vh;
|
||||
padding: $padding / 4 $padding;
|
||||
font-family: $font-monospace;
|
||||
font-size: smaller;
|
||||
white-space: pre;
|
||||
|
||||
.no-logs {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.new-logs-sep {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 0;
|
||||
border-top: 1px solid $primary;
|
||||
margin: $margin * 2;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
content: 'new';
|
||||
background: $primary;
|
||||
color: white;
|
||||
padding: $padding / 3 $padding /2;
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import "./pod-logs-dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { IPodContainer, Pod, podsApi } from "../../api/endpoints";
|
||||
import { Icon } from "../icon";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { Spinner } from "../spinner";
|
||||
import { cssNames, downloadFile, interval } from "../../utils";
|
||||
import AnsiUp from "ansi_up";
|
||||
import DOMPurify from "dompurify"
|
||||
|
||||
interface IPodLogsDialogData {
|
||||
pod: Pod;
|
||||
container?: IPodContainer;
|
||||
}
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodLogsDialog extends React.Component<Props> {
|
||||
@observable static isOpen = false;
|
||||
@observable static data: IPodLogsDialogData = null;
|
||||
|
||||
static open(pod: Pod, container?: IPodContainer) {
|
||||
PodLogsDialog.isOpen = true;
|
||||
PodLogsDialog.data = { pod, container };
|
||||
}
|
||||
|
||||
static close() {
|
||||
PodLogsDialog.isOpen = false;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return PodLogsDialog.data;
|
||||
}
|
||||
|
||||
private logsArea: HTMLDivElement;
|
||||
private refresher = interval(5, () => this.load());
|
||||
private containers: IPodContainer[] = []
|
||||
private initContainers: IPodContainer[] = []
|
||||
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
|
||||
private colorConverter = new AnsiUp();
|
||||
|
||||
@observable logs = ""; // latest downloaded logs for pod
|
||||
@observable newLogs = ""; // new logs since dialog is open
|
||||
@observable logsReady = false;
|
||||
@observable selectedContainer: IPodContainer;
|
||||
@observable showTimestamps = true;
|
||||
@observable tailLines = 1000;
|
||||
|
||||
lineOptions = [
|
||||
{ label: _i18n._(t`All logs`), value: Number.MAX_SAFE_INTEGER },
|
||||
{ label: 1000, value: 1000 },
|
||||
{ label: 10000, value: 10000 },
|
||||
{ label: 100000, value: 100000 },
|
||||
]
|
||||
|
||||
onOpen = async () => {
|
||||
const { pod, container } = this.data;
|
||||
this.containers = pod.getContainers();
|
||||
this.initContainers = pod.getInitContainers();
|
||||
this.selectedContainer = container || this.containers[0];
|
||||
await this.load();
|
||||
this.refresher.start();
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.resetLogs();
|
||||
this.refresher.stop();
|
||||
}
|
||||
|
||||
close = () => {
|
||||
PodLogsDialog.close();
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
if (!this.data) return;
|
||||
const { pod } = this.data;
|
||||
try {
|
||||
// if logs already loaded, check the latest timestamp for getting updates only from this point
|
||||
const logsTimestamps = this.getTimestamps(this.newLogs || this.logs);
|
||||
let lastLogDate = new Date(0)
|
||||
if (logsTimestamps) {
|
||||
lastLogDate = new Date(logsTimestamps.slice(-1)[0]);
|
||||
lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second
|
||||
}
|
||||
const namespace = pod.getNs();
|
||||
const name = pod.getName();
|
||||
const logs = await podsApi.getLogs({ namespace, name }, {
|
||||
container: this.selectedContainer.name,
|
||||
timestamps: true,
|
||||
tailLines: this.tailLines ? this.tailLines : undefined,
|
||||
sinceTime: lastLogDate.toISOString(),
|
||||
});
|
||||
if (!this.logs) {
|
||||
this.logs = logs;
|
||||
}
|
||||
else if (logs) {
|
||||
this.newLogs = `${this.newLogs}\n${logs}`.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logs = [
|
||||
_i18n._(t`Failed to load logs: ${error.message}`),
|
||||
_i18n._(t`Reason: ${error.reason} (${error.code})`),
|
||||
].join("\n")
|
||||
}
|
||||
this.logsReady = true;
|
||||
}
|
||||
|
||||
reload = async () => {
|
||||
this.resetLogs();
|
||||
this.refresher.stop();
|
||||
await this.load();
|
||||
this.refresher.start();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// scroll logs only when it's already in the end,
|
||||
// otherwise it can interrupt reading by jumping after loading new logs update
|
||||
if (this.logsArea && this.lastLineIsShown) {
|
||||
this.logsArea.scrollTop = this.logsArea.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
|
||||
const logsArea = evt.currentTarget;
|
||||
const { scrollHeight, clientHeight, scrollTop } = logsArea;
|
||||
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight;
|
||||
};
|
||||
|
||||
getLogs() {
|
||||
const { logs, newLogs, showTimestamps } = this;
|
||||
return {
|
||||
logs: showTimestamps ? logs : this.removeTimestamps(logs),
|
||||
newLogs: showTimestamps ? newLogs : this.removeTimestamps(newLogs),
|
||||
}
|
||||
}
|
||||
|
||||
getTimestamps(logs: string) {
|
||||
return logs.match(/^\d+\S+/gm);
|
||||
}
|
||||
|
||||
removeTimestamps(logs: string) {
|
||||
return logs.replace(/^\d+.*?\s/gm, "");
|
||||
}
|
||||
|
||||
resetLogs() {
|
||||
this.logs = "";
|
||||
this.newLogs = "";
|
||||
this.lastLineIsShown = true;
|
||||
this.logsReady = false;
|
||||
}
|
||||
|
||||
onContainerChange = (option: SelectOption) => {
|
||||
this.selectedContainer = this.containers
|
||||
.concat(this.initContainers)
|
||||
.find(container => container.name === option.value);
|
||||
this.reload();
|
||||
}
|
||||
|
||||
onTailLineChange = (option: SelectOption) => {
|
||||
this.tailLines = option.value;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
formatOptionLabel = (option: SelectOption) => {
|
||||
const { value, label } = option;
|
||||
return label || <><Icon small material="view_carousel"/> {value}</>;
|
||||
}
|
||||
|
||||
toggleTimestamps = () => {
|
||||
this.showTimestamps = !this.showTimestamps;
|
||||
}
|
||||
|
||||
downloadLogs = () => {
|
||||
const { logs, newLogs } = this.getLogs();
|
||||
const fileName = this.selectedContainer.name + ".log";
|
||||
const fileContents = logs + newLogs;
|
||||
downloadFile(fileName, fileContents, "text/plain");
|
||||
}
|
||||
|
||||
get containerSelectOptions() {
|
||||
return [
|
||||
{
|
||||
label: _i18n._(t`Containers`),
|
||||
options: this.containers.map(container => {
|
||||
return { value: container.name }
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: _i18n._(t`Init Containers`),
|
||||
options: this.initContainers.map(container => {
|
||||
return { value: container.name }
|
||||
}),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
renderControlsPanel() {
|
||||
const { logsReady, showTimestamps } = this;
|
||||
if (!logsReady) return;
|
||||
const timestamps = this.getTimestamps(this.logs + this.newLogs);
|
||||
let from = "";
|
||||
let to = "";
|
||||
if (timestamps) {
|
||||
from = new Date(timestamps[0]).toLocaleString();
|
||||
to = new Date(timestamps[timestamps.length - 1]).toLocaleString();
|
||||
}
|
||||
return (
|
||||
<div className="controls flex align-center">
|
||||
<div className="time-range">
|
||||
{timestamps && <Trans>From <b>{from}</b> to <b>{to}</b></Trans>}
|
||||
</div>
|
||||
<div className="control-buttons flex gaps">
|
||||
<Icon
|
||||
material="av_timer"
|
||||
onClick={this.toggleTimestamps}
|
||||
className={cssNames("timestamps-icon", { active: showTimestamps })}
|
||||
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
|
||||
/>
|
||||
<Icon
|
||||
material="save_alt"
|
||||
onClick={this.downloadLogs}
|
||||
tooltip={_i18n._(t`Save`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderLogs() {
|
||||
if (!this.logsReady) {
|
||||
return <Spinner center/>
|
||||
}
|
||||
const { logs, newLogs } = this.getLogs();
|
||||
if (!logs && !newLogs) {
|
||||
return <p className="no-logs"><Trans>There are no logs available for container.</Trans></p>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(logs))}} />
|
||||
{newLogs && (
|
||||
<>
|
||||
<p className="new-logs-sep" title={_i18n._(t`New logs since opening the dialog`)}/>
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(this.colorConverter.ansi_to_html(newLogs))}} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { selectedContainer, tailLines } = this;
|
||||
const podName = this.data ? this.data.pod.getName() : "";
|
||||
const header = <h5><Trans>{podName} Logs</Trans></h5>;
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
isOpen={PodLogsDialog.isOpen}
|
||||
className="PodLogsDialog"
|
||||
onOpen={this.onOpen}
|
||||
onClose={this.onClose}
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep hideNextBtn prevLabel={<Trans>Close</Trans>}>
|
||||
<div className="log-controls flex gaps align-center justify-space-between">
|
||||
<div className="container flex gaps align-center">
|
||||
<span><Trans>Container</Trans></span>
|
||||
{selectedContainer && (
|
||||
<Select
|
||||
className="container-selector"
|
||||
options={this.containerSelectOptions}
|
||||
themeName="light"
|
||||
value={{ value: selectedContainer.name }}
|
||||
onChange={this.onContainerChange}
|
||||
formatOptionLabel={this.formatOptionLabel}
|
||||
autoConvertOptions={false}
|
||||
/>
|
||||
)}
|
||||
<span><Trans>Lines</Trans></span>
|
||||
<Select
|
||||
value={tailLines}
|
||||
options={this.lineOptions}
|
||||
onChange={this.onTailLineChange}
|
||||
themeName="light"
|
||||
/>
|
||||
</div>
|
||||
{this.renderControlsPanel()}
|
||||
</div>
|
||||
<div className="logs-area" onScroll={this.onScroll} ref={e => this.logsArea = e}>
|
||||
{this.renderLogs()}
|
||||
</div>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.PodMenu {
|
||||
.StatusBrick {
|
||||
margin-right: $margin;
|
||||
@include pod-status-bgs;
|
||||
|
||||
&.running:not(.ready) {
|
||||
background-color: $pod-status-pending-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import "./pod-menu.scss";
|
||||
|
||||
import React from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { MenuItem, SubMenu } from "../menu";
|
||||
import { IPodContainer, Pod, nodesApi } from "../../api/endpoints";
|
||||
import { Icon } from "../icon";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
import { PodLogsDialog } from "./pod-logs-dialog";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import { terminalStore, createTerminalTab } from "../dock/terminal.store";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { hideDetails } from "../../navigation";
|
||||
|
||||
interface Props extends KubeObjectMenuProps<Pod> {
|
||||
}
|
||||
|
||||
export class PodMenu extends React.Component<Props> {
|
||||
async execShell(container?: string) {
|
||||
hideDetails();
|
||||
const { object: pod } = this.props
|
||||
const containerParam = container ? `-c ${container}` : ""
|
||||
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`
|
||||
if (window.navigator.platform !== "Win32") {
|
||||
command = `exec ${command}`
|
||||
}
|
||||
if (pod.getSelectedNodeOs() === "windows") {
|
||||
command = `${command} powershell`
|
||||
} else {
|
||||
command = `${command} sh -c "clear; (bash || ash || sh)"`
|
||||
}
|
||||
|
||||
const shell = createTerminalTab({
|
||||
title: _i18n._(t`Pod`) + `: ${pod.getName()} (namespace: ${pod.getNs()})`
|
||||
});
|
||||
|
||||
terminalStore.sendCommand(command, {
|
||||
enter: true,
|
||||
tabId: shell.id
|
||||
});
|
||||
}
|
||||
|
||||
showLogs(container: IPodContainer) {
|
||||
PodLogsDialog.open(this.props.object, container);
|
||||
}
|
||||
|
||||
renderShellMenu() {
|
||||
const { object: pod, toolbar } = this.props
|
||||
const containers = pod.getRunningContainers();
|
||||
if (!containers.length) return;
|
||||
return (
|
||||
<MenuItem onClick={prevDefault(() => this.execShell(containers[0].name))}>
|
||||
<Icon svg="ssh" interactive={toolbar} title={_i18n._(t`Pod shell`)}/>
|
||||
<span className="title"><Trans>Shell</Trans></span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
<Icon className="arrow" material="keyboard_arrow_right"/>
|
||||
<SubMenu>
|
||||
{
|
||||
containers.map(container => {
|
||||
const { name } = container;
|
||||
return (
|
||||
<MenuItem key={name} onClick={prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||
<StatusBrick/>
|
||||
{name}
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
</SubMenu>
|
||||
</>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
renderLogsMenu() {
|
||||
const { object: pod, toolbar } = this.props
|
||||
const containers = pod.getAllContainers();
|
||||
const statuses = pod.getContainerStatuses();
|
||||
if (!containers.length) return;
|
||||
return (
|
||||
<MenuItem onClick={prevDefault(() => this.showLogs(containers[0]))}>
|
||||
<Icon material="subject" title={_i18n._(t`Logs`)} interactive={toolbar}/>
|
||||
<span className="title"><Trans>Logs</Trans></span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
<Icon className="arrow" material="keyboard_arrow_right"/>
|
||||
<SubMenu>
|
||||
{
|
||||
containers.map(container => {
|
||||
const { name } = container
|
||||
const status = statuses.find(status => status.name === name);
|
||||
const brick = status ? (
|
||||
<StatusBrick
|
||||
className={cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
||||
/>
|
||||
) : null
|
||||
return (
|
||||
<MenuItem key={name} onClick={prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||
{brick}
|
||||
{name}
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
</SubMenu>
|
||||
</>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ...menuProps } = this.props;
|
||||
return (
|
||||
<KubeObjectMenu {...menuProps} className="PodMenu">
|
||||
{this.renderShellMenu()}
|
||||
{this.renderLogsMenu()}
|
||||
</KubeObjectMenu>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@import "../+workloads/workloads-mixins";
|
||||
|
||||
.Pods {
|
||||
.TableCell {
|
||||
&.name {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.containers {
|
||||
.StatusBrick {
|
||||
@include pod-status-bgs;
|
||||
|
||||
&.running:not(.ready) {
|
||||
background-color: $pod-status-pending-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.status {
|
||||
@include pod-status-colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import countBy from "lodash/countBy";
|
||||
import { action, observable } from "mobx";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
|
||||
import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class PodsStore extends KubeObjectStore<Pod> {
|
||||
api = podsApi;
|
||||
|
||||
@observable metrics: IPodMetrics = null;
|
||||
@observable kubeMetrics = observable.array<PodMetrics>([]);
|
||||
|
||||
@action
|
||||
async loadMetrics(pod: Pod) {
|
||||
this.metrics = await podsApi.getMetrics([pod], pod.getNs());
|
||||
}
|
||||
|
||||
loadContainerMetrics(pod: Pod) {
|
||||
return podsApi.getMetrics([pod], pod.getNs(), "container, namespace");
|
||||
}
|
||||
|
||||
async loadKubeMetrics(namespace?: string) {
|
||||
const metrics = await podMetricsApi.list({ namespace });
|
||||
this.kubeMetrics.replace(metrics);
|
||||
}
|
||||
|
||||
getPodsByOwner(workload: WorkloadKubeObject): Pod[] {
|
||||
if (!workload) return [];
|
||||
return this.items.filter(pod => {
|
||||
const owners = pod.getOwnerRefs()
|
||||
if (!owners.length) return
|
||||
return owners.find(owner => owner.uid === workload.getId())
|
||||
})
|
||||
}
|
||||
|
||||
getPodsByNode(node: string) {
|
||||
if (!this.isLoaded) return []
|
||||
return this.items.filter(pod => pod.spec.nodeName === node)
|
||||
}
|
||||
|
||||
getStatuses(pods: Pod[]) {
|
||||
return countBy(pods.map(pod => pod.getStatus()))
|
||||
}
|
||||
|
||||
getPodKubeMetrics(pod: Pod) {
|
||||
const containers = pod.getContainers();
|
||||
const empty = { cpu: 0, memory: 0 };
|
||||
const metrics = this.kubeMetrics.find(metric => {
|
||||
return [
|
||||
metric.getName() === pod.getName(),
|
||||
metric.getNs() === pod.getNs()
|
||||
].every(v => v);
|
||||
});
|
||||
if (!metrics) return empty;
|
||||
return containers.reduce((total, container) => {
|
||||
const metric = metrics.containers.find(item => item.name == container.name);
|
||||
let cpu = "0"
|
||||
let memory = "0"
|
||||
if (metric && metric.usage) {
|
||||
cpu = metric.usage.cpu || "0"
|
||||
memory = metric.usage.memory || "0"
|
||||
}
|
||||
return {
|
||||
cpu: total.cpu + cpuUnitsToNumber(cpu),
|
||||
memory: total.memory + unitsToBytes(memory)
|
||||
}
|
||||
}, empty);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.metrics = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const podsStore = new PodsStore();
|
||||
apiManager.registerStore(podsApi, podsStore);
|
||||
@@ -0,0 +1,130 @@
|
||||
import "./pods.scss"
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { podsStore } from "./pods.store";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
|
||||
import { IPodsRouteParams } from "../+workloads";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { Pod, podsApi } from "../../api/endpoints";
|
||||
import { PodMenu } from "./pod-menu";
|
||||
import { StatusBrick } from "../status-brick";
|
||||
import { cssNames, stopPropagation } from "../../utils";
|
||||
import { TooltipContent } from "../tooltip";
|
||||
import { KubeEventIcon } from "../+events/kube-event-icon";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import toPairs from "lodash/toPairs";
|
||||
import startCase from "lodash/startCase";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
containers = "containers",
|
||||
restarts = "restarts",
|
||||
age = "age",
|
||||
qos = "qos",
|
||||
owners = "owners",
|
||||
status = "status",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IPodsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Pods extends React.Component<Props> {
|
||||
renderContainersStatus(pod: Pod) {
|
||||
return pod.getContainerStatuses().map(containerStatus => {
|
||||
const { name, state, ready } = containerStatus;
|
||||
const tooltip = (
|
||||
<TooltipContent tableView>
|
||||
{Object.keys(state).map(status => (
|
||||
<Fragment key={status}>
|
||||
<div className="title">
|
||||
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
|
||||
</div>
|
||||
{toPairs(state[status]).map(([name, value]) => (
|
||||
<div key={name} className="flex gaps align-center">
|
||||
<div className="name">{startCase(name)}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</TooltipContent>
|
||||
);
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<StatusBrick className={cssNames(state, { ready })} tooltip={tooltip}/>
|
||||
</Fragment>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
className="Pods" store={podsStore}
|
||||
dependentStores={[volumeClaimStore, eventStore]}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (pod: Pod) => pod.getName(),
|
||||
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
|
||||
[sortBy.containers]: (pod: Pod) => pod.getContainers().length,
|
||||
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
|
||||
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
|
||||
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
|
||||
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
|
||||
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(pod: Pod) => pod.getSearchFields(),
|
||||
(pod: Pod) => pod.getStatusMessage(),
|
||||
]}
|
||||
renderHeaderTitle={<Trans>Pods</Trans>}
|
||||
renderTableHeader={[
|
||||
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
|
||||
{ className: "warning" },
|
||||
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||
{ title: <Trans>Containers</Trans>, className: "containers", sortBy: sortBy.containers },
|
||||
{ title: <Trans>Restarts</Trans>, className: "restarts", sortBy: sortBy.restarts },
|
||||
{ title: <Trans>Controlled By</Trans>, className: "owners", sortBy: sortBy.owners },
|
||||
{ title: <Trans>QoS</Trans>, className: "qos", sortBy: sortBy.qos },
|
||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
||||
]}
|
||||
renderTableContents={(pod: Pod) => [
|
||||
pod.getName(),
|
||||
pod.hasIssues() && <KubeEventIcon object={pod}/>,
|
||||
pod.getNs(),
|
||||
this.renderContainersStatus(pod),
|
||||
pod.getRestartsCount(),
|
||||
pod.getOwnerRefs().map(ref => {
|
||||
const { kind, name } = ref;
|
||||
const detailsLink = getDetailsUrl(lookupApiLink(ref, pod));
|
||||
return (
|
||||
<Link key={name} to={detailsLink} className="owner" onClick={stopPropagation}>
|
||||
{kind}
|
||||
</Link>
|
||||
)
|
||||
}),
|
||||
pod.getQosClass(),
|
||||
pod.getAge(),
|
||||
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }
|
||||
]}
|
||||
renderItemMenu={(item: Pod) => {
|
||||
return <PodMenu object={item}/>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
apiManager.registerViews(podsApi, {
|
||||
Menu: PodMenu,
|
||||
})
|
||||
Reference in new issue
Block a user