Added storage class selector to add pool modal (#699)

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-04-19 20:26:22 -05:00
committed by GitHub
parent c07b7b4772
commit c81da22e8a
8 changed files with 58 additions and 254 deletions

View File

@@ -1,8 +1,8 @@
{
"files": {
"main.css": "/static/css/main.a19f3d53.chunk.css",
"main.js": "/static/js/main.a8ae7284.chunk.js",
"main.js.map": "/static/js/main.a8ae7284.chunk.js.map",
"main.js": "/static/js/main.fb87f627.chunk.js",
"main.js.map": "/static/js/main.fb87f627.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.f48e99e5.js",
"runtime-main.js.map": "/static/js/runtime-main.f48e99e5.js.map",
"static/css/2.f324abd6.chunk.css": "/static/css/2.f324abd6.chunk.css",
@@ -20,6 +20,6 @@
"static/css/2.f324abd6.chunk.css",
"static/js/2.26e254ca.chunk.js",
"static/css/main.a19f3d53.chunk.css",
"static/js/main.a8ae7284.chunk.js"
"static/js/main.fb87f627.chunk.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.f324abd6.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.26e254ca.chunk.js"></script><script src="/static/js/main.a8ae7284.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.f324abd6.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.26e254ca.chunk.js"></script><script src="/static/js/main.fb87f627.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,244 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import { InputLabel, Tooltip } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {
fieldBasic,
tooltipHelper,
} from "../../Common/FormComponents/common/styleLibrary";
import DeleteIcon from "../../../../icons/DeleteIcon";
import HelpIcon from "../../../../icons/HelpIcon";
import { IPool } from "./types";
interface IPoolsMultiSelector {
elements: IPool[];
name: string;
label: string;
tooltip?: string;
classes: any;
onChange: (elements: IPool[]) => void;
}
const gridBasic = {
display: "grid",
gridTemplateColumns: "calc(50% - 20px) calc(50% - 20px) 30px",
gridGap: 8,
};
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
inputContainer: {
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
},
labelContainer: {
display: "flex",
},
inputGrid: {
...gridBasic,
},
inputTitles: {
...gridBasic,
marginBottom: 5,
},
deleteIconContainer: {
alignSelf: "center",
"& button": {
padding: 0,
marginBottom: 10,
},
},
});
const PoolsMultiSelector = ({
elements,
name,
label,
tooltip = "",
onChange,
classes,
}: IPoolsMultiSelector) => {
const defaultPool: IPool = {
name: "",
servers: 0,
capacity: "",
volumes: 0,
volumes_per_server: 0,
volume_configuration: { size: 0, storage_class: "", labels: null },
};
const [currentElements, setCurrentElements] = useState<IPool[]>([]);
const [internalCounter, setInternalCounter] = useState<number>(1);
const bottomList = createRef<HTMLDivElement>();
// Use effect to send new values to onChange
useEffect(() => {
onChange(currentElements);
}, [currentElements]);
// Use effect to set initial values
useEffect(() => {
if (currentElements.length === 0 && elements.length === 0) {
// Initial Value
setCurrentElements([{ ...defaultPool, name: "pool-1" }]);
} else if (currentElements.length === 0 && elements.length > 0) {
setCurrentElements(elements);
setInternalCounter(elements.length);
}
}, [currentElements, elements]);
// If the last input is not empty, we add a new one
const addEmptyRow = (elementsUp: IPool[]) => {
const lastElement = elementsUp[elementsUp.length - 1];
const internalElement = internalCounter + 1;
if (
lastElement.servers !== 0 &&
lastElement.name !== "" &&
!isNaN(lastElement.servers)
) {
elementsUp.push({
...defaultPool,
name: `pool-${internalElement}`,
});
const refScroll = bottomList.current;
setInternalCounter(internalElement);
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>, field: string) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", 0);
const rowPosition: IPool = updatedElement[index];
rowPosition.servers =
field === "servers" ? parseInt(e.target.value) : rowPosition.servers;
rowPosition.name = field === "name" ? e.target.value : rowPosition.name;
updatedElement[index] = rowPosition;
updatedElement = addEmptyRow(updatedElement);
setCurrentElements(updatedElement);
};
const deleteElement = (poolToRemove: number) => {
const poolsClone = [...currentElements];
const newArray = poolsClone
.slice(0, poolToRemove)
.concat(poolsClone.slice(poolToRemove + 1, poolsClone.length));
setCurrentElements(newArray);
};
const inputs = currentElements.map((element, index) => {
return (
<React.Fragment key={`pool-${name}-${index.toString()}`}>
<div>
<InputBoxWrapper
id={`${name}-${index.toString()}-name`}
label={""}
name={`${name}-${index.toString()}-name`}
value={currentElements[index].name}
onChange={(e) => onChangeElement(e, "name")}
index={index}
key={`Pools-${name}-${index.toString()}-name`}
/>
</div>
<div>
<InputBoxWrapper
type="number"
min="0"
id={`${name}-${index.toString()}-servers`}
label={""}
name={`${name}-${index.toString()}-servers`}
value={currentElements[index].servers.toString(10)}
onChange={(e) => onChangeElement(e, "servers")}
index={index}
key={`Pools-${name}-${index.toString()}-servers`}
/>
</div>
<div className={classes.deleteIconContainer}>
<IconButton
color="primary"
onClick={() => {
deleteElement(index);
}}
disabled={index === currentElements.length - 1}
>
<DeleteIcon />
</IconButton>
</div>
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<div>
<HelpIcon className={classes.tooltip} />
</div>
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12}>
<div className={classes.inputTitles}>
<div>Name</div>
<div>Servers</div>
</div>
<div className={classes.inputContainer}>
<div className={classes.inputGrid}>{inputs}</div>
</div>
<div ref={bottomList} />
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(PoolsMultiSelector);

View File

@@ -38,7 +38,7 @@ export interface IAddPoolRequest {
export interface IVolumeConfiguration {
size: number;
storage_class: string;
storage_class_name: string;
labels: { [key: string]: any } | null;
}

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import get from "lodash/get";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
@@ -10,6 +11,8 @@ import api from "../../../../common/api";
import { IAddPoolRequest, ITenant } from "../ListTenants/types";
import { IAffinityModel } from "../../../../common/types";
import { getHardcodedAffinity } from "./utils";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../ListTenants/utils";
interface IAddPoolProps {
tenant: ITenant;
@@ -67,10 +70,42 @@ const AddPoolModal = ({
const [numberOfNodes, setNumberOfNodes] = useState<number>(0);
const [volumeSize, setVolumeSize] = useState<number>(0);
const [volumesPerServer, setVolumesPerSever] = useState<number>(0);
const [selectedStorageClass, setSelectedStorageClass] = useState<string>("");
const [storageClasses, setStorageClasses] = useState<Opts[]>([]);
const instanceCapacity: number = volumeSize * 1073741824 * volumesPerServer;
const totalCapacity: number = instanceCapacity * numberOfNodes;
useEffect(() => {
setSelectedStorageClass("");
setStorageClasses([]);
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant.namespace}/resourcequotas/${tenant.namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
setStorageClasses(newStorage);
if (newStorage.length > 0) {
setSelectedStorageClass(newStorage[0].value);
}
})
.catch((err: any) => {
console.error(err);
});
}, [tenant]);
return (
<ModalWrapper
onClose={() => onClosePoolAndReload(false)}
@@ -97,7 +132,7 @@ const AddPoolModal = ({
volumes_per_server: volumesPerServer,
volume_configuration: {
size: volumeSize * 1073741824,
storage_class: "",
storage_class_name: selectedStorageClass,
labels: null,
},
affinity: hardCodedAffinity,
@@ -155,6 +190,19 @@ const AddPoolModal = ({
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSelectedStorageClass(e.target.value as string);
}}
label="Storage Class"
value={selectedStorageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>