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:
parent
c93ee4ea6d
commit
5b484ca692
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.pods {
|
&.pods {
|
||||||
flex-grow: 0.3;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user