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

Add stateful set scale slider (#1406)

Signed-off-by: vshakirova <vshakirova@mirantis.com>
This commit is contained in:
vshakirova 2020-11-25 16:21:54 +04:00 committed by GitHub
parent c93ee4ea6d
commit 5b484ca692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 476 additions and 10 deletions

View File

@ -738,6 +738,7 @@ msgstr "Current / Target"
msgid "Current Healthy" msgid "Current Healthy"
msgstr "Current Healthy" msgstr "Current Healthy"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}" msgid "Current replica scale: {currentReplicas}"
msgstr "Current replica scale: {currentReplicas}" msgstr "Current replica scale: {currentReplicas}"
@ -828,6 +829,7 @@ msgstr "Description"
msgid "Desired Healthy" msgid "Desired Healthy"
msgstr "Desired Healthy" msgstr "Desired Healthy"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "Desired number of replicas" msgstr "Desired number of replicas"
@ -1091,6 +1093,7 @@ msgstr "Helm branch <0>{0}</0> already in use"
msgid "Hide" msgid "Hide"
msgstr "Hide" msgstr "Hide"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues" msgid "High number of replicas may cause cluster performance issues"
msgstr "High number of replicas may cause cluster performance issues" msgstr "High number of replicas may cause cluster performance issues"
@ -2298,6 +2301,7 @@ msgstr "Runtime Class"
msgid "Save" msgid "Save"
msgstr "Save" msgstr "Save"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128
#: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:83
#: src/renderer/components/+workloads-deployments/deployments.tsx:84 #: src/renderer/components/+workloads-deployments/deployments.tsx:84
@ -2308,6 +2312,10 @@ msgstr "Scale"
msgid "Scale Deployment <0>{deploymentName}</0>" msgid "Scale Deployment <0>{deploymentName}</0>"
msgstr "Scale Deployment <0>{deploymentName}</0>" msgstr "Scale Deployment <0>{deploymentName}</0>"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr "Scale Stateful Set <0>{statefulSetName}</0>"
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45 #: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46 #: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule" msgid "Schedule"

View File

@ -734,6 +734,7 @@ msgstr ""
msgid "Current Healthy" msgid "Current Healthy"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}" msgid "Current replica scale: {currentReplicas}"
msgstr "" msgstr ""
@ -824,6 +825,7 @@ msgstr ""
msgid "Desired Healthy" msgid "Desired Healthy"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "" msgstr ""
@ -1082,6 +1084,7 @@ msgstr ""
msgid "Hide" msgid "Hide"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues" msgid "High number of replicas may cause cluster performance issues"
msgstr "" msgstr ""
@ -2281,6 +2284,7 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128
#: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:83
#: src/renderer/components/+workloads-deployments/deployments.tsx:84 #: src/renderer/components/+workloads-deployments/deployments.tsx:84
@ -2291,6 +2295,10 @@ msgstr ""
msgid "Scale Deployment <0>{deploymentName}</0>" msgid "Scale Deployment <0>{deploymentName}</0>"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr ""
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45 #: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46 #: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule" msgid "Schedule"

View File

@ -739,6 +739,7 @@ msgstr "Текущее / Цель"
msgid "Current Healthy" msgid "Current Healthy"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}" msgid "Current replica scale: {currentReplicas}"
msgstr "Текущий размер реплики: {currentReplicas}" msgstr "Текущий размер реплики: {currentReplicas}"
@ -829,6 +830,7 @@ msgstr "Описание"
msgid "Desired Healthy" msgid "Desired Healthy"
msgstr "" msgstr ""
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas" msgid "Desired number of replicas"
msgstr "Нужный уровень реплик" msgstr "Нужный уровень реплик"
@ -1092,6 +1094,7 @@ msgstr ""
msgid "Hide" msgid "Hide"
msgstr "Скрыть" msgstr "Скрыть"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues" msgid "High number of replicas may cause cluster performance issues"
msgstr "Большое количество реплик может вызвать проблемы с производительностью кластера" msgstr "Большое количество реплик может вызвать проблемы с производительностью кластера"
@ -2299,6 +2302,7 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "Сохранить" msgstr "Сохранить"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128
#: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:83
#: src/renderer/components/+workloads-deployments/deployments.tsx:84 #: src/renderer/components/+workloads-deployments/deployments.tsx:84
@ -2309,6 +2313,10 @@ msgstr "Масштабировать"
msgid "Scale Deployment <0>{deploymentName}</0>" msgid "Scale Deployment <0>{deploymentName}</0>"
msgstr "Масштабировать Deployment <0>{deploymentName}</0>" msgstr "Масштабировать Deployment <0>{deploymentName}</0>"
#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
msgid "Scale Stateful Set <0>{statefulSetName}</0>"
msgstr "Масштабировать Stateful Set <0>{statefulSetName}</0>"
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45 #: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46 #: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule" msgid "Schedule"

View File

@ -4,6 +4,29 @@ import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class StatefulSetApi extends KubeApi<StatefulSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return this.getUrl(params) + "/scale";
}
getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request
.get(this.getScaleApiUrl(params))
.then(({ status }: any) => status?.replicas);
}
scale(params: { namespace: string; name: string }, replicas: number) {
return this.request.put(this.getScaleApiUrl(params), {
data: {
metadata: params,
spec: {
replicas
}
}
});
}
}
@autobind() @autobind()
export class StatefulSet extends WorkloadKubeObject { export class StatefulSet extends WorkloadKubeObject {
static kind = "StatefulSet"; static kind = "StatefulSet";
@ -67,17 +90,22 @@ export class StatefulSet extends WorkloadKubeObject {
observedGeneration: number; observedGeneration: number;
replicas: number; replicas: number;
currentReplicas: number; currentReplicas: number;
readyReplicas: number;
currentRevision: string; currentRevision: string;
updateRevision: string; updateRevision: string;
collisionCount: number; collisionCount: number;
}; };
getReplicas() {
return this.spec.replicas || 0;
}
getImages() { getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
return [...containers].map(container => container.image); return [...containers].map(container => container.image);
} }
} }
export const statefulSetApi = new KubeApi({ export const statefulSetApi = new StatefulSetApi({
objectConstructor: StatefulSet, objectConstructor: StatefulSet,
}); });

View File

@ -0,0 +1,49 @@
.StatefulSetScaleDialog {
.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.1 0;
}
.slider-container {
flex: 1 0;
}
.plus-minus-container {
margin-left: $margin * 2;
.Icon {
--color-active: black;
}
}
.warning {
color: $colorSoftError;
font-size: small;
display: flex;
align-items: center;
.Icon {
margin: 0;
margin-right: $margin;
}
}
}
}

View File

@ -0,0 +1,167 @@
import '@testing-library/jest-dom/extend-expect';
jest.mock("../../api/endpoints");
import { statefulSetApi } from "../../api/endpoints";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { render, waitFor, fireEvent } from '@testing-library/react';
import React from 'react';
const dummyStatefulSet = {
apiVersion: 'v1',
kind: 'dummy',
metadata: {
uid: 'dummy',
name: 'dummy',
creationTimestamp: 'dummy',
resourceVersion: 'dummy',
selfLink: 'link',
},
selfLink: 'link',
spec: {
serviceName: 'dummy',
replicas: 1,
selector: {
matchLabels: { 'label': 'label' }
},
template: {
metadata: {
labels: {
app: 'app',
},
},
spec: {
containers: [{
name: 'dummy',
image: 'dummy',
ports: [{
containerPort: 1234,
name: 'dummy',
}],
volumeMounts: [{
name: 'dummy',
mountPath: 'dummy',
}],
}],
tolerations: [{
key: 'dummy',
operator: 'dummy',
effect: 'dummy',
tolerationSeconds: 1,
}],
},
},
volumeClaimTemplates: [{
metadata: {
name: 'dummy',
},
spec: {
accessModes: ['dummy'],
resources: {
requests: {
storage: 'dummy',
},
},
},
}],
},
status: {
observedGeneration: 1,
replicas: 1,
currentReplicas: 1,
readyReplicas: 1,
currentRevision: 'dummy',
updateRevision: 'dummy',
collisionCount: 1,
},
getImages: jest.fn(),
getReplicas: jest.fn(),
getSelectors: jest.fn(),
getTemplateLabels: jest.fn(),
getAffinity: jest.fn(),
getTolerations: jest.fn(),
getNodeSelectors: jest.fn(),
getAffinityNumber: jest.fn(),
getId: jest.fn(),
getResourceVersion: jest.fn(),
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
getFinalizers: jest.fn(),
getLabels: jest.fn(),
getAnnotations: jest.fn(),
getOwnerRefs: jest.fn(),
getSearchFields: jest.fn(),
toPlainObject: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
describe('<StatefulSetScaleDialog />', () => {
it('renders w/o errors', () => {
const { container } = render(<StatefulSetScaleDialog/>);
expect(container).toBeInstanceOf(HTMLElement);
});
it('init with a dummy stateful set and mocked current/desired scale', async () => {
// mock statefulSetApi.getReplicas() which will be called
// when <StatefulSetScaleDialog /> rendered.
const initReplicas = 1;
statefulSetApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
const { getByTestId } = render(<StatefulSetScaleDialog/>);
StatefulSetScaleDialog.open(dummyStatefulSet);
// we need to wait for the StatefulSetScaleDialog to show up
// because there is an <Animate /> in <Dialog /> which renders null at start.
await waitFor(async () => {
const [currentScale, desiredScale] = await Promise.all([
getByTestId('current-scale'),
getByTestId('desired-scale'),
]);
expect(currentScale).toHaveTextContent(`${initReplicas}`);
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
});
});
it('changes the desired scale when clicking the icon buttons +/-', async () => {
const initReplicas = 1;
statefulSetApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
const component = render(<StatefulSetScaleDialog/>);
StatefulSetScaleDialog.open(dummyStatefulSet);
await waitFor(async () => {
expect(await component.findByTestId('desired-scale')).toHaveTextContent(`${initReplicas}`);
expect(await component.findByTestId('current-scale')).toHaveTextContent(`${initReplicas}`);
expect((await component.baseElement.querySelector('input').value)).toBe(`${initReplicas}`);
});
const up = await component.findByTestId('desired-replicas-up');
const down = await component.findByTestId('desired-replicas-down');
fireEvent.click(up);
expect(await component.findByTestId('desired-scale')).toHaveTextContent(`${initReplicas + 1}`);
expect(await component.findByTestId('current-scale')).toHaveTextContent(`${initReplicas}`);
expect((await component.baseElement.querySelector('input').value)).toBe(`${initReplicas + 1}`);
fireEvent.click(down);
expect(await component.findByTestId('desired-scale')).toHaveTextContent(`${initReplicas}`);
expect(await component.findByTestId('current-scale')).toHaveTextContent(`${initReplicas}`);
expect((await component.baseElement.querySelector('input').value)).toBe(`${initReplicas}`);
// edge case, desiredScale must >= 0
let times = 10;
for (let i = 0; i < times; i++) {
fireEvent.click(down);
}
expect(await component.findByTestId('desired-scale')).toHaveTextContent('0');
expect((await component.baseElement.querySelector('input').value)).toBe('0');
// edge case, desiredScale must <= scaleMax (100)
times = 120;
for (let i = 0; i < times; i++) {
fireEvent.click(up);
}
expect(await component.findByTestId('desired-scale')).toHaveTextContent('100');
expect((component.baseElement.querySelector("input").value)).toBe('100');
expect(await component.findByTestId('warning'))
.toHaveTextContent('High number of replicas may cause cluster performance issues');
});
});

View File

@ -0,0 +1,164 @@
import "./statefulset-scale-dialog.scss";
import { StatefulSet, statefulSetApi } from "../../api/endpoints";
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 { Icon } from "../icon";
import { Slider } from "../slider";
import { Notifications } from "../notifications";
import { cssNames } from "../../utils";
interface Props extends Partial<DialogProps> {
}
@observer
export class StatefulSetScaleDialog extends Component<Props> {
@observable static isOpen = false;
@observable static data: StatefulSet = null;
@observable ready = false;
@observable currentReplicas = 0;
@observable desiredReplicas = 0;
static open(statefulSet: StatefulSet) {
StatefulSetScaleDialog.isOpen = true;
StatefulSetScaleDialog.data = statefulSet;
}
static close() {
StatefulSetScaleDialog.isOpen = false;
}
get statefulSet() {
return StatefulSetScaleDialog.data;
}
close = () => {
StatefulSetScaleDialog.close();
};
onOpen = async () => {
const { statefulSet } = this;
this.currentReplicas = await statefulSetApi.getReplicas({
namespace: statefulSet.getNs(),
name: statefulSet.getName(),
});
this.desiredReplicas = this.currentReplicas;
this.ready = true;
};
onClose = () => {
this.ready = false;
};
onChange = (evt: React.ChangeEvent, value: number) => {
this.desiredReplicas = value;
};
@computed get scaleMax() {
const { currentReplicas } = this;
const defaultMax = 50;
return currentReplicas <= defaultMax
? defaultMax * 2
: currentReplicas * 2;
}
scale = async () => {
const { statefulSet } = this;
const { currentReplicas, desiredReplicas, close } = this;
try {
if (currentReplicas !== desiredReplicas) {
await statefulSetApi.scale({
name: statefulSet.getName(),
namespace: statefulSet.getNs(),
}, desiredReplicas);
}
close();
} catch (err) {
Notifications.error(err);
}
};
desiredReplicasUp = () => {
this.desiredReplicas < this.scaleMax && this.desiredReplicas++;
};
desiredReplicasDown = () => {
this.desiredReplicas > 0 && this.desiredReplicas--;
};
renderContents() {
const { currentReplicas, desiredReplicas, onChange, scaleMax } = this;
const warning = currentReplicas < 10 && desiredReplicas > 90;
return (
<>
<div className="current-scale" data-testid="current-scale">
<Trans>Current replica scale: {currentReplicas}</Trans>
</div>
<div className="flex gaps align-center">
<div className="desired-scale" data-testid="desired-scale">
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
</div>
<div className="slider-container flex align-center" data-testid="slider">
<Slider value={desiredReplicas} max={scaleMax}
onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}
/>
</div>
<div className="plus-minus-container flex gaps">
<Icon
material="add_circle_outline"
onClick={this.desiredReplicasUp}
data-testid="desired-replicas-up"
/>
<Icon
material="remove_circle_outline"
onClick={this.desiredReplicasDown}
data-testid="desired-replicas-down"
/>
</div>
</div>
{warning &&
<div className="warning" data-testid="warning">
<Icon material="warning"/>
<Trans>High number of replicas may cause cluster performance issues</Trans>
</div>
}
</>
);
}
render() {
const { className, ...dialogProps } = this.props;
const statefulSetName = this.statefulSet ? this.statefulSet.getName() : "";
const header = (
<h5>
<Trans>Scale Stateful Set <span>{statefulSetName}</span></Trans>
</h5>
);
return (
<Dialog
{...dialogProps}
isOpen={StatefulSetScaleDialog.isOpen}
className={cssNames("StatefulSetScaleDialog", 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>
);
}
}

View File

@ -5,7 +5,7 @@
} }
&.pods { &.pods {
flex-grow: 0.3; flex-grow: 1;
} }
&.warning { &.warning {

View File

@ -3,21 +3,27 @@ import "./statefulsets.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import { Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { StatefulSet } from "../../api/endpoints"; import { StatefulSet, statefulSetApi } from "../../api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { statefulSetStore } from "./statefulset.store"; import { statefulSetStore } from "./statefulset.store";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectListLayout } from "../kube-object";
import { IStatefulSetsRouteParams } from "../+workloads"; import { IStatefulSetsRouteParams } from "../+workloads";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { MenuItem } from "../menu/menu";
import { _i18n } from "../../i18n";
import { Icon } from "../icon/icon";
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
enum sortBy { enum sortBy {
name = "name", name = "name",
namespace = "namespace", namespace = "namespace",
pods = "pods",
age = "age", age = "age",
replicas = "replicas",
} }
interface Props extends RouteComponentProps<IStatefulSetsRouteParams> { interface Props extends RouteComponentProps<IStatefulSetsRouteParams> {
@ -25,8 +31,9 @@ interface Props extends RouteComponentProps<IStatefulSetsRouteParams> {
@observer @observer
export class StatefulSets extends React.Component<Props> { export class StatefulSets extends React.Component<Props> {
getPodsLength(statefulSet: StatefulSet) { renderPods(statefulSet: StatefulSet) {
return statefulSetStore.getChildPods(statefulSet).length; const { readyReplicas, currentReplicas } = statefulSet.status;
return `${readyReplicas || 0}/${currentReplicas || 0}`;
} }
render() { render() {
@ -38,7 +45,7 @@ export class StatefulSets extends React.Component<Props> {
[sortBy.name]: (statefulSet: StatefulSet) => statefulSet.getName(), [sortBy.name]: (statefulSet: StatefulSet) => statefulSet.getName(),
[sortBy.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(), [sortBy.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(),
[sortBy.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp, [sortBy.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp,
[sortBy.pods]: (statefulSet: StatefulSet) => this.getPodsLength(statefulSet), [sortBy.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(),
}} }}
searchFilters={[ searchFilters={[
(statefulSet: StatefulSet) => statefulSet.getSearchFields(), (statefulSet: StatefulSet) => statefulSet.getSearchFields(),
@ -47,18 +54,43 @@ export class StatefulSets extends React.Component<Props> {
renderTableHeader={[ renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name }, { title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace }, { title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Pods</Trans>, className: "pods", sortBy: sortBy.pods }, { title: <Trans>Pods</Trans>, className: "pods" },
{ title: <Trans>Replicas</Trans>, className: "replicas", sortBy: sortBy.replicas },
{ className: "warning" }, { className: "warning" },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age }, { title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]} ]}
renderTableContents={(statefulSet: StatefulSet) => [ renderTableContents={(statefulSet: StatefulSet) => [
statefulSet.getName(), statefulSet.getName(),
statefulSet.getNs(), statefulSet.getNs(),
this.getPodsLength(statefulSet), this.renderPods(statefulSet),
statefulSet.getReplicas(),
<KubeObjectStatusIcon object={statefulSet}/>, <KubeObjectStatusIcon object={statefulSet}/>,
statefulSet.getAge(), statefulSet.getAge(),
]} ]}
renderItemMenu={(item: StatefulSet) => {
return <StatefulSetMenu object={item}/>;
}}
/> />
); );
} }
} }
export function StatefulSetMenu(props: KubeObjectMenuProps<StatefulSet>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => StatefulSetScaleDialog.open(object)}>
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
<span className="title"><Trans>Scale</Trans></span>
</MenuItem>
</>
);
}
kubeObjectMenuRegistry.add({
kind: "StatefulSet",
apiVersions: ["apps/v1"],
components: {
MenuItem: StatefulSetMenu
}
});

View File

@ -43,6 +43,7 @@ import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
import { TabLayoutRoute, TabLayout } from "./layout/tab-layout"; import { TabLayoutRoute, TabLayout } from "./layout/tab-layout";
import { Trans } from "@lingui/macro"; import { Trans } from "@lingui/macro";
import {StatefulSetScaleDialog} from "./+workloads-statefulsets/statefulset-scale-dialog";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -150,6 +151,7 @@ export class App extends React.Component {
<KubeConfigDialog/> <KubeConfigDialog/>
<AddRoleBindingDialog/> <AddRoleBindingDialog/>
<DeploymentScaleDialog/> <DeploymentScaleDialog/>
<StatefulSetScaleDialog/>
<CronJobTriggerDialog/> <CronJobTriggerDialog/>
</ErrorBoundary> </ErrorBoundary>
</Router> </Router>