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,20 @@
|
||||
.DeploymentDetails {
|
||||
.conditions {
|
||||
.Badge {
|
||||
&.available {
|
||||
color: white;
|
||||
background-color: $deployment-available;
|
||||
}
|
||||
|
||||
&.progressing {
|
||||
color: white;
|
||||
background-color: $deployment-progressing;
|
||||
}
|
||||
|
||||
&.replica-failure {
|
||||
color: white;
|
||||
background-color: $deployment-replicafailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import "./deployment-details.scss";
|
||||
|
||||
import React from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { Deployment, deploymentApi } from "../../api/endpoints";
|
||||
import { cssNames } from "../../utils";
|
||||
import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations";
|
||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics";
|
||||
import { deploymentStore } from "./deployments.store";
|
||||
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { ReplicaSets } from "../+workloads-replicasets";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DeploymentDetails extends React.Component<Props> {
|
||||
@disposeOnUnmount
|
||||
clean = reaction(() => this.props.object, () => {
|
||||
deploymentStore.reset();
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
if (!podsStore.isLoaded) {
|
||||
podsStore.loadAll();
|
||||
}
|
||||
if (!replicaSetStore.isLoaded) {
|
||||
replicaSetStore.loadAll();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
deploymentStore.reset();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { object: deployment } = this.props;
|
||||
if (!deployment) return null
|
||||
const { status, spec } = deployment
|
||||
const nodeSelector = deployment.getNodeSelectors()
|
||||
const selectors = deployment.getSelectors();
|
||||
const childPods = deploymentStore.getChildPods(deployment)
|
||||
const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment)
|
||||
const metrics = deploymentStore.metrics
|
||||
return (
|
||||
<div className="DeploymentDetails">
|
||||
{podsStore.isLoaded && (
|
||||
<ResourceMetrics
|
||||
loader={() => deploymentStore.loadMetrics(deployment)}
|
||||
tabs={podMetricTabs} object={deployment} params={{ metrics }}
|
||||
>
|
||||
<PodCharts/>
|
||||
</ResourceMetrics>
|
||||
)}
|
||||
<KubeObjectMeta object={deployment}/>
|
||||
<DrawerItem name={<Trans>Replicas</Trans>}>
|
||||
{_i18n._(t`${spec.replicas} desired, ${status.updatedReplicas || 0} updated`)},{" "}
|
||||
{_i18n._(t`${status.replicas || 0} total, ${status.availableReplicas || 0} available`)},{" "}
|
||||
{_i18n._(t`${status.unavailableReplicas || 0} unavailable`)}
|
||||
</DrawerItem>
|
||||
{selectors.length > 0 &&
|
||||
<DrawerItem name={<Trans>Selector</Trans>} labelsOnly>
|
||||
{
|
||||
selectors.map(label => <Badge key={label} label={label}/>)
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
{nodeSelector.length > 0 &&
|
||||
<DrawerItem name={<Trans>Node Selector</Trans>}>
|
||||
{
|
||||
nodeSelector.map(label => (
|
||||
<Badge key={label} label={label}/>
|
||||
))
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
<DrawerItem name={<Trans>Strategy Type</Trans>}>
|
||||
{spec.strategy.type}
|
||||
</DrawerItem>
|
||||
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
|
||||
{
|
||||
deployment.getConditions().map(condition => {
|
||||
const { type, message, lastTransitionTime, status } = condition
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
label={type}
|
||||
className={cssNames({ disabled: status === "False" }, kebabCase(type))}
|
||||
tooltip={(
|
||||
<>
|
||||
<p>{message}</p>
|
||||
<p><Trans>Last transition time: {lastTransitionTime}</Trans></p>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</DrawerItem>
|
||||
<PodDetailsTolerations workload={deployment}/>
|
||||
<PodDetailsAffinities workload={deployment}/>
|
||||
<ResourceMetricsText metrics={metrics}/>
|
||||
<ReplicaSets replicaSets={replicaSets}/>
|
||||
<PodDetailsList pods={childPods} owner={deployment}/>
|
||||
<KubeEventDetails object={deployment}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
apiManager.registerViews(deploymentApi, {
|
||||
Details: DeploymentDetails
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
.DeploymentScaleDialog {
|
||||
.Wizard {
|
||||
.header {
|
||||
span {
|
||||
color: #a0a0a0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.WizardStep {
|
||||
.step-content {
|
||||
min-height: 90px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.current-scale {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.desired-scale {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
flex: 1.3 0;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $colorSoftError;
|
||||
font-size: small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.Icon {
|
||||
margin: 0;
|
||||
margin-right: $margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import "./deployment-scale-dialog.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Deployment, deploymentApi } from "../../api/endpoints";
|
||||
import { Icon } from "../icon";
|
||||
import { Slider } from "../slider";
|
||||
import { Notifications } from "../notifications";
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DeploymentScaleDialog extends Component<Props> {
|
||||
@observable static isOpen = false;
|
||||
@observable static data: Deployment = null;
|
||||
|
||||
@observable ready = false;
|
||||
@observable currentReplicas = 0;
|
||||
@observable desiredReplicas = 0;
|
||||
|
||||
static open(deployment: Deployment) {
|
||||
DeploymentScaleDialog.isOpen = true;
|
||||
DeploymentScaleDialog.data = deployment;
|
||||
}
|
||||
|
||||
static close() {
|
||||
DeploymentScaleDialog.isOpen = false;
|
||||
}
|
||||
|
||||
get deployment() {
|
||||
return DeploymentScaleDialog.data;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
DeploymentScaleDialog.close();
|
||||
}
|
||||
|
||||
@computed get scaleMax() {
|
||||
const { currentReplicas } = this;
|
||||
const defaultMax = 50;
|
||||
return currentReplicas <= defaultMax
|
||||
? defaultMax * 2
|
||||
: currentReplicas * 2;
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
const { deployment } = this;
|
||||
this.currentReplicas = await deploymentApi.getReplicas({
|
||||
namespace: deployment.getNs(),
|
||||
name: deployment.getName(),
|
||||
});
|
||||
this.desiredReplicas = this.currentReplicas;
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
onChange = (evt: React.ChangeEvent, value: number) => {
|
||||
this.desiredReplicas = value;
|
||||
}
|
||||
|
||||
scale = async () => {
|
||||
const { deployment } = this;
|
||||
const { currentReplicas, desiredReplicas, close } = this;
|
||||
try {
|
||||
if (currentReplicas !== desiredReplicas) {
|
||||
await deploymentApi.scale({
|
||||
name: deployment.getName(),
|
||||
namespace: deployment.getNs(),
|
||||
}, desiredReplicas);
|
||||
}
|
||||
close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
renderContents() {
|
||||
const { currentReplicas, desiredReplicas, onChange, scaleMax } = this;
|
||||
const warning = currentReplicas < 10 && desiredReplicas > 90;
|
||||
return (
|
||||
<>
|
||||
<div className="current-scale">
|
||||
<Trans>Current replica scale: {currentReplicas}</Trans>
|
||||
</div>
|
||||
<div className="flex gaps align-center">
|
||||
<div className="desired-scale">
|
||||
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
|
||||
</div>
|
||||
<div className="slider-container">
|
||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
|
||||
</div>
|
||||
</div>
|
||||
{warning &&
|
||||
<div className="warning">
|
||||
<Icon material="warning"/>
|
||||
<Trans>High number of replicas may cause cluster performance issues</Trans>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, ...dialogProps } = this.props;
|
||||
const deploymentName = this.deployment ? this.deployment.getName() : "";
|
||||
const header = (
|
||||
<h5>
|
||||
<Trans>Scale Deployment <span>{deploymentName}</span></Trans>
|
||||
</h5>
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
isOpen={DeploymentScaleDialog.isOpen}
|
||||
className={cssNames("DeploymentScaleDialog", className)}
|
||||
onOpen={this.onOpen}
|
||||
onClose={this.onClose}
|
||||
close={this.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<WizardStep
|
||||
contentClass="flex gaps column"
|
||||
next={this.scale}
|
||||
nextLabel={<Trans>Scale</Trans>}
|
||||
disabledNext={!this.ready}
|
||||
>
|
||||
{this.renderContents()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@import "../+workloads/workloads-mixins";
|
||||
|
||||
.Deployments {
|
||||
.TableCell {
|
||||
&.name {
|
||||
flex-grow: 1.5;
|
||||
}
|
||||
|
||||
&.namespace {
|
||||
flex-grow: 1.5;
|
||||
}
|
||||
|
||||
&.pods {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.conditions {
|
||||
flex-grow: 1.5;
|
||||
|
||||
.condition {
|
||||
margin-right: $margin;
|
||||
|
||||
&.available {
|
||||
color: $deployment-available;
|
||||
}
|
||||
|
||||
&.progressing {
|
||||
color: $deployment-progressing;
|
||||
}
|
||||
|
||||
&.replica-failure {
|
||||
color: $deployment-replicafailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { observable } from "mobx";
|
||||
import { Deployment, deploymentApi, IPodMetrics, podsApi, PodStatus } from "../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
api = deploymentApi
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
protected sortItems(items: Deployment[]) {
|
||||
return super.sortItems(items, [
|
||||
item => item.getReplicas(),
|
||||
], "desc");
|
||||
}
|
||||
|
||||
loadMetrics(deployment: Deployment) {
|
||||
const pods = this.getChildPods(deployment);
|
||||
return podsApi.getMetrics(pods, deployment.getNs(), "").then(metrics =>
|
||||
this.metrics = metrics
|
||||
);
|
||||
}
|
||||
|
||||
getStatuses(deployments?: Deployment[]) {
|
||||
const status = { failed: 0, pending: 0, running: 0 }
|
||||
deployments.forEach(deployment => {
|
||||
const pods = this.getChildPods(deployment);
|
||||
if (pods.some(pod => pod.getStatus() === PodStatus.FAILED)) {
|
||||
status.failed++
|
||||
}
|
||||
else if (pods.some(pod => pod.getStatus() === PodStatus.PENDING)) {
|
||||
status.pending++
|
||||
}
|
||||
else {
|
||||
status.running++
|
||||
}
|
||||
})
|
||||
return status
|
||||
}
|
||||
|
||||
getChildPods(deployment: Deployment) {
|
||||
return podsStore
|
||||
.getByLabel(deployment.getTemplateLabels())
|
||||
.filter(pod => pod.getNs() === deployment.getNs())
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.metrics = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const deploymentStore = new DeploymentStore();
|
||||
apiManager.registerStore(deploymentApi, deploymentStore);
|
||||
@@ -0,0 +1,110 @@
|
||||
import "./deployments.scss"
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { Deployment, deploymentApi } from "../../api/endpoints";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
|
||||
import { deploymentStore } from "./deployments.store";
|
||||
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { IDeploymentsRouteParams } from "../+workloads";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { cssNames } from "../../utils";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { KubeEventIcon } from "../+events/kube-event-icon";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
replicas = "replicas",
|
||||
age = "age",
|
||||
condition = "condition",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IDeploymentsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Deployments extends React.Component<Props> {
|
||||
renderPods(deployment: Deployment) {
|
||||
const { replicas, availableReplicas } = deployment.status
|
||||
return `${availableReplicas || 0}/${replicas || 0}`
|
||||
}
|
||||
|
||||
renderConditions(deployment: Deployment) {
|
||||
const conditions = orderBy(deployment.getConditions(true), "type", "asc")
|
||||
return conditions.map(({ type, message }) => (
|
||||
<span key={type} className={cssNames("condition", kebabCase(type))} title={message}>
|
||||
{type}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
className="Deployments" store={deploymentStore}
|
||||
dependentStores={[replicaSetStore, podsStore, nodesStore, eventStore]}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (deployment: Deployment) => deployment.getName(),
|
||||
[sortBy.namespace]: (deployment: Deployment) => deployment.getNs(),
|
||||
[sortBy.replicas]: (deployment: Deployment) => deployment.getReplicas(),
|
||||
[sortBy.age]: (deployment: Deployment) => deployment.metadata.creationTimestamp,
|
||||
[sortBy.condition]: (deployment: Deployment) => deployment.getConditionsText(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(deployment: Deployment) => deployment.getSearchFields(),
|
||||
(deployment: Deployment) => deployment.getConditionsText(),
|
||||
]}
|
||||
renderHeaderTitle={<Trans>Deployments</Trans>}
|
||||
renderTableHeader={[
|
||||
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
|
||||
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||
{ title: <Trans>Pods</Trans>, className: "pods" },
|
||||
{ title: <Trans>Replicas</Trans>, className: "replicas", sortBy: sortBy.replicas },
|
||||
{ className: "warning" },
|
||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||
{ title: <Trans>Conditions</Trans>, className: "conditions", sortBy: sortBy.condition },
|
||||
]}
|
||||
renderTableContents={(deployment: Deployment) => [
|
||||
deployment.getName(),
|
||||
deployment.getNs(),
|
||||
this.renderPods(deployment),
|
||||
deployment.getReplicas(),
|
||||
<KubeEventIcon object={deployment}/>,
|
||||
deployment.getAge(),
|
||||
this.renderConditions(deployment),
|
||||
]}
|
||||
renderItemMenu={(item: Deployment) => {
|
||||
return <DeploymentMenu object={item}/>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
||||
const { object, toolbar } = props;
|
||||
return (
|
||||
<KubeObjectMenu {...props}>
|
||||
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
|
||||
<Icon material="control_camera" title={_i18n._(t`Scale`)} interactive={toolbar}/>
|
||||
<span className="title"><Trans>Scale</Trans></span>
|
||||
</MenuItem>
|
||||
</KubeObjectMenu>
|
||||
)
|
||||
}
|
||||
|
||||
apiManager.registerViews(deploymentApi, {
|
||||
Menu: DeploymentMenu,
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./deployments"
|
||||
export * from "./deployment-details"
|
||||
Reference in new issue
Block a user