Small Tweaks (#186)

* Support for MinDNS

* mindns option

* Added minDNS to summary table

* Validations of configure page

* Added create label & removed console logs

* Adding login workaround

* Added min limits to inputs

* Fixed issue with sizes

* Removed empty values from review page

* Added zone names

* Added validation to zones selector

* Fixed issue with back button in zones page

* Changed validation for zones filter & simplified clean zones

* Changed CredentialsPrompt to be a global component.

* Added assets

* Added hover to table & removed view button

* Added view links & actions to tables

* Added links for cloud & console in table

* Fixed position of progress bar

* Added advanced mode to wizard

* Added "zebra-style" tables

* Added servers field to simple form

* Fixes for demo

* Tweaks

* updated assets

* remove hardcoded bypass

* Address Comments

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Daniel Valdivia
2020-07-01 11:58:35 -07:00
committed by GitHub
parent 59a5c9dbf0
commit be069eddd5
25 changed files with 855 additions and 343 deletions

View File

@@ -1,5 +1,7 @@
FROM golang:1.13
RUN apt-get update -y && apt-get install -y ca-certificates
ADD go.mod /go/src/github.com/minio/mcs/go.mod
ADD go.sum /go/src/github.com/minio/mcs/go.sum
WORKDIR /go/src/github.com/minio/mcs/
@@ -12,7 +14,6 @@ WORKDIR /go/src/github.com/minio/mcs/
ENV CGO_ENABLED=0
RUN apt-get update -y && apt-get install -y ca-certificates
RUN go build -ldflags "-w -s" -a -o mcs ./cmd/mcs
FROM scratch

File diff suppressed because one or more lines are too long

View File

@@ -33,34 +33,35 @@ const GlobalCss = withStyles({
fontSize: "14px",
textTransform: "capitalize",
padding: "16px 25px 16px 25px",
borderRadius: "3px"
borderRadius: 3,
},
".MuiButton-sizeSmall": {
padding: "4px 10px",
fontSize: "0.8125rem"
fontSize: "0.8125rem",
},
".MuiTableCell-head": {
borderRadius: "3px 3px 0px 0px",
fontSize: "13px"
fontSize: 13,
},
".MuiPaper-root": {
borderRadius: "3px"
borderRadius: 3,
},
".MuiDrawer-paperAnchorDockedLeft": {
borderRight: "0px"
borderRight: 0,
},
".MuiDrawer-root": {
"& .MuiPaper-root": {
borderRadius: "0px"
}
}
}
borderRadius: 0,
},
},
},
})(() => null);
ReactDOM.render(
<Provider store={configureStore()}>
<GlobalCss />
<ThemeProvider theme={theme}>
{/*<ThemeProvider theme={newTheme}>*/}
<Routes />
</ThemeProvider>
</Provider>,

View File

@@ -19,7 +19,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import ModalWrapper from "../ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
const styles = (theme: Theme) =>
@@ -36,6 +36,7 @@ interface ICredentialsPromptProps {
classes: any;
newServiceAccount: NewServiceAccount | null;
open: boolean;
entity: string;
closeModal: () => void;
}
@@ -60,6 +61,7 @@ const CredentialsPrompt = ({
newServiceAccount,
open,
closeModal,
entity,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
@@ -71,12 +73,12 @@ const CredentialsPrompt = ({
onClose={() => {
closeModal();
}}
title="New Service Account Created"
title={`New ${entity} Created`}
>
<React.Fragment>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
A new service account has been created with the following details:
A new {entity} has been created with the following details:
<ul>
<li>
<b>Access Key:</b> {newServiceAccount.accessKey}

View File

@@ -0,0 +1,20 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
export interface NewServiceAccount {
accessKey: string;
secretKey: string;
}

View File

@@ -47,6 +47,8 @@ interface InputBoxProps {
error?: string;
required?: boolean;
placeholder?: string;
min?: string;
max?: string;
}
const styles = (theme: Theme) =>
@@ -110,8 +112,20 @@ const InputBoxWrapper = ({
error = "",
required = false,
placeholder = "",
min,
max,
classes,
}: InputBoxProps) => {
let inputProps: any = { "data-index": index };
if (type === "number" && min) {
inputProps["min"] = min;
}
if (type === "number" && max) {
inputProps["max"] = max;
}
return (
<React.Fragment>
<Grid
@@ -154,7 +168,7 @@ const InputBoxWrapper = ({
type={type}
multiline={multiline}
autoComplete={autoComplete}
inputProps={{ "data-index": index }}
inputProps={inputProps}
error={error !== ""}
helperText={error}
placeholder={placeholder}

View File

@@ -20,7 +20,7 @@ export const fieldBasic = {
inputLabel: {
fontWeight: 500,
marginRight: 10,
width: 100,
width: 160,
fontSize: 14,
color: "#393939",
textAlign: "right" as const,

View File

@@ -27,6 +27,7 @@ const styles = (theme: Theme) =>
flexGrow: 1,
},
wizardSteps: {
minWidth: 180,
marginRight: 10,
"& ul": {
padding: 15,

View File

@@ -59,8 +59,6 @@ const WizardPage = ({ classes, page, pageChange }: IWizardPage) => {
}
};
console.log("buttons", page);
return (
<div className={classes.wizardStepContainer}>
<div className={classes.wizardComponent}>{page.componentRender}</div>

View File

@@ -26,6 +26,7 @@ export interface IWizardElement {
label: string;
componentRender: any;
buttons: IWizardButton[];
advancedOnly?: boolean;
}
export interface IWizardMain {

View File

@@ -20,6 +20,8 @@ import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
import DescriptionIcon from "./TableActionIcons/DescriptionIcon";
import CloudIcon from "./TableActionIcons/CloudIcon";
import ConsoleIcon from "./TableActionIcons/ConsoleIcon";
import { Link } from "react-router-dom";
interface IActionButton {
@@ -42,6 +44,10 @@ const defineIcon = (type: string, selected: boolean) => {
return <DeleteIcon active={selected} />;
case "description":
return <DescriptionIcon active={selected} />;
case "cloud":
return <CloudIcon active={selected} />;
case "console":
return <ConsoleIcon active={selected} />;
}
return null;
@@ -64,7 +70,8 @@ const TableActionButton = ({
size={"small"}
onClick={
onClick
? () => {
? (e) => {
e.stopPropagation();
onClick(valueClick);
}
: () => null
@@ -79,7 +86,16 @@ const TableActionButton = ({
}
if (isString(to)) {
return <Link to={`${to}/${valueClick}`}>{buttonElement}</Link>;
return (
<Link
to={`${to}/${valueClick}`}
onClick={(e) => {
e.stopPropagation();
}}
>
{buttonElement}
</Link>
);
}
return null;

View File

@@ -0,0 +1,20 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const CloudIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill={active ? selected : unSelected}
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"
/>
</svg>
);
};
export default CloudIcon;

View File

@@ -0,0 +1,20 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const ConsoleIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill={active ? selected : unSelected}
d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3v-3h18v3z"
/>
</svg>
);
};
export default ConsoleIcon;

View File

@@ -32,6 +32,7 @@ import {
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
import history from "../../../../history";
import { checkboxIcons } from "../FormComponents/common/styleLibrary";
//Interfaces for table Items
@@ -143,6 +144,16 @@ const styles = (theme: Theme) =>
paddingTop: "100px",
paddingBottom: "100px",
},
rowElement: {
userSelect: "none",
"&:hover": {
backgroundColor: "#ececec",
},
},
rowClickable: {
cursor: "pointer",
},
...checkboxIcons,
});
@@ -190,6 +201,10 @@ const elementActions = (
idField: string
) => {
return actions.map((action: ItemActions, index: number) => {
if (action.type === "view") {
return null;
}
return (
<TableActionButton
type={action.type}
@@ -219,6 +234,24 @@ const TableWrapper = ({
stickyHeader = false,
paginatorConfig,
}: TableWrapperProps) => {
const findView = itemActions
? itemActions.find((el) => el.type === "view")
: null;
const clickAction = (rowItem: any) => {
if (findView) {
const valueClick = findView.sendOnlyId ? rowItem[idField] : rowItem;
if (findView.to) {
history.push(`${findView.to}/${valueClick}`);
return;
}
if (findView.onClick) {
findView.onClick(valueClick);
}
}
};
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
@@ -265,7 +298,15 @@ const TableWrapper = ({
: false;
return (
<TableRow key={`tb-${entityName}-${index.toString()}`}>
<TableRow
key={`tb-${entityName}-${index.toString()}`}
className={`${findView ? classes.rowClickable : ""} ${
classes.rowElement
}`}
onClick={() => {
clickAction(record);
}}
>
{onSelect && selectedItems && (
<TableCell
padding="checkbox"
@@ -278,6 +319,10 @@ const TableWrapper = ({
inputProps={{ "aria-label": "secondary checkbox" }}
checked={isSelected}
onChange={onSelect}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
/>

View File

@@ -378,10 +378,6 @@ const Console = ({
) : null}
</Switch>
</Router>
<Box pt={4}>
<Copyright />
</Box>
</Container>
</main>
</div>

View File

@@ -25,7 +25,7 @@ import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { NewServiceAccount } from "./types";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import HelpIcon from "@material-ui/icons/Help";
require("codemirror/mode/javascript/javascript");

View File

@@ -20,11 +20,11 @@ import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { NewServiceAccount } from "./types";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import AddServiceAccount from "./AddServiceAccount";
import DeleteServiceAccount from "./DeleteServiceAccount";
import CredentialsPrompt from "./CredentialsPrompt";
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
@@ -222,6 +222,7 @@ const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
closeModal={() => {
closeCredentialsModal();
}}
entity="Service Account"
/>
)}
<Grid container>

View File

@@ -18,8 +18,3 @@ export interface ServiceAccountsList {
service_accounts: string[];
total: number;
}
export interface NewServiceAccount {
accessKey: string;
secretKey: string;
}

View File

@@ -38,10 +38,14 @@ import {
} from "../../../../utils/validationFunctions";
import GenericWizard from "../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../Common/GenericWizard/types";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
interface IAddTenantProps {
open: boolean;
closeModalAndRefresh: (reloadData: boolean) => any;
closeModalAndRefresh: (
reloadData: boolean,
res: NewServiceAccount | null
) => any;
classes: any;
}
@@ -68,11 +72,17 @@ const styles = (theme: Theme) =>
paddingTop: 5,
marginBottom: 10,
backgroundColor: "#fff",
zIndex: 500,
},
tableTitle: {
fontWeight: 700,
width: "30%",
},
zoneError: {
color: "#dc1f2e",
fontSize: "0.75rem",
paddingLeft: 120,
},
...modalBasic,
});
@@ -86,6 +96,7 @@ const AddTenant = ({
closeModalAndRefresh,
classes,
}: IAddTenantProps) => {
// Fields
const [addSending, setAddSending] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [tenantName, setTenantName] = useState<string>("");
@@ -95,7 +106,7 @@ const AddTenant = ({
const [volumesPerServer, setVolumesPerServer] = useState<number>(0);
const [volumeConfiguration, setVolumeConfiguration] = useState<
IVolumeConfiguration
>({ size: "", storage_class: "" });
>({ size: 0, storage_class: "" });
const [mountPath, setMountPath] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
@@ -105,13 +116,23 @@ const AddTenant = ({
const [storageClasses, setStorageClassesList] = useState<Opts[]>([]);
const [validationErrors, setValidationErrors] = useState<any>({});
const [namespace, setNamespace] = useState<string>("");
const [advancedMode, setAdvancedMode] = useState<boolean>(false);
// Forms Validation
const [nameTenantValid, setNameTenantValid] = useState<boolean>(false);
const [configValid, setConfigValid] = useState<boolean>(false);
const [configureValid, setConfigureValid] = useState<boolean>(false);
const [zonesValid, setZonesValid] = useState<boolean>(false);
// Custom Elements
const [customACCK, setCustomACCK] = useState<boolean>(false);
const [customDockerhub, setCustomDockerhub] = useState<boolean>(false);
useEffect(() => {
fetchStorageClassList();
}, []);
/* Validations of pages */
useEffect(() => {
const commonValidation = commonFormValidation([validationElements[0]]);
@@ -121,17 +142,84 @@ const AddTenant = ({
}, [tenantName]);
useEffect(() => {
const commonValidation = commonFormValidation(
validationElements.slice(1, 3)
);
let subValidation = validationElements.slice(1, 3);
if (!advancedMode) {
subValidation.push({
fieldKey: "servers",
required: true,
pattern: /\d+/,
customPatternMessage: "Field must be numeric",
value: zones.length > 0 ? zones[0].servers.toString(10) : "0",
});
}
const commonValidation = commonFormValidation(subValidation);
setConfigValid(
!("volumes_per_server" in commonValidation) &&
!("volume_size" in commonValidation)
!("volume_size" in commonValidation) &&
!("servers" in commonValidation)
);
setValidationErrors(commonValidation);
}, [volumesPerServer, volumeConfiguration]);
}, [volumesPerServer, volumeConfiguration, zones]);
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (customACCK) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "access_key",
required: true,
value: accessKey,
},
{
fieldKey: "secret_key",
required: true,
value: secretKey,
},
];
}
if (customDockerhub) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "image",
required: true,
value: imageName,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/minio:VERSION'",
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
setConfigureValid(Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [customACCK, customDockerhub, accessKey, secretKey, imageName]);
useEffect(() => {
const filteredZones = zones.filter(
(zone) => zone.name !== "" && zone.servers !== 0 && !isNaN(zone.servers)
);
if (filteredZones.length > 0) {
setZonesValid(true);
setValidationErrors({});
return;
}
setZonesValid(false);
setValidationErrors({ zones_selector: "Please add a valid zone" });
}, [zones]);
/* End Validation of pages */
const validationElements: IValidation[] = [
{
@@ -145,34 +233,23 @@ const AddTenant = ({
{
fieldKey: "volumes_per_server",
required: true,
pattern: /\d+/,
customPatternMessage: "Field must be numeric",
value: volumesPerServer.toString(10),
},
{
fieldKey: "volume_size",
required: true,
value: volumeConfiguration.size,
},
{
fieldKey: "image",
required: false,
value: imageName,
pattern: /\d+/,
customPatternMessage: "Field must be numeric",
value: volumeConfiguration.size.toString(10),
},
{
fieldKey: "service_name",
required: false,
value: serviceName,
},
{
fieldKey: "access_key",
required: false,
value: accessKey,
},
{
fieldKey: "secret_key",
required: false,
value: secretKey,
},
];
const clearValidationError = (fieldKey: string) => {
@@ -184,38 +261,42 @@ const AddTenant = ({
useEffect(() => {
if (addSending) {
let cleanZones: IZone[] = [];
for (let zone of zones) {
if (zone.name !== "") {
cleanZones.push(zone);
}
}
let cleanZones = zones.filter(
(zone) => zone.name !== "" && zone.servers > 0 && !isNaN(zone.servers)
);
const commonValidation = commonFormValidation(validationElements);
setValidationErrors(commonValidation);
if (Object.keys(commonValidation).length === 0) {
const data: { [key: string]: any } = {
name: tenantName,
service_name: tenantName,
image: imageName,
enable_ssl: enableSSL,
enable_mcs: enableMCS,
access_key: accessKey,
secret_key: secretKey,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: `${volumeConfiguration.size}${sizeFactor}`,
storage_class: volumeConfiguration.storage_class,
},
zones: cleanZones,
};
api
.invoke("POST", `/api/v1/mkube/tenants`, {
name: tenantName,
service_name: tenantName,
image: imageName,
enable_ssl: enableSSL,
enable_mcs: enableMCS,
access_key: accessKey,
secret_key: secretKey,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: `${volumeConfiguration.size}${sizeFactor}`,
storage_class: volumeConfiguration.storage_class,
},
zones: cleanZones,
})
.then(() => {
.invoke("POST", `/api/v1/mkube/tenants`, data)
.then((res) => {
const newSrvAcc: NewServiceAccount = {
accessKey: res.access_key,
secretKey: res.secret_key,
};
setAddSending(false);
setAddError("");
closeModalAndRefresh(true);
closeModalAndRefresh(true, newSrvAcc);
})
.catch((err) => {
setAddSending(false);
@@ -228,9 +309,17 @@ const AddTenant = ({
}
}, [addSending]);
useEffect(() => {
if (advancedMode) {
setZones([{ name: "zone-1", servers: 0, capacity: "0", volumes: 0 }]);
} else {
setZones([{ name: "zone-1", servers: 1, capacity: "0", volumes: 0 }]);
}
}, [advancedMode]);
const setVolumeConfig = (item: string, value: string) => {
const volumeCopy: IVolumeConfiguration = {
size: item !== "size" ? volumeConfiguration.size : value,
size: item !== "size" ? volumeConfiguration.size : parseInt(value),
storage_class:
item !== "storage_class" ? volumeConfiguration.storage_class : value,
};
@@ -238,6 +327,14 @@ const AddTenant = ({
setVolumeConfiguration(volumeCopy);
};
const setServersSimple = (value: string) => {
const copyZone = [...zones];
copyZone[0].servers = parseInt(value, 10);
setZones(copyZone);
};
const fetchStorageClassList = () => {
api
.invoke("GET", `/api/v1/mkube/storage-classes`)
@@ -268,7 +365,7 @@ const AddTenant = ({
type: "other",
enabled: true,
action: () => {
closeModalAndRefresh(false);
closeModalAndRefresh(false, null);
},
};
@@ -276,24 +373,47 @@ const AddTenant = ({
{
label: "Name Tenant",
componentRender: (
<Grid item xs={12}>
<React.Fragment>
<div className={classes.headerElement}>
<h3>Name Tenant</h3>
<span>How would you like to name this new tenant?</span>
</div>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTenantName(e.target.value);
clearValidationError("tenant-name");
}}
label="Tenant Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTenantName(e.target.value);
clearValidationError("tenant-name");
}}
label="Tenant Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
</Grid>
<Grid item xs={12}>
<br />
<span>
Use Advanced mode to configure additional options in the tenant
</span>
<br />
<br />
<CheckboxWrapper
value="adv_mode"
id="adv_mode"
name="adv_mode"
checked={advancedMode}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setAdvancedMode(checked);
}}
label={"Advanced Mode"}
/>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
@@ -308,48 +428,109 @@ const AddTenant = ({
<h3>Configure</h3>
<span>Basic configurations for tenant management</span>
</div>
Please enter your access & secret keys
<Grid item xs={12}>
<InputBoxWrapper
id="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
clearValidationError("access_key");
<CheckboxWrapper
value="custom_acck"
id="custom_acck"
name="custom_acck"
checked={customACCK}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setCustomACCK(checked);
}}
label="Access Key"
value={accessKey}
error={validationErrors["access_key"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
clearValidationError("secret_key");
}}
label="Secret Key"
value={secretKey}
error={validationErrors["secret_key"] || ""}
/>
</Grid>
Please enter the MinIO image from dockerhub
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
clearValidationError("image");
}}
label="MinIO Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="Eg. minio/minio:RELEASE.2020-05-08T02-40-49Z"
label={"Use Custom Access Keys"}
/>
</Grid>
{customACCK && (
<React.Fragment>
Please enter your access & secret keys
<Grid item xs={12}>
<InputBoxWrapper
id="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
clearValidationError("access_key");
}}
label="Access Key"
value={accessKey}
error={validationErrors["access_key"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
clearValidationError("secret_key");
}}
label="Secret Key"
value={secretKey}
error={validationErrors["secret_key"] || ""}
required
/>
</Grid>
</React.Fragment>
)}
{advancedMode && (
<Grid item xs={12}>
<CheckboxWrapper
value="custom_dockerhub"
id="custom_dockerhub"
name="custom_dockerhub"
checked={customDockerhub}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setCustomDockerhub(checked);
}}
label={"Use custom image"}
/>
</Grid>
)}
{customDockerhub && (
<React.Fragment>
Please enter the MinIO image from dockerhub
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
clearValidationError("image");
}}
label="MinIO Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="Eg. minio/minio:RELEASE.2020-05-08T02-40-49Z"
required
/>
</Grid>
</React.Fragment>
)}
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: configureValid },
],
},
{
label: "Service Configuration",
advancedOnly: true,
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
<h3>Service Configuration</h3>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="service_name"
@@ -386,6 +567,7 @@ const AddTenant = ({
},
{
label: "Storage Class",
advancedOnly: true,
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
@@ -423,17 +605,38 @@ const AddTenant = ({
<h3>Server Configuration</h3>
<span>Define the server configuration</span>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="mount_path"
name="mount_path"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setMountPath(e.target.value);
}}
label="Mount Path"
value={mountPath}
/>
</Grid>
{advancedMode && (
<Grid item xs={12}>
<InputBoxWrapper
id="mount_path"
name="mount_path"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setMountPath(e.target.value);
}}
label="Mount Path"
value={mountPath}
/>
</Grid>
)}
{!advancedMode && (
<Grid item xs={12}>
<InputBoxWrapper
id="servers"
name="servers"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setServersSimple(e.target.value);
clearValidationError("servers");
}}
label="Number of Servers"
value={zones.length > 0 ? zones[0].servers.toString(10) : "0"}
min="0"
required
error={validationErrors["servers"] || ""}
/>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_server"
@@ -445,6 +648,7 @@ const AddTenant = ({
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
min="0"
required
error={validationErrors["volumes_per_server"] || ""}
/>
@@ -453,6 +657,7 @@ const AddTenant = ({
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
type="number"
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -460,9 +665,10 @@ const AddTenant = ({
clearValidationError("volume_size");
}}
label="Size"
value={volumeConfiguration.size}
value={volumeConfiguration.size.toString(10)}
required
error={validationErrors["volume_size"] || ""}
min="0"
/>
</div>
<div className={classes.sizeFactorContainer}>
@@ -489,6 +695,7 @@ const AddTenant = ({
},
{
label: "Zones Definition",
advancedOnly: true,
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
@@ -506,17 +713,21 @@ const AddTenant = ({
elements={zones}
/>
</div>
<div className={classes.zoneError}>
{validationErrors["zones_selector"] || ""}
</div>
</Grid>
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{ label: "Next", type: "next", enabled: true },
{ label: "Next", type: "next", enabled: zonesValid },
],
},
{
label: "Extra Configurations",
advancedOnly: true,
componentRender: (
<React.Fragment>
<div className={classes.headerElement}>
@@ -534,7 +745,7 @@ const AddTenant = ({
setEnableMCS(checked);
}}
label={"Enable mcs"}
label={"Enable Console"}
/>
</Grid>
<Grid item xs={12}>
@@ -588,48 +799,65 @@ const AddTenant = ({
</TableCell>
<TableCell>{tenantName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Access Key
</TableCell>
<TableCell>{accessKey}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Secret Key
</TableCell>
<TableCell>{secretKey}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
MinIO Image
</TableCell>
<TableCell>{imageName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Service Name
</TableCell>
<TableCell>{serviceName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Namespace
</TableCell>
<TableCell>{namespace}</TableCell>
</TableRow>
{customACCK && (
<React.Fragment>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Access Key
</TableCell>
<TableCell>{accessKey}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Secret Key
</TableCell>
<TableCell>{secretKey}</TableCell>
</TableRow>
</React.Fragment>
)}
{customDockerhub && (
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
MinIO Image
</TableCell>
<TableCell>{imageName}</TableCell>
</TableRow>
)}
{serviceName !== "" && (
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Service Name
</TableCell>
<TableCell>{serviceName}</TableCell>
</TableRow>
)}
{namespace !== "" && (
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Namespace
</TableCell>
<TableCell>{namespace}</TableCell>
</TableRow>
)}
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Storage Class
</TableCell>
<TableCell>{volumeConfiguration.storage_class}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Mount Path
</TableCell>
<TableCell>{mountPath}</TableCell>
</TableRow>
{mountPath !== "" && (
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Mount Path
</TableCell>
<TableCell>{mountPath}</TableCell>
</TableRow>
)}
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Volumes per Server
@@ -650,37 +878,34 @@ const AddTenant = ({
</TableCell>
<TableCell>{zones.length}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable SSL
</TableCell>
<TableCell>{enableSSL ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable MCS
</TableCell>
<TableCell>{enableMCS ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
{advancedMode && (
<React.Fragment>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable SSL
</TableCell>
<TableCell>{enableSSL ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable MCS
</TableCell>
<TableCell>{enableMCS ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
</React.Fragment>
)}
</TableBody>
</Table>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</React.Fragment>
),
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Save",
label: "Create",
type: "submit",
enabled: !addSending,
action: () => {
console.log("Save");
setAddSending(true);
},
},
@@ -688,18 +913,29 @@ const AddTenant = ({
},
];
let filteredWizardSteps = wizardSteps;
if (!advancedMode) {
filteredWizardSteps = wizardSteps.filter((step) => !step.advancedOnly);
}
return (
<ModalWrapper
title="Create Tenant"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh(false);
closeModalAndRefresh(false, null);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<GenericWizard wizardSteps={wizardSteps} />
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<GenericWizard wizardSteps={filteredWizardSteps} />
</ModalWrapper>
);
};

View File

@@ -30,6 +30,8 @@ import { ITenant, ITenantsResponse } from "./types";
import { niceBytes } from "../../../../common/utils";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
interface ITenantsList {
classes: any;
@@ -90,10 +92,23 @@ const ListTenants = ({ classes }: ITenantsList) => {
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
createdAccount,
setCreatedAccount,
] = useState<NewServiceAccount | null>(null);
const closeAddModalAndRefresh = (reloadData: boolean) => {
const closeAddModalAndRefresh = (
reloadData: boolean,
res: NewServiceAccount | null
) => {
setCreateTenantOpen(false);
if (res !== null) {
setShowNewCredentials(true);
setCreatedAccount(res);
}
if (reloadData) {
setIsLoading(true);
}
@@ -112,6 +127,11 @@ const ListTenants = ({ classes }: ITenantsList) => {
setDeleteOpen(true);
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setCreatedAccount(null);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
@@ -124,6 +144,10 @@ const ListTenants = ({ classes }: ITenantsList) => {
setRowsPerPage(rPP);
};
const openLink = (link: string) => {
window.open(link, "_blank");
};
const tableActions = [
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
@@ -206,6 +230,16 @@ const ListTenants = ({ classes }: ITenantsList) => {
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={createdAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
entity="Tenant"
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Tenants</Typography>

View File

@@ -34,6 +34,7 @@ interface IZonesMultiSelector {
label: string;
tooltip?: string;
classes: any;
onChange: (elements: IZone[]) => void;
}
@@ -87,9 +88,8 @@ const ZonesMultiSelector = ({
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0, capacity: "", volumes: 0 };
const [currentElements, setCurrentElements] = useState<IZone[]>([
{ ...defaultZone },
]);
const [currentElements, setCurrentElements] = useState<IZone[]>([]);
const [internalCounter, setInternalCounter] = useState<number>(1);
const bottomList = createRef<HTMLDivElement>();
// Use effect to send new values to onChange
@@ -97,13 +97,34 @@ const ZonesMultiSelector = ({
onChange(currentElements);
}, [currentElements]);
// Use effect to set initial values
useEffect(() => {
if (currentElements.length === 0 && elements.length === 0) {
// Initial Value
setCurrentElements([{ ...defaultZone, name: "zone-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: IZone[]) => {
const lastElement = elementsUp[elementsUp.length - 1];
if (lastElement.servers !== 0 && lastElement.name !== "") {
elementsUp.push({ ...defaultZone });
const internalElement = internalCounter + 1;
if (
lastElement.servers !== 0 &&
lastElement.name !== "" &&
!isNaN(lastElement.servers)
) {
elementsUp.push({
...defaultZone,
name: `zone-${internalElement}`,
});
const refScroll = bottomList.current;
setInternalCounter(internalElement);
if (refScroll) {
refScroll.scrollIntoView(false);
}
@@ -158,6 +179,7 @@ const ZonesMultiSelector = ({
<div>
<InputBoxWrapper
type="number"
min="0"
id={`${name}-${index.toString()}-servers`}
label={""}
name={`${name}-${index.toString()}-servers`}

View File

@@ -23,7 +23,7 @@ export interface IZone {
}
export interface IVolumeConfiguration {
size: string;
size: number;
storage_class: string;
}

View File

@@ -110,7 +110,7 @@ const AddZoneModal = ({
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
}}
label="Volumes per Server"
label="Drives per Server"
value={numberOfInstances.toString(10)}
/>
</Grid>

View File

@@ -6,18 +6,18 @@ const theme = createMuiTheme({
light: "#757ce8",
main: "#201763",
dark: "#362585",
contrastText: "#fff"
contrastText: "#fff",
},
secondary: {
light: "#ff7961",
main: "#f44336",
dark: "#ba000d",
contrastText: "#000"
contrastText: "#000",
},
error: {
light: "#e03a48",
main: "#dc1f2e",
contrastText: "#ffffff"
contrastText: "#ffffff",
},
grey: {
100: "#f0f0f0",
@@ -28,39 +28,39 @@ const theme = createMuiTheme({
600: "#737373",
700: "#666666",
800: "#4d4d4d",
900: "#333333"
900: "#333333",
},
background: {
default: "#F4F4F4"
}
default: "#F4F4F4",
},
},
typography: {
fontFamily: ["Lato", "sans-serif"].join(","),
h1: {
fontWeight: "bold",
color: "#201763"
color: "#201763",
},
h2: {
fontWeight: "bold",
color: "#201763"
color: "#201763",
},
h3: {
fontWeight: "bold",
color: "#201763"
color: "#201763",
},
h4: {
fontWeight: "bold",
color: "#201763"
color: "#201763",
},
h5: {
fontWeight: "bold",
color: "#201763"
color: "#201763",
},
h6: {
fontWeight: "bold",
color: "#000000"
}
}
color: "#000000",
},
},
});
export default theme;

View File

@@ -0,0 +1,66 @@
import { createMuiTheme } from "@material-ui/core";
const newTheme = createMuiTheme({
palette: {
primary: {
light: "#0c4453",
main: "#01262e",
dark: "#001115",
contrastText: "#fff",
},
secondary: {
light: "#ff7961",
main: "#f44336",
dark: "#01262E",
contrastText: "#000",
},
error: {
light: "#e03a48",
main: "#dc1f2e",
contrastText: "#ffffff",
},
grey: {
100: "#F7F7F7",
200: "#D8DDDE",
300: "#BAC3C5",
400: "#9BA9AC",
500: "#7C8F93",
600: "#5D7479",
700: "#3F5A60",
800: "#204047",
900: "#01262E",
},
background: {
default: "#F4F4F4",
},
},
typography: {
fontFamily: ["Lato", "sans-serif"].join(","),
h1: {
fontWeight: "bold",
color: "#01262E",
},
h2: {
fontWeight: "bold",
color: "#01262E",
},
h3: {
fontWeight: "bold",
color: "#01262E",
},
h4: {
fontWeight: "bold",
color: "#01262E",
},
h5: {
fontWeight: "bold",
color: "#01262E",
},
h6: {
fontWeight: "bold",
color: "#01262E",
},
},
});
export default newTheme;